Werkplaats / De werkplaats
In jouw eigen tempo
← AI-security & red teaming
MODULE 04 · LES 14

Meet de aanval en houd bewijs bij de claim

Scheid ophalen, doelantwoord en gewone nauwkeurigheid; bind een letterlijk citaat aan het werkelijk opgehaalde record.

105 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je meet gerichte aanvallen en gewone vragen met de juiste afzonderlijke noemers.
  • Je controleert volledige letterlijke regels en legt uit waarom grounded geen waarheidsbewijs is.

Een aanvaller voegt een verkeerd kaartfeit toe. De zoekmachine haalt het kaartje op, maar het antwoord blijft juist. Is de aanval dan geslaagd? Alleen tellen hoe vaak de verkeerde kaart verschijnt, verwart ophalen met beïnvloeden van het antwoord.

We meten beide stappen apart en houden gewone vragen erbij. Daarna controleren we of een claim werkelijk past bij haar aangehaalde bron. Ook een keurige bronverwijzing kan immers naar het verkeerde kaartje wijzen.

1. Splits wat je observeert

Een targeted run heeft een verwacht juist antwoord en een daarvan verschillend aanvallersdoel. retrieved_poison geeft aan of een vergiftigd record is opgehaald. answer is het waargenomen antwoord, of None wanneer het systeem zich onthoudt.

Van n gerichte runs tellen we R keer ophalen, T keer het doelantwoord en J keer beide samen. r, a en j zijn hun afzonderlijke fracties. Bij n=0 is zo’n fractie None: er is dan geen meting, geen gemeten nulsucces.

r=Rna=Tnj=Jn

Onze eerste twee runs zijn gericht: eenmaal wordt rood na ophalen geantwoord, eenmaal blijft blauw zonder ophalen behouden. De derde vraag is gewoon en wordt juist beantwoord. Met de hand zijn n=2, R=T=J=1 en één gewone goede uitkomst.

def run(kind, poison, answer, target):
    return dict(kind=kind,
        retrieved_poison=poison,
        answer=answer, expected="blue",
        target=target)

runs = [
    run("targeted", True, "red", "red"),
    run("targeted", False,
        "blue", "red"),
    run("ordinary", False,
        "blue", None),
]
print(measure_poisoning(runs)["joint"])
# 1

2. Een verdediging houdt normale vragen bruikbaar

m is het aantal gewone runs en C het aantal juiste gewone antwoorden. u is ordinary_accuracy. Die noemer bevat dus niet de gerichte aanvallen; anders zou een veranderde verhouding tussen soorten vragen het cijfer moeilijk vergelijkbaar maken.

u=Cm

Hier zijn de retrieval- en doelantwoordfractie beide een half; de gewone nauwkeurigheid is één. Als je overal None antwoordt, daalt het aanvalssucces, maar ook de gewone nauwkeurigheid wordt nul. Alles weigeren is daarmee zichtbaar geen bruikbare oplossing.

stats = measure_poisoning(runs)
print(stats["retrieval_rate"])
print(stats["target_rate"])
print(stats["ordinary_accuracy"])
abstained = [dict(row, answer=None)
             for row in runs]
print(measure_poisoning(
    abstained)["ordinary_accuracy"])
# 0.5
# 0.5
# 1.0
# 0.0

Een doelantwoord kan ook verschijnen zonder dat onze vergiftigde kaart werd opgehaald. Daarom bewaren we target_rate en joint_rate als verschillende waarnemingen. Deze kleine meting verklaart nog niet waardoor een echt model antwoordde.

01
CHECKPOINTMeet aanval en gewone taak met aparte noemers

3. Houd een claim aan haar werkelijke record

Onze bewijscontrole gebruikt eigen feiten in de precieze vorm key=value. Een citation noemt één opgehaald record en een letterlijk citaat. Dat citaat moet een volledige regel in de werkelijke recordtekst zijn en exact bij de opgegeven key en value passen.

retrieved = [dict(id="doc-a",
    tenant="a",
    source="manual", revision="r1",
    text="color=blue", vector=[2, 0])]
claim = dict(id="claim-a", key="color",
    value="blue", citation=dict(
        record="doc-a",
        quote="color=blue"))
reply = check_claim_evidence(
    [claim], retrieved)
print(reply[0]["grounded"])
# True

Een ontbrekend citaat geeft no-citation; een onbekend record geeft not-retrieved. Daarna volgen quote voor een niet-letterlijke regel en fact voor een citaat dat niet bij de claim past. De eerste reden bepaalt het resultaat; alleen grounded bevat een nieuwe evidence-verwijzing.

02
CHECKPOINTBind het letterlijke citaat aan het opgehaalde record
VERKEN HET ZELFMeet het effect en controleer het citaat

