The data was correct and still wrong
Here's a kind of bug that doesn't announce itself: the pipeline runs, the files are the right size, the token counts look plausible, the loss goes down. Everything is green. And the data is quietly wrong.
I hit one today. It's a good example, so here it is in full.
What I saw
I was building a training corpus for a small language model by scraping documentation sites and turning HTML into plain text. About an hour and a half into a test run I sampled the first saved checkpoint, just to see it produce something. Prompt: Linux is. Output, roughly:
Linux is
Linux BIOS kernel is installed (
boot loader
is done without
GRUB
.
The words are plausible. The shape is wrong: boot loader, is done without, GRUB and . are each on their own line. A sentence had been shredded into fragments.
Why nothing had flagged it
My HTML-to-text function called soup.get_text("\n"). That inserts a newline between every string in the document, and in HTML almost every inline element (<a>, <code>, <b>) is its own string. So a sentence like
<p>Linux is <a href="#">installed</a> with <code>GRUB</code>, then run <b>this</b>.</p>
became one fragment per line. The output was valid UTF-8, the right size, tokenized without error, and it did reduce the loss. A model can learn a shredded format perfectly well. It just learns the shredded format.
No metric could have caught this, because none of them measure "does this look like a human wrote it." Only a human, or a model that had learned what a human's text looks like, could tell.
The fix
Keep inline tags inside their sentence, put newlines only around block elements, and wrap inline code in backticks so it survives:
for c in soup.find_all(["code", "tt", "kbd", "samp"]):
c.replace_with("`" + c.get_text() + "`")
for b in soup.find_all(["p", "div", "li", "tr", "table", "ul", "ol"]):
b.append("\n")
text = soup.get_text("") # no separator: inline tags stay put
The same look at the samples turned up two more problems: page furniture in another language (I'd asked a wiki's API without saying which language I wanted, and got "Megjegyzés", Hungarian for "Note") and boilerplate banners ("This article or section is...") that the model had started reciting.
What I'd tell myself next time
- Read samples of your data before you scale anything. Not a summary, actual samples, end to end. Ten minutes.
- Sample the model early. Even a barely-trained model reflects its data honestly. It's a very blunt data-quality test.
- A falling loss curve means the model is learning something. It says nothing about whether that something is what you wanted.
I found this on a two-hour test run instead of a twenty-five-hour real one. That's the entire reason to do a test run.