MediumResNet

BatchNorm in ResNet

Deep Residual Learning

Medium

Problem

Implement a matrix-based residual block that compares the post-activation ordering from the original ResNet with a pre-activation variant. Matrix multiplication stands in for convolution so the exercise focuses on normalization order and the residual path.

For each feature, batch normalization uses

\widehat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \varepsilon}}

y = \gamma \widehat{x} + \beta

Here, (\mu_B) and (\sigma_B^2) are the batch mean and population variance, (\gamma) is the scale, (\beta) is the shift, and (\varepsilon=10^{-5}). In post mode, use linear, normalization, ReLU, linear, normalization, residual addition, then ReLU. In pre mode, use normalization, ReLU, linear, normalization, ReLU, linear, then residual addition. Return a dictionary containing output as rounded nested lists and mode as the supplied string.

Theory

Batch Normalization (BatchNorm) normalizes activations across the batch dimension for each channel, stabilizing and accelerating deep network training. Introduced by Ioffe and Szegedy (2015), it became a standard building block after He et al. adopted it in ResNet with the Conv->BN->ReLU ordering that is now ubiquitous in convolutional architectures.


What It Is

Batch Normalization is a layer inserted between a linear operation (convolution or fully connected) and its activation function. It normalizes the pre-activation values so that, across the current mini-batch, each feature channel has approximately zero mean and unit variance. After normalizing, it applies a learned affine transform that gives the network the ability to recover any distribution it finds useful.

The core idea: if the inputs to each layer keep shifting in distribution as earlier layers update their weights, training becomes unstable and slow. By re-centering and re-scaling activations at every layer, BatchNorm decouples layers from each other, allowing each one to learn more independently. The original paper calls this problem internal covariate shift.

In a convolutional network, BatchNorm operates per channel. For an input tensor of shape (B, C, H, W), the normalization statistics (mean and variance) are computed over all B \times H \times W values for each of the C channels independently. This preserves the translation equivariance property of convolutions.


Key Equations

Batch Mean

For a given channel c, compute the mean over all elements in the batch and all spatial positions:

\mu_B^{(c)} = \frac{1}{B \cdot H \cdot W} \sum_{b=1}^{B} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{b,c,h,w}

Batch Variance

\sigma_B^{2(c)} = \frac{1}{B \cdot H \cdot W} \sum_{b=1}^{B} \sum_{h=1}^{H} \sum_{w=1}^{W} (x_{b,c,h,w} - \mu_B^{(c)})^2

Normalize

Subtract the batch mean and divide by the standard deviation plus a small constant \epsilon for numerical stability:

\hat{x}_{b,c,h,w} = \frac{x_{b,c,h,w} - \mu_B^{(c)}}{\sqrt{\sigma_B^{2(c)} + \epsilon}}

A typical value for \epsilon is 10^{-5}. It prevents division by zero when a channel has zero variance.

Scale and Shift

The normalized value is transformed by learnable parameters \gamma^{(c)} and \beta^{(c)}:

y_{b,c,h,w} = \gamma^{(c)} \hat{x}_{b,c,h,w} + \beta^{(c)}

Each channel has its own \gamma (scale) and \beta (shift), so for C channels the layer adds 2C learnable parameters.

Running Statistics (EMA)

During training, BatchNorm maintains running estimates of the mean and variance using an exponential moving average. After each training step:

\hat{\mu}_{\text{running}}^{(c)} \leftarrow (1 - m) \cdot \hat{\mu}_{\text{running}}^{(c)} + m \cdot \mu_B^{(c)}

\hat{\sigma}_{\text{running}}^{2(c)} \leftarrow (1 - m) \cdot \hat{\sigma}_{\text{running}}^{2(c)} + m \cdot \sigma_B^{2(c)}

Here m is the momentum parameter (default 0.1 in PyTorch). The running statistics are accumulated purely for inference. Note: PyTorch's convention defines momentum so that a larger $$ gives more weight to the current batch. Some frameworks use the opposite convention, so always check the documentation.


Training vs. Inference

Training Mode

During training, BatchNorm uses the current mini-batch statistics (\mu_B, \sigma_B^2) to normalize activations. This is essential because the batch statistics provide a differentiable path for backpropagation. Simultaneously, the running mean and running variance are updated via EMA. These buffers are stored in the model's state dict but do not participate in gradient computation.

Inference Mode

At inference time, there may be no batch at all (batch size = 1), or the batch may not represent the training distribution. BatchNorm switches to the accumulated running statistics:

\hat{x} = \frac{x - \hat{\mu}_{\text{running}}}{\sqrt{\hat{\sigma}_{\text{running}}^2 + \epsilon}}

With running statistics and the learned \gamma, \beta, the entire BatchNorm layer reduces to a fixed affine transform at inference. This means it can be fused into the preceding convolution with no computational overhead, a common optimization in deployed models.


Why Batch Normalize

Internal Covariate Shift

