AI EngineeringZero to ProductionHome·About·What’s new·Contact
Fine-tuning · Chapter FT2

Dataset preparation

The part that decides success: sourcing, formatting to the chat template, cleaning, dedup, and leakage-free splits. A few hundred clean examples beat tens of thousands of noisy ones.

⏱️ ~2 hours🧪 2 labs🎯 Intermediate→Advanced

Learning objectives

  • Explain why data quality decides fine-tuning success more than any hyperparameter.
  • Format examples with the model's chat template.
  • Clean, deduplicate, and split a dataset correctly.
  • Estimate how much data you actually need.
📋 Before you startThis page builds on: When fine-tuning wins. Jumping in? Skim those first — or open the recap.
30-second recap
  • When fine-tuning wins: Prompting and RAG change what the model sees; fine-tuning changes the model itself — worth it for consistent style, format or narrow skills, not for adding facts.
▶ Runnable companionThe code in this lesson is also saved under code/ft2-data/ in the course, with a README. Run the scripts or copy the configs directly.

Data is the whole game essential

Here's the scene that plays out over and over. A team spends a week arguing about learning rate, rank, and epochs, kicks off a costly GPU run, and the tuned model comes back worse than the base one — more confident, more fluent, and more confidently wrong. They tweak the hyperparameters and run again. Same result. What they never opened was the dataset, where a quarter of the "answers" were truncated, a third were near-duplicates, and a handful taught the model the exact opposite of what they wanted. The knobs were never the problem. The data was.

The oldest law in machine learning is garbage in, garbage out, and fine-tuning obeys it more strictly than almost anything else you'll do. Think of it like teaching an apprentice by example: the apprentice doesn't argue with your demonstrations, doesn't sanity-check them, doesn't know which ones were sloppy. They copy exactly what you show them, warts and all. A fine-tune is that apprentice with a perfect memory and no judgement — every bad example you feed it is a lesson it dutifully learns. Show it ten contradictory answers to the same question and you don't get an average; you get a model that has learned to be inconsistent.

The single biggest predictor of a good fine-tune is data quality, not learning rate or rank. A few hundred clean, consistent, on-distribution examples beat tens of thousands of noisy ones. Most failed fine-tunes are data problems wearing a hyperparameter costume.

The most common mistake is treating data prep as the boring chore you rush through to get to the "real" work of training. It's backwards: the training run is the cheap, automatable part — a script you launch and walk away from — while the dataset is the one thing only you can get right, because only you know what "good" looks like for your task. Every hour you spend reading your own examples and fixing what's wrong buys you more than any hyperparameter sweep. That's why this whole chapter is a pipeline: source it, format it to the model's template, clean and dedup it, then split it so your evaluation stays honest. Get that order right and the model almost tunes itself; get it wrong and no learning rate will save you. The diagram below is the assembly line every serious dataset walks through, in the one order that works.

Raw examples your source Chat template role turns Clean + dedup quality Train / val split no leakage
🗺️ How to read this diagram

This is the assembly line every training dataset goes through, left to right. Each box is a stage, and the arrows show the order — you can't skip ahead.

  • Raw examples (left) is your source: whatever question→answer pairs you collected, in whatever messy shape they arrived.
  • Format (chat template) turns each example into the exact role turns (user says X, assistant says Y) that the model expects — Lab FT2.1 does this.
  • Clean + dedup is the quality stage: throw out broken examples and remove duplicates so the same thing isn't counted twice.
  • Train / val split (right) divides the data into a part to learn from and a held-back part to test on — the no leakage label means those two parts must never share examples.

In short: Data flows one way: raw → format → clean → split. Getting this pipeline right matters more than any training setting — clean data is the whole game.

Format with the chat template essential

This is the section where the most fine-tunes quietly die — and the failure leaves no error message. Your data is clean, your training loss drops beautifully, the run completes, and the model comes back subtly broken: rambling, ignoring instructions, or refusing to stop talking. Nine times out of ten the cause is here, in formatting, because you hand-built the prompt structure and got a character wrong that you never even saw.

