<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>Mez Gebre</title>
 <link href="http://mez.sh/atom.xml" rel="self"/>
 <link href="http://mez.sh/"/>
 <updated>2026-10-14T00:23:42+00:00</updated>
 <id>http://mez.sh/</id>
 <author>
   <name>Mez Gebre</name>
 </author>

 
 <entry>
   <title>Why I Stopped Trusting Validation Loss</title>
   <link href="http://mez.sh/2026/09/02/why-i-stopped-trusting-validation-loss/"/>
   <updated>2026-09-02T00:00:00+00:00</updated>
   <id>http://mez.sh/2026/09/02/why-i-stopped-trusting-validation-loss</id>
   <content type="html">&lt;p&gt;My last post was about failing to validate a world model. It assumed something I never argued for,
namely that you need a world model to evaluate a driving policy in the first place. Why not just hold out
some data, measure the loss, and pick the policy with the lower number?&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;Two things I read this month say that doesn’t work. They come at it from completely different
angles and neither is about driving, but they land in the same place, and it’s a place that makes
the problem I couldn’t solve considerably more urgent than I’d been treating it.&lt;/p&gt;

&lt;h2 id=&quot;the-first-one-four-things-that-shouldnt-happen&quot;&gt;The first one, four things that shouldn’t happen&lt;/h2&gt;

&lt;p&gt;Seohong Park, an RL researcher at Berkeley, &lt;a href=&quot;https://seohong.me/blog/behavioral-cloning-mystery/&quot;&gt;wrote up four
mysteries&lt;/a&gt; he ran into after moving into
robotics. His starting observation is that real demonstration data behaves nothing like the data
in the benchmarks everybody tests on. Human demos are narrow, temporally correlated, and smoothly
random in a way scripted benchmark data isn’t. So he built a simulator that fakes those
properties. Piecewise Hermite splines, randomized grasp angles and contact points, occasional
mistakes and recoveries. Then he reproduced four phenomena that shouldn’t be reproducible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overfitting helps.&lt;/strong&gt; Train longer, performance keeps climbing, while validation flow loss climbs
too. That’s not a subtle effect, and it gets worse. A 10,000-episode dataset sometimes beats a 50,000-episode
one drawn from the same distribution, averaged over four seeds with four independently sampled
datasets each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acting blind beats reacting.&lt;/strong&gt; Have the policy predict 25 moves and execute all of them without
looking again, and it works. Have it predict one move, look at the result, predict the next, and the policy
doesn’t touch the cube. This is with infinite data, since he regenerates fresh demos continuously
and trains on each batch once, so it isn’t overfitting or scarcity. Adding history conditioning,
which should close the expressivity gap, makes it worse, even though the history-conditioned
policies fit the data better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The policy has to be enormous.&lt;/strong&gt; A 37-dimensional, single-task, state-based pick-and-place needs residual MLPs around eight layers deep and 4,096 units wide before it works well. Doubling the width to 8,192 improves it further, so that isn’t even the ceiling. That’s on the order of half a billion parameters for a task where you’d reach for three layers of 512 without thinking about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature scaling matters with infinite data.&lt;/strong&gt; Two feature sets containing identical information,
differing only in per-dimension scaling, produce nearly identical flow losses and clearly different
performance. Scaling up the object’s xyz coordinates relative to everything else improves things
further.&lt;/p&gt;

&lt;p&gt;His diagnosis for all four comes down to test-time distribution shift. The policy drifts into states
the training data never covered, and every one of these mysteries is really about behavior out
there rather than behavior on the data you have. His read is that this may be unfixable except by
scaling until nothing is out-of-distribution, which is his explanation for why LLMs don’t suffer
from it the way robot policies do.&lt;/p&gt;

&lt;p&gt;The bit I keep coming back to is smaller than any of the four. It’s an aside where he notes that
the metric you’d actually want is the error between generated and target actions &lt;em&gt;under the state
distribution the policy itself induces at test time&lt;/em&gt;, rather than under the dataset’s state
distribution. But that metric is generally inaccessible, since you don’t have target labels
for the test-time states.&lt;/p&gt;

&lt;p&gt;That’s the whole problem in two sentences.&lt;/p&gt;

&lt;h2 id=&quot;the-second-one-the-thing-everyone-believed-was-wrong&quot;&gt;The second one, the thing everyone believed was wrong&lt;/h2&gt;

&lt;p&gt;The other is a paper out of CMU, MIT and Toyota Research called &lt;a href=&quot;https://arxiv.org/abs/2512.01809&quot;&gt;Much Ado About
Noising&lt;/a&gt;. It asks why diffusion and flow policies beat plain
regression on manipulation benchmarks. The standard answer has been multi-modality. A human might
sensibly do several different things from the same state, regression averages them into mush, and
a generative model can represent the full distribution.&lt;/p&gt;

&lt;p&gt;They tested it and it’s wrong. The key methodological move is that they hold architecture constant.
They implement regression policies inside the exact same backbones the diffusion papers use, by
setting the noise and time index to zero, which as far as I can tell nobody had done before. Under
that comparison, across 28 benchmarks, generative and regression policies come out level almost
everywhere. Gaps show up only on a handful of high-precision tasks.&lt;/p&gt;

&lt;p&gt;Then they go after multi-modality directly, three ways.&lt;/p&gt;

&lt;p&gt;First they sample many actions at states where several answers should obviously be valid. Push-T, where the robot shoves a T-shaped block into a target outline, has configurations where pushing from the left and pushing from the right are equally sensible. Franka Kitchen has independent subtasks, so opening the microwave first and turning the burner first are both fine. These are the two places in the standard benchmark suite where distinct modes should be least controversial. They get single clusters at both.&lt;/p&gt;

&lt;p&gt;Second, they replace stochastic sampling with the mean action. If the policy really were representing several distinct options, averaging them would produce something between two valid choices, which is usually invalid. Performance barely moves.&lt;/p&gt;

&lt;p&gt;Third, they rebuild the dataset from a deterministic policy in a deterministic environment, so multi-modality is impossible even in principle. The gap between generative and regression policies persists anyway.&lt;/p&gt;

&lt;p&gt;What’s actually doing the work turns out to be two things. Injecting noise during training, and generating the output over several steps where every intermediate step gets supervised. Both are procedural habits rather than distributional ones. Keep them, drop the distribution-fitting entirely, and you get MIP, an ordinary regression policy that predicts a single action, refines it over two supervised passes, and matches flow across state, image and point-cloud benchmarks at two function evaluations instead of nine.&lt;/p&gt;

&lt;p&gt;The part I found most useful comes later, when they look for why MIP and flow win. They expected better reconstruction on held-out data, and instead found that every method achieves roughly the same validation loss. What separates the good policies from the bad ones is a metric they had to invent, which they call manifold adherence. Roughly, whether the action you produce is still plausible when the observation is off-distribution. Validation loss doesn’t discriminate. Off-manifold error does.&lt;/p&gt;

&lt;p&gt;Worth being precise about the metric, because “plausible” is doing a lot of work. They perturb
expert trajectories to reach states slightly off the training distribution, ask the policy what it
would do there, and measure how much of the predicted action falls outside the span of expert
actions at nearby states. Only the component orthogonal to that span counts. Distance to the
nearest expert action doesn’t, which is what you’d probably assume they meant.&lt;/p&gt;

&lt;p&gt;The numbers on state-based Tool-Hang, a high-precision insertion task from Robomimic and the one where the performance gap between methods is widest, averaged over three architectures and 32 states.&lt;/p&gt;

&lt;table class=&quot;no-row-accent&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;Regression&lt;/th&gt;
      &lt;th&gt;SF&lt;/th&gt;
      &lt;th&gt;RR&lt;/th&gt;
      &lt;th&gt;MIP&lt;/th&gt;
      &lt;th&gt;Flow&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Validation L2&lt;/td&gt;
      &lt;td&gt;0.073&lt;/td&gt;
      &lt;td&gt;0.071&lt;/td&gt;
      &lt;td&gt;0.062&lt;/td&gt;
      &lt;td&gt;0.069&lt;/td&gt;
      &lt;td&gt;0.074&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Off-manifold L2&lt;/td&gt;
      &lt;td&gt;0.058&lt;/td&gt;
      &lt;td&gt;0.061&lt;/td&gt;
      &lt;td&gt;0.057&lt;/td&gt;
      &lt;td&gt;0.043&lt;/td&gt;
      &lt;td&gt;0.032&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Validation loss sits between 0.062 and 0.074 across five methods whose success rates run from 0.31
to 0.74. Residual regression has the best validation loss of the five and the worst performance of
the five.&lt;/p&gt;

&lt;p&gt;One caveat that a fast read would miss. Off-manifold error cleanly separates flow and MIP from the other three, but it doesn’t order the three below them. RR has the lowest off-manifold error of that group at 0.057, and the worst success rate at 0.31. Straight flow has the highest error at 0.061 and the best success rate of the three at 0.54. The metric identifies a property the winners share. It isn’t a dial where a lower number means a better policy.&lt;/p&gt;

&lt;p&gt;MIP’s objective is two regression terms, both aiming at the true action. The first is ordinary regression from the observation. The second asks the same network to recover that action from a noised version of it, at one fixed noise level. At inference you run the first, feed its output into the second, and stop. Two function evaluations.&lt;/p&gt;