Ioffe and Szegedy (2015) motivated BatchNorm as a solution to internal covariate shift: the continuous change in the distribution of a layer's inputs caused by updates to preceding layers. By fixing the first two moments of each layer's inputs, BatchNorm reduces this instability. Later research (Santurkar et al., 2018) argued that the benefit is better explained by smoothing the loss landscape, making gradient directions more reliable and allowing larger steps.

Enables Higher Learning Rates

Without BatchNorm, large learning rates cause activations to explode or collapse in deep networks. By keeping activations bounded in a normalized range, BatchNorm allows learning rates that would otherwise diverge. Ioffe and Szegedy reported training BN-Inception with learning rates 10x-30x higher than the baseline while reaching the same accuracy faster.

Regularization Effect

Because batch statistics are computed from a random mini-batch, the normalization introduces noise into activations. Each sample's normalized value depends on what other samples happen to be in the batch. This acts as a mild regularizer, similar in spirit to dropout. Networks trained with BatchNorm often need less dropout or can omit it entirely. The regularization strength decreases with larger batch sizes.


The Learnable Parameters

After normalization, every channel's activations have zero mean and unit variance. If this were the final output, the network would lose representational power. The learnable parameters \gamma and \beta restore it.

If the network learns \gamma^{(c)} = \sqrt{\sigma_B^{2(c)} + \epsilon} and \beta^{(c)} = \mu_B^{(c)}, the BatchNorm transformation becomes the identity: y = x. This means the network can undo normalization entirely if that is optimal. In practice, the network finds an intermediate setting where normalization helps but some deviation from strict zero-mean/unit-variance is beneficial.

For a conv layer with C output channels, BatchNorm adds 2C parameters. This is tiny compared to the conv itself. A 3 \times 3 conv with 256 input and 256 output channels has $$ parameters; its BatchNorm adds only 512.


Running Statistics

The EMA Update Rule

With momentum m = 0.1 (PyTorch default), after each batch:

\hat{\mu}_{\text{running}} \leftarrow 0.9 \cdot \hat{\mu}_{\text{running}} + 0.1 \cdot \mu_B

\hat{\sigma}_{\text{running}}^2 \leftarrow 0.9 \cdot \hat{\sigma}_{\text{running}}^2 + 0.1 \cdot \sigma_B^2

The running mean is initialized to 0 and the running variance to 1. Over many training iterations, these converge to the global dataset statistics for each channel. Smaller m means slower adaptation but smoother estimates; larger m tracks recent batches more closely.

Why Running Statistics Are Needed

At inference time, you may process a single image (batch size = 1). Computing a "batch mean" from one sample would just be the sample itself, making normalization meaningless -- the output would always be zero. Running statistics provide a stable, batch-size-independent estimate representing the training distribution. They also ensure determinism: two identical inputs always produce identical outputs regardless of batch composition.


Paper Context

Batch Normalization was introduced by Ioffe and Szegedy in "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" (2015). They applied BatchNorm to Inception (GoogLeNet) and demonstrated that BN-Inception matched the original Inception's accuracy in 14x fewer training steps.

He et al. (2015) adopted BatchNorm as a core component of ResNet. In ResNet, every convolutional layer is followed immediately by BatchNorm before the ReLU activation: Conv -> BN -> ReLU. The paper states: "We adopt batch normalization right after each convolution and before activation." This ordering ensures normalization operates on the raw convolution output before the non-linearity clips negative values.

The Conv -> BN -> ReLU pattern has a specific benefit in residual blocks. The shortcut connection adds the input x to the residual branch F(x). BatchNorm keeps the magnitude of the residual branch controlled. Without it, outputs could grow unboundedly as the network deepens to 50, 101, or 152 layers. ResNet's success -- winning ILSVRC 2015 with 3.57% top-5 error -- cemented BatchNorm as indispensable in convolutional architectures.


Numerical Example

Consider a single channel (c = 0) with a mini-batch of B = 3 scalar values (e.g., after global average pooling). Use \epsilon = 0.00001, \gamma = 1.5, \beta = 0.5. Current running mean = 0.0, running variance = 1.0, momentum m = 0.1.

x_1 = 2.0, \quad x_2 = 4.0, \quad x_3 = 6.0

Step 1: Batch Mean

\mu_B = \frac{2.0 + 4.0 + 6.0}{3} = \frac{12.0}{3} = 4.0

Step 2: Batch Variance

\sigma_B^2 = \frac{(2.0 - 4.0)^2 + (4.0 - 4.0)^2 + (6.0 - 4.0)^2}{3} = \frac{4.0 + 0.0 + 4.0}{3} = \frac{8.0}{3} \approx 2.6667

Step 3: Normalize

\sqrt{\sigma_B^2 + \epsilon} = \sqrt{2.6667 + 0.00001} \approx 1.6330

\hat{x}_1 = \frac{2.0 - 4.0}{1.6330} \approx -1.2247, \quad \hat{x}_2 = \frac{4.0 - 4.0}{1.6330} = 0.0, \quad \hat{x}_3 = \frac{6.0 - 4.0}{1.6330} \approx 1.2247

Step 4: Scale and Shift

y_1 = 1.5 \times (-1.2247) + 0.5 = -1.8371 + 0.5 = -1.3371

