Skip to main content

Cleaning, Dedup & Formatting

Introduction

You've sourced (L196), generated (L197), and maybe distilled (L198) a pile of examples. It is not a training set yet — it's raw material full of junk, duplicates, PII, leaks, and the wrong format. This lesson is the final pass that turns it into something you can actually train on: cleaning, deduplication, and formatting.

Think of it as the last gate — an ordered funnel where every bad row either gets fixed or dropped before it can teach the model the wrong thing. It's unglamorous and it's where careless teams lose: a forgotten eval leak inflates your scores into a lie, and a wrong chat template means the model never learns to stop. Three jobs, in order: clean (drop the junk), dedup (remove the repeats and the eval leakage), format (wrap it the way the trainer expects). Get this right and your L195 "good dataset" finally becomes a training-ready one.

An infographic titled 'Cleaning, Dedup & Formatting', the fifth lesson of section three on dataset engineering, the final cleanup pass that turns raw, sourced, synthetic, and distilled data into a training-ready set. It is an ordered funnel run cheap filters first and expensive ones last, so only high-quality data reaches the costly steps, and it has three jobs. The first job is cleaning: heuristic rule-based filters drop too-short and pathologically long examples, off-language documents detected by a language classifier, broken encoding and malformed rows, and toxic or illegal content, while a PII scrub redacts names, emails, and card numbers hidden in free text, followed by optional model-based quality scoring. The second job is deduplication at three levels: exact dedup hashes normalized text to drop identical rows, near-duplicate dedup uses MinHash with locality-sensitive hashing over n-gram shingles to catch lexically similar rows fast, and semantic dedup embeds text with a sentence encoder and clusters it to remove rows that share meaning even when the wording differs, and advanced pipelines combine all three because each catches a different kind of repeat. Critically, deduplication must include decontamination against the evaluation and benchmark sets, because exact dedup does not catch paraphrased or fragmentary leakage, and train-test leakage silently inflates scores through memorization rather than skill. The third job is formatting: every row must be wrapped in the model's own chat template applied through apply_chat_template, the loss must be masked to the assistant response tokens so the model learns to generate answers rather than reproduce prompts, and each example must end with the EOS token so the model learns to stop. The wrong template, no loss mask, or a missing EOS are the number-one silent fine-tuning bugs. The takeaway is that cleaning is the last gate before training: clean, dedup and decontaminate, then format, or the model learns the wrong thing or never stops.

The Funnel: Cheap Filters First

The whole pipeline follows one design principle: run the cheap, aggressive filters first, and save the expensive operations for last — so only already-clean data reaches the costly steps. There's no point running a GPU-heavy semantic-dedup pass over rows that a one-line length check would have thrown away.

So the order is roughly:

  1. Heuristic cleaning (length, language, format, toxicity) — milliseconds per row.
  2. PII scrub — cheap regex + NER.
  3. Exact dedup — a hash lookup.
  4. Near-dup (MinHash) — fast, scalable fuzzy matching.
  5. Semantic dedup — embeddings + clustering (the expensive one).
  6. Decontamination — n-gram overlap against your eval set.
  7. Formatting — the chat template, last.

Heuristic, rule-based filters are also preferred over ML-based ones where possible — they're transparent and don't inject model bias. You'll feel this order in the lab (it even warns you if you run semantic dedup before the cheap filters).

Job 1 — Cleaning: Drop the Junk

Cleaning is quality-filtering the rows. The standard heuristic screens:

  • Length — drop examples that are too short (e.g. < ~20 tokens — no signal) or pathologically long (often a scraping artifact).
  • Language — a fast classifier (e.g. FastText) drops documents below a confidence threshold for your target language.
  • Format / encoding — broken Unicode, leftover HTML, truncated responses, empty fields.
  • Toxicity / illegal — remove harmful or non-compliant content.

Then the privacy pass from L196 (Sourcing & Annotating Data): PII scrub — redact names, emails, phone numbers, and card numbers in the free text, not just labeled fields (it hides in the body of a ticket or transcript). Finally, optional model-based quality scoring (the LLM-as-judge from L197 (Synthetic Data Generation)) drops low-quality rows. Remember the order: heuristics are cheap, the judge is expensive — filter with rules first, then spend judge tokens only on what survives.

Job 2 — Dedup: Three Levels of Repeat

