Most weeks I write about something new I built. This week is a plot twist: I caught my own model cheating, threw out a result I was secretly proud of, and ended up somewhere way more interesting. Honestly? This might be my favorite week so far.

Let me tell you the story.

The score that looked a little too good

A few weeks ago my model hit 96% accuracy at telling apart two kinds of embryos, and I thought, nice, we’re cooking. But something felt off. Every single time I trained it, it landed on the exact same number and just parked there. Real learning is usually a bit messier than that.

So I sat down and actually checked the math. And oof.

Picture a class of 26 students where 25 are boys and 1 is a girl. If I ask you to guess each student’s gender and you just yell “boy!” every single time, you’ll be right 25 out of 26 times, about 96%. You didn’t learn anything. You just noticed the room was mostly boys and played the odds.

That’s exactly what my model was doing. My data had 254 embryos in one group and only 6 in the other, so the model figured out it could ignore everything and shout the popular answer. The 96% wasn’t intelligence. It was a shrug.

The two groups were 254 vs 6, so 96% was just guessing the popular answer

The deeper problem: the test was never fair

Okay, so can I just even out the two groups and try again? Here’s the kicker: no. Because the two groups weren’t really two groups at all.

The embryos in my dataset glow under the microscope, and I’d been using that glow to sort them. But the glow is just a little marker scientists add so they can see the cells. It’s a flashlight, not a symptom. Almost all these embryos are perfectly healthy and normal; they’re just holding different colored flashlights. My sorting rule had looked at ~254 healthy embryos and slapped a “something’s wrong with this one!” label on them for no good reason.

So the real issue wasn’t my code. It was that this dataset was never built to answer the question I was asking it. The engine I’d spent weeks building, the part that watches cells connect and grow, is solid and genuinely works. It was the question bolted on top that was hollow. Time for a better question.

The reframe: stop asking about the embryo, start asking about the cell

Here’s the pivot I’m excited about. Instead of asking one big vague question about the whole embryo, ask a sharp little question about every single cell: when this cell grows up, what does it become?

A brain cell? A muscle cell? Part of the gut?

Before: one label for the whole embryo. After: a “what will you become” label for every cell

I love this switch for two reasons. First, it turns one weak question into hundreds of real ones: every cell is now its own little prediction. Second, it fits the model I already have. The engine from Week 3, the friendship-rater, the relationship-tracker, and the growth-updater, runs completely unchanged. I just point it at each cell and ask “so… what’s this one going to be?”

The best part: the answers were already hiding in the data

Here’s the little bit of biology magic that makes this possible.

The worm I’m studying, C. elegans, is famously predictable. Every healthy embryo grows the exact same way, every time, cell for cell. Scientists have mapped the entire family tree. They know that this specific cell always becomes a neuron, that one always becomes muscle, and so on. It never changes.

Which means I don’t need any new microscope footage. The “correct answers” for my new question have been sitting inside the data I downloaded months ago, just waiting to be read off the family tree.

For cells that have already grown up, it’s a straight lookup. For a cell that’s still young and hasn’t picked a path yet, I peek at what all its future children become and go with the majority: if most of its descendants turn into neurons, I call the parent a neuron-in-progress too. If it’s genuinely still on the fence, I just label it “undecided” and move on. No pretending.

The answers come free from the worm’s fixed family tree, just look at what the descendants become

There’s one rule I’m strict about, though: the model never gets to see the cell’s name. The name is where I got the answer from, so letting the model peek at it would be like handing someone the answer key during the exam. The model has to figure out a cell’s future from where it sits, when it appears, its size, and who it’s touching (the actual clues), not from a label that gives the game away.

So what does the new dataset look like?

Running all of this gave me 2,769 labeled cells across roughly a dozen different fates. And this time the mix is healthy: a real spread of answers instead of one option drowning out the rest.

2,769 cells sorted into a dozen fates: neurons, pharynx, muscle, skin and more

Neurons lead the pack, which is perfect. C. elegans is a bit of a brainiac, packing a huge share of its cells into its nervous system. Then come the pharynx (its throat and feeding parts), muscle, skin, and gut cells. The grey bars are my honest “still undecided” pile, which I keep as its own thing rather than forcing a fake answer.

What I actually built this week

Under the hood, this was a real chunk of new work:

  • A fate dictionary that knows what every cell in the worm grows up to be, and can trace those answers up the family tree.
  • A relabeling script that turns the raw data into my new set of 2,769 answers.
  • A new head for the model that reads each cell and predicts its fate, reusing the whole engine from before.
  • A fairer training setup that keeps whole embryos on either the “study” or “test” side (so no cell can secretly memorize its own answer), and, this one’s personal now, reports metrics that would catch a lazy model instead of getting fooled by one. After the 96% embarrassment, I don’t trust a single accuracy number ever again.

The whole thing runs end to end on the GPU. Next up: the full training run with the honest scorecard.

What’s next

  • Run the real training and see how well the model actually predicts cell fates, with metrics that can’t be gamed this time.
  • Color my attention maps by fate, so the “which connections matter” story from earlier weeks ties directly into “and here’s what these cells become.”
  • I’ve also written up a note to my mentors about optionally bringing in a second dataset that was purpose-built for the healthy-vs-affected comparison I originally wanted, so if we still want that angle, we can do it for real. That’s their call to make, and I want their input before I commit.

If there’s one thing this week hammered home, it’s this: a score you can’t explain isn’t a win. It’s a question in disguise. This time I chased the question all the way down, and the project came out the other side stronger, sharper, and honestly a lot more fun.

More soon.