← All posts

June 11, 2026

How to Clean a Dataset for Fine-Tuning: What Bad Data Actually Looks Like

Bad training data does not always look broken. It can pass a syntax check, load without errors, and produce a loss curve that looks fine while quietly training your model to behave in ways you did not intend. Here is what bad data actually looks like and how to find it before it costs you a training run.

TL;DR

Data quality is the single biggest variable in fine-tuning outcomes. A great config and the right method cannot save a model trained on bad data. But bad data is not always obvious. It passes syntax checks. It loads without errors. Sometimes the loss curve even looks fine. This post covers what bad fine-tuning data actually looks like across six categories, how to find each type systematically without manually reviewing thousands of examples, and the cleaning steps that have the highest impact on final model quality.

Here is a situation that is more common than people admit.

The training run completes cleanly. The loss curve looks reasonable. You load the model and test it. And the outputs are just slightly off. Not wrong enough to immediately diagnose. Wrong enough to know the model is not doing what you wanted.

You check the config. Looks fine. You check the training setup. Looks fine. And then eventually, after enough debugging, you look more carefully at the data.

And there it is.

Bad data does not always announce itself. That is what makes it the hardest fine-tuning problem to solve. Here is how to find it before it costs you another training run.

What Bad Data Actually Looks Like

Bad training data falls into six categories. Each one affects model behavior in a different way. Knowing which type you have tells you how to fix it.

Category One: Inconsistent Output Style

This is the most common quality problem and the easiest to miss.

Your dataset has 2,000 examples. 1,400 of them have outputs that are two to three sentences. 600 of them have outputs that are one sentence. Another 200 have outputs that are full paragraphs.

None of these are wrong individually. But together they teach the model that any output length is acceptable for this task. The result is a model that is unpredictable in response length in ways that have nothing to do with the input.

How to find it:

import json

lengths = []
with open("your_data.jsonl") as f:
for line in f:
example = json.loads(line)
lengths.append(len(example["output"].split()))

import statistics
print(f"Mean: {statistics.mean(lengths):.0f} words")
print(f"Std dev: {statistics.stdev(lengths):.0f} words")
print(f"Min: {min(lengths)} | Max: {max(lengths)}")

A standard deviation that is more than half your mean is a signal of high inconsistency. Look at the examples at both extremes. Decide what the right output style is and filter or rewrite examples that fall outside it.

Category Two: Incorrect or Low-Quality Output Content

Your outputs are the right length and style. But some of them are just wrong. Incorrect facts, incomplete answers, hedging language that your model should not produce, or responses that technically answer the question but not in the way you want.

This is the hardest category to catch at scale because it requires understanding the content, not just the format.

How to find it:

Do not try to review every example. Sample strategically.

Take 50 random examples and review them manually. You are looking for patterns, not individual errors. If 10 of your 50 samples have a specific problem, that problem likely affects 20 percent of your dataset.

If you generated your dataset using a model like GPT-4 or Claude, look specifically for:

  • Hedging phrases like "I'm not entirely sure" or "It depends on the context"
  • Refusal patterns on edge cases the generating model was cautious about
  • Formatting inconsistencies between batches generated at different times

Any of these become patterns your fine-tuned model will reproduce.

How to fix it:

Write a filter that catches known bad patterns:

bad_patterns = [
"I'm not sure",
"it depends",
"as an AI",
"I cannot",
"I don't have access to"
]

clean_examples = []
with open("your_data.jsonl") as f:
for line in f:
example = json.loads(line)
output = example["output"].lower()
if not any(pattern in output for pattern in bad_patterns):
clean_examples.append(example)

print(f"Kept {len(clean_examples)} examples after filtering")

Adjust the bad_patterns list to match what you find in your data.

Category Three: Duplicates

Duplicate examples cause the model to overfit to those specific inputs. They also inflate your dataset size, making you think you have more coverage than you do.

Exact duplicates are easy to catch. Near-duplicates are harder but often more impactful, because they represent cases where slightly different phrasing maps to the same underlying input, which the model over-learns.

How to find and remove exact duplicates:

import json
import hashlib

seen = set()
unique_examples = []

