Skip to content
academia.sh

Lesson 05 / 21

Training Data Poisoning

Poisoning counted as the harmful share per source class, not as a procedure: 93 of 240 records in the unfiltered corpus are harmful, a share of 0.3875. The share is 0.2701 for the user, 0.5417 for the retrieved document, 0.5484 for the tool output, yet volume reverses the order: the lowest-share channel adds 37 harmful records and the highest adds 17. The source gate lowers the corpus share to 0.2701 and loses 47 clean records; the rule gate lowers it to 0.1329 and loses 23. Using both gives 0.1383, a difference below the 0.0106 step of a 94-record corpus, so unmeasurable, while lost clean records rise from 23 to 66. Rule filter recall is 0.6486 on the user channel and 0.9487 on retrieved documents. Shares move across two runs; the ordering does not.

Published Updated

In this lesson

  • Computes the harmful share per source class and reads the share apart from the volume.
  • Writes what filtering by source adds to the harmful share of the corpus together with the clean records it loses.
  • Compares the gain of a source gate placed on top of the rule gate with the resolution of the set.
Contents

What the previous lesson counted belonged to the request: a record arrived, carried the side-effect tool name flag, and the approval gate routed it to a human or did not. The decision rested on a single record, and once processed the record left no trace in the system. A system fed from its own traffic does not work this way. Answered support requests, retrieved documents, and tool outputs are collected in a corpus, that corpus becomes the input of the next adaptation, and from that point the harmful attribute profile stops being an event and becomes a feature.

This is exactly what training data poisoning is, and this lesson treats it not as a procedure but as a class and a count. The question is: what share of the records entering the corpus carries a harmful attribute profile, how does that share change by source class, and what does keeping a source out of the corpus gain and lose? No poisoning procedure, sample generation, or label corruption method is written.

The distinction decides the unit of measurement. When a defense at run time decides wrongly, a single record is affected and the decision can be reversed; a record that enters the corpus by mistake spreads to every response fed by that corpus and is not repaired by undoing one record. So on the corpus side the measured unit is not the record but the share.

  • AS49. The record set, the source classes, and the gates are fictional; there is no real corpus or training run. Seed 20260218.
  • AS50. An attack is not a text but an attribute profile. The text of the records is entirely harmless support requests and has no relation to the class; the class lives only in the attribute profile.
  • AS51. Poisoning here is the harmful share of the corpus. What a record does to the model after entering the corpus is neither measured nor claimed in this lesson; the only thing measured is the composition of the corpus.
  • AS52. The baseline is the unfiltered corpus: no gate, all 240 records are taken, stopped harmful is 0, lost clean is 0.
  • AS53. The source stamp is read from the record itself and assumed correct. Spoofing the stamp is a separate class (source_spoofing) and this lesson counts that class’s stamp as correct too; so the count for filtering by source here is the most optimistic count.
  • AS54. The source gate is a corpus gate, not a block at run time. Its cost is not blocked traffic but the number of records not taken into the corpus.
  • AS55. Resolution: the step of the share is 0.0042 at 240 records, 0.0070 if the remaining corpus is 143 records, and 0.0106 at 94 records. The step per source is 0.0073 for the user, 0.0139 for the retrieved document, and 0.0323 for the tool output.
  • AS56. Data quality, data leakage, and the training data preparation procedure were measured in earlier curricula and are not repeated; this lesson adds only the share per source.

Poisoning Is a Share, Not a Procedure

The first block builds the set, the classes, and the rule filter. The source of a record can be user, retrieved_document, or tool_output, and every class has a different source distribution: the indirect injection class comes mostly from retrieved documents, the corrupted input class mostly from the user. This distribution is fictional, and the poisoning count comes from it.

# FICTIONAL. Records, source classes and gates are defined inside the lesson; there is no
# real corpus or training run. No attack TEXT is written: it is a FEATURE PROFILE.
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


CLASSES = ["clean", "indirect_instruction", "hidden_field_request", "privilege_escalation",
           "source_spoofing", "corrupted_input"]