&lt;p&gt;Flow instead predicts a velocity field across a continuum of noise levels and integrates it. That continuum is the distributional part, and MIP doesn’t have it. Everything MIP kept is procedural. The noise and the second pass survive, the distribution fitting doesn’t, and performance holds.&lt;/p&gt;

&lt;p&gt;They’re also honest that they can’t explain the mechanism. There’s theory saying generative models learn to project onto the data manifold, but nothing explaining why that bias would be stronger than a well-trained regression model’s, and they say so plainly rather than gesturing at it.&lt;/p&gt;

&lt;h2 id=&quot;what-they-agree-on&quot;&gt;What they agree on&lt;/h2&gt;

&lt;p&gt;Two groups, different problems, different methods, neither one about driving. Park is chasing why
robot policies behave strangely on human data. The CMU group is auditing a design choice everybody
copied without checking. They arrive at the same finding from opposite directions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The number you compute offline does not tell you which policy is better.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Park watches validation loss rise while performance improves. The CMU group finds identical
validation losses across methods with very different success rates, and has to build a new metric
because the obvious one doesn’t discriminate at all.&lt;/p&gt;

&lt;p&gt;I don’t think either paper frames this as its headline result. For Park it’s the thread connecting
four mysteries. For the CMU group it’s a step on the way to explaining MIP. But it’s the part that
matters most to me, because if offline metrics don’t rank policies, then you have to actually run
the policy to find out which one is better.&lt;/p&gt;

&lt;p&gt;That’s the distinction I flagged above. Scoring a policy against a recording is cheap, but the
recording plays out the same way no matter what the policy says, so you never see the consequences
of its decisions. Letting the policy’s choices change what happens next means either real miles,
which are expensive and still can’t answer what-if questions, or a simulator you trust.&lt;/p&gt;

&lt;p&gt;Which is where my last few weeks went.&lt;/p&gt;

&lt;h2 id=&quot;the-part-thats-uncomfortable&quot;&gt;The part that’s uncomfortable&lt;/h2&gt;

&lt;p&gt;The whole argument for generative world models as evaluation substrate rests on this. You can’t
answer “what would have happened if the policy had swerved” from a recording, and rare events are
too rare to collect at the volume closed-loop evaluation needs. Fine. But that argument only bites
if the offline alternative genuinely doesn’t work, and I’d been treating that as background rather
than something to establish. These two results establish it better than I could have.&lt;/p&gt;

&lt;p&gt;So closed-loop evaluation isn’t a nicer way to do this. It’s the only way. And
the thing standing between us and closed-loop evaluation at scale is whether anyone can tell a
trustworthy generated rollout from an untrustworthy one, which is the question I spent a week
failing to answer.&lt;/p&gt;

&lt;p&gt;That’s a worse position than “I couldn’t build the checker.” It’s “I couldn’t build the checker,
and there’s no fallback.”&lt;/p&gt;

&lt;h2 id=&quot;three-groups-three-scorers-none-audited&quot;&gt;Three groups, three scorers, none audited&lt;/h2&gt;

&lt;p&gt;The pattern shows up a third time, inside the CMU paper itself. They needed a way to tell whether an action was plausible off-distribution. Nothing existing worked, so they built manifold adherence, and then wrote plainly that they can’t explain why it works. There’s theory saying generative models learn to project onto the data manifold, but nothing explaining why that bias should be stronger than a well-trained regression model’s.&lt;/p&gt;

&lt;p&gt;That’s the same shape as everything in my last post. An automatic evaluator standing in for judgment you can’t collect at scale, load-bearing for a central claim, and unmeasured. Theirs might be excellent. Mine turned out to be blind in three separate ways, and I only found out because I went looking.&lt;/p&gt;

&lt;p&gt;Park needs a metric he says is inaccessible. The CMU group builds one and can’t account for it. I built one and found it couldn’t see the thing it was built to detect. Three groups, three attempts, three different problems, and in none of the three cases does anyone know how much to trust the instrument.&lt;/p&gt;

&lt;h2 id=&quot;where-this-leaves-me&quot;&gt;Where this leaves me&lt;/h2&gt;

&lt;p&gt;Not anywhere different, practically. I’m still building a geometric consistency score in a
synthetic environment, for the same reason as before. It’s the only place I can validate the
instrument against ground truth instead of against another instrument’s opinion.&lt;/p&gt;

&lt;p&gt;What changed is how I’d describe the problem to someone who asked why it matters. A week ago I’d
have said generated rollouts are useful and we should check whether they’re trustworthy. Now I’d
say the field is being pushed toward closed-loop evaluation because the offline alternative
doesn’t work, and nobody has an audited instrument for deciding whether a closed-loop rollout is
worth believing.&lt;/p&gt;

&lt;p&gt;Park’s benchmark is due for release around October, which I’m waiting on. A controlled environment
with scripted demonstrations, ground-truth state, and enough realism to reproduce the mysteries is
close to what I’d want anyway, and it would be a much better starting point than building the whole
thing myself.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Three Ways to Think You've Validated a World Model</title>
   <link href="http://mez.sh/2026/08/29/three-ways-to-think-youve-validated-a-world-model/"/>
   <updated>2026-08-29T00:00:00+00:00</updated>
   <id>http://mez.sh/2026/08/29/three-ways-to-think-youve-validated-a-world-model</id>
   <content type="html">&lt;p&gt;I set out to test a distillation question. I ended up writing a paper about why that question was premature.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;The plan was simple, on paper. Vista, OpenDriveLab’s action-conditioned driving world model,
generates a rollout: give it a starting frame and a trajectory, and it imagines the next 25 frames
of driving. It ships with 50 sampling steps by default. Distill it down to 4, or 2, or 1, and you
get a much cheaper rollout, if the quality holds. The question I wanted to answer was whether a
&lt;em&gt;policy-ranking verdict&lt;/em&gt; survives that distillation. Rank two driving policies with Vista: does the
ranking stay the same at 50 steps and at 2?&lt;/p&gt;

&lt;p&gt;That’s still an interesting question. I haven’t answered it. Instead I spent most of this project
discovering that the question has two silent prerequisites nobody in the driving-world-model
literature seems to state out loud, and neither one held up when I actually checked. This is the
story of finding that out, what it cost to check properly instead of assuming, and what I’m
building next because of it.&lt;/p&gt;

&lt;h2 id=&quot;prerequisite-one-does-the-model-even-listen-to-you&quot;&gt;Prerequisite one: does the model even listen to you?&lt;/h2&gt;

&lt;p&gt;Vista takes an 8-number trajectory: four future waypoints, each a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(lateral, forward)&lt;/code&gt; pair in
meters, at half-second intervals. Feed it a left turn, it should generate a left turn. Feed it a
right turn, a right turn. That’s the entire premise of using it for closed-loop policy evaluation.
The model’s response has to be attributable to the policy’s actual decision.&lt;/p&gt;

&lt;p&gt;First bug, and if you’re adapting Vista to anything other than nuScenes you will hit this too: the
axis order is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(lateral, forward)&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(forward, lateral)&lt;/code&gt;. The failure mode is nasty precisely
because it doesn’t look like a bug. You get blurry, indecisive turning behavior, output that reads
as “the model doesn’t turn very well” instead of “the input is wrong.” I only caught it by
triangulating three independent sources: the paper, the original author’s own preprocessing
script, and a direct statistical decode of the nuScenes annotation data Vista was trained on. If
your Vista turns look weak or wrong, check your axis order before you conclude anything about the
model.&lt;/p&gt;

&lt;p&gt;Fixing that revealed a second problem, and this one isn’t a bug. It’s arithmetic. Vista encodes
each trajectory value through a sinusoidal embedding built for diffusion timesteps, which run in
the hundreds to thousands. Real driving trajectories are single-digit meters, three orders of
magnitude smaller than what the embedding was tuned for. I checked whether that actually matters
instead of assuming it either way, and it does. Cosine similarity between the embeddings of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+3m&lt;/code&gt;
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-3m&lt;/code&gt; (same magnitude, opposite direction, exactly the pair a left-vs-right decision depends
on) is 0.735. Cosine similarity between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+3m&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+15m&lt;/code&gt; (same direction, five times the
magnitude) is 0.662. Lower. At realistic driving scale, Vista’s own conditioning discriminates
magnitude more reliably than direction. Behavioral tests agreed: no reliable directional response
at realistic magnitude, and only a real but seed-dependent effect at ten times the scale.&lt;/p&gt;

&lt;p&gt;I didn’t introduce that bug. It’s a property of Vista’s own embedding design, and you can verify
it yourself with ten lines of numpy and no GPU. It means the first prerequisite, that a generated
rollout is attributable to the policy’s action, doesn’t cleanly hold at the magnitudes a real
policy actually produces.&lt;/p&gt;

&lt;h2 id=&quot;prerequisite-two-can-anyone-tell-if-a-rollout-is-any-good&quot;&gt;Prerequisite two: can anyone tell if a rollout is any good?&lt;/h2&gt;

&lt;p&gt;Say the conditioning problem didn’t exist. There’s a second prerequisite underneath the ranking
question: something has to look at a generated rollout and say whether it’s a fair test of the
policy or an artifact of the generator falling apart. A rollout can look plausible frame-to-frame
while a static pole bends, or the ego vehicle’s implied speed does something a real car can’t do.
So I built a geometric checker: VGGT, a feed-forward 3D reconstruction model, validated first
against real Waymo pose (11.5cm mean position error, 0.987 heading correlation on a real
49-degree turn) and cross-checked against real lidar depth.&lt;/p&gt;