To understand why the exact format matters so much, picture how an instruction model actually learned to behave. It wasn't shown plain text — it was shown millions of conversations wrapped in a rigid chat template: invisible special tokens that mark "here begins a user turn," "here begins the assistant's reply," "the turn ends here." Those markers are the model's grammar of conversation. It learned that right after the assistant-start token, it's its turn to speak; it learned to stop at the end token. The template is the punctuation of a language the model speaks fluently — and if you feed it sentences with the wrong punctuation, it gets confused in ways that look like the model being dumb but are really you speaking broken grammar.

The common mistake — and it's seductive. You look at what the template produces, see something like [INST] question [/INST] answer, and think "I can just write that myself with an f-string." Then you get a space wrong, or use the wrong bracket style, or forget the leading beginning-of-sequence token, or use a different model's format entirely. Every one of those is a silent mismatch: the tokens you emit don't line up with the tokens the model was trained on, so it's now learning from examples written in a dialect it half-recognizes. Nothing crashes. The loss still goes down. But you've taught it noise. The analogy: it's like training a translator on sentences where the quotation marks are randomly wrong — they'll learn something, but not the clean signal you meant.

The fix is refreshingly simple: never write the format by hand. Every tokenizer ships with the exact template its model was trained on, and one function call, apply_chat_template, stamps your messages into it perfectly — right tokens, right spacing, right order, every time. You hand it a plain list of role/content turns and it hands back the model's native format. Below, the lab does exactly that; the rule to burn in is that the tokenizer already knows the format, so your job is only to give it clean turns and let it do the stamping.

Instruction models expect a specific structure of role turns. Use the tokenizer's apply_chat_template so your data matches exactly what the model saw in training — a common silent bug is hand-formatting prompts that don't match the template.

your plain turns messages [{role, content}] the one function call apply_chat_template stamps the model's format native format exact special tokens the tempting shortcut hand f-string 1 wrong token → broken Let the tokenizer stamp the format. The same plain [{role, content}] turns become the model's exact native tokens through apply_chat_template (top path); hand-writing the format (bottom path) silently mismatches one token and mistrains the model.
🗺️ How to read this diagram

Read the two paths left-to-right: both start from the same plain turns on the left, but only the top one arrives at a correct format.

  • The left blue box is your input — a plain list of {role, content} turns, no special tokens yet.
  • The top path sends those turns through the purple apply_chat_template box, which stamps in the model's exact special tokens and spacing → the green native format the model was trained on.
  • The bottom red path is the seductive shortcut: hand-writing the format with an f-string. One wrong bracket, space, or missing token lands you in the red box — a silent mismatch that mistrains the model with no error.

In short: Same input, two roads — the tokenizer's road always emits the right tokens; the hand-built road only has to be wrong once to poison the run.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab FT2.1 · Format to the model's template
format.pyfrom transformers import AutoTokenizer
from datasets import load_dataset

tok = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.3")

def to_text(ex):
    msgs = [
        {"role": "user", "content": ex["question"]},
        {"role": "assistant", "content": ex["answer"]},
    ]
    # exactly the format the model was trained on:
    return {"text": tok.apply_chat_template(msgs, tokenize=False)}

ds = load_dataset("json", data_files="raw.jsonl", split="train").map(to_text)
print(ds[0]["text"][:300])
▶ How this works

A model was trained to expect messages in one very specific layout. This script takes your plain question/answer pairs and rewrites them into that exact layout, so the model recognizes them.

  1. AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.3") downloads the tokenizer for a specific model — it knows the precise chat format that model was trained with.
  2. to_text(ex) is a small function run on each example. It builds a msgs list of two turns: a user turn holding ex["question"] and an assistant turn holding ex["answer"].
  3. tok.apply_chat_template(msgs, tokenize=False) is the key line: it stamps those turns into the model's official format and returns it as text (tokenize=False means "give me readable text, not number IDs").
  4. load_dataset(...).map(to_text) loads every row from raw.jsonl and runs to_text on each one, adding the new formatted "text" field.

