Evaluating Fine-Tuned Models
Measure whether fine-tuning helped: held-out test sets, task metrics, LLM-as-judge, regression checks and catastrophic forgetting.
Did fine-tuning actually help?
A lower training loss doesn't mean a better model. A fine-tuned model can sound more confident while being less accurate, follow the format but lose general ability, or simply memorise the training set. Evaluation is how you prove the fine-tune is worth deploying.
Always compare against a baseline
Evaluate at least two systems on the same test set:
- Baseline — the base model with your best prompt (and RAG if you use it).
- Fine-tuned model — with the production system prompt.
If the fine-tuned model isn't clearly better on the metrics you care about, don't ship it.
The test set
- Use the held-out test split the model never saw during training or tuning.
- Include easy, hard and edge cases, plus inputs the model should refuse or ask about.
- Make it representative of real traffic — if 30% of real questions are about fees, roughly 30% of the test set should be too.
- 100–300 well-chosen examples is enough to start.
Choose metrics that match the task
| Task type | Good metrics |
|---|---|
| Classification / routing | Accuracy, precision, recall, F1, confusion matrix |
| Extraction to JSON | Valid-JSON rate, field-level exact match |
| Fixed-format output | Format compliance rate (checked in code) |
| Open-ended answers | LLM-as-judge with a rubric, human ratings |
| Style / tone | Pairwise preference (which answer is better?) |
Automatic checks in code are the most reliable — use them wherever the output has a checkable structure.
Example: scoring a classification fine-tune
from collections import Counter
def evaluate(predict, test_rows):
"""predict: function(text) -> label; test_rows: list of {"text": ..., "label": ...}"""
correct, confusion = 0, Counter()
for row in test_rows:
pred = predict(row["text"]).strip().lower()
gold = row["label"].lower()
correct += pred == gold
confusion[(gold, pred)] += 1
print(f"accuracy: {correct / len(test_rows):.1%}")
for (gold, pred), n in confusion.most_common():
if gold != pred:
print(f" {gold!r} predicted as {pred!r}: {n}")
test_rows = [
{"text": "How much is the semester fee?", "label": "fees"},
{"text": "My portal password isn't working", "label": "technical"},
{"text": "Can I change my programme after admission?", "label": "admissions"},
]
def baseline_predict(text): # replace with a call to your model
return "fees" if "fee" in text else "admissions"
evaluate(baseline_predict, test_rows)
Output for this toy baseline:
accuracy: 66.7%
'technical' predicted as 'admissions': 1
Run the same function for the base model and the fine-tuned model, and compare. The confusion list tells you exactly which categories need more training examples.
LLM-as-judge for open-ended outputs
For answers without one correct string, use a strong LLM as a judge with a clear rubric:
You are grading an admissions assistant's answer.
Question: {question}
Reference facts: {reference}
Answer to grade: {answer}
Score each from 1 to 5:
- Correctness: matches the reference facts, no invented details
- Completeness: answers everything that was asked
- Format: 2-3 sentences, polite tone
Return JSON: {"correctness": n, "completeness": n, "format": n, "reason": "..."}
Good practice: hide which model produced which answer, randomise the order in pairwise comparisons, and spot-check the judge against human ratings on a sample.
Check for regressions and forgetting
Fine-tuning can damage abilities you didn't train on — catastrophic forgetting. Keep a small regression set of general tasks (simple maths, summarisation, instruction following, refusing harmful requests) and confirm scores didn't drop.
Watch the training curves
- Training loss falling, validation loss falling → still learning.
- Training loss falling, validation loss rising → overfitting; use an earlier checkpoint or fewer epochs.
- Both flat → learning rate too low, or the data isn't teaching anything new.
Before you ship: a checklist
- Beats the baseline on the main metric by a meaningful margin.
- No significant drop on the regression set.
- Format compliance is near 100% where the format matters.
- Safety behaviour (refusals) is unchanged or better.
- Latency and cost are acceptable.
- Results are saved with the dataset version and training settings, so they're reproducible.
Common mistakes
- Evaluating on training data. It measures memorisation, not skill.
- Judging by a handful of examples. Ten nice-looking answers prove nothing.
- Using only BLEU/ROUGE for chat answers. Word overlap metrics miss correctness and helpfulness.
- No baseline. Without it you can't tell whether the fine-tune helped.
Key takeaways
- Compare fine-tuned and baseline models on the same held-out test set.
- Prefer automatic, task-specific checks; use LLM-as-judge with a rubric for open-ended answers.
- Test for forgetting and safety regressions, not just the target task.
- Ship only when the improvement is clear, measured and reproducible.