Datasets, Labels, and Instruction Format

Fine-tuning quality is mostly dataset quality. A mediocre base model with clean, consistent examples often beats a strong model trained on noisy Slack dumps. This lesson covers how engineers structure labels and instruction formats so training teaches the behavior you intend.

Intuition

Supervised fine-tuning (SFT) teaches: "given this instruction (and context), produce that completion." Every row is a tiny contract:

If half your rows say "return bare JSON" and half wrap answers in prose, the model will randomly pick a style. Labels are not just "correct answers"—they are the style guide baked into weights.

How it works

Common record shapes

Instruction / response (Alpaca-style):

instruction: Summarize the ticket in one sentence.
input: User cannot reset MFA after phone change.
output: User blocked on MFA reset after updating their phone number.

Chat messages (preferred for chat models):

{
  "messages": [
    {"role": "system", "content": "You are a support triage bot. Reply in JSON."},
    {"role": "user", "content": "I can't log in after the MFA change."},
    {"role": "assistant", "content": "{\"intent\":\"auth\",\"priority\":\"P2\"}"}
  ]
}

Chat format matters because production inference uses the same chat template (special tokens for roles). Train the way you serve.

What "label" means here

For causal LMs, the usual SFT loss is next-token prediction on the assistant tokens. You often mask system/user tokens so the loss focuses on what the assistant should say. Conceptually:

loss = -mean(log p(token_t | tokens_<t))  for t in assistant_span

Bad labels (wrong JSON, leaked PII, contradictory policies) pull probability mass toward those mistakes.

Quality bar before training

Check Why
Schema validity Invalid JSON teaches invalid JSON
Single style guide Mixed tone/format increases entropy
Deduplication Near-duplicates inflate overfit
PII / secrets scrub Models can memorize and regurgitate
Train / eval split by user or ticket Random split leaks near-duplicates
Difficulty mix Only easy rows → brittle on live mess

Aim for consistency over volume early. A few hundred excellent rows beat ten thousand noisy ones for many format/style tasks. Scale data once the format is locked.

Mixing sources

In code

Validate and lightly normalize JSONL before any trainer sees it.

import json
from pathlib import Path


REQUIRED_ROLES = {"system", "user", "assistant"}


def load_jsonl(path: str) -> list[dict]:
    rows = []
    for line in Path(path).read_text().splitlines():
        if line.strip():
            rows.append(json.loads(line))
    return rows


def validate_chat_row(row: dict) -> list[str]:
    errs = []
    msgs = row.get("messages")
    if not isinstance(msgs, list) or len(msgs) < 2:
        return ["messages must be a list with >= 2 turns"]
    roles = [m.get("role") for m in msgs]
    if roles[-1] != "assistant":
        errs.append("last message must be assistant (the label)")
    for m in msgs:
        if m.get("role") not in REQUIRED_ROLES:
            errs.append(f"bad role: {m.get('role')}")
        if not str(m.get("content", "")).strip():
            errs.append("empty content")
    # Example task rule: assistant must be JSON object
    try:
        json.loads(msgs[-1]["content"])
    except Exception:
        errs.append("assistant content is not valid JSON")
    return errs


# Illustrative rows in memory (same shape as a .jsonl file)
demo = [
    {
        "messages": [
            {"role": "system", "content": "Reply with JSON only."},
            {"role": "user", "content": "Password reset loop"},
            {"role": "assistant", "content": '{"intent":"auth","priority":"P2"}'},
        ]
    },
    {
        "messages": [
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi there!"},  # fails JSON rule
        ]
    },
]

for i, row in enumerate(demo):
    print(i, validate_chat_row(row) or "ok")

HuggingFace-style packaging (pseudocode):

# from datasets import Dataset
# ds = Dataset.from_list(clean_rows)
# def to_text(example, tokenizer):
#     return tokenizer.apply_chat_template(
#         example["messages"], tokenize=False, add_generation_prompt=False
#     )
# texts = [to_text(r, tokenizer) for r in ds]

What goes wrong

One-line summary

Structure SFT data as consistent instruction or chat turns, put the quality bar on assistant labels, and train with the same template you will use at serving time.

Key terms