&lt;p&gt;Then I tried to use it, and this is where the project turned into a different paper. Four times a
check looked like it had found a real problem. Three of those four turned out, once I built the
control I should have built from the start, to be measuring something else entirely.&lt;/p&gt;

&lt;p&gt;The sharpest one: a scale-consistency score checking whether a rollout’s implied camera speed
evolves smoothly. On a real, dead-straight clip it looked clean. On a real 49-degree turn, at a
5-frame averaging window, it reported 4.6% deviation, comfortably under the 5% threshold I’d
pinned in advance. A clean pass. The actual ground truth, on the exact same clip, was 13.3%. A
clear fail. The checker wasn’t being cautious. It was confidently wrong: it reported smoother
implied camera motion than the real clip actually had, and widening the averaging window made
things worse, which ruled out “just needs more data” as the explanation.&lt;/p&gt;

&lt;p&gt;Then I ran the same statistic on the straight clip and found it was wrong there too, just in the
opposite direction: jumpier than reality on the easy clip, smoother than reality on the hard one.
Same estimator, two opposite failure directions. I checked this wasn’t a coincidence by sweeping
the averaging window from 3 to 15 frames on both clips. On the straight clip, the gap to ground
truth shrank as the window widened, the signature of high-frequency noise averaging itself out. On
the turning clip, the gap grew as the window widened, the signature of a low-pass filter smoothing
away real, slower variation instead of noise. That crossover, not just the two headline numbers,
is why I call it a bandpass distortion rather than a simple too-strict or too-lax bias.&lt;/p&gt;

&lt;p&gt;My best explanation, and I want to be clear it’s an explanation rather than something I isolated
with an ablation: a joint-attention model is trained to build one temporally consistent story out
of the whole clip, and that’s exactly the property that would make it bad at flagging temporal
inconsistency, since flagging it means contradicting the story it just built.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/world_model/fig2_window_sweep.png&quot; alt=&quot;VGGT&apos;s reported deviation against ground truth, swept across window sizes 3-15 frames, on both
clips. Straight clip: VGGT starts noisier than reality and they converge as the window widens.
Turning clip: VGGT collapses toward zero while ground truth stays high, a false pass from window 5
onward.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-near-miss-that-would-have-been-a-confidently-wrong-published-result&quot;&gt;The near-miss that would have been a confidently wrong published result&lt;/h2&gt;

&lt;p&gt;Before I trusted that finding, I hit a smaller, uglier version of the same problem. It’s the one
I’d want another engineer to see before they publish a negative result of their own. Extracting
the recovered heading change on that same turning clip, my first pass returned -1.6 degrees
against a ground truth of 49.0. Wrong sign, off by an order of magnitude. Read on its own, that’s a
near-total failure to recover the turn, and it would have been easy to believe, because it matched
a limitation I half-expected going in.&lt;/p&gt;

&lt;p&gt;It wasn’t real. The position trajectory, computed completely independently, visibly curved and
matched ground truth to 11.5cm. A straight predicted path can’t be rigidly aligned onto a
genuinely curved one and look that good, so the heading number and the position number couldn’t
both be telling the truth. The bug was a coordinate-convention mismatch. I’d extracted heading by
reading yaw directly off VGGT’s rotation matrix under the wrong axis convention: Waymo’s
vehicle-frame assumption, applied to OpenCV’s camera-frame output. The number that came out wasn’t
heading with some noise on it. It was a completely different physical quantity wearing heading’s
name.&lt;/p&gt;

&lt;p&gt;The generalizable lesson isn’t “check your axis conventions,” though that’s true here too. It’s
about what to do when two independent measurements of the same thing disagree: trust the one with
fewer assumptions baked in. Position, computed from consecutive frame deltas, has no
rotation-matrix convention to get wrong. Heading, decoded from a rotation matrix, does. A
plausible-looking negative result that happens to confirm something you already suspected is
exactly the shape of finding that turns into a confidently wrong published claim, if you have no
independent check to catch it.&lt;/p&gt;

&lt;h2 id=&quot;the-pattern-that-showed-up-three-times&quot;&gt;The pattern that showed up three times&lt;/h2&gt;

&lt;p&gt;By the time I’d chased down why a sparse-point rigidity score kept passing rollouts it shouldn’t
have, I’d found the same design mistake twice in my own code. The first version treated any point
that never reached enough tracking confidence as simply excluded from scoring. That’s the wrong
default for this metric specifically, because low confidence isn’t independent of rigidity. Thin,
high-frequency structure like poles and wires is low-confidence by construction, so the filter
removes exactly the structure most likely to reveal a problem. A rollout falling apart
geometrically and a rollout with no data at all look identical under a pooled pass/fail statistic.
The fix: report the unscoreable fraction as a first-class output.&lt;/p&gt;

&lt;p&gt;The second version, one level deeper, evaded the exact fix I’d just built. It used an adaptive
confidence threshold, set to each frame’s own median. That sounds principled. It generalizes
across clips with different absolute confidence scales. It’s actually the same bug wearing a
disguise: the threshold gets set by whatever surface happens to dominate a given frame, so a
perfectly trackable point can fail simply by sharing a frame with something more confident
elsewhere. Reporting the unscoreable fraction within one run doesn’t catch this, because the bias
doesn’t look like missing data. It looks like a systematic skew toward whichever surface is
nearest or most reflective, invisible unless you go looking for it specifically.&lt;/p&gt;

&lt;p&gt;Here’s what that subtle deformation actually looks like, next to the real, unwarped frame and the
large positive control used to confirm any checker in this story could still fire on something
obvious:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/world_model/fig5_warp_visualization.png&quot; alt=&quot;Left: the real frame. Middle: the subtle warp used throughout this project&apos;s testing, a
15-35px wiggle confined to one window. Right: a large, unambiguous 120px positive control. The
middle one is visually obvious, not a marginal or borderline case, and both the rigidity score
above and GeCo below missed it.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The third time is the one that convinced me it isn’t just my code. I ran the same subtle,
carefully controlled deformation against GeCo, a published geometric consistency checker I had no
hand in writing. It missed the deformation completely: no separation from the unwarped clip.
Before trusting that null result, I ran a positive control, a large, obvious deformation, the kind
GeCo’s own published validation uses to confirm the metric fires at all. It fired clearly. The
pipeline worked, and the subtle warp genuinely sat inside the range GeCo reads as unremarkable.
Then, instead of just accepting “not much signal,” I checked why the response was smaller than the
visual deformation suggested, and found a clean, monotonic dose-response: pixels displaced under
5px lost almost no coverage, pixels displaced over 90px lost twenty times as much. The
most-deformed pixels weren’t scoring badly. They were disappearing from the denominator, in
proportion to how deformed they were. Silent selection again, in a system I didn’t write, using a
motion model I didn’t train.&lt;/p&gt;

&lt;p&gt;Twice in code I control, found by fixing them. Once in code I don’t control, found only because I
ran a positive control to rule out a wiring bug and got a dose-response instead. If there’s one
operational takeaway from this whole project, it’s this: &lt;strong&gt;a scoring pipeline should never report
a number without also reporting what fraction of the data that number is actually based on.&lt;/strong&gt;
“Zero problems detected” and “the checker had no opinion” look identical in a summary table. They
are not the same claim.&lt;/p&gt;

&lt;h2 id=&quot;what-honoring-a-pre-committed-stopping-rule-actually-costs&quot;&gt;What honoring a pre-committed stopping rule actually costs&lt;/h2&gt;

&lt;p&gt;Partway through, I pinned a rule before I had any results to argue about: any geometric score gets
at most two failed validation attempts. Fail twice, and it drops from a pass/fail gate to a
reported diagnostic. I wrote that down because the rigidity score had already burned most of a
week: five separate validation attempts, each one individually reasonable, each one turning up one
more confound worth ruling out before the next. That’s exactly how a week disappears one week into
a fourteen-week plan. Not one bad decision, just a chain of locally justified “one more check.”&lt;/p&gt;

&lt;p&gt;The rule did its job, in a way I didn’t love in the moment. Scale consistency’s redesigned,
self-contained version, the one that doesn’t need ground truth, which is the actual production
requirement, got exactly two attempts. The first found a false pass on the turning clip. The
second, a window sweep, confirmed it wasn’t a window-size artifact: VGGT’s own reported deviation
trended toward zero as the window widened, while the real deviation stayed substantial. Two
strikes. Demoted. No third redesign, no “but this version is different.” Writing the rule down
before I had a result to protect is the only reason I actually stopped there instead of finding a
reason this particular case was special.&lt;/p&gt;

&lt;h2 id=&quot;where-this-leaves-the-project&quot;&gt;Where this leaves the project&lt;/h2&gt;

&lt;p&gt;I did not answer “does a policy-ranking verdict survive distillation.” I found that the question
presupposes two things, attributable conditioning and a trustworthy geometric checker, and neither
one holds without real, load-bearing work first. That’s the paper I ended up writing, &lt;strong&gt;Three Ways
to Think You’ve Validated a World Model&lt;/strong&gt;. It argues that building trustworthy automatic
evaluators, not sampling cost and not generation fidelity, is the actual binding constraint on
using generative world models for driving evaluation. I also put together a companion notebook
repo so the numbers above are checkable rather than just asserted: no GPU required for four of the
five notebooks, and the fifth needs nothing more than a laptop and 60MB of bundled data. I’m not
releasing either publicly yet, but if you want to read the paper or run the notebooks yourself,
reach out and I’ll send them over.&lt;/p&gt;

