All posts

Learning

Contrast is the cheapest depth

A single run that works teaches almost nothing you'd actually believe. What teaches is the second run — the same idea with one variable changed, so the property you're trying to show appears in one and vanishes in the other. It turned out to be the cheapest depth I could add to a lesson, and the one I kept reaching for.

June 14, 20267 min read

The perceptron lesson has a dataset you can switch to where it never stops. You watch the line jitter back and forth, settle for half a second, then get knocked loose again, forever. That dataset is XOR, and the perceptron provably cannot separate it — no straight line ever will. For a while that felt like a bug I should hide. It turned out to be the most important thing in the lesson.

Because the other datasets, the ones where it works, teach you almost nothing on their own. You watch a line swing around and snap into place between two clouds of dots, and you think "sure, it learned." You don't actually know what you learned. Maybe it always works. Maybe it got lucky. The converging run is a demo; it asks nothing of you. It's only when you flip to XOR and watch the exact same rule spin forever that the property snaps into focus: this works if and only if a straight line can split the data, and here's what "can't" looks like. The failure is what makes the success mean something.

You can't just say the property

This is the thing I kept relearning. An educational animation that faithfully replays the code is the floor, not the lesson. A trace that shows bubble sort sorting is a demo of bubble sort working; it is not, by itself, the idea of bubble sort. The idea is "this does an amount of work that grows like the square of the input," and you can write that sentence under the animation all day without anyone believing it in the way they'd believe something they watched happen.

The test I started holding lessons to is simple and a little brutal: after watching, what can the learner predict that they couldn't predict from reading the code? If the honest answer is "nothing, they just saw it run," I've built decoration — boxes moving around, calling itself teaching. A property the learner can state and defend — "a million sorted elements cost binary search at most twenty probes," "the perceptron converges on separable data and never on XOR" — is what I'm actually after. Everything else is in service of getting one of those sentences into their head in a form they trust.

The cheapest way to make a property visible

You'd think making a property visible would be expensive — some clever custom visualization that somehow renders "quadratic" or "convergent" directly. Sometimes it is. But going through lesson after lesson, the move I kept reaching for was much dumber and worked far more often: run it twice, change one thing.

Quadratic work is hard to show in the abstract and obvious the instant you put the n=5 run next to the n=10 run and the second one takes about four times as long, not twice. Memoization is a footnote until the plain recursion tree explodes into hundreds of nodes and the memoized one stays small, same number, same screen. The whole point of an optimization is invisible until you can see the thing it's an optimization over. Contrast doesn't explain the property. It stages the conditions where the property appears, and lets the learner read it off the difference themselves — which is the kind that tends to stick.

And the whole comparison lives inside one animation, which is the part that makes it land. The variant is a tab in the same player: flip it and the algorithm swaps in place, and the code panel swaps with it to the snippet that's actually running. Two-sum's brute-force and hash-map runs are genuinely different code on different traces — flip the tab and you watch the O(n²) double loop collapse into the O(n) single pass, one click, without ever leaving the screen or holding the previous run in your head. The contrast isn't two browser tabs you alt-tab between hoping to remember the first; it's one animation that becomes the other.

It's not just two-sum. Fibonacci puts three variants on one selector — the same n as a recursion tree, a memo table, a bottom-up array. The perceptron has its three datasets, the scheduler its three policies. Bubble sort goes furthest: it stacks three ways to feed one algorithm — flip the variant tab between classic and optimized, pull a boundary case from a tray (sorted, reversed, all-equal, a lone element, each paired with the variant that makes its point), or just type your own. Three knobs on one lesson, every one the same move: change one thing, watch the property appear.

And it's cheap in the most literal sense: it reuses the machine that's already there. At the cheap end a contrast is one trace generator and a slider — gradient descent feeds the same loop a different learning rate. At the expensive end it's a second generator and a second snippet, still a rounding error next to writing a second lesson. You're adding a tab, not a topic. The contrast carries the insight, and it costs almost nothing to add.

The breaking point is the lesson, not the footnote

My reflex was to treat the case where something fails as an edge case — handle it so it doesn't crash, mention it at the bottom, move on. For teaching, I had that backwards. The place an idea breaks is usually where the understanding lives.

So the divergent run isn't an appendix to the gradient-descent lesson; it's half of it. There's a learning rate above which gradient descent stops converging and starts flinging itself uphill, and the lesson lets you push the slider across that line — η just past 1 — and watch the loss explode instead of telling you the bound exists. Knowing where a thing falls apart, and having watched it fall apart, is what separates understanding it from merely recognizing it. A lesson that only ever shows the happy path has quietly decided the learner doesn't need to know where the edges are. I used to build those. The edges are the shape of the thing.

One input, or one per mode

Here's the part I got wrong before I got it right, and it's subtler than "add contrast." Once a lesson has multiple modes, I learned to ask whether a single input can exercise all of them, and the answer decides everything about how the defaults get set.

Sometimes one input lights up every mode, and then I share it, because the shared input is the contrast. Two-sum's one array runs both the brute-force O(n²) scan and the hash-map O(n) pass — handed the same numbers, the learner reads the difference straight off them; give the two approaches different arrays and you've thrown the comparison away. Fibonacci's single n drives the recursion tree, the memo, and the table alike. When that's true, splitting the input is a mistake.

But sometimes no single input can, and then forcing them to share is the mistake. Bubble sort's classic variant shows its O(n²) worst case on a "turtle" array where a small value has to crawl from the back to the front — and that exact array hides the optimized variant's early exit, which only shows up on data that's nearly sorted already. One input makes one mode lie about itself. The perceptron's separable set and its XOR set are different datasets on purpose; gradient descent's gentle learning rate and its divergent one are different numbers on purpose. The check I ended up using is one question: pick the input for a mode, then ask whether running that mode on that input actually triggers the property the mode exists to show. If half the modes fail that question on a shared input, they each need their own.

None of this was a plan I started with. It accumulated, one lesson at a time, mostly from catching a run that was teaching nothing and asking what it would take to make it teach something. The answer was almost always the same: show me the other one. Contrast is the cheapest depth there is, and most of the time it's the only depth a lesson needs.

See it: open two-sum and flip the tab from brute force to hashing — same array, and the nested scan collapses into a single pass while the code panel swaps under your eyes. Then the perceptron: switch the dataset from a separable one to XOR — same rule, and it never stops. And gradient descent — nudge the learning rate past 1, where convergence turns into an explosion you can watch.