What the output means: print(ds[0]["text"][:300]) shows the first 300 characters of one formatted example — you'll see the special role markers the model expects wrapped around your Q and A.

Try this: Change [:300] to [:800] to see a whole formatted example. Compare it to your raw question/answer — those special tokens around the text are exactly what hand-formatting usually gets wrong.

Clean, dedup, split intermediate

Formatting made your data the right shape; this step decides whether it's telling the truth. A dataset can be perfectly formatted and still poison a run — full of half-finished answers, the same example copied forty times, or a validation set that secretly overlaps with training. These aren't exotic edge cases; they're what real collected data looks like before anyone cleans it. This section is the quality gate, and skipping it is the difference between an eval score you can trust and one that lies to your face.

Take each job in turn, because each fixes a distinct failure. Cleaning is throwing out the examples that teach the wrong thing — a truncated answer teaches the model to stop mid-sentence; an off-task row teaches it to wander. Remember the apprentice: it copies whatever you show it, so a broken demonstration is a broken lesson, not a harmless one it ignores. Dedup matters because duplicates aren't free — they're a thumb on the scale. If one question appears fifty times, the model treats that pattern as fifty times more important than a question it saw once, quietly skewing what it learns toward whatever your scraper happened to over-collect. Most duplicates in real data aren't identical either; they differ only in casing or stray whitespace, which is why you compare a normalized form, not the raw string.

The most dangerous mistake lives in the split, and it has a name: leakage. You hold back a slice of data to measure how well the model generalizes to things it hasn't seen — that's the entire point of a validation set. But if a training example (or a near-duplicate of one) sneaks into that held-back slice, you're no longer measuring generalization; you're measuring memorization. The model saw the answer during training, so of course it aces the "test." Your eval lights up green, you ship with confidence, and production falls flat because the real world doesn't hand you questions you've already memorized. That's why the order is non-negotiable: dedup first, then split — and split on a fixed seed so the exact same partition happens every run and your results are reproducible. Do it the other way around and a duplicate can land on both sides of the wall, and the leak is invisible until it costs you. The checklist below is that order made concrete; treat it as a sequence, not a menu.

✓ dedup first, then split A A' B C dedup A B C train: A B val: C disjoint — honest eval ✗ split first, then dedup A A' B C train: A B val: A' C A' ≈ A → leakage A' is a near-duplicate of A: seen in training, "tested" in val Order decides whether your eval is honest. Dedup first (top) and the near-duplicate A' is gone before the split, so train and val never share an example; split first (bottom) and A' lands in val while A is in train — the model memorized the answer, so the eval lies.
🗺️ How to read this diagram

Compare the two rows top-to-bottom: both start from the same raw set A A' B C where A' is a near-duplicate of A. Only the order of the steps differs.

  • The left grey box in each row is the raw data — note it contains both A and its near-twin A'.
  • The top (green) path deduplicates first: A' is dropped, leaving A B C, so the train/val split can only put each example on one side — the sets are disjoint and the eval is honest.
  • The bottom (red) path splits first: A falls into train and A' into val. Because A' ≈ A, the model already saw that answer in training — the green "test" score is measuring memorization, not generalization. That is leakage.

In short: Same data, two orders — dedup→split keeps train and val truly separate; split→dedup lets a twin slip across the wall and fakes a good score.

A dataset-prep checklist

  1. Clean: remove truncated, empty, or off-task examples; fix inconsistent formatting.
  2. Dedup: near-duplicate examples inflate size without adding signal (and leak into val).
  3. Balance: if it's classification, check class balance; over-represented classes bias the model.
  4. Split: hold out a validation set BEFORE training — and make sure no example leaks across.
Lab FT2.2 · Dedup and split
prep.pyfrom datasets import load_dataset
import hashlib

ds = load_dataset("json", data_files="raw.jsonl", split="train")

seen, keep = set(), []
for ex in ds:
    h = hashlib.md5(ex["question"].strip().lower().encode()).hexdigest()
    if h not in seen:
        seen.add(h); keep.append(ex)