y_2 = 1.5 \times 0.0 + 0.5 = 0.5

y_3 = 1.5 \times 1.2247 + 0.5 = 1.8371 + 0.5 = 2.3371

Step 5: Update Running Statistics

\hat{\mu}_{\text{running}} \leftarrow 0.9 \times 0.0 + 0.1 \times 4.0 = 0.4

\hat{\sigma}_{\text{running}}^2 \leftarrow 0.9 \times 1.0 + 0.1 \times 2.6667 = 0.9 + 0.2667 = 1.1667

After this batch, the running mean moved from 0.0 to 0.4 (toward the batch mean of 4.0) and the running variance from 1.0 to 1.1667 (toward 2.6667). Over thousands of batches, these converge to the true dataset statistics for this channel.


BatchNorm vs. LayerNorm vs. RMSNorm

When to use which: BatchNorm for convolutional architectures with batch sizes of 16+. LayerNorm for transformers and sequence models. RMSNorm as a drop-in replacement for LayerNorm when training efficiency matters. BatchNorm is rarely used in transformers because sequences in a batch differ in length and content, making batch statistics unreliable.


Pitfalls


Examples

Example 1

Input
x = [[-0.5653, 0.3481, -0.2161], [0.3705, -0.2391, 0.693], [0.0626, 0.5744, -1.1751]], W1 = [[0.0916, -0.1557, -0.147], [0.2, 0.5028, 0.2508], [-0.0003, 0.652, 0.5367]], W2 = [[-0.2577, 0.8315, 0.2759], [-0.2897, -0.4244, -0.2054], [1.0803, -0.7788, 0.0674]], gamma1 = [1.0027, 1.3233, 0.704], beta1 = [0.0552, 0.1378, 0.0504], gamma2 = [0.9413, 0.5823, 1.6512], beta2 = [0.1957, 0.0577, 0.0163], mode = "post"
Output
{"output": [[0.0, 0.1843, 0.0], [1.6257, 0.0, 0.0], [0.0, 1.4297, 1.1527]], "mode": "post"}
Explanation
Post-activation order: Conv -> BN -> ReLU -> Conv -> BN -> Add Skip -> ReLU. Batch normalization normalizes across the batch dimension using batch mean and variance, then applies learnable gamma (scale) and beta (shift).

Example 2

Input
x = [[0.3017, -0.3008, -0.2935], [0.2416, 0.324, -0.3666], [0.2154, 0.3158, 0.0383]], W1 = [[0.1265, -0.3916, 0.5808], [1.0436, 0.6305, 0.0941], [-0.1571, -0.0329, 0.8322]], W2 = [[-0.44, 0.1317, -0.3285], [0.1949, -0.0915, -0.2772], [0.2316, 0.6021, -0.3556]], gamma1 = [0.2648, 1.3138, 1.8375], beta1 = [0.2128, 0.1408, -0.1618], gamma2 = [0.9366, 0.9082, 0.5768], beta2 = [0.0208, -0.0825, -0.0786], mode = "pre"
Output
{"output": [[0.3017, -0.3008, -0.2935], [-0.115, 0.4102, -0.894], [0.4069, 0.7354, -0.4184]], "mode": "pre"}

Example 3

Input
x = [[0.0979, -0.9793, -0.0267, -0.2912], [0.6751, -0.5632, 0.4476, -1.5262], [-0.4027, 0.3876, -0.0889, 0.0597], [0.0463, -0.2985, -0.3164, 0.226]], W1 = [[0.3251, 0.9961, 0.1917, -0.6487], [0.6199, -0.2509, -0.6504, 0.4924], [-1.0969, -0.3445, -0.2625, -0.5728], [-0.9859, -0.4261, 0.636, 0.5007]], W2 = [[0.2065, -0.2423, -0.0363, 0.1718], [0.5084, 0.3609, -0.7219, -0.038], [-0.4183, 0.0357, -0.6081, 0.2697], [-0.4538, 0.7101, -0.6692, 0.2723]], gamma1 = [0.421, 1.3054, 1.4115, 0.2933], beta1 = [-0.0481, 0.0174, 0.1284, -0.0478], gamma2 = [0.7978, 1.234, 0.8163, 0.3063], beta2 = [-0.0125, -0.014, 0.1555, 0.1241], mode = "post"
Output
{"output": [[0.0, 0.0, 0.0, 0.1309], [1.9895, 1.5427, 0.0, 0.0], [0.0, 0.0, 1.3868, 0.0], [0.0, 0.0, 0.0, 0.6626]], "mode": "post"}

Hints

  1. Use x.mean(axis=0) and x.var(axis=0) for each normalization.
  2. Store the residual before applying the main path.
  3. Apply the final ReLU only in post mode.

Requirements

Constraints

Starter Code

import numpy as np

def batch_norm_block(x, W1, W2, gamma1, beta1, gamma2, beta2, mode):
    """
    Returns the normalized residual-block result and selected mode in a dictionary.
    """
    pass

Test Cases

CaseMatches
Post-activation 3x3public
Pre-activation 3x3public
Post-activation 4x4public