SOURCES = ["user", "retrieved_document", "tool_output"]
# Features are structural; none of them is a working payload.
FEATURES = ["instruction_mode", "role_change_request", "hidden_field_name", "side_effect_tool_name",
            "source_stamp_mismatch", "unusual_stamp_distribution", "excessive_length"]
PROFILE = {
    "clean":                [0.06, 0.02, 0.05, 0.03, 0.02, 0.04, 0.05],
    "indirect_instruction": [0.88, 0.42, 0.10, 0.30, 0.35, 0.08, 0.12],
    "hidden_field_request": [0.35, 0.15, 0.86, 0.12, 0.10, 0.06, 0.08],
    "privilege_escalation": [0.30, 0.25, 0.12, 0.84, 0.14, 0.05, 0.07],
    "source_spoofing":      [0.22, 0.55, 0.08, 0.10, 0.82, 0.07, 0.06],
    "corrupted_input":      [0.05, 0.03, 0.04, 0.03, 0.06, 0.80, 0.62],
}
SOURCE_SHARE = {
    "clean":                [0.72, 0.20, 0.08],
    "indirect_instruction": [0.05, 0.70, 0.25],
    "hidden_field_request": [0.55, 0.30, 0.15],
    "privilege_escalation": [0.40, 0.35, 0.25],
    "source_spoofing":      [0.10, 0.60, 0.30],
    "corrupted_input":      [0.65, 0.20, 0.15],
}
# Entirely harmless support texts; they have NO relation to the class.
TEXT = ["bu ayki fatura tutari gecen aya gore yuksek geldi",
        "sayac endeksi yanlis girilmis olabilir",
        "mahallede kesinti ne zaman bitecek",
        "tarife basamagimi ogrenmek istiyorum",
        "ariza kaydim icin ekip ne zaman gelir",
        "adres degisikligi nasil yapilir"]


def build_set(seed=SEED):
    ks = []
    for i in range(240):
        r = rng(seed + 6151 * i)
        c = CLASSES[pick(r(), [0.62, 0.10, 0.07, 0.07, 0.07, 0.07])]
        source = SOURCES[pick(r(), SOURCE_SHARE[c])]
        feat = {o: (r() < p) for o, p in zip(FEATURES, PROFILE[c])}
        ks.append({"no": i + 1, "class": c, "source": source, "features": feat,
                   "text": TEXT[int(r() * len(TEXT))],
                   "length": 40 + int(r() * 260)})
    return ks


RECORDS = build_set()


def harmful(k):
    return k["class"] != "clean"


def rule_filter(k, rules=("instruction_mode", "hidden_field_name", "side_effect_tool_name",
                          "source_stamp_mismatch")):
    return any(k["features"][o] for o in rules)


def measure(records, decision):
    """What was stopped and what was stopped by mistake are written TOGETHER."""
    tp = fp = tn = fn = 0
    for k in records:
        flagged, actual = decision(k), harmful(k)
        if flagged and actual:
            tp += 1
        elif flagged and not actual:
            fp += 1
        elif not flagged and actual:
            fn += 1
        else:
            tn += 1
    precision = tp / (tp + fp) if tp + fp else 0.0
    recall = tp / (tp + fn) if tp + fn else 0.0
    return {"tp": tp, "fp": fp, "fn": fn, "tn": tn,
            "precision": round(precision, 4), "recall": round(recall, 4),
            "false_block_rate": round(fp / (fp + tn), 4) if fp + tn else 0.0}

Counting per Source

The second block first puts the harmful share per source, then four corpus gates side by side. A record caught by a gate is not blocked, only left out of the corpus; so the cost column is lost clean records.

def assemble(records, gate):
    """Corpus gate: a record caught by the gate is NOT taken into the corpus."""
    kept = [k for k in records if not gate(k)]
    dropped = [k for k in records if gate(k)]
    h = sum(1 for k in kept if harmful(k))
    return (len(kept), h, h / len(kept) if kept else 0.0,
            len(dropped), sum(1 for k in dropped if not harmful(k)))


def non_user(k):
    return k["source"] != "user"