ds = ds.select([i for i, ex in enumerate(ds)
                if hashlib.md5(ex["question"].strip().lower().encode()).hexdigest() in seen])

split = ds.train_test_split(test_size=0.1, seed=42)   # val holdout, fixed seed
print("train:", len(split["train"]), "val:", len(split["test"]))
▶ How this works

This script does two cleanup jobs that quietly decide whether your evaluation later tells the truth: it removes duplicate examples, then splits the data into a training set and a held-back validation set.

  1. seen is a set of fingerprints and keep collects the unique rows. For each example, hashlib.md5(ex["question"].strip().lower().encode()).hexdigest() makes a short fingerprint of the question (lower-cased and trimmed) so near-identical questions get the same fingerprint.
  2. if h not in seen: keeps a row only the first time its fingerprint appears — that's how duplicates get dropped.
  3. ds.train_test_split(test_size=0.1, seed=42) splits off 10% as the validation ("test") set. seed=42 fixes the randomness so you get the same split every run — that's what makes results reproducible.

What the output means: print("train:", ..., "val:", ...) prints the two sizes, e.g. train: 450 val: 50 — most rows for learning, a small slice held back to test on.

Try this: Change test_size=0.1 to 0.2 and re-run — the val count grows and the train count shrinks. Deduping before this split is what stops the same example landing in both sides and faking a good score.

Validation leakage silently lies to youIf a val example (or a near-duplicate) is also in training, your eval will look great and production will disappoint. Dedup before splitting, and split on a fixed seed so results are reproducible.

How much data? advanced

"How much data do I need?" is the first question everyone asks, and it's almost always the wrong one. The instinct — inherited from the old days of training models from scratch, where more data was nearly always better — is to assume you need tens of thousands of examples and to feel stuck until you have them. For fine-tuning an already-capable model, that instinct will send you collecting for weeks when you could have shipped in an afternoon. The base model already knows the language, the facts, the reasoning; you are not teaching it to think. You're teaching it a style, a format, or a narrow skill — and nudging a model that already knows almost everything takes far less than you'd guess.