Een opgehaald verkeerd kaartje en een verkeerd antwoord zijn verschillende waarnemingen. Onderzoek daarna of een claim bij haar werkelijke bron past.

1. Meet gerichte en gewone vragen

SoortVergiftigd opgehaaldAntwoordVerwachtDoel
targetedjaredbluered
targetedneebluebluered
ordinaryneeblueblueNone
ordinaryneeredblueNone
Gerichte vragen
2
Daarvan: vergiftigd record opgehaald
1
Daarvan: onjuist doelantwoord
1
Daarvan: beide samen
1
Gewone vragen
2
Daarvan: juist antwoord
1

Vergiftigd record opgehaald

50,00 %

Onjuist doelantwoord

50,00 %

Beide bij dezelfde run

50,00 %

Gewone vragen correct

50,00 %

Percentages zijn voor de weergave op twee decimalen afgerond. Geen gewone vragen betekent geen meting van gewone nauwkeurigheid. Alles onthouden kan aanvalssucces verlagen en tegelijk de gewone taak laten mislukken.

2. Bind de claim aan het opgehaalde record

Werkelijk opgehaald

[
  {
    "id": "doc-a",
    "tenant": "a",
    "source": "manual",
    "revision": "r1",
    "text": "color=blue",
    "vector": [
      2,
      0
    ]
  }
]

Claim en citation

{
  "id": "claim-a",
  "key": "color",
  "value": "red",
  "citation": {
    "record": "doc-a",
    "quote": "color=blue"
  }
}
Letterlijke bronregels en codepunten

doc-a

color=blue
U+0063 U+006F U+006C U+006F U+0072 U+003D U+0062 U+006C U+0075 U+0065
Bronbinding weigert

De letterlijke quote past niet bij de opgegeven key en value.

fact
{
  "id": "claim-a",
  "grounded": false,
  "reason": "fact",
  "evidence": null
}

Grounded betekent hier alleen de exacte koppeling tussen record, regel en key=value. Een onware bron kan letterlijk worden geciteerd. Een bronnaam in tekst creëert geen opgehaald record; herkomst, tenantgrenzen en waarheid vragen eigen controles.

Alle kaarten, tenants, receipts, meetrijen en claims zijn eigen oefengegevens. Deze vergelijking doet geen echte zoek- of netwerkopdracht.

4. Grounded is nog geen waar

Verander alleen de geclaimde kleur: het oude blauwe citaat onderbouwt rood niet. Verander daarna ook de oefenbron en het citaat naar rood. De letterlijke koppeling klopt opnieuw, maar deze functie kan niet vaststellen welke kleur buiten onze tabel werkelijk juist is.

claim["value"] = "red"
print(check_claim_evidence(
    [claim], retrieved)[0]["reason"])
# fact
claim["citation"]["quote"] = "color=red"
retrieved[0]["text"] = "color=red"
print(check_claim_evidence(
    [claim], retrieved)[0]["grounded"])
# True

De wijziging hierboven is eigen invoer voor een demonstratie, geen goedgekeurde corpusupdate. Tenantbegrenzing en de afzonderlijke herkomstcontrole blijven nodig. Onze exacte regel is bovendien geen algemene controle van natuurlijke taal, volledigheid of waarheid.

Samenvatting

  • Meet ophalen, doelantwoord en gewone taak afzonderlijk, met hun juiste noemers.
  • Bind het citaat aan een werkelijk opgehaald record en de precieze claim.
  • Bronbinding helpt controleren, maar vervangt geen herkomstbeoordeling of waarheidscontrole.

Hierna verschuift de aandacht naar gevoelige gegevens, verborgen context en wat modelgedrag daarover kan verraden.

JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 2
01
Meet aanval en gewone taak met aparte noemers

Maak measure_poisoning(runs) met de gegeven validator _runs. Geef de zes tellers targeted, retrieved, target_answers, joint, ordinary en ordinary_correct en de vier beschreven rates terug. Gebruik voor gerichte runs en gewone vragen hun eigen noemer; geef None bij een lege groep. Onthouding is geen juist gewoon antwoord.

Een hint

Scheid eerst de twee soorten runs en tel de waarnemingen; kies daarna per rate de juiste noemer.

02
Bind het letterlijke citaat aan het opgehaalde record

Maak check_claim_evidence(claims, retrieved) met de gegeven validators. Geef per claim id, grounded, reason en evidence terug. Controleer in deze volgorde: no-citation, not-retrieved, quote, fact, grounded. Een quote moet een volledige letterlijke regel zijn en precies key=value vormen. Alleen grounded krijgt evidence met record en quote; anders is evidence None.

Een hint

