AID-H-005.004: LLM Training Data Deduplication

Version 1.20261015 · Actionable sub-technique

A data-centric hardening technique that involves systematically removing duplicate or near-duplicate sequences from the training datasets for Large Language Models (LLMs). This pre-processing step directly mitigates the risk of unintended memorization, where LLMs are prone to learn and regenerate specific training examples verbatim, which can lead to the leakage of sensitive or copyrighted information.

Pillars: data

Lifecycle: building

Scope boundary

Owns exact and near-duplicate removal from LLM training corpora before training, together with coverage evidence and post-training memorization checks for the deduplication objective.

AID-H-005.003: AID-H-005.004 reduces repeated exposure by changing the training corpus; AID-H-005.003 changes the training objective or inputs through a non-DP regularization mechanism. Deduplication does not provide regularization, and regularization does not remove duplicated sensitive passages.

Implementation guidance

AID-H-005.004-G001

Implement exact sequence deduplication using hashing, including distributed execution for large corpora.
Production implementation

Stream every object; fail on unreadable or unaccounted inputs; normalize without crossing document boundaries; hash a domain-separated length-delimited representation; globally group hashes across all workers; choose one survivor by signed policy; and atomically publish immutable input, cluster, survivor, removal and output manifests. Per-partition distinct is not global deduplication.

Independent verification

An independent job recomputes normalization/hashes for the full authoritative population, confirms each input is in exactly one cluster, validates survivor precedence and cardinality, samples byte equivalence, and reproduces the output-manifest digest.

Executable exact-deduplication stage
# File: data/exact_deduplicate.py
from __future__ import annotations

import hashlib
import json
import unicodedata
from pathlib import Path


def canonical(text: str) -> bytes:
    normalized = unicodedata.normalize("NFC", text)
    normalized = "\n".join(line.rstrip() for line in normalized.splitlines()).strip()
    return normalized.encode("utf-8")


seen: dict[str, str] = {}
kept = []
duplicates = []
for number, line in enumerate(
    Path("input.jsonl").read_text(encoding="utf-8").splitlines(), 1
):
    row = json.loads(line)
    record_id, text = row.get("id"), row.get("text")
    if not isinstance(record_id, str) or not record_id or not isinstance(text, str):
        raise ValueError(f"invalid record at line {number}")
    digest = hashlib.sha256(canonical(text)).hexdigest()
    if digest in seen:
        duplicates.append({"id": record_id, "duplicate_of": seen[digest], "sha256": digest})
    else:
        seen[digest] = record_id
        kept.append(row)

Path("deduplicated.jsonl").write_text(
    "".join(json.dumps(row, sort_keys=True) + "\n" for row in kept),
    encoding="utf-8",
)
Path("duplicates.jsonl").write_text(
    "".join(json.dumps(row, sort_keys=True) + "\n" for row in duplicates),
    encoding="utf-8",
)
if len(kept) + len(duplicates) != len(seen) + len(duplicates):
    raise RuntimeError("population reconciliation failed")

For distributed execution, partition by the full canonical SHA-256 and perform a global key reduction; a per-worker set is not a corpus-wide deduplicator. Retain input/output manifests, duplicate links, canonicalizer version, and population counts.

AID-H-005.004-G002

Use MinHash LSH to detect and remove near-duplicate documents.
Load a versioned corpus and policy

Near-duplicate thresholds depend on language, document length, shingling, and corpus purpose. Store the Jaccard threshold and permutation count in versioned policy rather than treating one value as universal.

# File: hardening/near_deduplication.py
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path

from datasketch import MinHash, MinHashLSH


def shingles(text: str, size: int) -> set[str]:
    tokens = re.findall(r"\w+", text.casefold())
    return {" ".join(tokens[i:i + size]) for i in range(max(0, len(tokens) - size + 1))}


def signature(text: str, num_perm: int, shingle_size: int) -> MinHash:
    values = shingles(text, shingle_size)
    if not values:
        raise ValueError("document has no usable tokens")
    result = MinHash(num_perm=num_perm)
    for value in sorted(values):
        result.update(value.encode("utf-8"))
    return result