Here's the mental model that reframes it. Fine-tuning isn't filling an empty bucket; it's showing a fluent expert how you'd like them to behave in one specific situation. To get a brilliant colleague to always answer support tickets in your house tone, you don't hand them a library — you show them a few dozen good examples and they get it. The same holds here: for style or format tuning, hundreds of clean examples often move the needle decisively; for a genuinely narrow new skill, low thousands. What matters far more than raw count is that the examples are consistent (they don't contradict each other) and on-distribution (they look like the inputs you'll actually see in production). Ten thousand inconsistent examples teach the model to be inconsistent; three hundred coherent ones teach it one clear behavior.

The mistake this section exists to prevent is "we need more data" when the honest diagnosis is "we need cleaner data." When a first run underperforms, adding rows is the tempting reflex — but if a third of your set is duplicates and another third is noise, doubling the pile just doubles the mess. Count only your usable examples (raw count discounted by your duplicate and noise rates) and the gap to "enough" often closes without collecting a single new row. So the discipline is the opposite of the reflex: start small, tune, then measure against a real held-out eval (that's FT6), and let the evals — not your anxiety — tell you whether the bottleneck is quantity or quality. Add data only when the measurement demands it. Below is the rule of thumb, but keep the reframe in front of it: consistency beats volume, and cleaning beats collecting.

For style/format tuning, hundreds of high-quality examples often suffice. For a narrow skill, low thousands. More matters less than consistent and on-distribution. Start small, measure (FT6), and add data only if evals say you need it.

10,000 raw examples raw = 10,000 dups noise usable − dup rate − noise rate ≈ 3,300 usable 400 clean examples raw = 400 nearly all usable low dup, low noise ≈ 360 usable, consistent count what's USABLE, not what's raw — cleaning beats collecting Usable beats raw. A 10,000-row pile, once you subtract duplicates and noise, may leave only ~3,300 truly usable rows — while 400 clean, consistent examples give ~360 that all pull the same way. The honest count is raw × (1 − dup rate) × (1 − noise rate).
🗺️ How to read this diagram

Compare the two columns left-to-right; each shows a pile of data and what survives once you count only the usable rows.

  • The left column is a big raw pile (10,000). The row of small boxes splits it into red dups and noise that teach nothing (or the wrong thing) and a green usable slice — after discounting, only ~3,300 rows actually help.
  • The right column is a small pile (400) that is already clean: low duplicate and noise rates, so ~360 rows are usable and, crucially, consistent — they don't contradict each other.
  • The payoff is the mono line under each: the usable count is what matters. A curated 360 can out-tune a messy 3,300, because consistency and on-distribution examples move the model, not raw volume.

In short: Multiply your raw count by (1 − dup rate) × (1 − noise rate) before you decide you need more — the gap to "enough" often closes by cleaning, not collecting.

Exercise FT2.1 — Build a clean dataset

Context: Nothing teaches data quality like running the whole pipeline on your own examples and eyeballing the output — if a formatted example looks wrong to you, it looks wrong to the model.

Your task: Take 200–500 examples for a task you care about, format them with the chat template, dedup, and split 90/10, then inspect ten random formatted examples by eye.

Requirements:

  • Use 200–500 real examples
  • Format with the model's chat template
  • Dedup, then split 90/10 without leakage
  • Manually inspect ten random formatted examples
  • Fix anything that reads wrong before you'd train on it

💡 Hint: The eyeball pass is the point — run the full format → dedup → split pipeline from the earlier rungs, then read the output like a reviewer.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Shape examples into chat messagesBeginner

Context: Instruction models expect role-tagged turns, and getting that message shape wrong is the single most common silent fine-tuning bug.

Your task: Without any libraries, transform raw question/answer pairs into the [{role, content}] message shape a tokenizer's chat template consumes.

Requirements:

  • Map each {question, answer} to a user turn then an assistant turn
  • Produce a two-element list of {role, content} dicts
  • Keep it pure Python — no libraries
  • Frame this as the pre-template step apply_chat_template consumes

💡 Hint: This is deliberately upstream of the tokenizer — you're producing the message list, not the final templated string.

Show solution

The pre-template step, in plain Python:

raw = [
    {"question": "Reset my password?", "answer": "Go to Settings > Security."},
    {"question": "Enable 2FA?",         "answer": "Same page, toggle 2FA."},
]
def to_messages(ex):
    return [{"role": "user",      "content": ex["question"]},
            {"role": "assistant", "content": ex["answer"]}]

for ex in raw:
    print(to_messages(ex))
# [{'role': 'user', 'content': 'Reset my password?'}, {'role': 'assistant', ...}]

This message list is exactly what tokenizer.apply_chat_template turns into the model's training format. Matching that format is the single most common silent bug to avoid.

Exercise 2 · Dedup exact and near-duplicate examplesIntermediate

Context: Duplicates make the model overweight repeated examples, and most duplicates in scraped data differ only in casing or whitespace.

Your task: Write a deduper that drops exact duplicates and simple near-duplicates (same normalized text) and reports how many were removed.

Requirements:

  • Normalize by lowercasing and collapsing runs of whitespace
  • Key a seen set on the normalized (question, answer) pair
  • Keep the first occurrence of each
  • Report kept-of-total counts
  • Target casing/whitespace variants, the common scraped-data case

💡 Hint: Normalize first, then dedup on the normalized key — the raw text still gets kept, you just compare on a canonical form.

Show solution

Normalize, then key on the normalized form:

import re
def norm(s):
    return re.sub(r"\s+", " ", s.strip().lower())

def dedup(examples):
    seen, kept = set(), []
    for ex in examples:
        key = (norm(ex["question"]), norm(ex["answer"]))
        if key not in seen:
            seen.add(key); kept.append(ex)
    return kept

data = [
    {"question":"Reset password?","answer":"Settings > Security."},
    {"question":"reset password?","answer":"settings > security."},  # near-dup
    {"question":"Enable 2FA?","answer":"Toggle 2FA."},
]
kept = dedup(data)
print(f"kept {len(kept)} of {len(data)} ({len(data)-len(kept)} removed)")  # kept 2 of 3

Casing/whitespace variants are the most common duplicates in scraped data. Removing them stops the model from silently over-training on the same content.

Exercise 3 · Train/val split with no leakageAdvanced

Context: A train/val split must never share examples, or your validation score measures memorization instead of generalization.

Your task: Write a deterministic split that also guards against leakage: an example whose question already appears in train cannot land in val.

Requirements:

  • Assign each example deterministically by hashing its text into a 0–99 bucket
  • Compare the bucket to val_frac so runs are reproducible
  • Track seen questions and demote a would-be-val example to train if its question already appeared
  • Verify the final train/val overlap is empty

💡 Hint: Hashing gives reproducibility; the extra seen-question guard is what turns a random split into a leakage-free one.

Show solution

Deterministic hashing split plus a leakage guard:

import hashlib
def bucket(text, val_frac=0.2):
    h = int(hashlib.md5(text.encode()).hexdigest(), 16)
    return "val" if (h % 100) < val_frac*100 else "train"

def split(examples, val_frac=0.2):
    train, val, seen_q = [], [], set()
    for ex in examples:
        q = ex["question"].strip().lower()
        where = bucket(q, val_frac)
        if where == "val" and q in seen_q:   # would leak -> keep in train
            where = "train"
        (val if where == "val" else train).append(ex)
        seen_q.add(q)
    return train, val

data = [{"question":f"q{i}","answer":f"a{i}"} for i in range(10)]
tr, va = split(data)
print(f"train={len(tr)} val={len(va)}  overlap={set(e['question'] for e in tr) & set(e['question'] for e in va)}")
# overlap=set()  -- no leakage

Hashing makes the split reproducible across runs; the guard ensures a val example never duplicates a train question, so your val score reflects generalization, not memorization.

Exercise 4 · Estimate how much data you needExpert

Context: A few hundred clean examples often beat tens of thousands of noisy ones, so “we need more data” is frequently really “we need cleaner data”.

Your task: Write an estimator that recommends a dataset size from task narrowness and desired quality, and flags when the real fix is cleaning rather than collecting.

Requirements:

  • Pick a base target by narrowness (very narrow < narrow < broad)
  • Compute usable count as raw × (1 − dup rate) × (1 − noise rate)
  • Return ENOUGH-but-clean vs collect-to-target
  • Separate usable examples from raw counts to push cleaning first

💡 Hint: Discount the raw count by the dup and noise rates before comparing to the target — often the gap closes once you count only usable rows.

Show solution

A rule-of-thumb estimator that favors quality:

def recommend_size(task_narrowness, current_n, current_dup_rate, current_noise_rate):
    base = {"very narrow": 300, "narrow": 800, "broad": 3000}[task_narrowness]
    clean_n = current_n * (1 - current_dup_rate) * (1 - current_noise_rate)
    if clean_n >= base:
        return f"ENOUGH clean examples (~{int(clean_n)} of {current_n}); CLEAN, don't collect"
    return f"collect to ~{base} clean; you have ~{int(clean_n)} usable now"

print(recommend_size("narrow", current_n=5000, current_dup_rate=0.3, current_noise_rate=0.4))
# ENOUGH clean examples (~2100 of 5000); CLEAN, don't collect
print(recommend_size("narrow", current_n=400, current_dup_rate=0.05, current_noise_rate=0.05))
# collect to ~800 clean; you have ~361 usable now

Often you already have enough raw rows — the fix is dedup + noise removal, not more collection. The estimator separates "usable" from "raw" so you clean before you scrape.

Exercise 5 · Format with the real chat template (real, needs libs)Professional

Context: The format step must match exactly what the model saw in pretraining, which is why you never hand-write special tokens — you call the model's own chat template.

Your task: Show the lesson's real format step using transformers/datasets so it matches the model's template exactly, labelling it as needing the libraries.

Requirements:

  • Load the tokenizer with AutoTokenizer.from_pretrained
  • Build user/assistant messages and format via apply_chat_template(msgs, tokenize=False)
  • Load JSONL with load_dataset and .map the formatter
  • Never hand-format special tokens
  • Note the code needs pip install transformers datasets

💡 Hint: The tokenizer already carries the model's template — let apply_chat_template emit the tokens so you can't get them wrong.

Show solution

The real formatting pass — needs pip install transformers datasets:

from transformers import AutoTokenizer
from datasets import load_dataset

tok = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.3")

def to_text(ex):
    msgs = [{"role": "user",      "content": ex["question"]},
            {"role": "assistant", "content": ex["answer"]}]
    # apply_chat_template emits EXACTLY the format the model was trained on:
    return {"text": tok.apply_chat_template(msgs, tokenize=False)}

ds = load_dataset("json", data_files="raw.jsonl", split="train").map(to_text)
print(ds[0]["text"][:300])

Never hand-format the special tokens — apply_chat_template uses the model's own template, which is the difference between a working tune and a silently mistrained one.

Exercise 6 · Audit a dataset before an expensive runIndustry scenario

Context: Most failed fine-tunes are data problems in disguise, so a cheap pre-flight audit pays for itself before you commit any GPU hours.

Your task: Before an expensive run, write a data audit that reports count, exact-dup rate, empty/short answers, and label imbalance, then gates the run on quality thresholds and prints PASS/FAIL with reasons.

Requirements:

  • Compute example count and dup rate from a normalized (question, answer) set
  • Count short answers (e.g. fewer than two words)
  • Assemble a report dict of the rates
  • Gate: FAIL on high dup rate, high short-answer rate, or too few examples
  • Print PASS/FAIL joined with the specific failing reasons

💡 Hint: Each threshold breach becomes a reason string; PASS is simply the case where the reason list came back empty.

Show solution

A pre-flight audit that gates the training run:

import re
def audit(examples):
    n = len(examples)
    norm = lambda s: re.sub(r"\s+"," ",s.strip().lower())
    dups = n - len({(norm(e['question']), norm(e['answer'])) for e in examples})
    short = sum(1 for e in examples if len(e['answer'].split()) < 2)
    report = {"n": n, "dup_rate": round(dups/n,2), "short_rate": round(short/n,2)}
    problems = []
    if report["dup_rate"] > 0.10: problems.append("too many duplicates")
    if report["short_rate"] > 0.05: problems.append("too many empty/short answers")
    if n < 200: problems.append("dataset likely too small")
    report["gate"] = "PASS" if not problems else "FAIL: " + "; ".join(problems)
    return report

data = [{"question":f"q{i%40}","answer":"ok"} for i in range(300)]
print(audit(data))  # high dup_rate + short answers -> FAIL

Most failed fine-tunes are data problems in disguise. A cheap audit that blocks the run on dup/short/size thresholds saves the GPU hours a bad dataset would waste.

✓ Checkpoint — you can move on when you can…

  • Explain why data quality outweighs hyperparameters.
  • Format examples with apply_chat_template.
  • Clean, dedup, and split without leakage.
  • Estimate the data volume a task needs.

Knowledge check check yourself

✓ Knowledge check

Why must you format training examples with the tokenizer's apply_chat_template rather than hand-writing the prompt structure?

Show answer
Instruction models expect the exact role-turn structure and special tokens they were trained on. apply_chat_template reproduces that format precisely; hand-formatting is a common silent bug because the mismatched tokens don't match what the model saw in training.
✓ Knowledge check

Why must deduplication happen before the train/validation split, and what goes wrong if it doesn't?

Show answer
If a near-duplicate lands in both the training and validation sets, that is leakage: the eval will look great because the model effectively saw the val examples in training, then production disappoints. Deduping first (and splitting on a fixed seed) prevents the leak and keeps results honest and reproducible.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in