As a deep network trains, EVERY layer's weights are changing simultaneously, on every single step. This creates a subtle but real problem for any given layer, say, layer 5: the distribution of activations it RECEIVES from layer 4 keeps shifting around, not because layer 5's own input data changed, but because layers 1 through 4 upstream of it are all being updated too. Layer 5 essentially has to keep re-adapting to a constantly moving target, on top of the actual learning problem it's trying to solve, this shifting-distribution effect was named "internal covariate shift" in the original 2015 Batch Normalization paper (Ioffe & Szegedy), as the specific problem BatchNorm was designed to address (though later research has debated how much of BatchNorm's actual benefit really comes from directly fixing this effect, versus other side benefits, like smoothing the loss landscape, that come along with it).
[02-layers/04-weight-initialization] addresses a RELATED but distinct problem: getting activation variance right at the very START of training. BatchNorm addresses the SAME kind of variance-control problem, but continuously, DURING every single training step, by explicitly re-centering and re-scaling each layer's activations back to a controlled distribution (mean 0, variance 1, then rescaled by learnable gamma/beta) every time they pass through, regardless of how much the upstream layers have shifted since the last step.
Implement batchnorm_forward. In TRAINING mode, compute the CURRENT batch's own mean and (biased) variance, per feature, normalize x using them, then rescale by the learnable gamma (scale) and beta (shift) parameters. Simultaneously, update running_mean and running_var (exponential moving averages, using momentum) so they track training statistics over time. In EVAL mode, skip computing batch statistics entirely and normalize using the STORED running_mean/running_var instead, this is what makes a single test-time sample (where "this batch's mean" would be meaningless, being just that one sample) still normalize sensibly.
ddof=0, NumPy's default for .var()) for the actual normalization of x.running_var UPDATE specifically must use the batch's UNBIASED variance (ddof=1, i.e. batch_var * n / (n - 1)), matching torch.nn.BatchNorm1d's exact convention, even though the normalization itself uses the biased variance.running_mean/running_var directly, computing NO batch statistics at all, and must leave running_mean/running_var unchanged (no update in eval mode).gamma and beta must be applied AFTER normalization, as gamma * x_norm + beta, per feature.batch_mean = x.mean(axis=0) and batch_var = x.var(axis=0) (NumPy's .var() defaults to biased/population variance, exactly what's needed here). x_norm = (x - batch_mean) / np.sqrt(batch_var + eps), then out = gamma * x_norm + beta.
For the RUNNING statistics update only (not the normalization itself), convert the biased variance to unbiased: batch_var_unbiased = batch_var * n / (n - 1), where n = x.shape[0]. Then running_mean = (1 - momentum) * running_mean + momentum * batch_mean and running_var = (1 - momentum) * running_var + momentum * batch_var_unbiased.
In eval mode, skip all of the above: x_norm = (x - running_mean) / np.sqrt(running_var + eps), then out = gamma * x_norm + beta, exactly the same rescaling step as training mode, just using the stored running statistics instead of freshly-computed batch statistics.
Click "Run Tests" to test your implementation