Fine-tune a small open model for a narrow task, the right way: baseline first, clean data, QLoRA with TRL, honest evaluation, merge, quantize, and ship with a model card.
Intro
Capstone 3: Fine-Tune a Model
Most fine-tuning projects fail before the first training step. Not because of the GPU, or the learning rate, but because nobody wrote down what success means. Today we build a complete fine-tune from start to finish, and we do it in a way that you can trust the result.
- End-to-end pipeline
- Honest results only
Putting the Lessons Together
This is episode thirty-eight. Last time we built a coding assistant. Now we take what we learned in the fine-tuning lessons, supervised fine-tuning, LoRA, QLoRA and DPO, and put it all into one project with runnable scripts.
- SFT, LoRA, QLoRA, DPO
- One project
The Task: Email to JSON
Our project is structured extraction. We take messy support emails and produce a strict JSON object with fields like product, issue category, urgency and order number. The same recipe works for domain style or any other narrow task. One important rule: every number you see in your results must come from your own run, so I will show you how to measure, not quote scores.
- Messy text in
- Strict JSON out
Define Task and Metric
Should We Fine-Tune?
Start with the question: should you fine-tune at all? From the earlier lesson on fine-tune versus RAG versus prompt, fine-tuning shines for stable formats, consistent style, and cheaper small models. It is a poor choice for injecting fast-changing facts. Extraction into a fixed schema is a textbook fit.
- Format and style: yes
- Fresh facts: use RAG
Pick Metrics First
Next, pick metrics before you touch a model. For extraction I use three. First, valid JSON rate: does the output parse and match the schema. Second, field-level exact match: for each field, is the value equal to the gold value. Third, a full-record match, where every field must be right. Valid rate is a floor, field match shows partial skill, and record match is the strict number.
- Valid JSON rate
- Field exact match
- Full record match
Field vs Record Match
Here is a worked example with small numbers. Say the test set has ten records with four fields each, so forty fields. The model gets thirty-two fields right: field match is thirty-two over forty, which is zero point eight. If only three records have all four fields right, record match is three over ten, zero point three. Same model, two very different numbers, which is why you report both.
- 32 of 40 fields = 0.8
- 3 of 10 records = 0.3
Baseline with Prompting
Always Get a Baseline
Now the step people skip: the baseline. Before training anything, run the small base model with a good prompt, a schema description and a few examples. If you do not have this number, you can never say your fine-tune helped. This is mistake number one: no baseline.
- Prompt first
- No baseline, no claim
The Baseline Script
Here is the shape of the baseline script. It loads the model with the transformers pipeline, builds a prompt with the schema and three examples, runs every test email, and tries to parse the output as JSON. It then calls a scoring function that returns valid rate, field match and record match. Save the raw outputs to disk so you can read the failures later.
- Schema plus few-shot
- Parse, score, save outputs
Read the Failures
Then read the failures, do not only look at the score. Typical small-model errors are extra chatty text around the JSON, invented fields, wrong enum values like urgent versus high, and numbers reformatted. These patterns tell you what the training data must teach, and they also suggest whether a better prompt alone could fix things.
- Chatty wrapper text
- Wrong enum values
- Invented fields
Build the Dataset
Data Quality Wins
Data is where fine-tunes are won. Quality beats quantity: a few thousand clean, consistent examples often beat a huge noisy pile. Start from whatever real labelled emails you are allowed to use, and check privacy: remove names, addresses and anything personal before it goes anywhere near a training file.
- Clean beats big
- Scrub personal data
Synthetic Augmentation
If you have too few examples, add synthetic ones. Use a strong model to write new emails from a seed: give it a target JSON and ask for a realistic email that implies exactly those values. Generating the text from the label is safer than labelling generated text, because the label is correct by construction. Then still validate every record against the schema and spot-check a sample by hand.
- Label first, text second
- Validate every record
Mix Your Sources
Synthetic data has a trap. If all your emails come from one generator prompt, they share a voice, and the model may learn the voice instead of the task. Mix sources: real examples, several prompt styles, different lengths, typos, and also hard negatives such as emails with missing fields where the right answer is null. Keep real data in your validation and test sets so you measure the real world.
- Several styles
- Hard negatives with nulls
- Real data for testing
Deduplicate
Now deduplicate. Exact duplicates are easy: hash the normalized text. Near duplicates need more care, like the same email with a changed order number. A simple approach is to compute a shingle set or an embedding for each email and drop pairs above a similarity threshold. Choose the threshold by reading examples near it, not by guessing.
- Exact hash first
- Near-duplicates by similarity
Train, Val, Test Splits
Splits come next, and the order matters. Deduplicate first, then split, so that near copies cannot land on both sides. A common split is eighty percent train, ten percent validation, ten percent test. If emails come in threads or from the same customer, split by group, not by row, otherwise the model sees the same customer in training and test.
- Dedup before splitting
- Split by group
- 80, 10, 10 as a start
Check for Leakage
Mistake number two: the contaminated test set. Run an explicit leakage check. Compare every test email to every training email by exact match, by near-duplicate similarity, and by shared order numbers or identifiers. Any hit means remove it from train, not from test. Also check that synthetic examples were not seeded from test labels. A leaked test set gives a beautiful score that means nothing.
- Exact, near, shared IDs
- Remove hits from train
Chat Format JSONL
Finally, format the data as chat messages. Each record has a system message with the schema instruction, a user message with the email, and an assistant message containing only the JSON. Save it as JSON lines. Use the same system prompt at training and at inference, because a mismatch there quietly costs accuracy.
- System, user, assistant
- Assistant says only JSON
QLoRA Training with TRL
QLoRA Recap
Time to train. Recall QLoRA from the earlier lesson: the base model is loaded in four-bit precision and frozen, and we train small low-rank adapter matrices in higher precision. That lets a model of a few billion parameters train on a single consumer or free-tier GPU. We use the TRL library, which wraps this in an SFT trainer.
- Frozen 4-bit base
- Trainable low-rank adapters
The Three Configs
The setup has three parts. A BitsAndBytes config sets four-bit NF4 quantization with bfloat16 compute. A LoRA config sets the rank, say sixteen, alpha, dropout, and the target modules, usually the attention and MLP projections. An SFT config sets batch size, gradient accumulation, learning rate and epochs. Pass these with the model and dataset to the trainer and call train.
- Quantization config
- LoRA config
- SFT config
Practical Settings
Some practical choices. A learning rate around one to two times ten to the minus four is a common starting point for LoRA. Use gradient accumulation to reach an effective batch size you can afford: batch four with accumulation eight is an effective batch of thirty-two. Train on the assistant tokens only if your setup supports it, so the loss focuses on the JSON and not on repeating the email.
- Batch 4 times 8 equals 32
- Mask the prompt in the loss
Spot Overfitting
Watch two curves: training loss and validation loss. If both fall and flatten, good. If training loss keeps dropping while validation loss turns upward, that is overfitting, mistake number three. The model is memorizing your examples. The fixes are fewer epochs, a lower rank or learning rate, more varied data, or early stopping at the best validation checkpoint.
- Train down, val up
- Stop at best checkpoint
Measure Real Metrics
Loss is only a proxy. After each run, generate on the validation set and compute your real metrics: valid rate, field match, record match. Use greedy decoding, temperature zero, for extraction, since you want determinism. Keep a small log of each run with its settings and scores, so changes are comparable and you do not fool yourself with memory.
- Greedy decoding
- Log every run
Optional DPO Pass
Optional DPO Pass
An optional step is a DPO pass, from our earlier lesson on preference optimization without reinforcement learning. When is it worth it? When SFT leaves a specific bad habit, like occasionally adding prose or choosing a wrong enum. You build pairs: a chosen output that is correct, and a rejected output that is a real failure from your own model.
- Chosen vs rejected pairs
- Fix specific bad habits
Mining Preference Pairs
Mining rejected examples is simple here. Sample from the SFT model on training prompts, score each output with your metric, and keep the failures as rejected, paired with the gold answer as chosen. Run TRL's DPO trainer with a small learning rate and a small beta, then re-evaluate. Keep it only if the held-out metrics improve. If they do not, skip it, and that is a fine result.
- Failures become rejected
- Keep only if it helps
Evaluate Honestly
Compare All Systems
Now the moment of truth, and only now do we touch the test set. Run the same evaluation script on four systems: the base model with a prompt, the same prompt with retrieved examples as a RAG-style variant, the SFT model, and the SFT plus DPO model if you made one. Same inputs, same scoring, same decoding settings.
- Same test, same scorer
- Prompt, RAG, SFT, DPO
Report Your Own Numbers
Report the table from your own run and nothing else. Include the metrics, the number of test examples, and the cost: latency per request and model size. A fine-tuned small model might match a bigger prompted one at a fraction of the cost, or it might not. Either is a real finding. With a small test set, remember that differences of a point or two can be noise.
- Metrics plus cost
- Small test sets are noisy
Error Analysis Loop
Then do error analysis. Group the remaining mistakes by field and by type. If failures cluster on one field, add targeted data for it. If the model fails on long emails, check the max sequence length you trained with. Each round of analysis, data fix and retrain is how real projects improve, and it is usually the data, not the hyperparameters.
- Cluster the failures
- Fix data first
Merge, Quantize, Ship
Merge the Adapters
Once you are happy, merge the adapters. The merge computes the new weight as the original weight plus the scaled product of B and A, then saves a single standalone model. Load the base model in half precision, not four-bit, for the merge, then attach the adapter and call merge and unload. Merging in four-bit gives lossy results.
- Merge into half-precision base
- Single standalone model
Quantize, Then Re-Evaluate
Merging removes adapter overhead at inference, but the model is still large. Quantize it for serving, as in our quantization lesson: for example GPTQ or AWQ for GPU servers, or a GGUF four-bit file for local runtimes. After quantizing, run the evaluation script again. Quantization can cost accuracy, and you only know by measuring on your own test set.
- GPTQ, AWQ or GGUF
- Measure the quality cost
Ship With a Model Card
Finally, save artifacts. Push the adapter, the merged model and the tokenizer to the Hub or to your own storage. Write a model card that states the task, base model and its licence, training data sources and size, the metrics with the test set description, known limitations, and the exact prompt format. Check the licence of your data and base model before you publish, and keep the repo private if you are unsure.
- Adapter, merged, quantized
- Model card with limits
Common Mistakes
The Five Big Mistakes
Let us collect the mistakes. No baseline, so you cannot prove improvement. A contaminated test set, so the score is fiction. Overfitting, so the model shines on training data and fails in the wild. Add to those a prompt-format mismatch between training and inference, and tuning on the test set by running it after every change.
- No baseline
- Contaminated test
- Overfitting
- Format mismatch
Lock the Test Set
A related habit is to keep the test set locked. Use validation for decisions and touch test once per major comparison. If you peek and tune on it repeatedly, it quietly becomes a second validation set and you have no honest estimate left. If it happens, collect a fresh test set from new data.
- Decide on validation
- Open test once
Recap and Next
Recap the Pipeline
Let us recap the pipeline. Define the task and metrics. Baseline with prompting. Build a clean dataset with synthetic augmentation, dedup, group-aware splits and a leakage check. Train with QLoRA and TRL, watch validation loss. Optionally apply DPO. Evaluate against the baselines on the locked test set, merge, quantize, re-evaluate, and publish with a model card.
- Task, baseline, data
- Train, evaluate, ship
Fine-Tuning Is a Loop
The big idea is that fine-tuning is an engineering loop, not a single run. Your edge is not a magic hyperparameter but a clear metric, clean data and honest evaluation. Do those, and a small model you own can do one job very well, cheaply and privately.
- Metric, data, honesty
- Small model, one job
Next: The Deployed Agent
In the next and final capstone, we deploy a tool-using agent: a loop that calls functions, remembers context and runs behind an API, and we can plug in the model you just trained.
- Tool-using agent
- Subscribe for more