&lt;p&gt;What’s next follows directly from what killed this project’s original scope. The driving geometric
checker never got the one thing it actually needed: &lt;strong&gt;ground truth for the geometric coherence of
a generated scene.&lt;/strong&gt; The scene never existed, so there was nothing to validate the instrument
against except another model’s opinion, and that instrument turned out to be structurally blind to
exactly what it was asked to measure. A synthetic environment solves this by construction. Control
the world, and you know its exact geometry, poses, and depth for every frame, so a geometric
reward can be validated directly against truth instead of against another instrument’s opinion.&lt;/p&gt;

&lt;p&gt;The plan is staged, so it produces a result at every stage instead of betting everything on the
last one. Stage one: build a geometric-consistency score in a synthetic environment and validate
it against known ground truth, the step the driving version never reached, and report its
sensitivity floor, the smallest deformation it can detect. Stage two: use that score as a
rejection filter on a batch of generated rollouts and check whether downstream conclusions
actually change between the filtered and unfiltered library. That second half might be the whole
result on its own. If filtering changes nothing downstream, that’s a real finding about how much
geometric coherence actually matters, and it costs no training at all. Stage three, only if the
first two justify it: use the validated score as an RL reward and fine-tune a generative model
against it directly.&lt;/p&gt;

&lt;p&gt;I don’t know yet whether a synthetic result transfers to driving. Thin structures, weak parallax,
and sparse texture are domain-specific failure modes a clean synthetic environment won’t
reproduce, and I’m not pretending otherwise going in. But proving the mechanism works somewhere
you can actually check it beats assuming it works and finding out the hard way, which is more or
less the summary of everything above.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Audio Classification: A Weekend of Experiments (FAAAAHH!)</title>
   <link href="http://mez.sh/2026/02/17/audio-classification-ssl-deep-dive/"/>
   <updated>2026-02-17T00:00:00+00:00</updated>
   <id>http://mez.sh/2026/02/17/audio-classification-ssl-deep-dive</id>
   <content type="html">&lt;p&gt;I went down an audio classification rabbit hole. What started as “how hard can ESC-50 be?” turned into a weekend of building CNNs, Transformers, and finally catching up on self-supervised learning, only a few years behind everyone else.&lt;/p&gt;

&lt;p&gt;This post documents everything I tried, what worked, what didn’t, and the lessons learned.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-dataset-esc-50&quot;&gt;The Dataset: ESC-50&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/karolpiczak/ESC-50&quot;&gt;ESC-50 dataset&lt;/a&gt; contains:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;2,000 audio clips&lt;/strong&gt; (5 seconds each)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;50 classes&lt;/strong&gt; of environmental sounds&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;5 folds&lt;/strong&gt; for cross-validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With only 40 samples per class, this is a challenging dataset that rewards good representations over brute-force memorization.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-supervised-baseline&quot;&gt;The Supervised Baseline&lt;/h2&gt;

&lt;h3 id=&quot;resnet-34-from-scratch&quot;&gt;ResNet-34 from Scratch&lt;/h3&gt;

&lt;p&gt;Classic approach: convert audio to mel spectrograms and treat them as images for a CNN.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;learning_rate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1e-3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;optimizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AdamW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;weight_decay&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.01&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;scheduler&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OneCycleLR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Result: 80.5% accuracy&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;test-time-augmentation&quot;&gt;Test-Time Augmentation&lt;/h3&gt;

&lt;p&gt;I noticed validation performance was inconsistent, so I added TTA, averaging predictions across multiple augmented versions of each test sample.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;predict_with_tta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spectrogram&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_augments&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;predictions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_augments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aug_spec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apply_augmentation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spectrogram&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pred&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aug_spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;predictions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pred&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;predictions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dim&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Result: 83.5% accuracy&lt;/strong&gt; (+3% from TTA alone!)&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;transfer-learning-detour&quot;&gt;Transfer Learning Detour&lt;/h2&gt;

&lt;p&gt;Before diving into SSL, I tried transfer learning with ImageNet-pretrained EfficientNet-B0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: 81.5% accuracy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This performed &lt;em&gt;worse&lt;/em&gt; than my from-scratch ResNet. ImageNet features (edges, textures, objects) don’t transfer perfectly to spectrograms. Good to know.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;self-supervised-learning-experiments&quot;&gt;Self-Supervised Learning Experiments&lt;/h2&gt;

&lt;p&gt;This is where things got interesting. I wanted to understand &lt;em&gt;why&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; SSL works, not just use pretrained models.&lt;/p&gt;

&lt;h3 id=&quot;simclr-contrastive-learning&quot;&gt;SimCLR (Contrastive Learning)&lt;/h3&gt;

&lt;p&gt;The idea: learn representations by pulling augmented views of the same audio together while pushing different audios apart.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Audio → Mel Spectrogram → ResNet Encoder → Projection Head → Contrastive Loss
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key components:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;NT-Xent Loss:&lt;/strong&gt; Normalized Temperature-scaled Cross Entropy&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Projection Head:&lt;/strong&gt; 512 → 512 → 128 (discarded after pretraining)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Temperature:&lt;/strong&gt; 0.5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result: 82.5% accuracy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But here’s the catch: the contrastive task itself reached &lt;strong&gt;98% accuracy during pretraining&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The model was solving a nearly trivial task.&lt;/p&gt;

&lt;p&gt;Environmental sounds in ESC-50 are already highly separable in the spectral domain (dog bark vs chainsaw vs rain, etc.). With relatively mild augmentations, the identity of each sound barely changes. That means the encoder can solve the contrastive objective using coarse cues (energy bands, temporal envelope) instead of learning robust, invariant representations.&lt;/p&gt;

&lt;p&gt;In other words, the model learned:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“these sounds are different”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;instead of:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“these two transformed versions of the same sound are meaningfully the same”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Contrastive learning only works when the task is hard enough. You need augmentations and negatives that force the model to learn invariances, not shortcuts.&lt;/p&gt;

&lt;p&gt;Even with this limitation, the learned representation still reached 82.5% accuracy, which is competitive with standard supervised CNN baselines on ESC-50.&lt;/p&gt;

&lt;h3 id=&quot;masked-prediction-beats-style&quot;&gt;Masked Prediction (BEATs-style)&lt;/h3&gt;

&lt;p&gt;Inspired by BERT and BEATs: hide patches of the spectrogram and predict discrete tokens.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This was a simplified, educational implementation to understand the BEATs framework, not a full reproduction. The real BEATs includes iterative tokenizer refinement, larger models (90M+ params), and training on millions of samples. My goal was to grasp the core concepts: patch embeddings, masking, and discrete token prediction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Spectrogram → Patch Embedding → Encoder → Predict Masked Tokens
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The approach:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Split spectrogram into patches (16×16)&lt;/li&gt;
  &lt;li&gt;Randomly mask 40% of patches&lt;/li&gt;
  &lt;li&gt;Predict the cluster ID of masked patches&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The tokenizer (codebook):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;K-means clustering on spectrogram patches&lt;/li&gt;
  &lt;li&gt;Each patch gets assigned to nearest centroid&lt;/li&gt;
  &lt;li&gt;Model predicts which cluster the masked patch belongs to&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Tokenizing a patch
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;distances&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codebook&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;²&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Distance to each centroid
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argmin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;distances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;# Closest centroid = token ID
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Result: 74.5% accuracy&lt;/strong&gt; (with CNN encoder)&lt;/p&gt;

&lt;p&gt;Underperformed the supervised baseline. Hmm.&lt;/p&gt;

&lt;h3 id=&quot;transformer-beats&quot;&gt;Transformer-BEATs&lt;/h3&gt;

&lt;p&gt;I tried a full Transformer encoder instead of CNN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;6 Transformer layers&lt;/li&gt;
  &lt;li&gt;256 embedding dimension&lt;/li&gt;
  &lt;li&gt;8 attention heads&lt;/li&gt;
  &lt;li&gt;~5M parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result: 40% accuracy&lt;/strong&gt; (nearly random, FAAAHH!!)&lt;/p&gt;

&lt;p&gt;This was humbling. Transformers need &lt;em&gt;massive&lt;/em&gt; amounts of data. With only 1,600 training samples, it couldn’t learn meaningful patterns. CNNs have stronger inductive biases that help with limited data.&lt;/p&gt;

&lt;hr /&gt;

&lt;div style=&quot;max-width: 400px; margin: 2rem auto;&quot;&gt;
  &lt;div class=&quot;tenor-gif-embed&quot; data-postid=&quot;13987821127347517247&quot; data-share-method=&quot;host&quot; data-aspect-ratio=&quot;1.74074&quot; data-width=&quot;100%&quot;&gt;&lt;a href=&quot;https://tenor.com/view/call-an-ambulance-but-not-for-me-but-not-for-me-call-an-ambulance-gif-13987821127347517247&quot;&gt;Call An Ambulance But Not For Me GIF&lt;/a&gt;from &lt;a href=&quot;https://tenor.com/search/call+an+ambulance+but+not+for+me-gifs&quot;&gt;Call An Ambulance But Not For Me GIFs&lt;/a&gt;&lt;/div&gt;
  &lt;script type=&quot;text/javascript&quot; async=&quot;&quot; src=&quot;https://tenor.com/embed.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-beats-revelation&quot;&gt;The BEATs Revelation&lt;/h2&gt;