with open("your_data.jsonl") as f:
for line in f:
example = json.loads(line)
# hash on both input and output together
key = hashlib.md5(
(example.get("instruction", "") + example.get("output", "")).encode()
).hexdigest()
if key not in seen:
seen.add(key)
unique_examples.append(example)

print(f"Removed {total - len(unique_examples)} duplicates")

For near-duplicate detection, the simplest approach at small dataset sizes is to sort your examples by output and review consecutive pairs manually. At larger scales, use a library like datasketch for MinHash-based approximate deduplication.

Category Four: Length Outliers

Examples that are dramatically longer or shorter than the rest of your dataset cause two problems.

Very short examples often represent incomplete outputs that do not demonstrate the behavior you want. A two-word output in a dataset of 100-word outputs teaches the model nothing useful.

Very long examples dominate gradient updates disproportionately and can cause training instability. They also consume a lot of sequence length, which reduces your effective batch size.

How to find and remove them:

import json

examples = []
with open("your_data.jsonl") as f:
for line in f:
example = json.loads(line)
output_length = len(example["output"].split())
examples.append((output_length, example))

lengths = [e[0] for e in examples]
mean = sum(lengths) / len(lengths)
std = (sum((x - mean) ** 2 for x in lengths) / len(lengths)) ** 0.5

# Keep examples within 2 standard deviations of the mean
filtered = [e[1] for e in examples if abs(e[0] - mean) <= 2 * std]
print(f"Removed {len(examples) - len(filtered)} length outliers")

Two standard deviations is a reasonable starting threshold. Adjust based on what you see in your data.

Category Five: Train and Validation Set Overlap

This is the one that silently ruins your evaluation metrics.

If any of your validation examples appear in your training set, your model will look like it performs better on validation than it actually does. You will see great eval metrics, ship the model, and discover the performance gap in production.

How to find it:

import json
import hashlib

def hash_example(example):
return hashlib.md5(
(example.get("instruction", "") + example.get("output", "")).encode()
).hexdigest()

train_hashes = set()
with open("train.jsonl") as f:
for line in f:
train_hashes.add(hash_example(json.loads(line)))

overlap = 0
with open("val.jsonl") as f:
for line in f:
if hash_example(json.loads(line)) in train_hashes:
overlap += 1

print(f"Overlap: {overlap} examples")

If overlap is greater than zero, regenerate your split. Always split before deduplication, not after. Deduplicating first and then splitting can create hidden overlap.

Category Six: Format and Encoding Issues

These are the ones that cause silent training failures. The JSONL loads fine. The tokenizer processes it. But something about the encoding or format produces training signal that is different from what you intended.

Common culprits:

  • Unicode characters that tokenize differently than ASCII equivalents
  • HTML entities that were not decoded before going into the dataset
  • Trailing whitespace in output fields that adds tokenization noise
  • Mixed newline styles between examples on different operating systems

Quick check:

python

import json

with open("your_data.jsonl") as f:

for i, line in enumerate(f):

example = json.loads(line)

output = example.get("output", "")

if output != output.strip():

print(f"Line {i}: leading or trailing whitespace in output")

if "\r" in output: print(f"Line {i}: Windows line endings in output")

if "&amp;" in output or "&lt;" in output: print(f"Line {i}: unescaped HTML entities")

Fix these with a simple preprocessing pass before training:

import html

def clean_example(example):

for key in ["instruction", "input", "output"]:

if key in example:

example[key] = html.unescape(example[key].strip().replace("\r\n", "\n"))

return example

The Pre-Training Cleaning Checklist

Run these six checks on every dataset before training. In order.

One: validate JSONL syntax. Every line parses without errors.

Two: check output length distribution. Flag and review outliers.

Three: remove exact duplicates on instruction plus output hash.

Four: sample 50 random examples and manually review for content quality and style consistency.

Five: filter known bad output patterns specific to how your data was generated.

Six: check for train and validation set overlap. Fix the split if any overlap exists.

This takes 30 to 60 minutes on a dataset of a few thousand examples. It is the highest-leverage 30 minutes you can spend before a training run because it eliminates the most expensive failure mode: a run that completes and produces a model that almost works but never quite does.