A Flappy Bird, a 4-number observation, twenty future moves to predict. The simplest imitation-learning algorithm there is — why it flies clean on easy mode, why it drives straight into the wall on hard mode, and the two orthogonal fixes that rescue it.
A companion & practice forge for Stanford's CS 224R Homework 1. It credits the public course materials and teaches you to implement the core math yourself — it is not a copy-paste answer bank. The real starter-code contracts (BCPolicy, mse_loss, FlowMatchingSchedule, DeterministicExpert) are the ones you meet here, shrunk to a scale you can run in your browser.
You have trained a bird to fly through gaps in pipes. It learned by watching an expert — thousands of frames of "here is what I saw, here is the height I aimed for." On the easy course, one gap per pipe, it is flawless: it threads every opening and never crashes.
Then you switch to the hard course. Now each pipe has two gaps, an upper one and a lower one, and the bird can take either. And your flawless policy — the same network, the same training, the same loss — flies straight into the solid wall between the two gaps. Every single time.
It is not confused. It is not undertrained. It is doing exactly what you asked it to do. That is the unsettling part, and unpacking it is the whole point of this homework.
Two valid demonstrations from the same state: one expert aims at the upper gap (y = 0.70), another aims at the lower gap (y = 0.30). Press play. Watch where a mean-seeking policy — one trained to minimize squared error — decides to fly.
This is not a bug in your code. It is a mathematical property of the loss function you chose. Fixing it is what launches the rest of imitation learning — and this lesson walks the whole road: how behavior cloning works, why the wall appears, and the two very different escapes (a richer model called flow matching, and a data trick called DAgger).
Before we can fix anything we need to know the machine. The environment is a physics-based Flappy Bird. A bird falls under gravity. Pipes scroll left. Each pipe has one gap (easy) or two (hard). The bird must thread the gaps without touching a pipe, the ceiling, or the floor.
The agent does not press "flap." At every timestep it outputs a single number: a target y-position, normalized to [0, 1], where 0 is the top of the screen and 1 is the bottom. A built-in PD controller — a proportional-derivative feedback loop — converts that target into thrust, so the bird has momentum and cannot teleport. Aim for a height; the controller flies you there with realistic lag.
The policy sees a 4-dimensional vector, every entry normalized to roughly [0, 1]:
| Index | Meaning | Note |
|---|---|---|
obs[0] | distance to the next pipe | counts down as the pipe approaches |
obs[1] | y-position of gap 1 (upper) | the two gaps are ~0.40 normalized (about 179 px) apart |
obs[2] | y-position of gap 2 (lower) | on easy mode, equal to gap 1 |
obs[3] | the bird's current y-position | the bird's velocity is not observed |
obs[1] ≠ obs[2] — there are two gaps — but nothing in the observation tells the policy which gap the expert will pick. Same four numbers, two valid answers. That is the seed of the whole multimodality problem.| Mode | Pipes | Expert | Plain BC |
|---|---|---|---|
| Easy | one gap (gap1 == gap2) | always aims at the one gap → unimodal | works fine |
| Hard | alternating single- and double-gap pipes | randomly picks a gap → multimodal | fails spectacularly |
Internalize this distinction before anything else. Imitation learning (IL) is not reinforcement learning (RL). They solve the same kind of problem — choose good actions — from completely different signals.
| Reinforcement Learning | Imitation Learning | |
|---|---|---|
| Data source | trial and error in the environment | expert demonstrations — no env interaction while training |
| Signal | a reward function | the expert's actions, treated as labels |
| Algorithm family | Q-learning, policy gradients, … | supervised learning |
| Hard part | exploration, sparse reward, credit assignment | distribution shift, multimodal experts |
| Analogy | learn chess by self-play | learn chess by watching humans |
Strip behavior cloning (BC) down to its essence and it is literally just supervised learning. If you have ever trained an image classifier, you have already done every mechanical step of BC. The only thing that changes is the data: states in, expert actions out.
Run the expert in the environment N times. Record every (state, action) pair. You get
where each ai* is the height the expert aimed for at state si. In the real homework the default is roughly 500 demonstration episodes, which window out to several thousand training pairs.
Find the network parameters θ that make the policy's action match the expert's action, averaged over the whole dataset:
Here L is a loss that measures how far off the prediction is. For Problem 1 of the homework, L is mean squared error. Problem 2 swaps in a generative loss (flow matching). Same overall structure — different L.
s of shape [B, 4]. Output tensor πθ(s) of shape [B, 20] (twenty future target heights — Chapter 3 explains the 20). Target tensor a* of shape [B, 20]. The loss reduces the two to a single scalar. Backprop, Adam step, repeat. Nothing exotic.The optimization is easy. The hardness lives in three failure modes — and each of the follow-up problems attacks one:
| Failure mode | What goes wrong | Fixed by |
|---|---|---|
| Multimodal experts | two valid actions per state get averaged into an invalid one (the wall) | Problem 2 · flow matching or Problem 3 · deterministic expert |
| Distribution shift | small errors compound until the bird is in states the expert never visited | Problem 3 · DAgger |
| Causal confusion | the network learns a shortcut that works on training data but not in deployment | better data / representations (beyond HW1) |
A single state, several expert demonstrations (dots), and the line a mean-squared-error fit draws through them. Drag the slider to add spread. On unimodal data the fit sits right on the cluster. Keep this picture — Chapter 5 shows what happens when the dots split into two clusters.
Most textbook BC predicts one action at a time: see a state, output an action, repeat. Modern robot-learning policies — Diffusion Policy, ALOHA, RT-1, π0 — do something that looks strange at first: they predict a whole chunk of future actions in a single forward pass.
In this homework ACTION_CHUNK = 20: the policy predicts twenty future target heights at once. But it does not execute all twenty. At rollout time it executes only the first EXECUTE_STEPS = 10 before firing again. That "predict 20, execute 10, re-plan" pattern is called receding-horizon control.
The blue bar is the 20-step plan the policy just predicted. The solid segment is the 10 steps actually executed; the faded segment is discarded. Press play: watch the plan slide forward, always re-planning before the executed part runs out.
Three concrete reasons, each a real engineering decision:
| Chunk vs execute | Effect |
|---|---|
| large chunk, execute few (T ≫ Texec) | strong temporal consistency, but the policy commits to a stale plan for longer |
| chunk == execute | always responsive to fresh observations, but weaker consistency |
| predict 20, execute 10 (this HW) | the standard compromise — predict more than you execute, re-query before the buffer runs dry |
This is the whole reason the policy's output has 20 dimensions. Input: a state of shape [B, 4]. Output: a chunk of shape [B, 20] — each output number is one future target height. The action_dim argument of BCPolicy defaults to 20 because that is the chunk size. Keep that number in mind; it is the shape everything downstream must match.
Now the two design decisions that are Problem 1: the network architecture and the loss. Both are textbook — the interesting part (Chapter 5) is what they do together on bimodal data.
The policy, BCPolicy, is a plain multilayer perceptron — the simplest neural network, a stack of matrix multiplies with nonlinearities between them:
Why an MLP and nothing fancier? The problem is small — a 4-D input, a 20-D output, simple physics. A convolutional net would be overkill (no spatial structure). A transformer would be overkill (no sequential input structure). Two hidden layers of 256 units is the natural choice. (Compare: Problem 2's flow-matching policy is a 1D U-Net, because learning a vector field is a much richer function class.)
Why ReLU in the hidden layers? ReLU(x) = max(0, x). It is cheap, and — critically — it does not saturate for positive inputs, so gradients stay healthy (they are 1 or 0) no matter the magnitude. Sigmoid or tanh would flatten and vanish the gradient across layers.
Why Sigmoid on the output? The action is a target height in [0, 1]. The sigmoid σ(x) = 1 / (1 + e−x) maps any real number into (0, 1), so the network can never emit an out-of-range height. Baking the constraint into the architecture gives a clean gradient signal everywhere, instead of a kink from clamping after the fact.
nn.Sequential(nn.Linear(4,256), nn.ReLU(), nn.Linear(256,256), nn.ReLU(), nn.Linear(256,20), nn.Sigmoid()). The one silent trap: forgetting super().__init__() — without it, PyTorch never registers the layer parameters, the optimizer updates nothing, and training "runs" while the loss never moves. The starter code includes it for you.Mean squared error is the average, over the dataset and over all 20 chunk dimensions, of the squared gap between prediction and expert action:
What does MSE regression actually converge to? Take the expected squared error at a state and set the derivative with respect to the prediction p to zero:
The MSE-minimizing prediction is the conditional mean of the expert's actions at that state:
| Loss | Minimizer | Tradeoff |
|---|---|---|
| L2 · MSE · (p−a)2 | conditional mean | smooth gradient; but averages multimodal targets |
| L1 · MAE · |p−a| | conditional median | robust to outliers; non-smooth at 0, harder for SGD |
For smooth control tasks, MSE is the standard. Its smoothness is a gift for optimization — and its mean-seeking is a curse for multimodal data.
This is the conceptual climax of Problem 1. Everything before was setup; everything after is a fix. Spend time here.
Hard mode alternates single-gap and double-gap pipes. When the expert sees a double-gap pipe it hovers at the midpoint while far away, then — when it gets within a commit distance of the pipe — randomly picks one of the two gaps and aims there. Same observation, two different choices, decided by a coin flip inside the expert. So the dataset literally contains:
s (a double-gap configuration), action = aim at the upper gap, y = 0.70.s (the same state), action = aim at the lower gap, y = 0.30.Two valid expert actions, from an identical state, differing only by the expert's private coin flip. The observation carries no signal that would let any function distinguish them.
From Chapter 4: the MSE minimizer is the conditional mean. The conditional mean of 0.70 and 0.30 is
So the trained policy outputs 0.50 at this state — which, because the two gaps are about 0.40 apart (~179 px) and each opening is only about 0.167 tall (75 px, i.e. half-opening ±0.084), is exactly the solid wall between them. The policy is not doing anything wrong. It is faithfully reproducing the conditional mean of the expert distribution. But the conditional mean is not a valid action.
Start with all demos aiming at one gap (unimodal). Drag the slider to send more and more of them to the other gap. The dashed line is the MSE optimum — the mean of the demos. Watch it leave the safe gap and drift into the wall as the data becomes bimodal.
Problem 1 is designed to fail on hard mode. You will run it, see a mean episode length far below the 1000-step cap, and the homework asks you to explain why in a couple of sentences. The answer is exactly: multimodality of the expert + mean-seeking of MSE.
And there are two clean escapes, which take orthogonal routes:
| Fix | Idea | Changes |
|---|---|---|
| Problem 2 · Flow matching | make the model richer — learn the full distribution p(a | s), then sample one mode | the model (data untouched) |
| Problem 3 · DAgger | make the data unimodal — a deterministic expert that always picks the same gap | the data (model untouched) |
Both work. Real robot-learning systems often use both at once: a rich generative policy trained on the cleanest data available.
The first escape keeps the data exactly as-is and makes the model richer. Instead of a regressor that outputs one number per state, we train a generative model that learns the whole distribution of expert actions at each state — and then samples from it. Same state, different samples: sometimes the upper gap, sometimes the lower, never the average. This is flow matching, the tool of Problem 2. (For a gentler standalone tour see the site's Flow Matching Gleam.)
| Regressor (Problem 1) | Generative model (Problem 2) | |
|---|---|---|
| Given a state, returns | one deterministic action | a sample from p(a | s) |
| Same state twice | same action | possibly different actions |
| On bimodal data | the mean (the wall) | either mode — never the mean |
Flow matching generates a sample from a complex distribution by continuously carrying a sample from a simple one — Gaussian noise — along a learned vector field. Picture the 20-D action space (think of it as a plane). At every point and time, an arrow says "flow this way." Follow the arrows from time τ=0 to τ=1 and a noise sample is carried to a data sample.
The magic is that different noise samples get carried to different modes. Noise near the "upper-gap attractor" flows to the upper gap; noise near the "lower-gap attractor" flows to the lower gap. That is how multimodality survives.
The training objective is startlingly simple. Take a clean expert action a1. Draw independent noise a0 ~ N(0, I). Pick a random time τ ~ U(0,1) and interpolate along the straight line between them:
Differentiate that straight line and the velocity is constant — it does not even depend on τ:
So the loss is: regress the network's output toward that constant difference.
To generate an action for state s, start from noise and take small steps in the direction of the learned velocity. With num_steps = 20, the step size is h = 1/20 = 0.05:
Repeat 20 times from τ=0 to τ=1, then clamp the result to [0,1]. Because the training paths are straight lines, few steps are needed — that is flow matching's practical edge over diffusion (curved paths, many steps).
Twenty noise samples on the left (τ=0) integrate rightward under a learned field toward a bimodal target — upper gap (teal) and lower gap (blue). Press play. Notice: no sample ends in the wall between them. Each one commits.
num_steps = 1 and flow matching collapses back into plain MSE regression: a single Euler step from noise toward the conditional mean of the velocity gives you the conditional mean of the action — the wall again. The multimodality is only preserved because you integrate through many timesteps. Straightness is what lets 20 steps suffice.The second escape keeps the model and the loss exactly as Problem 1 — plain MLP, plain MSE — and changes the data. It is the tool of Problem 3, and it fixes two things at once: the multimodality you just met, and a second, more fundamental failure of BC called distribution shift.
BC trains the policy to mimic the expert at the states the expert visits. But at deployment the policy visits the states its own behavior produces. It makes a small error, lands in a slightly weird state the expert never quite visited, makes a slightly larger error there, drifts further, and by fifty timesteps in it is in state space the expert never saw and the policy has no idea what to do.
The green band is the states the expert visited (the only states BC was trained on). Press play: the agent starts inside it, but each small error nudges it out, and the further out it goes the worse it acts — a runaway. Raise the per-step error to see the drift accelerate.
DAgger (Dataset Aggregation; Ross, Gordon, Bagnell 2011) closes the gap directly. Each round: roll out the current policy to collect the states it actually visits, ask the expert what to do at those states, add those (state, expert-action) pairs to the dataset, and retrain. Over rounds, the training distribution converges to the deployment distribution.
Here is the cleverest part of HW1. The original expert is multimodal — it randomly picks a gap. If we relabeled with it, we would keep adding multimodal labels, and MSE would keep averaging into the wall. DAgger would fix distribution shift but not multimodality.
So for relabeling we swap in a deterministic expert that always picks the same gap (gap 1, the upper one) when it commits:
Original expert (multimodal):
if dist < commit_dist: target_gap = np.random.choice([0, 1]) # coin flip! committed = True
Deterministic expert (unimodal):
if dist < commit_dist: committed = True raw_target = float(gap1_y) # ALWAYS gap 1
raw_target = float(gap1_y) — turns multimodal data into unimodal data. Now every label says "gap 1, gap 1, gap 1," so the conditional mean is gap 1, a valid action. DAgger's aggregation fixes distribution shift; the deterministic expert fixes multimodality. Same MLP, same MSE as Problem 1 — just cleaner data. Which gap you pick is arbitrary; consistency is the point.One wrinkle: the policy predicts 20-step chunks but executes 10 (receding horizon). During a rollout you query the expert at every step, storing a per-step list of (state, expert-action). Afterward you window that list into chunks: state st gets paired with the next 20 expert actions [a*t, …, a*t+19]. Any 20 consecutive expert actions form a valid expert chunk, so windowing makes new training pairs for free — the same pattern the original demo collector uses.
Here is the payoff. Three methods — plain BC regression, flow matching, DAgger — attacking the same problem: a multimodal expert on a long-horizon control task. Pick a method, press play, and watch the bird fly the hard course. The one thing that changes across methods is where the policy decides to aim when it sees a double-gap pipe.
Buttons choose the policy. BC-MSE aims for the mean of the two gaps → the wall → crash. Flow samples one gap and commits. DAgger was retrained on deterministic-expert labels and always takes gap 1. Watch the "aiming at" readout and the crash counter.
| Method | Hard-mode result | How it solves the problem |
|---|---|---|
| BC regression (P1) | ~200–400 steps | doesn't — the baseline failure. MSE averages the modes into the wall. |
| Flow matching (P2) | ~700–1000 steps | keep the data, enrich the model — a generative policy samples one mode per rollout. |
| DAgger (P3, final round) | ~800–1000 steps | keep the model, fix the data — deterministic relabeling makes the training distribution unimodal and covers the deployment states. |
BCPolicy.forward, mse_loss, flow_matching_loss, the schedule’s interpolate + sample, DeterministicExpert.act, and rollout_and_relabel — on a live Flappy Bird stage. The bird crashes until your code goes green, then clears the gap. Finish the forge and you have implemented the assignment, at browser scale.Everything worth carrying out of HW1, on one page. If you can reconstruct this from memory, you can teach it.
| P1 · BC-MSE | P2 · Flow Matching | P3 · DAgger | |
|---|---|---|---|
| What you change | — (baseline) | the model | the data |
| Model | 3-layer MLP | 1-D U-Net (given) | 3-layer MLP (same as P1) |
| Loss | MSE on actions | MSE on velocities | MSE on actions (same as P1) |
| Fixes multimodality? | no | yes (samples a mode) | yes (deterministic expert) |
| Fixes distribution shift? | no | no | yes (relabels visited states) |
| Inference cost | 1 forward pass | 20 (Euler) | 1 forward pass |
| Needs interactive expert? | no | no | yes |
| You implement | File | In one line |
|---|---|---|
BCPolicy.__init__ / forward | networks.py | the nn.Sequential MLP; return self.net(state) |
mse_loss | losses.py | F.mse_loss(policy(s), a) |
FlowMatchingSchedule.interpolate | networks.py | x_t = t*x1+(1-t)*eps; v = x1-eps |
FlowMatchingSchedule.sample | networks.py | Euler loop over num_steps, then clamp(0,1) |
flow_matching_loss | losses.py | interpolate → model → mse_loss(v_pred, v_target) |
DeterministicExpert.act | dagger.py | raw_target = float(gap1_y) when committed |
rollout_episode / rollout_and_relabel | dagger.py | roll out policy, relabel with expert, window into chunks |
| Failure mode | Symptom you would actually see |
|---|---|
| Multimodal collapse | hard-mode episode length stuck ~300 while easy mode is ~1000; the bird hits the wall between two open gaps |
Forgot super().__init__() | training loss never moves off its random-init value; optimizer has no parameters to update |
| Forgot the output Sigmoid | predictions leave [0,1]; unstable, random-looking performance even on easy mode |
Wrong action_dim | outputs one number instead of a 20-chunk; the training loop crashes on a shape mismatch |
| Flow: querying model at τ+h | subtly off-distribution samples; performance drops with no crash |
Flow: num_steps = 1 | collapses back to MSE — the wall returns, multimodality lost |
| DAgger: labels from policy not expert | no improvement over rounds; the policy just reinforces its own mistakes |
DAgger: forgot obs.copy() | every stored state is identical (the last one) — numpy aliasing bug |
num_steps = 1 in the flow-matching sampler, what does the method reduce to?If a friend asks "Why does behavior cloning fail on hard mode?" — you say:
"It's not BC's fault — it's the loss. MSE regression converges to the conditional mean of the expert's actions. When the expert is bimodal — sometimes go up, sometimes go down — the mean is in the middle, which is a wall. The fix is either a richer model (flow matching captures both modes) or cleaner data (a deterministic expert that always picks the same gap, which DAgger relabels with while also curing distribution shift)."
This forge is the entry point to the CS224R arc. Related on the site:
"What I cannot create, I do not understand." — Feynman. You have now created all three: the mean-seeking regressor, the noise-transporting flow, and the self-correcting relabeler. Build them in the Studio and the understanding is yours.
Companion & practice forge for Stanford CS 224R Homework 1 (Imitation Learning). Credits the public course materials. Implement the core math yourself; this is not a solution answer bank.