&lt;p&gt;After all my experiments, I tried Microsoft’s BEATs model, pretrained on AudioSet (2 million+ clips).&lt;/p&gt;

&lt;p&gt;I tested two approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frozen encoder:&lt;/strong&gt; Only train a new classifier head on top.&lt;br /&gt;
&lt;strong&gt;Result: 94.50% accuracy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-tuned with differential learning rates:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;param_groups&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;params&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;lr&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1e-5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# Slow for pretrained
&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;params&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;classifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;lr&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1e-3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Fast for new head
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Result: 95.25% accuracy&lt;/strong&gt; 🎯&lt;/p&gt;

&lt;p&gt;The frozen approach gets you 94.5% with minimal compute. The AudioSet pretraining is &lt;em&gt;that&lt;/em&gt; good. Fine-tuning squeezes out another 0.75%, which matters if you’re chasing leaderboards but honestly? Frozen is probably fine for most use cases.&lt;/p&gt;

&lt;p&gt;Even though I knew I probably wouldn’t make a dent against the pretrained model, I had to try it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;things-i-had-to-figure-out&quot;&gt;Things I Had to Figure Out&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What are “tokens” in audio?&lt;/strong&gt; In BEATs, tokens are discrete IDs representing audio patterns. K-means clustering groups similar spectrogram patches, and each patch gets a cluster ID. It’s like creating a vocabulary of audio building blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why iterate on the tokenizer?&lt;/strong&gt; Clever trick: the tokenizer and model improve each other. Each iteration creates more meaningful tokens, forcing the model to learn finer distinctions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why two learning rates?&lt;/strong&gt; Differential rates balance preservation and adaptation. Without this, you either destroy pretrained features (high LR) or the classifier never converges (low LR).&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;final-results&quot;&gt;Final Results&lt;/h2&gt;

&lt;table style=&quot;width:100%; border-collapse: collapse; margin: 1.5rem 0;&quot;&gt;
  &lt;thead&gt;
    &lt;tr style=&quot;background: #2a2a2a; color: #fff;&quot;&gt;
      &lt;th style=&quot;padding: 12px 15px; text-align: left;&quot;&gt;Method&lt;/th&gt;
      &lt;th style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;Accuracy&lt;/th&gt;
      &lt;th style=&quot;padding: 12px 15px; text-align: left;&quot;&gt;Approach&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr style=&quot;background: #e8f5e9;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px; font-weight: bold;&quot;&gt;BEATs (fine-tuned)&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center; font-weight: bold;&quot;&gt;95.25%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Pretrained + differential LR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #f1f8e9;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;BEATs (frozen)&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;94.50%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Pretrained, classifier only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #fff;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;ResNet-34 + TTA&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;83.50%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Supervised baseline&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #f5f5f5;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;SimCLR&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;82.50%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Contrastive SSL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #fff;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;EfficientNet-B0&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;81.50%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;ImageNet transfer&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #f5f5f5;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;CNN-BEATs&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;74.50%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Masked prediction&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #ffebee;&quot;&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Transformer-BEATs&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px; text-align: center;&quot;&gt;40.00%&lt;/td&gt;
      &lt;td style=&quot;padding: 12px 15px;&quot;&gt;Tomfoolery 💀&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The gap between my from-scratch SSL attempts and the pretrained BEATs tells the whole story.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Scale matters enormously for SSL.&lt;/strong&gt; My weekend experiments couldn’t match 2M-sample pretraining, but they taught me &lt;em&gt;how&lt;/em&gt; these methods work.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Contrastive learning needs hard negatives.&lt;/strong&gt; Trivial pretext tasks don’t force useful representations.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Transformers are data-hungry.&lt;/strong&gt; CNNs win on small datasets.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;TTA is free accuracy.&lt;/strong&gt; 3% boost for minimal effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why I love jumping between domains: contrastive learning from NLP, masked prediction from BERT, spectrogram tricks from signal processing. The dots connect in unexpected ways.&lt;/p&gt;

&lt;p&gt;The real takeaway? Running these experiments taught me more than any tutorial ever could. Sometimes you have to build the thing yourself to understand why it works, and why it doesn’t.&lt;/p&gt;

&lt;p&gt;What surprised me most is that self-supervised learning doesn’t automatically produce meaningful representations. It will happily learn shortcuts if the task allows it. Designing the right objective turns out to matter just as much as the model itself.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>I'm Back</title>
   <link href="http://mez.sh/2026/02/16/im-back/"/>
   <updated>2026-02-16T00:00:00+00:00</updated>
   <id>http://mez.sh/2026/02/16/im-back</id>
   <content type="html">&lt;p&gt;It’s been a while. Almost nine years since my last post. Life happened, priorities shifted, and this blog sat quietly collecting dust.&lt;/p&gt;

&lt;p&gt;A lot has changed. I have a family now, including a 13-month-old daughter who’s been teaching me more about persistence and curiosity than any codebase ever could.&lt;/p&gt;

&lt;p&gt;But I’ve missed having a place to think out loud. So here I am, back at it.&lt;/p&gt;

&lt;p&gt;No grand promises, just an intention: write more, share ideas, and see where it goes.&lt;/p&gt;

&lt;p&gt;Let’s ship.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>HomographyNet: Deep Image Homography Estimation</title>
   <link href="http://mez.sh/2017/07/21/homographynet-deep-image-homography-estimation/"/>
   <updated>2017-07-21T00:00:00+00:00</updated>
   <id>http://mez.sh/2017/07/21/homographynet-deep-image-homography-estimation</id>
   <content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Today we are going to talk about a paper I read a month ago titled &lt;a href=&quot;https://arxiv.org/abs/1606.03798&quot;&gt;Deep Image Homography Estimation&lt;/a&gt;. It is a paper that presents a deep convolutional neural network for estimating the relative homography between a pair of images.&lt;/p&gt;

&lt;h3 id=&quot;what-is-a-homography&quot;&gt;&lt;em&gt;What is a Homography?&lt;/em&gt;&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;In projective geometry, a homography is an isomorphism of projective spaces, induced by an isomorphism of the vector spaces from which the projective spaces derive. - &lt;a href=&quot;https://en.wikipedia.org/wiki/Homography&quot;&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you understood that, go ahead and skip directly to the deep learning model discussion. For the rest of us, let’s go ahead and learn some terminology. If you can forget about that definition from Wikipedia for a second, let’s use some anologies to at least get a high-level idea.&lt;/p&gt;

&lt;h3 id=&quot;isomorphism&quot;&gt;Isomorphism?&lt;/h3&gt;
&lt;p&gt;If you decide to bake a cake; the process of taking the ingredients and following the recipe to create the cake could be seen as morphing. The recipe allowed you to morph/transition from ingredients to a cake. If you’re thinking wtf, just stick with me a bit longer. Now, imagine the same recipe could allow you to take a cake and morph/transition back to the ingredients if you follow it in reverse! In a nutshell, that is the definition of an isomorphism. A recipe that can allow you to transition between the two, loosely speaking.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Isomorphism in mathematics is morphism or a mapping (recipe) that can also give you the inverse, again loosely speaking.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;projective-geometry&quot;&gt;Projective geometry?&lt;/h3&gt;
&lt;p&gt;I don’t have a clever analogy here, but I’ll give you a simple example to tie it all together. Imagine you’re driving and your dash cam snapped a picture of the road in front of you (let’s call this pic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt;); at the same time, imagine there was a drone right above you and it also took a picture of the road in front of you (let’s call this pic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt;). You can see that pic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; are related, but how? They’re both pictures of the road in front you, only difference is the perspective! The big question….&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Is there a recipe/isomorphism that can take you from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; and vice versa?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There you go, the question you just asked is what a Homography tries to answer. Homography is an isomorphism of perspectives. A 2D homography between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; would give you the projection transformation between the two images! It is a 3x3 matrix that descibes the affine transformation. Entire books are written on these concepts, but hopefully we now have the general idea to continue.&lt;/p&gt;

&lt;h3 id=&quot;note&quot;&gt;&lt;em&gt;NOTE&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;There are some constraints about a homography I have not mentioned such as:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Usually both images are taken from the same camera.&lt;/li&gt;
  &lt;li&gt;Both images should be viewing the same plane.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;/h2&gt;

&lt;h3 id=&quot;where-is-homography-used&quot;&gt;&lt;em&gt;Where is homography used?&lt;/em&gt;&lt;/h3&gt;

&lt;p&gt;To name a few, the homography is an essential part of the following&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Creating panoramas&lt;/li&gt;
  &lt;li&gt;Monocular SLAM&lt;/li&gt;
  &lt;li&gt;3D image reconstruction&lt;/li&gt;
  &lt;li&gt;Camera calibration&lt;/li&gt;
  &lt;li&gt;Augmented Reality&lt;/li&gt;
  &lt;li&gt;Autonomous Cars&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;how-do-you-get-the-homography&quot;&gt;&lt;em&gt;How do you get the homography?&lt;/em&gt;&lt;/h3&gt;