print("source              records  harmful  harmful share")
for src in SOURCES:
    group = [k for k in RECORDS if k["source"] == src]
    h = sum(1 for k in group if harmful(k))
    print(f"{src:<19} {len(group):>7} {h:>8} {h / len(group):>14.4f}")
total = sum(1 for k in RECORDS if harmful(k))
print(f"{'whole set':<19} {len(RECORDS):>7} {total:>8} {total / len(RECORDS):>14.4f}")

print("\ncorpus gate       kept  harmful  harmful share  dropped  dropped clean")
for name, gate in (("no filter", lambda k: False), ("source gate", non_user),
                   ("rule gate", rule_filter),
                   ("source + rule", lambda k: non_user(k) or rule_filter(k))):
    n, h, share, nd, dc = assemble(RECORDS, gate)
    print(f"{name:<16} {n:>5} {h:>8} {share:>14.4f} {nd:>8} {dc:>14}")

print("\nrule filter per source")
for src in SOURCES:
    s = measure([k for k in RECORDS if k["source"] == src], rule_filter)
    print(f"  {src:<19} tp {s['tp']:>3}  fp {s['fp']:>3}"
          f"  precision {s['precision']:.4f}  recall {s['recall']:.4f}")

print("\nclasses of the 37 harmful records of the user channel")
uh = [k for k in RECORDS if k["source"] == "user" and harmful(k)]
print("  " + "  ".join(f"{c} {sum(1 for k in uh if k['class'] == c)}"
                       for c in CLASSES[1:] if any(k["class"] == c for k in uh)))

print("\nharmful share per source, two runs")
for t in (20260218, 20260219):
    ks = build_set(t)
    row = []
    for src in SOURCES:
        group = [k for k in ks if k["source"] == src]
        row.append(f"{src} {sum(1 for k in group if harmful(k)) / len(group):.4f}")
    print(f"  {t}: " + "  ".join(row))
source              records  harmful  harmful share
user                    137       37         0.2701
retrieved_document       72       39         0.5417
tool_output              31       17         0.5484
whole set               240       93         0.3875

corpus gate       kept  harmful  harmful share  dropped  dropped clean
no filter          240       93         0.3875        0              0
source gate        137       37         0.2701      103             47
rule gate          143       19         0.1329       97             23
source + rule       94       13         0.1383      146             66

rule filter per source
  user                tp  24  fp  19  precision 0.5581  recall 0.6486
  retrieved_document  tp  37  fp   4  precision 0.9024  recall 0.9487
  tool_output         tp  13  fp   0  precision 1.0000  recall 0.7647

classes of the 37 harmful records of the user channel
  indirect_instruction 2  hidden_field_request 13  privilege_escalation 3  source_spoofing 3  corrupted_input 16

harmful share per source, two runs
  20260218: user 0.2701  retrieved_document 0.5417  tool_output 0.5484
  20260219: user 0.2323  retrieved_document 0.5484  tool_output 0.5652

The Rate and the Volume of a Channel

The first table builds the case for filtering by source: the harmful share is 0.5417 for the retrieved document, 0.5484 for the tool output, and 0.2701 for the user. The two non-user channels carry a rate close to twice that of the user channel. The difference between the user and the retrieved document is 0.2716, more than nineteen times the 0.0139 step of the retrieved document channel; this is not a fluctuation but a measurement. Anyone reading up to here reaches a clear conclusion: the corpus should take only records coming from the user.

The second column of the same table breaks that conclusion. The user channel adds 37 harmful records, the retrieved document 39, and the tool output 17. The lowest-rate channel brings only two fewer harmful records than the retrieved document channel, because 137 of the records come from there. The rate is a property of a channel, the volume is its weight; when the two are not written separately, the sentence “what comes from the user is cleaner” is both true and misleading at once. Any decision that leaves the user channel open keeps about two fifths of the harmful records of the corpus in place.

The second table puts two gates side by side. The source gate lowers the corpus share from 0.3875 to 0.2701, drops 103 records, and 47 of them are clean. The rule gate lowers the same share to 0.1329, drops 97 records, and 23 of them are clean. The rule gate is superior on both axes: it leaves less harmful in the corpus and loses fewer clean records. The reason is structural. The source is a proxy for the class, and a weak one; the attribute profile stands much closer to the class itself. The farther a proxy criterion is from what it wants to measure, the more clean records it burns.