Duplicates add tokens, not signal — they waste compute, bias the model toward the repeated examples, and (worst) cause train/eval leakage. But "duplicate" has three levels, and you need all three because each catches a different kind of repeat:

  1. Exact dedup — normalize (lowercase, collapse whitespace) and hash; drop identical rows. Cheap, first.
  2. Near-duplicate (fuzzy)MinHash + LSH over n-gram shingles (e.g. 5-grams). Fast and scalable; catches rows that differ by a word or two ("reset my password?" vs "reset my password??"). It's lexical — driven by surface overlap.
  3. Semantic dedup — embed each row with a sentence encoder (e.g. all-MiniLM-L6-v2), then cluster (SemDeDup / k-means) and drop near-identical meaning even when the wording is totally different ("reset my password" vs "recover my login"). A cosine threshold around 0.8 is common.

Strong pipelines chain all three: exact → MinHash → SemDeDup. The MinHash pass is cheap and catches the obvious near-copies; the semantic pass is expensive but catches the paraphrases MinHash misses. Which sets up the step everyone forgets.

The Leak Nobody Catches: Decontamination

Here's the one that silently ruins evaluations: train/eval contamination. If a training example is also in your eval/test set (or a public benchmark like MMLU or GSM8K), the model memorizes it and your eval score goes up — but the model isn't actually better, you're just measuring memorization, not skill. Your numbers become a lie you tell yourself.

The trap: exact dedup does NOT catch this. Paraphrased questions, solution fragments, distilled samples, and discussion-board copies are all semantically close to the eval items but lexically different — they sail past a hash check while still leaking the answer. So decontamination is its own step: scan training rows for n-gram overlap (the practical standard) — and ideally semantic overlap — against your held-out eval and any benchmarks you report on, and remove the matches.

It's a balance: over-aggressive filtering creates false positives that throw away good training data; incomplete filtering leaves leakage that inflates scores. But the rule is non-negotiable: your training set and your eval set must not overlap — decontaminate before you trust a single eval number (this is what makes the Container 4 evals honest).

Job 3 — Formatting: The Silent Killers