def find_candidates(records: list[dict], threshold: float, num_perm: int, shingle_size: int) -> list[dict]:
    if not 0.0 < threshold <= 1.0 or num_perm < 1 or shingle_size < 1:
        raise ValueError("invalid near-duplicate policy")
    ids = [str(record["document_id"]) for record in records]
    if len(ids) != len(set(ids)):
        raise ValueError("document_id values must be unique")

    index = MinHashLSH(threshold=threshold, num_perm=num_perm)
    signatures: dict[str, MinHash] = {}
    for record in records:
        document_id = str(record["document_id"])
        value = signature(str(record["text"]), num_perm, shingle_size)
        signatures[document_id] = value
        index.insert(document_id, value)

    pairs: set[tuple[str, str]] = set()
    for document_id, value in signatures.items():
        for candidate_id in index.query(value):
            if candidate_id != document_id:
                pairs.add(tuple(sorted((document_id, candidate_id))))

    return [
        {
            "left_document_id": left,
            "right_document_id": right,
            "estimated_jaccard": signatures[left].jaccard(signatures[right]),
        }
        for left, right in sorted(pairs)
    ]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", required=True, help="JSONL with document_id and text")
    parser.add_argument("--policy", required=True, help="JSON policy with version, threshold, num_perm")
    parser.add_argument("--output", required=True)
    args = parser.parse_args()

    policy = json.loads(Path(args.policy).read_text(encoding="utf-8"))
    records = [
        json.loads(line)
        for line in Path(args.input).read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    candidates = find_candidates(
        records,
        threshold=float(policy["threshold"]),
        num_perm=int(policy["num_perm"]),
        shingle_size=int(policy["shingle_size"]),
    )
    report = {
        "schema": "aidefend.near-duplicate-review/v1",
        "policy_version": str(policy["version"]),
        "input_count": len(records),
        "candidate_count": len(candidates),
        "status": "REVIEW_REQUIRED" if candidates else "PASS",
        "candidates": candidates,
    }
    Path(args.output).write_text(
        json.dumps(report, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )


if __name__ == "__main__":
    main()
Quarantine before removal

Block dataset promotion while candidate pairs are unresolved. Apply a versioned canonical-record rule based on source authority, license, timestamp, and provenance; quarantine the rejected record rather than deleting it immediately. Rebuild the corpus manifest and independently verify that approved unique documents remain and rejected IDs are absent.

Evidence

Preserve input and output manifest digests, tokenizer/shingle implementation version, policy version, MinHash library version, candidate pairs and scores, reviewer or deterministic retention decisions, quarantine receipts, rebuilt-corpus digest, and explicit PASS/REVIEW_REQUIRED/FAIL/ERROR verdict.

AID-H-005.004-G003

Evaluate the effectiveness of deduplication by testing the final model for memorization of specific canary phrases.
Production implementation

Generate non-sensitive canaries before training, record protected placement, query matched baseline/candidate under the identical bounded schedule, score only authorized canaries, protect responses, and block promotion on any privacy or utility failure; never tune thresholds on the measured release.

Independent verification

A separate evaluator confirms synthetic provenance and documented absence boundary, replays every trial from signed seeds, recomputes exposure/extraction and utility metrics, and reads back promoted artifact/decision.

Executable canary memorization probe
# File: evaluation/canary_memorization.py
from __future__ import annotations

import json
from pathlib import Path


def evaluate(rows: list[dict], maximum_exposure: float) -> dict:
    if not 0 <= maximum_exposure <= 1 or not rows:
        raise ValueError("invalid policy or empty canary population")
    required = {"case_id", "canary", "exact_match", "exposure_score"}
    findings = []
    for row in rows:
        if set(row) != required:
            raise ValueError("canary result schema differs")
        score = row["exposure_score"]
        if isinstance(score, bool) or not isinstance(score, (int, float)):
            raise ValueError("exposure score is not numeric")
        if row["exact_match"] is True or not 0 <= score <= maximum_exposure:
            findings.append(row["case_id"])
    return {
        "case_count": len(rows),
        "finding_count": len(findings),
        "finding_case_ids": findings,
        "outcome": "PASS" if not findings else "FAIL",
    }


rows = [
    json.loads(line)
    for line in Path("evidence/canary-results.jsonl").read_text(
        encoding="utf-8"
    ).splitlines()
    if line.strip()
]
result = evaluate(rows, maximum_exposure=float(
    Path("policy/maximum-exposure.txt").read_text().strip()
))
Path("evidence/canary-verdict.json").write_text(
    json.dumps(result, sort_keys=True) + "\n", encoding="utf-8"
)

Create the signed canary population before training, query the exact deployed model through its production tokenizer/template, and compute exposure with the approved evaluator. Deduplication passes only when every canary is reconciled and independent replay stays within the versioned release threshold.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-005.004: LLM Training Data Deduplication | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version