&lt;p&gt;In traditional computer vision, the homography estimation process is done in two stages.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Corner estimation&lt;/li&gt;
  &lt;li&gt;Homography estimation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Due to the nature of this problem, these pipelines are only producing estimates. To make the estimates more robust, practitioners go as far as manually engineering corner-ish features, line-ish features etc as mentioned by the paper.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Robustness is introduced into the corner detection stage by returning a large and over-complete set of points, while robustness into the homography estimation step shows up as heavy use of RANSAC or robustification of the squared loss function. - &lt;a href=&quot;https://arxiv.org/abs/1606.03798&quot;&gt;Deep Image Homography Estimation&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is a very hard problem that is error-prone and requires heavy compute to get any sort of robustness. Here we are, finally ready to talk about the question this paper wants to answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Is there a single robust algorithm that, given a pair of images, simply returns the homography relating the pair?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;homographynet-the-model&quot;&gt;HomographyNet: The Model&lt;/h2&gt;

&lt;p&gt;HomographyNet is a VGG style CNN which produces the homography relating two images. The model doesn’t require a two stage process and all the parameters are trained in an end-to-end fashion!&lt;br /&gt;
&lt;img src=&quot;/public/img/hn/homographynet.png&quot; alt=&quot;alt HomographyNet Model&quot; /&gt;&lt;/p&gt;

&lt;p&gt;HomographyNet as descibed in the paper comes in two flavors, classification and regression. Based on the version you decide to use, they have their own pros and cons. Including different loss functions.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/hn/twoheads.png&quot; alt=&quot;alt Classification HomographyNet vs Regression HomographyNet.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The regression network produces eight real-valued numbers and uses the Euclidean (L2) loss as the final layer. The classification network uses a quantization scheme and uses a softmax as the final layer. Since they stick a continuous range into finite bins, they end up with quantization error; hence, the classification network additionally also produces a confidence for each of the corners produced. The paper uses 21 quantization bins for each of the eight output dimensions, which results in a final layer of 168 output neurons. Each of the corner’s 2D grid of scores are interpreted as a distribution.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/hn/classificationconfidence.png&quot; alt=&quot;alt Corner Confidences Measure&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The architecture seems simple enough, but how is the model trained? Where is the labeled dataset coming from? Glad you asked!&lt;/p&gt;

&lt;p&gt;As the paper states and I agree, the simplist way to parameterize a homography is with a 3x3 matrix and a fixed scale. In the 3x3 homography matrix, [H11:H21, H12:H22] are responsible for the rotation and [H13:H23] handle the translational offset. This way you can map each pixel at position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[u,v,1]&lt;/code&gt; from the image against the homograpy like the figure below, to get the new projected transformation &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[u&apos;,v&apos;,1]&lt;/code&gt;.&lt;br /&gt;
&lt;img src=&quot;/public/img/hn/homographymatrix.png&quot; alt=&quot;alt HomographyNet 3x3 matrix&quot; /&gt;&lt;/p&gt;

&lt;p&gt;However! If you try to unroll the 3x3 matrix and use it as the label (ground truth), you’d end up mixing the rotational and translational components. Creating a loss function that balances the mixed components would have been an unnecessary hurdle. Hence, it was better to use well known loss functions (L2 and Cross-Entropy) and instead figure out a way to &lt;em&gt;re-parameterize&lt;/em&gt; the homography matrix!&lt;/p&gt;

&lt;h3 id=&quot;the-4-point-homography-parameterization&quot;&gt;The 4-Point Homography Parameterization&lt;/h3&gt;

&lt;p&gt;The 4-Point parameterization is based on corner locations, removing the need to store rotational and translation terms in the label! This is not a new method by any means, but the use of it as a label was clever! Here is how it works.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/hn/4point.png&quot; alt=&quot;alt 4-Point Homography Parameterization&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Earlier we saw how each pixel &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[u,v,1]&lt;/code&gt; was transformed by the 3x3 matrix to produce &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[u&apos;,v&apos;,1]&lt;/code&gt;.
Well if you have at least four pixels where you calculate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delta_u = u&apos; - u&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delta_v = v&apos; - v&lt;/code&gt;
then it is possible to reconstruct the 3x3 homography matrix that was used! You could for example use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getPerspectiveTransform()&lt;/code&gt; method in OpenCV. From here on out we will call these four pixels (points) as corners.&lt;/p&gt;

&lt;h3 id=&quot;training-data-generation&quot;&gt;Training Data Generation&lt;/h3&gt;

&lt;p&gt;Now that we have a way to represent the homography such that we can use well known loss functions, we can start talking about how the training data is generated.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
    &lt;img src=&quot;/public/img/hn/datagen.png&quot; alt=&quot;Training Data Generation&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;As a sidenote, &lt;em&gt;H&lt;sup&gt;AB&lt;/sup&gt; denotes the homorgraphy matrix between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Creating the dataset was pretty straightforward, so I’ll only highlight some of the things that might get tricky. The steps as the describe in the above figure are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Randomly crop a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;128x128&lt;/code&gt; patch at position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; from the grayscale image &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I&lt;/code&gt; and call it patch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt;. &lt;em&gt;Staying away from the edges!&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Randomly perturb the four corners of patch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; within the range &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[-rho, rho]&lt;/code&gt; and let’s call this new position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&apos;&lt;/code&gt;. &lt;em&gt;The paper used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rho=32&lt;/code&gt;&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Compute the homography of patch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; using position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&apos;&lt;/code&gt; and we’ll call this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;H&lt;/code&gt;&lt;sup&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AB&lt;/code&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;Take the inverse of the homography (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;H&lt;/code&gt;&lt;sup&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AB&lt;/code&gt;&lt;/sup&gt;)&lt;sup&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-1&lt;/code&gt;&lt;/sup&gt; which equals &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;H&lt;/code&gt;&lt;sup&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BA&lt;/code&gt;&lt;/sup&gt; and apply that to image &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I&lt;/code&gt;, calling this new image &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I&apos;&lt;/code&gt;. Crop a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;128x128&lt;/code&gt; patch at position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; from image &lt;strong&gt;&lt;em&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I&apos;&lt;/code&gt;&lt;/em&gt;&lt;/strong&gt; and call it patch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Finally, take patch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; and stack them channel-wise. This will give you a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;128x128x2&lt;/code&gt; image that you’ll use as input to the models! The label would be the 4-point parameterization of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;H&lt;/code&gt;&lt;sup&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AB&lt;/code&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They cleverly used this five step process on random images from the &lt;em&gt;MS-COCO&lt;/em&gt; dataset to create 500,000 training examples. Pretty damn cool (excuse my english).&lt;/p&gt;

&lt;h2 id=&quot;the-code&quot;&gt;The Code&lt;/h2&gt;

&lt;p&gt;If you’d like to see the regression network coded up in Keras &lt;em&gt;and&lt;/em&gt; the data generation process visualized, you’re in luck! Follow the links below to my Github repo.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mez/deep_homography_estimation/blob/master/HomograpyNET.ipynb&quot;&gt;HomographyNet Regression variant&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mez/deep_homography_estimation/blob/master/Dataset_Generation_Visualization.ipynb&quot;&gt;Dataset Generation Visualization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;

&lt;p&gt;That pretty much highlights the major parts of the paper. I am hoping you now have an idea of what the paper was about and learned something new! Go read the paper because I didn’t talk about the results etc.&lt;/p&gt;

&lt;p&gt;I’d like to think easy future improvements could be to swap out the heavy VGG network for squeezenet! Giving you the improvement of a smaller network. I’ll maybe experiment with this idea and see if I can match or improve on their results. As for possible uses today, I could see this network being used as a cascade classifier. The traditional methods are very compute heavy, so if we could maybe have this network in front to filter out the easy wins, we could cut down on compute cost.&lt;/p&gt;

&lt;p&gt;Stay tuned for the next paper and please comment with any corrections or thoughts. That is all folks!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>SqueezeDet: Deep Learning for Object Detection</title>
   <link href="http://mez.sh/2017/04/21/squeezedet-deep-learning-for-object-detection/"/>
   <updated>2017-04-21T00:00:00+00:00</updated>
   <id>http://mez.sh/2017/04/21/squeezedet-deep-learning-for-object-detection</id>
   <content type="html">&lt;h3 id=&quot;why-bother-writing-this-post&quot;&gt;&lt;em&gt;Why bother writing this post?&lt;/em&gt;&lt;/h3&gt;

&lt;p&gt;Often, examples you see around computer vision and deep learning is about classification. Those class of problems are asking what do you see in the image? Object detection is another class of problems that ask where in the image do you see it?&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Classification answers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;what&lt;/code&gt; and Object Detection answers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;where&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Object detection has been making great advancement in recent years. The &lt;a href=&quot;https://en.wikipedia.org/wiki/%22Hello,_World!%22_program&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;hello world&lt;/em&gt;&lt;/a&gt; of object detection would be using &lt;a href=&quot;https://en.wikipedia.org/wiki/Histogram_of_oriented_gradients&quot; target=&quot;_blank&quot;&gt;HOG&lt;/a&gt; features combined with a classifier like &lt;a href=&quot;https://en.wikipedia.org/wiki/Support_vector_machine&quot; target=&quot;_blank&quot;&gt;SVM&lt;/a&gt; and using sliding windows to make predictions at different patches of the image. This complex pipeline has a major drawback!&lt;/p&gt;

&lt;h3 id=&quot;cons&quot;&gt;Cons:&lt;/h3&gt;
&lt;ol&gt;
  &lt;li&gt;Computationally expensive.&lt;/li&gt;
  &lt;li&gt;Multiple step pipeline.&lt;/li&gt;
  &lt;li&gt;Requires feature engineering.&lt;/li&gt;
  &lt;li&gt;Each step in the pipeline has parameters that need to be tuned individually, but can only be tested together. Resulting in a complex trial and error process that is not unified.&lt;/li&gt;
  &lt;li&gt;Not realtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;pros&quot;&gt;Pros:&lt;/h3&gt;