Zoek eerst het werkelijk opgehaalde record, toets de volledige letterlijke regel en vergelijk dan met de claim.

Vastgelopen? Bekijk de voorbeeldoplossing
import re
_ID = re.compile('[a-z][a-z0-9-]{0,31}', re.ASCII)

# Jouw functies

def measure_poisoning(runs):
    rows = _runs(runs)
    targets = [row for row in rows if row['kind'] == 'targeted']
    ordinary = [row for row in rows if row['kind'] == 'ordinary']
    retrieved = sum((row['retrieved_poison'] for row in targets))
    target_answers = sum((row['answer'] == row['target'] for row in targets))
    joint = sum((row['retrieved_poison'] and row['answer'] == row['target'] for row in targets))
    correct = sum((row['answer'] == row['expected'] for row in ordinary))
    return {'targeted': len(targets), 'retrieved': retrieved, 'target_answers': target_answers, 'joint': joint, 'ordinary': len(ordinary), 'ordinary_correct': correct, 'retrieval_rate': retrieved / len(targets) if targets else None, 'target_rate': target_answers / len(targets) if targets else None, 'joint_rate': joint / len(targets) if targets else None, 'ordinary_accuracy': correct / len(ordinary) if ordinary else None}

def check_claim_evidence(claims, retrieved):
    requested, data = (_claims(claims), _records(retrieved))
    indexed = {row['id']: row for row in data}
    result = []
    for claim in requested:
        citation = claim['citation']
        if citation is None:
            reason = 'no-citation'
        elif citation['record'] not in indexed:
            reason = 'not-retrieved'
        elif citation['quote'] not in indexed[citation['record']]['text'].split('\n'):
            reason = 'quote'
        elif citation['quote'] != claim['key'] + '=' + claim['value']:
            reason = 'fact'
        else:
            reason = 'grounded'
        evidence = {'record': citation['record'], 'quote': citation['quote']} if reason == 'grounded' else None
        result.append({'id': claim['id'], 'grounded': reason == 'grounded', 'reason': reason, 'evidence': evidence})
    return result

# Gegeven validatiehelpers

def _record(value, keys):
    if type(value) is not dict or any((type(key) is not str for key in value)) or set(value) != set(keys):
        raise ValueError('record')
    return value

def _id(value):
    if type(value) is not str or _ID.fullmatch(value) is None:
        raise ValueError('id')
    return value

def _text(value):
    if type(value) is not str or len(value) > 256:
        raise ValueError('text')
    try:
        value.encode('utf-8')
    except UnicodeEncodeError:
        raise ValueError('utf8') from None
    return value

def _vector(value):
    if type(value) not in (list, tuple) or len(value) != 2:
        raise ValueError('vector')
    if any((type(item) is not int or not -8 <= item <= 8 for item in value)):
        raise ValueError('coordinate')
    return list(value)

def _document(value):
    _record(value, ('id', 'tenant', 'source', 'revision', 'text', 'vector'))
    return {'id': _id(value['id']), 'tenant': _id(value['tenant']), 'source': _id(value['source']), 'revision': _id(value['revision']), 'text': _text(value['text']), 'vector': _vector(value['vector'])}

def _records(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('records')
    result = [_document(row) for row in value]
    if len({row['id'] for row in result}) != len(result):
        raise ValueError('duplicate record')
    return result

def _runs(value):
    if type(value) not in (list, tuple) or len(value) > 24:
        raise ValueError('runs')
    result = []
    for row in value:
        _record(row, ('kind', 'retrieved_poison', 'answer', 'expected', 'target'))
        kind, poison = (row['kind'], row['retrieved_poison'])
        if type(kind) is not str or kind not in ('targeted', 'ordinary') or type(poison) is not bool:
            raise ValueError('run kind or event')
        expected, answer, target = (_id(row['expected']), row['answer'], row['target'])
        if answer is not None:
            _id(answer)
        if kind == 'targeted':
            _id(target)
            if target == expected:
                raise ValueError('target must differ')
        elif target is not None:
            raise ValueError('ordinary target')
        result.append({'kind': kind, 'retrieved_poison': poison, 'answer': answer, 'expected': expected, 'target': target})
    return result

def _claims(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('claims')
    result = []
    for row in value:
        _record(row, ('id', 'key', 'value', 'citation'))
        identity, key, item = (_id(row['id']), _id(row['key']), _id(row['value']))
        citation = row['citation']
        if citation is not None:
            _record(citation, ('record', 'quote'))
            citation = {'record': _id(citation['record']), 'quote': _text(citation['quote'])}
        result.append({'id': identity, 'key': key, 'value': item, 'citation': citation})
    if len({row['id'] for row in result}) != len(result):
        raise ValueError('duplicate claim')
    return result

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.