The data is clean and unique — now it has to be in the exact shape the trainer (and model) expect. This is where the #1 silent fine-tuning bugs live, because the model imitates the format exactly (the L195 mirror). Three things must be right:

  • The chat template. Modern models ship a Jinja2 chat template in their tokenizer config (ChatML-style: <|im_start|>role … <|im_end|>, with the model's special tokens). You must wrap every row in the model's own template via apply_chat_template — using the wrong template (or none) teaches the model a format it'll never see at inference, and quality collapses. Be consistent: one template, every row.
  • Loss masking (train on completions only). Compute the loss only on the assistant's response tokens, masking the prompt/instruction. You want the model to learn to generate answers, not reproduce prompts. This is the common default and TRL does it for you.
  • The EOS token. Every example must end with EOS so the model learns to stop. Forget it and your fine-tuned model never stops generating — a classic, baffling bug.

In TRL this is mostly handled for you if you format correctly:

from trl import SFTTrainer, SFTConfig

# Each row is the 'messages' shape (L195). Let the tokenizer's OWN chat template format it.
def to_text(ex):
    return {'text': tokenizer.apply_chat_template(
        ex['messages'], tokenize=False, add_generation_prompt=False)}  # adds special tokens + EOS

ds = ds.map(to_text)

trainer = SFTTrainer(
    model=model, train_dataset=ds,
    args=SFTConfig(
        completion_only_loss=True,   # LOSS-MASK the prompt -> learn to generate, not echo
        # SFTTrainer also appends EOS so the model learns to STOP.
    ),
)
# Wrong template, no completion mask, or missing EOS = the 3 most common silent FT bugs.

In Code: The Cleaning Funnel

Here's the whole funnel as a pipeline — cheap filters, three dedup levels, decontamination, then format:

# CHEAP heuristics first (drop the obvious junk before spending compute).
ds = ds.filter(lambda r: 20 <= n_tokens(r) <= 4096)          # length
ds = ds.filter(lambda r: lang_id(r) == 'en' and conf > 0.7)  # language (FastText)
ds = ds.map(scrub_pii)                                       # redact names/emails/cards

# DEDUP - three levels, each catches a different repeat.
ds = exact_dedup(ds)                       # hash normalized text
ds = minhash_dedup(ds, ngram=5, thresh=0.8)# near-duplicates (lexical)
ds = sem_dedup(ds, encoder='all-MiniLM-L6-v2', thresh=0.8)   # paraphrases (semantic)

# DECONTAMINATE - the step everyone forgets. No train/eval overlap!
ds = ds.filter(lambda r: not ngram_overlap(r, eval_set, n=13))

# FORMAT last - the model's own chat template + EOS, loss-masked to the response.
ds = ds.map(lambda r: {'text': tokenizer.apply_chat_template(r['messages'], tokenize=False)})
ds.to_json('train.jsonl')                  # -> ready for TRL / Unsloth

See It: The Cleaning Pipeline

Run the funnel and watch the rows fall. Start with everything off — 12 messy rows. Now turn stages on one at a time and watch each defect get caught: the too-short row dies at length, the French row at language, the exact copy at exact dedup, the reworded one at MinHash, the same-meaning one at semantic dedup. Then the two that matter most: leave Decontaminate off and watch the eval-leak row survive (your scores will lie); leave Format off and the verdict refuses to call it training-ready (no template / EOS). Hit run full pipeline to see a clean, deduped, formatted set.

Interactive: the Cleaning Pipeline. The user runs a raw twelve-row dataset through an ordered cleaning funnel and watches every row get dropped, fixed, or kept live. Eight toggleable stages run cheap to expensive: length filter, language filter, PII scrub, exact dedup, MinHash near-duplicate dedup, semantic dedup, decontaminate against the eval set, and format with chat template plus loss mask plus EOS. A min-length slider and a near-duplicate similarity slider tune the thresholds. A funnel strip with arrow connectors shows raw rows to cleaned-and-deduped to decontaminated to training-ready, with the count removed by each stage. The row table shows each row's fate and reason, such as too short, off-language, exact duplicate, near-dup, same meaning new words, eval-set leak removed, PII redacted, or template and EOS applied. A training-ready verdict flags the two traps everyone forgets: eval leakage when decontamination is off, which inflates scores through memorization, and unformatted rows with no chat template or EOS, where the model never learns to stop. A tip reminds the user to run the cheap heuristic filters before the expensive semantic dedup.

Notice the order tip: run semantic dedup with the cheap filters off and it nags you — clean first, then spend compute. That's the funnel principle in one interaction.

Why This Matters

This is the step that separates a dataset that looks fine from one that actually trains a good model. Skip dedup and you waste compute and skew the model toward repeated examples. Skip decontamination and every eval number you report is inflated by memorization — you'll ship a model you think is great and discover in production that it isn't. Botch the formatting and you get the two most baffling beginner bugs in all of fine-tuning: a model that learned the wrong template, or one that never stops generating. Mastering this funnel is what makes the difference between "I fine-tuned a model" and "I fine-tuned a model that works and I can trust my evals." It's the bridge to the capstone — L200 (Hands-On: Build a Fine-Tuning Dataset) — where you assemble everything from this section end to end.

🧪 Try It Yourself

Find the traps in the Cleaning Pipeline:

  1. The dedup ladder. Turn on exact dedup only — does it catch the reworded row (#5) or the same-meaning row (#6)? Now add MinHash, then semantic. Why do you need all three?
  2. The silent leak. Get every dedup stage on but leave Decontaminate off. Which row survives, and why is exact dedup not enough to catch it? What does its survival do to your eval scores?
  3. Not training-ready. Clean and dedup everything but leave Format off. Why does the verdict still refuse to ship it — name the three things the format step does.

Bonus: you ran the expensive semantic dedup but forgot the length and language filters. What does the lab nag you about, and why is that the wrong order?

Mental-Model Corrections

  • "Exact dedup is enough." No — it misses near-duplicates (MinHash) and paraphrases (semantic), and crucially it does not catch eval leakage.
  • "Decontamination is just deduplication." Different goal: dedup removes repeats within the training set; decontamination removes overlap with the eval set — the thing that inflates your scores.
  • "Formatting is trivial." It's the #1 silent bug: wrong chat template, no loss mask, or missing EOS quietly wreck the run (or make the model never stop).
  • "Order doesn't matter." Run cheap filters first — don't waste expensive semantic dedup / judge passes on rows a length check would have dropped.
  • "Clean once at the end." Cleaning is the last gate, but quality starts upstream (L195–L198); this pass enforces it, it can't create quality that was never sourced.

Key Takeaways

  • Cleaning is the last gate, run as an ordered funnelcheap, transparent heuristics first, expensive model-based steps last.
  • Clean: drop too-short / off-language / malformed / toxic rows, scrub PII from free text, then optional model-based quality scoring.
  • Dedup at three levels: exact (hash) → near-dup (MinHash+LSH, lexical) → semantic (embeddings, paraphrases). Each catches a different repeat.
  • Decontaminate against your eval/benchmarks — exact dedup won't catch leakage, and leakage inflates scores via memorization. Train and eval must not overlap.
  • Format with the model's own chat template (apply_chat_template), loss-masked to the response, ending in EOS — the three most common silent fine-tuning bugs.

Next: L200 — Hands-On: Build a Fine-Tuning Dataset — the capstone that assembles sourcing, synthesis, distillation, and this cleaning funnel into one real, training-ready dataset.