&lt;ol&gt;
  &lt;li&gt;Easy to implement, relatively speaking…&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Speed becomes a major concern when we are thinking of running these models on the edge (IoT, mobile, cars). For example, a car needs to detect where other cars, people and bikes are to name a few; I could go on… puppies, kittens… you get the idea. The major motivation for me is the need for speed given the constraints that edge computes have; we need compact models that can make quick predictions and are energy efficient.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-squeezedet-model&quot;&gt;The SqueezeDet Model&lt;/h2&gt;
&lt;p&gt;The latest in object detection is to use a convolutional neural network (CNN) that outputs a regression to predict the bounding boxes. This post is about &lt;a href=&quot;https://arxiv.org/abs/1612.01051&quot; target=&quot;_blank&quot;&gt;SqueezeDet&lt;/a&gt;. I got interested because they used one of my favorite cnn, SqueezeNet! You can read my last post on &lt;a href=&quot;https://mez.github.io/deep%20learning/2017/02/14/mainsqueeze-the-52-parameter-model-that-drives-in-the-udacity-simulator/&quot; target=&quot;_blank&quot;&gt;SqueezeNet&lt;/a&gt; if you haven’t yet. To be fair SqueezeDet is pretty much just the &lt;a href=&quot;https://pjreddie.com/media/files/papers/yolo.pdf&quot; target=&quot;_blank&quot;&gt;YOLO&lt;/a&gt; model that uses a SqueezeNet.&lt;/p&gt;

&lt;h3 id=&quot;highlevel-squeezedet&quot;&gt;Highlevel SqueezeDet&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/sd/squeezedet.png&quot; alt=&quot;alt SqueezeDet Model&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Inspired by YOLO, SqueezeDet is a single stage detection pipeline that does region proposal and classification by one single network. The cnn first extracts feature maps from the input image and feeds it to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvDet&lt;/code&gt; layer. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvDet&lt;/code&gt; takes the feature maps, overlays them with a WxH grid and at each cell computes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; pre-computed bounding boxes called anchors. Each bounding box has the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Four scalars (x, y, w, h)&lt;/li&gt;
  &lt;li&gt;A confidence score ( Pr(Object)xIOU )&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; conditional classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hence SqueezeDet has a fixed output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WxHxK(4+1+C)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final step is to use non max suppression aka &lt;a href=&quot;http://www.pyimagesearch.com/2014/11/17/non-maximum-suppression-object-detection-python/&quot; target=&quot;_blank&quot;&gt;NMS&lt;/a&gt; to filter the bounding boxes to make the final predictions.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/sd/kanchors.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The networks regresses and learns how to transform the highest probably bounding box for the prediction. Since there are bounding boxes being generated at each cells of the grid, the top &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; bounding boxes sorted by the confidence score are kept as the predictions.&lt;/p&gt;

&lt;h3 id=&quot;the-composite-loss-function&quot;&gt;The Composite Loss function&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/public/img/sd/squeezedet_loss.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The figure above is the four part loss function that makes this entire model possible. Don’t get intimidated by it; let’s take it apart and see how it fits together. Each loss function is described below and highlighted:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: The yellow that bleed into the blue loss function is actually suppose to be blue, sorry!&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Yellow: Regression of the scalars for the anchors&lt;/li&gt;
  &lt;li&gt;Green: The confidence score regression which uses &lt;a href=&quot;http://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/&quot; target=&quot;_blank&quot;&gt;IOU&lt;/a&gt; of ground and predicted bounding boxes.&lt;/li&gt;
  &lt;li&gt;Blue: Penalize anchors that are not responsible for detection by dropping their confidence score.&lt;/li&gt;
  &lt;li&gt;Pink: The cross entropy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;using-squeezedet&quot;&gt;Using SqueezeDet&lt;/h2&gt;

&lt;p&gt;The authors of the paper did implement the model via TensorFlow!
Go check it out on github &lt;a href=&quot;https://github.com/BichenWuUCB/squeezeDet&quot; target=&quot;_blank&quot;&gt;https://github.com/BichenWuUCB/squeezeDet&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;thresholding-squeezedet&quot;&gt;Thresholding SqueezeDet&lt;/h3&gt;

&lt;p&gt;You have to tweak how confident or doubtful you want the model to be; the predictions are centered around &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; bounding boxes at each cell. So we have to use the top &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; bouding boxes, sorted by confidence score and then you can do additional thresholding on the class conditional probability score.&lt;/p&gt;

&lt;p&gt;Here is an example of a recent project I did where I tweak the params:&lt;/p&gt;

&lt;p&gt;Like the paper:
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N = 64&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The image below shows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mc.PLOT_PROB_THRESH      = 0.1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/img/sd/without_thres_test5.jpg&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The image below shows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mc.PLOT_PROB_THRESH      = 0.5&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;&lt;img src=&quot;/public/img/sd/out_test5.jpg&quot; alt=&quot;alt text&quot; /&gt;&lt;/h2&gt;

&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;

&lt;p&gt;Reading through the paper was a real grind. Some math notations were a bit wonky; the paper referenced a lot and it was a recursive process. I literally had to crunch through the entire history of object detection to understand this paper. At the very least I hope you were able to get a high level understanding of this paper. Comment below with any corrections or questions!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>MainSqueeze: The 52 parameter model that drives in the Udacity simulator</title>
   <link href="http://mez.sh/deep%20learning/2017/02/14/mainsqueeze-the-52-parameter-model-that-drives-in-the-udacity-simulator/"/>
   <updated>2017-02-14T00:00:00+00:00</updated>
   <id>http://mez.sh/deep%20learning/2017/02/14/mainsqueeze-the-52-parameter-model-that-drives-in-the-udacity-simulator</id>
   <content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;What a time to be alive! The year is 2017, Donald Trump is president of the United States of America and autonomous vehicles are all the rage. Still at its infancy, the winning solution to dominate the mass production of autonomous vehicles are ongoing. The two main factions currently are the robotics approach and the end-to-end neural networks approach. Like the four seasons, the AI winter has come and gone. It’s Spring and this is the story of one man’s attempt to explore the pros and cons of the end-to-end neural networks faction in a controlled environment. The hope is to draw some conclusions that will help the greater community advance as a whole.&lt;/p&gt;

&lt;h2 id=&quot;the-controlled-environment&quot;&gt;The Controlled Environment&lt;/h2&gt;

&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/sim_image.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/udacity/self-driving-car-sim&quot;&gt;Udacity Simulator&lt;/a&gt; which is open sourced will be our controlled environment for this journey. It has two modes, training and autonomous mode. Training mode is for the human to drive and record/collect the driving. The result would be a directory of images from three cameras (left, center, right) and a driver log CSV file that records the image along with steering angle, speed etc. Autonomous mode requires a model that can send the simulator steering angle predictions.&lt;/p&gt;

&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/driver_log.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;The goals of this project are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Use the simulator to collect data of good driving behavior.&lt;/li&gt;
  &lt;li&gt;Construct a convolution neural network in &lt;a href=&quot;https://keras.io/&quot;&gt;Keras&lt;/a&gt; that predicts steering angles from images.&lt;/li&gt;
  &lt;li&gt;Train and validate the model with a training and validation set.&lt;/li&gt;
  &lt;li&gt;Test that the model successfully drives around track one without leaving the road!&lt;/li&gt;
  &lt;li&gt;Draw conclusions for future work.
&lt;!-- more --&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;data-exploration&quot;&gt;Data Exploration&lt;/h2&gt;

&lt;p&gt;If you think the Lewis and Clark expedition was tough, try exploring an unknown dataset. I am being dramatic. When you are exploring data; you want to keep thinking, what is the least amount of data you can sample which will represent your problem (population if you like stats). For this problem we will be using the provided Udacity dataset. Let’s dive in!&lt;/p&gt;

&lt;p&gt;Our goal is steering angle prediction, so let’s take a look at what the dataset shows us! The plot below shows a few take aways:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Range is [-1,1]&lt;/li&gt;
  &lt;li&gt;Clustering around [-0.5, 0.5]&lt;/li&gt;
&lt;/ol&gt;

&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/data1.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;Let’s take a look at another angle, pun intended, of the steering data.&lt;/p&gt;

&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/data2.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;This histogram is the making of sampling nightmares; this is how alternate facts are created! Unbalanced data sampling would train our model to be very biased so we will clean this up.&lt;/p&gt;

&lt;h2 id=&quot;clean-up-steps&quot;&gt;Clean up steps:&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;Downsample the over represented examples&lt;/li&gt;
  &lt;li&gt;Upsample the under represented examples&lt;/li&gt;
  &lt;li&gt;Expose more varied examples to try and represent a uniform distribution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;1-downsample&quot;&gt;1. Downsample&lt;/h3&gt;

&lt;p&gt;Steering angle zero is over represented, so drop 90% of the examples. Easy!&lt;/p&gt;

&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/data3v2.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;h3 id=&quot;2-upsample&quot;&gt;2. Upsample&lt;/h3&gt;

