LLM-output watermerken voor herkomst
Geavanceerde technieken voor het watermerken van door LLM gegenereerde tekst om herkomst vast te stellen, inclusief deployment-architecturen, multi-bit-coderingsschema's, robuustheidsoverwegingen en de rol van watermerken in raamwerken voor AI-security en verantwoording.
Outputwatermerken voor herkomst gaat verder dan eenvoudige detectie van door AI gegenereerde tekst. Het beoogt deze vragen te beantwoorden: welk model heeft deze tekst gegenereerd, wanneer, voor welke gebruiker en via welke deployment? Deze herkomstinformatie is cruciaal voor verantwoording in AI-systemen, forensisch onderzoek naar misbruik en naleving van opkomende regelgeving die traceerbaarheid van door AI gegenereerde inhoud vereist.
Architectuur van herkomstwatermerken
Systeemcomponenten
┌────────────────────────────────────────────────────────────┐
│ Watermarked LLM Serving System │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ User │ │ Provenance │ │ LLM Inference │ │
│ │ Request │──▶│ Metadata │──▶│ + Watermark │ │
│ │ │ │ Generator │ │ Injection │ │
│ └──────────┘ └──────────────┘ └─────────┬──────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ Watermarked Output │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐│
│ │ Key Store │ │ Verification Service ││
│ │ (secrets) │──▶│ - Extract watermark from text ││
│ │ │ │ - Decode provenance metadata ││
│ │ │ │ - Verify against key store ││
│ └──────────────┘ └──────────────────────────────────────┘│
└────────────────────────────────────────────────────────────┘
Velden van herkomstmetadata
| Veld | Vereiste bits | Doel | Voorbeeld |
|---|---|---|---|
| Model-ID | 8-16 | Identificeer welke modelversie | GPT-4-turbo-2025-01 |
| Deployment-ID | 8-16 | Identificeer serveringsomgeving | prod-us-east-1 |
| Gebruikers-/Sessie-ID | 16-32 | Toeschrijven aan specifieke gebruiker | hash van gebruikers-ID |
| Tijdstempel | 16-32 | Wanneer de tekst werd gegenereerd | Unix-epoch afgekapt |
| Request-ID | 16-32 | Koppelen aan specifieke API-aanroep | Hash van request |
| Beleidsversie | 4-8 | Welk veiligheidsbeleid actief was | v2.3 |
| Totaal | 68-136 bits |
Multi-bit-watermerkschema's
Herkomst coderen in tokenselectie
import hashlib
import numpy as np
from typing import Optional
class ProvenanceWatermark:
"""
Multi-bit-watermerkschema dat herkomstmetadata codeert
in LLM-output via vertekende tokenselectie.
"""
def __init__(
self,
secret_key: bytes,
gamma: float = 0.5,
delta: float = 1.5,
window_size: int = 4,
):
self.secret_key = secret_key
self.gamma = gamma
self.delta = delta
self.window_size = window_size
def encode_provenance(
self,
model_id: int,
deployment_id: int,
user_hash: int,
timestamp: int,
) -> bytes:
"""Verpak herkomstmetadata in een binaire payload."""
import struct
payload = struct.pack(
">HHIH",
model_id & 0xFFFF,
deployment_id & 0xFFFF,
user_hash & 0xFFFFFFFF,
timestamp & 0xFFFF,
)
return payload
def get_bit_assignment(
self,
prev_tokens: list[int],
vocab_size: int,
bit_value: int,
) -> set:
"""
Partitioneer de woordenschat op basis van het te coderen bit.
Voor bit=1, verteken richting de groene lijst.
Voor bit=0, verteken richting de rode lijst (of geen vertekening).
"""
seed = hashlib.sha256(
self.secret_key
+ bytes(prev_tokens[-self.window_size:])
).digest()
rng = np.random.RandomState(int.from_bytes(seed[:4], "big"))
green_count = int(vocab_size * self.gamma)
green_list = set(rng.choice(vocab_size, green_count, replace=False))
if bit_value == 1:
return green_list # Verteken richting deze tokens
else:
return set() # Geen vertekening (natuurlijke verdeling)
def apply_watermark(
self,
logits: np.ndarray,
prev_tokens: list[int],
payload: bytes,
token_position: int,
) -> np.ndarray:
"""Pas watermerkvertekening toe op logits op basis van het huidige payload-bit."""
# Bepaal welk bit op deze positie gecodeerd moet worden
bit_index = token_position % (len(payload) * 8)
byte_index = bit_index // 8
bit_offset = bit_index % 8
current_bit = (payload[byte_index] >> (7 - bit_offset)) & 1
green_list = self.get_bit_assignment(
prev_tokens, len(logits), current_bit
)
modified = logits.copy()
for token_id in green_list:
modified[token_id] += self.delta
return modifiedVerificatie en decodering
class ProvenanceVerifier:
"""Verifieer en decodeer herkomstwatermerken uit tekst."""
def __init__(self, secret_key: bytes, gamma: float = 0.5, window_size: int = 4):
self.secret_key = secret_key
self.gamma = gamma
self.window_size = window_size
def extract_provenance(
self,
token_ids: list[int],
vocab_size: int,
payload_bits: int = 80,
) -> dict:
"""
Extraheer gecodeerde herkomst uit tekst met watermerk.
Gebruikt meerderheidsstemming over meerdere herhalingen van de payload.
"""
bit_votes = {i: [] for i in range(payload_bits)}
for pos in range(self.window_size, len(token_ids)):
prev_tokens = token_ids[pos - self.window_size:pos]
bit_index = (pos - self.window_size) % payload_bits
# Reconstrueer de groene lijst voor deze positie
seed = hashlib.sha256(
self.secret_key + bytes(prev_tokens)
).digest()
rng = np.random.RandomState(int.from_bytes(seed[:4], "big"))
green_count = int(vocab_size * self.gamma)
green_list = set(rng.choice(vocab_size, green_count, replace=False))
# Stem: zit dit token in de groene lijst?
is_green = token_ids[pos] in green_list
bit_votes[bit_index].append(1 if is_green else 0)
# Meerderheidsstemming voor elk bit
decoded_bits = []
confidence_scores = []
for i in range(payload_bits):
votes = bit_votes[i]
if votes:
avg = np.mean(votes)
decoded_bits.append(1 if avg > self.gamma + 0.05 else 0)
confidence_scores.append(abs(avg - self.gamma))
else:
decoded_bits.append(0)
confidence_scores.append(0)
# Reconstrueer de payload
payload_bytes = self._bits_to_bytes(decoded_bits)
return {
"payload": payload_bytes,
"provenance": self._decode_payload(payload_bytes),
"mean_confidence": np.mean(confidence_scores),
"min_confidence": np.min(confidence_scores),
"reliable": np.mean(confidence_scores) > 0.1,
}
def _bits_to_bytes(self, bits: list[int]) -> bytes:
result = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for j in range(8):
if i + j < len(bits):
byte = (byte << 1) | bits[i + j]
result.append(byte)
return bytes(result)
def _decode_payload(self, payload: bytes) -> dict:
import struct
try:
model_id, deployment_id, user_hash, timestamp = struct.unpack(
">HHIH", payload[:10]
)
return {
"model_id": model_id,
"deployment_id": deployment_id,
"user_hash": user_hash,
"timestamp": timestamp,
}
except struct.error:
return {"error": "Payload decode failed"}Patronen voor deployment-architectuur
Patroon 1: inline watermerken
Het watermerk wordt tijdens inferentie toegepast als een logits-processor:
# Integratie met vLLM-servering
from vllm import LLM, SamplingParams
class WatermarkedLLM:
"""Wrapper die herkomstwatermerken toepast tijdens inferentie."""
def __init__(self, model_name: str, watermark_key: bytes):
self.llm = LLM(model=model_name)
self.watermark = ProvenanceWatermark(secret_key=watermark_key)
def generate(
self,
prompt: str,
user_id: str,
deployment_id: int,
max_tokens: int = 512,
) -> dict:
provenance_payload = self.watermark.encode_provenance(
model_id=1,
deployment_id=deployment_id,
user_hash=hash(user_id) & 0xFFFFFFFF,
timestamp=int(time.time()) & 0xFFFF,
)
params = SamplingParams(
max_tokens=max_tokens,
logits_processors=[
lambda token_ids, logits: self.watermark.apply_watermark(
logits, token_ids, provenance_payload, len(token_ids)
)
],
)
outputs = self.llm.generate([prompt], params)
return {
"text": outputs[0].outputs[0].text,
"watermark_payload": provenance_payload.hex(),
}Patroon 2: watermerken na generatie
Pas het watermerk toe door gegenereerde tekst te herschrijven (minder precies maar modelonafhankelijk):
| Benadering | Kwaliteitsimpact | Robuustheid | Bitcapaciteit | Latency-impact |
|---|---|---|---|---|
| Inline (logits-processor) | Minimaal bij lage delta | Gemiddeld | Hoog (multi-bit) | Laag (~5% overhead) |
| Herschrijven na generatie | Gemiddeld | Laag | Laag (weinig bits) | Hoog (tweede doorgang) |
| Semantische inbedding | Laag | Hoog | Laag-gemiddeld | Gemiddeld |
| Steganografisch | Zeer laag | Zeer hoog | Laag | Gemiddeld |
Robuustheidsoverwegingen
Dreigingsmodel voor herkomstwatermerken
Threat Level 1: Casual removal
- Simple paraphrasing, light editing
- Defense: standard token-level watermarks survive
Threat Level 2: Informed removal
- Attacker knows watermark is present, uses paraphrase model
- Defense: semantic watermarks, multi-layer encoding
Threat Level 3: Targeted removal
- Attacker knows the scheme and has detection access
- Defense: dynamic key rotation, multiple encoding layers
Threat Level 4: Adaptive removal
- Attacker has white-box access to watermark algorithm
- Defense: unbiased watermarks (Christ et al.), moving to architectural solutions
Afweging tussen robuustheid en capaciteit
def analyze_robustness_capacity_tradeoff(
text_length_tokens: int,
payload_bits: int,
gamma: float = 0.5,
target_confidence: float = 0.99,
):
"""
Bereken de relatie tussen watermerkcapaciteit,
tekstlengte en detectiebetrouwbaarheid.
Meer payload-bits vereisen meer tekst voor betrouwbare extractie.
"""
from scipy import stats
# Elk payload-bit krijgt text_length / payload_bits stemmen
votes_per_bit = text_length_tokens / payload_bits
# Onder de watermerkverdeling is de groene fractie = gamma + shift
# Voor betrouwbare detectie hebben we voldoende stemmen per bit nodig
min_votes_needed = stats.norm.ppf(target_confidence) ** 2 / (0.1 ** 2)
return {
"text_length": text_length_tokens,
"payload_bits": payload_bits,
"votes_per_bit": votes_per_bit,
"min_votes_needed": min_votes_needed,
"sufficient_text": votes_per_bit >= min_votes_needed,
"min_text_for_payload": int(min_votes_needed * payload_bits),
}
# Voorbeeld: 80-bit herkomst-payload
# analyze_robustness_capacity_tradeoff(1000, 80)
# → votes_per_bit: 12.5, mogelijk onvoldoende
# analyze_robustness_capacity_tradeoff(5000, 80)
# → votes_per_bit: 62.5, waarschijnlijk voldoendeIntegratie met AI-governance
Regelgevende vereisten
Opkomende AI-regelgeving vereist in toenemende mate traceerbaarheid van output:
| Regelgeving | Relevantie van watermerken | Vereiste |
|---|---|---|
| EU AI Act (Artikel 50) | Output van AI met hoog risico moet identificeerbaar zijn | Machineleesbare markering van door AI gegenereerde inhoud |
| US Executive Order 14110 | Standaarden voor contentauthenticatie | NIST-standaarden voor authenticatie van AI-inhoud |
| China AI Regulations | Markering van door AI gegenereerde inhoud | Verplichte markering van door AI gegenereerde tekst, afbeelding, video |
| C2PA Standard | Contentherkomst | Cryptografische herkomst voor digitale inhoud |
Compliance-architectuur
class ComplianceWatermarkService:
"""
Watermerkdienst ontworpen om te voldoen aan regelgevende compliancevereisten
voor de herkomst van door AI gegenereerde inhoud.
"""
def __init__(self, config: dict):
self.watermark = ProvenanceWatermark(
secret_key=config["watermark_key"],
delta=config.get("delta", 1.5),
)
self.audit_log = config["audit_log_backend"]
def generate_with_provenance(
self,
llm,
prompt: str,
user_context: dict,
) -> dict:
"""Genereer output met watermerk en volledige audit trail."""
# Maak een herkomstrecord
provenance = {
"request_id": generate_uuid(),
"timestamp": datetime.utcnow().isoformat(),
"model_id": llm.model_id,
"deployment": llm.deployment_id,
"user_id_hash": hash_user_id(user_context["user_id"]),
"safety_policy_version": llm.safety_policy_version,
}
# Genereer met watermerk
output = llm.generate(
prompt=prompt,
watermark_payload=self.watermark.encode_provenance(**provenance),
)
# Log naar onveranderlijke audit store
self.audit_log.write({
**provenance,
"prompt_hash": hashlib.sha256(prompt.encode()).hexdigest(),
"output_hash": hashlib.sha256(output["text"].encode()).hexdigest(),
"output_length_tokens": output["token_count"],
})
return {
"text": output["text"],
"provenance_id": provenance["request_id"],
"compliance_record": True,
}Sleutelbeheer voor watermerken
Sleutelrotatie en levenscyclus
Watermerksleutels moeten met dezelfde nauwgezetheid beheerd worden als cryptografische sleutels:
class WatermarkKeyManager:
"""Beheer de levenscyclus van watermerksleutels, inclusief rotatie en intrekking."""
def __init__(self, key_store):
self.key_store = key_store
def rotate_key(self, deployment_id: str) -> dict:
"""
Roteer de watermerksleutel voor een deployment.
De oude sleutel moet behouden blijven voor verificatie van eerder
van een watermerk voorziene inhoud.
"""
import secrets
new_key = secrets.token_bytes(32)
old_key = self.key_store.get_current_key(deployment_id)
# Archiveer de oude sleutel met vervalmetadata
self.key_store.archive_key(
deployment_id=deployment_id,
key=old_key,
retired_at=datetime.utcnow(),
verify_until=datetime.utcnow() + timedelta(days=365),
)
# Activeer de nieuwe sleutel
self.key_store.set_current_key(
deployment_id=deployment_id,
key=new_key,
activated_at=datetime.utcnow(),
)
return {
"deployment_id": deployment_id,
"key_rotated": True,
"old_key_archived": True,
}Beperkingen en eerlijke beoordeling
| Capaciteit | Realiteit |
|---|---|
| Voorkomen van toevallig misbruik | Effectief — toevallige gebruikers zullen geen verwijdering proberen |
| Afschrikken van geavanceerde aanvallers | Beperkt — parafraseaanvallen zijn eenvoudig |
| Forensische toeschrijving | Nuttig als de tekst niet zwaar gewijzigd is |
| Regelgevende compliance | Voldoet aan markeringsvereisten indien gecombineerd met audit logs |
| Bewijs van generatie | Niet cryptografisch bindend — kan juridisch in twijfel worden getrokken |
Gerelateerde onderwerpen
- Watermerken & detectie van door AI gegenereerde tekst -- red team-perspectief op het aanvallen van watermerken
- Geavanceerde defensietechnieken -- breder landschap van geavanceerde defensie
- Defensiebenchmarking -- effectiviteit van watermerken meten
- Input/output-filtersystemen -- complementaire outputcontroles
- Onderzoek naar datalekken voor AI -- wanneer herkomstgegevens incidentrespons ondersteunen
Referenties
- Kirchenbauer et al., "A Watermark for Large Language Models" (2023) - Foundational token-level watermarking research
- Christ et al., "Unbiased Watermark for Large Language Models" (2024) - Provably unbiased watermarking with zero quality impact
- Zhao et al., "Provable Robust Watermarking for AI-Generated Text" (2024) - Robustness guarantees for watermarking against text modifications
- C2PA (Coalition for Content Provenance and Authenticity) (2025) - Industry standard for digital content provenance
- EU AI Act, Article 50 (2024) - Regulatory requirements for AI-generated content marking and traceability
Wat is het belangrijkste verschil tussen binaire detectiewatermerken en herkomstwatermerken voor LLM-output?