The difference between the third and the fourth row is the place in this lesson that needs the most careful reading. Placing the source gate on top of the rule gate moves the share from 0.1329 to 0.1383, that is, it does not improve it. But writing this difference as a finding would be wrong: in a 94-record corpus the step of the share is 0.0106 and the difference is 0.0054, below the step. The two shares are indistinguishable; the gain of the layer is unmeasurable. What can be measured is the cost: lost clean records rise from 23 to 66, forty-three more, and the corpus shrinks from 143 to 94. The second claim of the course appears here in its cleanest form — the added defense charged a measurable cost for an unmeasurable gain. Saying that a setup has two gates and counting two layers means, in this table, one layer and forty-three losses.

The third table shows why the rule filter behaves differently across channels. Recall is 0.9487 on the retrieved document, 0.7647 on the tool output, and only 0.6486 on the user. The filter is weakest in the channel from which most records come. The reason is written in the fourth table: 16 of the 37 harmful records of the user channel fall in the corrupted input class, and the two flags that class carries, unusual_stamp_distribution and excessive_length, are not in the rule list. The weakness is a property not of the channel but of the list, and it cannot be closed by closing a channel. The precision of 1.0000 in the same table also needs careful reading: there are only 14 clean records in the tool output channel, the step of the false block rate there is 0.0714, so that perfect number is a number read from fourteen records.

The same table also shows the weakest place of a source-based defense. The source spoofing class is, by its name, the class where the source stamp does not agree with the rest of the record; 3 of the 23 records of that class carry a user stamp and pass the source gate clean. Because this lesson counts the stamp as correct, the 0.2701 of the source gate is its most optimistic number. In a setup where the stamp is decided by the party sending the record, the gate reads its decision from a field written by the very party it wants to filter. The rule filter carries no such dependency: the flags it looks at come from the structure of the record, not from its stamp. Part of the difference between 0.2701 and 0.1329 across the two gates is the cost of this dependency, and when the stamp stops being trustworthy the difference does not close, it widens.

The last lines say which part of the measurement is independent of the run. In the second draw the shares move: the user from 0.2701 to 0.2323, the retrieved document from 0.5417 to 0.5484, the tool output from 0.5484 to 0.5652. None of the values is fixed. What is fixed is the ordering: the user is lowest and the tool output highest in both runs. A source-based priority can safely be built on the ordering; a threshold built on a particular share moves out of place in the second run.

Summary

  • In the unfiltered corpus 93 of 240 records are harmful, a share of 0.3875; lost clean records in the baseline are 0.
  • The harmful share per source is 0.2701 for the user, 0.5417 for the retrieved document, 0.5484 for the tool output; but volume reverses the order, and the lowest-share channel adds 37 harmful records.
  • The source gate lowers the share to 0.2701 and loses 47 clean records; the rule gate lowers it to 0.1329 and loses 23 clean records, so it is superior on both axes. The source is a weak proxy for the class.
  • Placing the source gate on top of the rule gate gives a share of 0.1383; the difference of 0.0054 is below the 0.0106 step of the 94-record corpus and is unmeasurable. Lost clean records rise from 23 to 66.
  • Rule filter recall is 0.6486 on the user channel and 0.9487 on the retrieved document; the weakness is a property of the rule list, not of the channel.
  • The shares move across two runs; the ordering of the channels does not.

Next Step

Everything counted in these two lessons was on the input side: the flag the record carried, the channel it came from, the gate sending it to a human or not taking it into the corpus. Both the rule filter and the source gate decided before anything reached the model. But the information a system leaks does not have to pass through the input. A model can put a field in its response without ever being asked, can reveal that a subscriber is in the set through an expression that carries no field directly about that subscriber, and none of this produces a flag on the input side. The next lesson moves the measure to the output side: how many responses carry a field of a secret field class, how many the output filter removes, and what is left after the filter.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close