&lt;p&gt;Time to augment so we can start upsampling under represented examples. We start by flipping 40% of the examples that are not zero steering angle. So far so good!&lt;/p&gt;
&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/data4.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;h3 id=&quot;3-expose-more-varied-examples&quot;&gt;3. Expose more varied examples&lt;/h3&gt;

&lt;p&gt;Variety is the spice of life; this is also true to training a well generalized model. For this problem we will introduce more steering angles by shifting the examples horizontally and adding or subtracting the appropriate angles corresponding to the shift we performed. There was no magic shift amount, you get this by experimenting. The result is below:&lt;/p&gt;
&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/data5.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;Great our dataset/sample is looking better. Next, let’s trim this to look more uniform. This was accomplished by:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Grabbing the bin ranges of the 100 bins.&lt;/li&gt;
  &lt;li&gt;Finding bins that have more than 400 examples.&lt;/li&gt;
  &lt;li&gt;Randomly sample and drop examples so we have no more than 400 examples.&lt;/li&gt;
&lt;/ol&gt;

&lt;div align=&quot;center&quot;&gt;
   &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/data6v2.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;How did I know 400 examples was enough? I simply kept reducing the number until I had reached a point where my model can still produced stable results. Below 400 my model started getting unstable results.&lt;/p&gt;

&lt;h2 id=&quot;model-architecture-and-training-strategy&quot;&gt;Model Architecture and Training Strategy&lt;/h2&gt;

&lt;h4 id=&quot;main-goals-for-us-to-think-about-while-creating-a-model&quot;&gt;Main goals for us to think about while creating a model&lt;/h4&gt;
&lt;ol&gt;
  &lt;li&gt;Is it efficient for the task at hand?&lt;/li&gt;
  &lt;li&gt;If this was to be downloaded onto hardware in a car what would the power consumption and usability be like?
    &lt;ul&gt;
      &lt;li&gt;There is an interesting paper called &lt;a href=&quot;https://arxiv.org/abs/1510.00149&quot;&gt;Deep Compression&lt;/a&gt;, which we don’t implement here but it is food for thought and shows that model can be tiny and still work!&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I started with a modified comma.ai model and I had a successful model, but it needed 300,000 trainable parameters. Reading the Deep compression paper and a blog post titled &lt;a href=&quot;https://medium.com/@xslittlegrass/self-driving-car-in-a-simulator-with-a-tiny-neural-network-13d33b871234#.x1kdv5hgt&quot;&gt;Self-driving car in a simulator with a tiny neural network&lt;/a&gt; I quickly realized that we can do better. The blog post linked shows a 63 parameter model that uses tiny images and a small network to get a stable model. I wanted to experiment and see if I could make it smaller and still stable. The solution developed over a number of experiments is a modified &lt;a href=&quot;https://arxiv.org/abs/1602.07360&quot;&gt;SqueezeNet&lt;/a&gt; implementation. With a squeeze net you get three additional hyperparameters that are used to generate the fire module:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;S1x1: Number of 1x1 kernels to use in the squeeze layer within the fire module&lt;/li&gt;
  &lt;li&gt;E1x1: Number of 1x1 kernels to use in the expand layer within the fire module&lt;/li&gt;
  &lt;li&gt;E3x3: Number of 3x3 kernels to use in the expand layer within the fire module&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;fire-module-zoomed-in&quot;&gt;Fire Module Zoomed In&lt;/h4&gt;

&lt;div align=&quot;center&quot;&gt;
  &lt;img src=&quot;/public/img/bc/fire_module.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;The fire module is the workhorse of squeezenet. Sqeezenet as described in the paper is around 700k trainable parameters. I went through a process, where I kept reducing the number of parameters while all other variables were kept constant. You get the theme here? We are at the frontier and this calls for empirical testing. Through some experiments I went from 10k parameters to 1005, then 329, then 159, then &lt;strong&gt;63&lt;/strong&gt; and finally &lt;strong&gt;52&lt;/strong&gt;!!&lt;/p&gt;

&lt;h2 id=&quot;the-final-52-parameter-squeezenet-variant-model&quot;&gt;The final 52 parameter squeezenet variant Model!!&lt;/h2&gt;

&lt;div align=&quot;left&quot;&gt;
  &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/SqueezeNet52.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;p&gt;This model combats overfitting by being super tiny and for kicks I added a small dropout layer. The model works on both tracks and has a six second epoch on my late 2012 Macbook air!! From the comma.ai model, it was evident that a validation loss of around 0.03 on 30% of &lt;strong&gt;this&lt;/strong&gt; dataset results in a stable model that can handle the track at a throttle of around 0.2, which is a speed of around 20mph in the simulator. So, I didn’t bother worrying about the epoch hyperparameter. I simply created a custom early termination Keras callback that stopped the training when we hit our requirement.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;One good rule of thumb I developed from this project is &lt;strong&gt;to try and reduce the number of variables you are tuning to gain better results faster.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;training-strategy&quot;&gt;Training Strategy&lt;/h2&gt;

&lt;p&gt;To get the model to drive in the simulator, you need:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Show the model how to drive straight&lt;/li&gt;
  &lt;li&gt;How to recover if it drifts off track&lt;/li&gt;
  &lt;li&gt;How to handle turns&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Udacity slack community was a huge help here; from their experience, I used the left and right camera images and adjusted the steering (+.25 for left, -.25 for right) angles to show the model how to correct steering back to center. Then I used the horizontal shifting to capture more angles. This was enough to get a stable model working on the fastest setting (means lowest resolution) on both tracks.&lt;/p&gt;

&lt;p&gt;Reducing input image size was the next challenge. We do this by first cropping the top and bottom from the image that would be noise to the model. Then we resize the image to (64,64) and convert to HSV and only returning the S channel. This takes us from (160,320,3) to (64, 64, 1)!!&lt;/p&gt;

&lt;h3 id=&quot;before-cropping-and-resizing&quot;&gt;Before Cropping and Resizing.&lt;/h3&gt;
&lt;div align=&quot;left&quot;&gt;
  &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/crop1.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;h3 id=&quot;after-cropping-and-resizing&quot;&gt;After Cropping and Resizing.&lt;/h3&gt;
&lt;div align=&quot;left&quot;&gt;
  &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/crop2.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;h2 id=&quot;hyperparameters&quot;&gt;Hyperparameters&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Learning rate: 1e-1 (very aggressive!)&lt;/li&gt;
  &lt;li&gt;Batch size: 128 (tried 64, 128, 256, 1024)&lt;/li&gt;
  &lt;li&gt;Adam optimizer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used Keras to do a validation split on 30% of the data and my custom early termination to stop the training when we we reach a validation loss of around 0.03!&lt;/p&gt;

&lt;div align=&quot;left&quot;&gt;
  &amp;lt;/br&amp;gt;
  &lt;img src=&quot;/public/img/bc/loss_plot.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Note: this loss plot is from a previous run without early termination&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This model was tiny (52 params!) and with only ~20k images I was able to train it on a 2012 Macbook air. An epoch was about six seconds. The memory requirement were small, so I just loaded the entire dataset!&lt;/p&gt;

&lt;div align=&quot;left&quot;&gt;
  &lt;br /&gt;
  &lt;img src=&quot;/public/img/bc/callback.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;
  &lt;img src=&quot;/public/img/bc/train.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;/div&gt;

&lt;h2 id=&quot;pros&quot;&gt;Pros&lt;/h2&gt;
&lt;ol&gt;
  &lt;li&gt;Can train without a GPU.&lt;/li&gt;
  &lt;li&gt;Enough to pass the challenge.&lt;/li&gt;
  &lt;li&gt;Smaller model meant I could experiment with more variables and different models to gain better intuition.&lt;/li&gt;
  &lt;li&gt;Learned that our current method of training with back prop is not efficient and that we can achieve a lot with a smaller network.&lt;/li&gt;
  &lt;li&gt;Get to use aggressive learning rate for faster convergence :D&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;cons&quot;&gt;Cons&lt;/h2&gt;
&lt;ol&gt;
  &lt;li&gt;Can not handle highest resolution setting.&lt;/li&gt;
  &lt;li&gt;Can not go over .22 throttle and still be stable.&lt;/li&gt;
  &lt;li&gt;As the network increases in size, hard to understand why the decisions are being made. I can see this being a &lt;strong&gt;huge&lt;/strong&gt; problem for legal reasons. The end-to-end neural network faction is not looking so good here!&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
  &lt;p&gt;This bring me to the other rule of thumb, &lt;strong&gt;every problem will have tradeoffs&lt;/strong&gt;. The question becomes what are the tradeoffs you are willing to make? This will depend on the business case you are solving!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;the-github-repo-link&quot;&gt;The github repo link&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mez/carnd/blob/master/P3_behavioral_cloning/&quot;&gt;P3_behavioral_cloning&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1602.07360&quot;&gt;SqueezeNet&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1510.00149&quot;&gt;Deep Compression&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://chatbotslife.com/learning-human-driving-behavior-using-nvidias-neural-network-model-and-image-augmentation-80399360efee#.zbkgfcnoz&quot;&gt;Vivek Yadav’s Learning human driving behavior using NVIDIA’s neural network model and image augmentation.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/@xslittlegrass/self-driving-car-in-a-simulator-with-a-tiny-neural-network-13d33b871234#.x1kdv5hgt&quot;&gt;xslittlegrass’ Self-driving car in a simulator with a tiny neural network&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/commaai/research/blob/master/train_steering_model.py&quot;&gt;Comma.ai steering model&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content>
 </entry>
 

</feed>
