Batch Normalization: Stabilizing Deep Network Training

Michael BrenndoerferApril 19, 202554 min read

Part of Language AI Handbook

Explains how batch normalization eliminates internal covariate shift by normalizing layer activations.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Batch Normalization

Training deep neural networks is notoriously difficult. As networks grow deeper, a subtle problem emerges: the distribution of inputs to each layer shifts continuously during training as the weights in earlier layers update. This forces every downstream layer to constantly adapt to new input statistics, slowing convergence and destabilizing learning. Batch normalization, introduced by Sergey Ioffe and Christian Szegedy in 2015, addresses this directly by normalizing layer inputs using statistics computed from the current mini-batch. The result is one of the most impactful regularization and stability techniques in deep learning, and understanding it deeply is essential for anyone working with modern neural architectures.

In the previous chapter on weight initialization, we saw how proper initialization of weights, through Xavier or He initialization, helps gradients flow cleanly at the start of training. Batch normalization takes this idea further: it actively maintains healthy activation statistics throughout training, not just at initialization. Together, these two techniques form the foundation for training reliably deep architectures. Weight initialization sets the stage; batch normalization keeps it stable as training unfolds.

This chapter covers the full story of batch normalization: why internal covariate shift causes training problems, how the normalization algorithm works step by step, what the running statistics mechanism does at inference time, how batch normalization affects gradient flow and the optimization landscape, where to place it in a network, its limitations and the alternative normalization schemes those limitations inspired, and finally a complete code implementation that makes all of these ideas concrete.

The Problem: Internal Covariate Shift

To understand why batch normalization matters, consider what happens inside a deep network during training. Each layer receives inputs from the previous layer, computes a transformation, and passes its outputs forward. When the weights of any layer update via backpropagation, the distribution of its outputs changes. The next layer, which adapted its weights to a certain input distribution, now faces a different one. This cascading effect of distributional drift is called internal covariate shift.

Internal Covariate Shift

The change in the distribution of a layer's inputs caused by updates to the parameters of all preceding layers during training. The term "covariate shift" is borrowed from statistics, where it refers to a change in the distribution of input features between training and test time.

The deeper the network, the more severe this problem. Consider a ten-layer network. Even a small weight update in layer one changes the inputs to layer two, which changes the inputs to layer three, and so on. By the time these perturbations reach layer ten, the distributional shift can be substantial. Each layer must continuously "chase" a moving target, which requires small, careful learning rate choices to avoid divergence. This fundamentally limits training speed.

To appreciate just how significant this constraint is, consider what happens when you try to increase the learning rate in a deep network without batch normalization. A larger learning rate produces larger weight updates, which produce larger distributional shifts downstream, which cause later layers to receive wildly out-of-distribution inputs. This spirals into numerical instabilities, with activations exploding toward infinity or collapsing toward zero. The safe learning rate for a 20-layer network without stabilization might be an order of magnitude smaller than what would be optimal for a 3-layer network. The practical effect is that deep networks without batch normalization train painfully slowly and require extensive, fragile hyperparameter tuning.

Batch normalization breaks this cycle by explicitly normalizing the inputs to each layer before applying weights and activations. By ensuring that layer inputs maintain stable statistics, roughly zero mean and unit variance, throughout training, the optimization landscape becomes far smoother. Layers no longer need to compensate for upstream distributional drift; they can focus entirely on learning useful transformations.

The intuition is similar to standardizing features before training a logistic regression or SVM model. When features have consistent scales, gradient-based optimization works much more effectively because the loss surface is more isotropic: gradients in different directions have comparable magnitudes, so gradient descent makes better progress per step. Batch normalization applies the same logic inside the network, dynamically, at every layer, using the statistics of each mini-batch.

Subsequent research has questioned this explanation: subsequent research has questioned whether internal covariate shift is the actual mechanism by which batch normalization helps. A 2018 paper by Santurkar et al. argued that the primary benefit of batch normalization is smoothing the loss landscape rather than directly reducing internal covariate shift. In their experiments, even deliberately noisier internal covariate shift did not harm performance when batch normalization was present. This debate does not diminish the empirical effectiveness of batch normalization, but it does suggest the mechanism may be richer than originally described. What is unambiguous is the outcome: batch normalization reliably enables faster training with higher learning rates, less brittle hyperparameter choices, and often better generalization.

The Batch Normalization Algorithm

Batch normalization operates on a mini-batch of activations. For a single feature dimension (one neuron's pre-activation), consider a mini-batch B={x1,x2,,xm}\mathcal{B} = \{x_1, x_2, \ldots, x_m\} of mm samples. Batch normalization transforms these values through four steps.

This section covers all four steps: computing the batch mean, computing the batch variance, normalizing, and then applying a learned scale and shift.

Step 1: Compute the Batch Mean

First, compute the mean activation across the mini-batch:

μB=1mi=1mxi\mu_{\mathcal{B}} = \frac{1}{m} \sum_{i=1}^{m} x_i

where:

  • μB\mu_{\mathcal{B}}: the mean of activations over the current mini-batch
  • mm: the number of samples in the mini-batch
  • xix_i: the activation value for sample ii

This mean captures the "center" of the activation distribution for this batch. If activations are consistently large and positive, the mean will be large and positive. The goal is to shift this center to zero. Note that this is computed independently for each feature dimension: if a layer has 256 neurons, there are 256 separate mean computations, one for each neuron's activation values across the batch.

Step 2: Compute the Batch Variance

Next, measure how spread out the activations are around the mean:

σB2=1mi=1m(xiμB)2\sigma^2_{\mathcal{B}} = \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_{\mathcal{B}})^2

where:

  • σB2\sigma^2_{\mathcal{B}}: the variance of activations over the current mini-batch
  • (xiμB)2(x_i - \mu_{\mathcal{B}})^2: the squared deviation of each activation from the batch mean

This variance tells us how much the activations fluctuate around their mean. High variance means some activations are very large and others very small. Our goal is to compress this spread to approximately one. Note that this uses the population variance formula (dividing by mm, not m1m-1), which is a deliberate design choice: the original paper uses population statistics, and PyTorch's implementation follows this convention by default.

Step 3: Normalize

With the mean and variance in hand, we normalize each activation:

x^i=xiμBσB2+ϵ\hat{x}_i = \frac{x_i - \mu_{\mathcal{B}}}{\sqrt{\sigma^2_{\mathcal{B}} + \epsilon}}

where:

  • x^i\hat{x}_i: the normalized activation for sample ii
  • ϵ\epsilon: a small positive constant (typically 10510^{-5}) added for numerical stability to prevent division by zero when variance is very small

This step produces activations with zero mean and unit variance within the batch. The ϵ\epsilon term prevents numerical problems when the variance is close to zero, which can happen if all activations in the batch are nearly identical. Concretely, if a batch happens to contain nearly identical inputs to a neuron (for example, if the preceding layer has collapsed its outputs to a constant), the variance would be zero and division would produce either infinity or NaN. The ϵ\epsilon ensures the denominator is always at least ϵ0.003\sqrt{\epsilon} \approx 0.003, which is small enough not to distort the normalization but large enough to prevent numerical failure.

Step 4: Scale and Shift (Learnable Parameters)

Pure normalization would be too aggressive. Consider a layer followed by a sigmoid activation: if all inputs to the sigmoid are normalized to zero mean and unit variance, they land in the linear regime of the sigmoid, essentially turning the sigmoid into a linear function. The network would lose its nonlinearity. More generally, normalization restricts what each layer can represent: a layer that learned to produce large-scale outputs for particular inputs would have that learned information erased by normalization.

To restore representational flexibility, batch normalization introduces two learnable parameters per feature:

yi=γx^i+βy_i = \gamma \hat{x}_i + \beta

where:

  • γ\gamma: the scale parameter (learned during training), initialized to 1
  • β\beta: the shift parameter (learned during training), initialized to 0
  • yiy_i: the final batch-normalized output for sample ii

These parameters allow the network to learn the optimal output scale and shift for each feature. If the network determines that a particular feature dimension is most useful when centered at 5 with a spread of 2, gradient descent will drive β5\beta \to 5 and γ2\gamma \to 2. In effect, γ\gamma and β\beta allow the network to undo the normalization if that is beneficial, making normalization a learnable transformation rather than a rigid constraint.

This might seem paradoxical: why normalize and then allow the network to un-normalize? even if γ\gamma and β\beta eventually learn to reproduce the original un-normalized distribution, the gradients flowing through the network during training are better conditioned because of the normalization. The gradient updates to γ\gamma and β\beta happen in a well-behaved, smooth optimization landscape, whereas the gradient updates to the weights before batch normalization would have faced the full chaos of internal covariate shift. The normalization changes the geometry of the optimization landscape even if the final learned transformation looks similar to the pre-normalization case.

Each feature dimension in each layer has its own independent γ\gamma and β\beta. For a layer with dd features, batch normalization adds 2d2d learnable parameters. For large networks with many layers and many features per layer, this adds up, but the count is small relative to the weight matrices.

The Complete Transformation

Putting all four steps together, the full batch normalization transformation for feature dimension jj across a mini-batch is:

μj=1mi=1mxi,jσj2=1mi=1m(xi,jμj)2x^i,j=xi,jμjσj2+ϵyi,j=γjx^i,j+βj\begin{aligned} \mu_j &= \frac{1}{m} \sum_{i=1}^{m} x_{i,j} \\ \sigma^2_j &= \frac{1}{m} \sum_{i=1}^{m} (x_{i,j} - \mu_j)^2 \\ \hat{x}_{i,j} &= \frac{x_{i,j} - \mu_j}{\sqrt{\sigma^2_j + \epsilon}} \\ y_{i,j} &= \gamma_j \hat{x}_{i,j} + \beta_j \end{aligned}

where xi,jx_{i,j} is the activation of feature jj for sample ii. Notice that normalization happens independently per feature dimension. Each neuron's output is normalized using only that neuron's statistics across the batch.

This independence is important. It means that batch normalization does not impose any relationship between different features or neurons. Each feature dimension is treated as its own mini-distribution, normalized independently, and then rescaled with its own learned parameters. This preserves the network's ability to learn arbitrary correlations between features through the weight matrices, while still ensuring each individual feature maintains a stable distribution.

Running Statistics for Inference

During training, batch normalization uses statistics computed from the current mini-batch: μB\mu_{\mathcal{B}} and σB2\sigma^2_{\mathcal{B}} change with every batch. This works because mini-batches are reasonably representative of the full dataset, and the stochastic nature of batch statistics even provides a mild regularization effect (discussed further below).

At inference time, however, there is typically only a single sample or a small batch that may not represent the training distribution. Computing statistics over a single sample would be meaningless: the "mean" of a single value is that value itself, and the "variance" is zero, so normalization would map every single input to zero regardless of its content. Instead, batch normalization switches to using fixed statistics estimated from the entire training set.

These fixed estimates, called running statistics or population statistics, are accumulated during training using exponential moving averages:

μrunning(1α)μrunning+αμBσrunning2(1α)σrunning2+ασB2\begin{aligned} \mu_{\text{running}} &\leftarrow (1 - \alpha) \cdot \mu_{\text{running}} + \alpha \cdot \mu_{\mathcal{B}} \\ \sigma^2_{\text{running}} &\leftarrow (1 - \alpha) \cdot \sigma^2_{\text{running}} + \alpha \cdot \sigma^2_{\mathcal{B}} \end{aligned}

where:

  • μrunning\mu_{\text{running}}: the exponential moving average of batch means, updated after each training batch
  • σrunning2\sigma^2_{\text{running}}: the exponential moving average of batch variances, updated after each training batch
  • α\alpha: the momentum parameter controlling how quickly the running estimates update (typically 0.1 in PyTorch, meaning new batches contribute 10%)

At inference time, normalization uses these stable estimates:

x^i=xiμrunningσrunning2+ϵ\hat{x}_i = \frac{x_i - \mu_{\text{running}}}{\sqrt{\sigma^2_{\text{running}} + \epsilon}}

This means batch normalization layers have two distinct behaviors: a training mode that uses batch statistics and an inference mode that uses running statistics. In PyTorch, this is controlled with model.train() and model.eval().

Why Training and Inference Modes Differ

Forgetting to call model.eval() before inference is a common bug in PyTorch code. In training mode, the layer uses batch statistics that fluctuate with each batch, which introduces noise. In eval mode, it uses the stable running statistics. Running a model in training mode during evaluation produces noisier, slightly inconsistent predictions.

The momentum parameter α=0.1\alpha = 0.1 is a careful engineering choice. A larger α\alpha causes the running statistics to update more aggressively toward each new batch, which makes them track recent batches closely but can cause instability if the data distribution is noisy or the batch size is small. A smaller α\alpha makes the running estimates more stable but slower to converge to the true population statistics. The PyTorch default of 0.1 is a reasonable balance for most training setups with moderate batch sizes.

One subtlety worth understanding: the running statistics are not trained by gradient descent. They are updated via this exponential moving average rule and are never part of the computational graph during the forward pass in training mode. They are purely a bookkeeping mechanism for inference. This is why they are registered as "buffers" in PyTorch rather than "parameters": they are saved with the model state but not updated by the optimizer.

The figure below illustrates how running statistics converge toward the true dataset statistics over training batches, while individual batch estimates fluctuate around them.

Out[4]:
Visualization
Scatter plot of batch mean estimates with converging running mean line over 50 batches.
Running mean vs batch estimates over 50 training batches. The exponential moving average (red) converges to the true mean (green dashed), while individual batch estimates (blue dots) fluctuate around the true value.
Scatter plot of batch variance estimates with converging running variance line over 50 batches.
Running variance vs batch estimates over 50 training batches. The running variance stabilizes near the true population variance after roughly 30 batches, while batch estimates show greater fluctuation.

Batch Normalization and Gradient Flow

One reason batch normalization dramatically improves trainability is its effect on gradient flow during backpropagation. Recall from the backpropagation chapter that gradients can vanish (approach zero) or explode (grow unboundedly) as they flow through deep networks. Batch normalization addresses both failure modes through several complementary mechanisms.

The most direct mechanism is preventing activation saturation. By keeping activations normalized, batch normalization prevents activations from saturating nonlinear functions like sigmoid and tanh. Saturation occurs when activations are very large or very small, pushing them into the flat regions of these functions where gradients are near zero. In the flat region of a sigmoid, the derivative is less than 0.01, meaning each layer through which a gradient passes reduces its magnitude by at least 100-fold. After ten such layers, the gradient would be reduced by a factor of 102010^{-20}, rendering it numerically zero. A normalized activation stays near the linear region of these functions, where gradients remain healthy.

The second mechanism involves the loss landscape. The 2018 paper by Santurkar et al. provided strong evidence that batch normalization's primary effect is making the loss surface smoother and more predictable. They showed that with batch normalization, the gradient of the loss with respect to network parameters is more stable: it does not change drastically when you take a gradient step, which means gradient descent can use larger steps and still reliably descend. Mathematically, batch normalization reduces the Lipschitz constant of both the loss and its gradients, which directly translates to safer, faster optimization.

The third mechanism involves the interdependence of normalization statistics. The normalization step creates a dependency between training samples within a batch. When computing the gradient with respect to a single sample's pre-normalized activation xix_i, we must account for that sample's effect on μB\mu_{\mathcal{B}} and σB2\sigma^2_{\mathcal{B}}, which in turn affect all other samples' normalized activations. This interdependence produces a gradient smoothing effect: no single sample's gradient can dominate because its influence is distributed across the batch.

The scale parameter γ\gamma also gives the network a way to calibrate gradient magnitude. If a feature is becoming too saturated, the network can reduce γ\gamma to compress its output range. This provides an explicit, learnable knob for controlling the effective depth of gradient flow through any particular feature dimension.

Taken together, these mechanisms explain why networks with batch normalization can be trained with learning rates that are five to ten times larger than those needed without it. Larger learning rates mean faster convergence, which translates directly to reduced training time.

Backpropagation Through Batch Normalization

Understanding the backward pass through batch normalization requires careful accounting of how μB\mu_{\mathcal{B}} and σB2\sigma^2_{\mathcal{B}} depend on the inputs. Since both statistics are functions of all inputs in the batch, the gradient of the loss with respect to a pre-normalized input xix_i includes three terms: the direct effect through x^i\hat{x}_i, the indirect effect through μB\mu_{\mathcal{B}}, and the indirect effect through σB2\sigma^2_{\mathcal{B}}.

Let LL denote the loss, and let Lyi\frac{\partial L}{\partial y_i} denote the upstream gradient from the layer above. The gradients flow as follows:

Lγ=i=1mLyix^i,Lβ=i=1mLyi\frac{\partial L}{\partial \gamma} = \sum_{i=1}^{m} \frac{\partial L}{\partial y_i} \cdot \hat{x}_i, \quad \frac{\partial L}{\partial \beta} = \sum_{i=1}^{m} \frac{\partial L}{\partial y_i}

These are the gradients used to update the learnable parameters. The gradient with respect to the normalized activation is:

Lx^i=Lyiγ\frac{\partial L}{\partial \hat{x}_i} = \frac{\partial L}{\partial y_i} \cdot \gamma

And the full gradient with respect to the pre-normalized input, accounting for all dependencies, is:

Lxi=γmσB2+ϵ(mLx^ij=1mLx^jx^ij=1mLx^jx^j)\frac{\partial L}{\partial x_i} = \frac{\gamma}{m \cdot \sqrt{\sigma^2_{\mathcal{B}} + \epsilon}} \left( m \cdot \frac{\partial L}{\partial \hat{x}_i} - \sum_{j=1}^m \frac{\partial L}{\partial \hat{x}_j} - \hat{x}_i \sum_{j=1}^m \frac{\partial L}{\partial \hat{x}_j} \cdot \hat{x}_j \right)

This formula looks complicated but has a clean interpretation. The subtracted terms remove the mean and variance components of the gradient, which mirrors the mean-subtraction and variance-normalization of the forward pass. The 1m\frac{1}{m} factors ensure gradients are properly averaged across the batch. Modern deep learning frameworks compute this automatically through automatic differentiation, so you never need to implement it by hand, but understanding its structure helps diagnose issues like gradient instability or unexpected behavior when batch sizes change.

Where to Place Batch Normalization

One of the most debated aspects of batch normalization is its placement relative to the activation function. The original paper placed batch normalization before the activation (pre-activation), while many practitioners prefer placing it after the activation (post-activation). A third variant, used in residual networks, places batch normalization inside the residual branch. Each approach has distinct tradeoffs.

This section covers the arguments for each placement and the modern consensus, along with the specific considerations for residual architectures.

Pre-activation (Original Paper)

The original Ioffe and Szegedy paper placed batch normalization between the linear transformation and the activation:

Linear layer \to Batch Norm \to Activation

The argument: activations enter the normalization step before nonlinearity is applied. For sigmoid and tanh, this ensures the inputs land in the sensitive linear region of the activation, preventing saturation. The normalized distribution, centered at zero with unit variance, places most inputs in the range [2,2][-2, 2], which is exactly where sigmoid and tanh have meaningful gradients.

For ReLU, pre-activation normalization has a subtle problem. After normalizing, roughly half the activations will be negative (since the distribution is centered at zero). These negative activations get zeroed by ReLU, meaning that half the neurons are effectively dead after each batch normalization layer. The learned shift β\beta can compensate somewhat, shifting the distribution so fewer values fall below zero, but this is a real limitation. Networks with many such layers can suffer from significant dead-neuron problems.

Post-activation (Common in Practice)

Many modern implementations place batch normalization after the activation:

Linear layer \to Activation \to Batch Norm

The argument: this preserves the nonlinear output of the activation before normalization. For ReLU networks, post-activation normalization operates on non-negative values, which are more informative. The normalized values can then be scaled and shifted by γ\gamma and β\beta to the appropriate range for the next layer. Since ReLU outputs are always non-negative, the distribution being normalized is asymmetric, and batch normalization will shift it to have mean close to zero and unit variance, which is a sensible starting distribution for the next layer.

Pre-activation ResNets

For residual networks, He et al. (2016) proposed a "pre-activation" ordering that places batch normalization before both the activation and the linear layer within the residual branch:

Batch Norm \to Activation \to Linear layer (inside residual branch)

This ordering has a useful property: the signal through the shortcut connection is entirely un-modified by batch normalization. It passes directly from one residual block's output to the next addition. This means gradients can flow cleanly through the shortcut path without being affected by the normalization's scaling, which is particularly beneficial in very deep networks (hundreds of layers) where even small gradient modifications compound severely across depth. He et al. showed that this ordering improved training for networks deeper than 100 layers.

In practice, neither pre-activation nor post-activation placement is universally superior for all cases. For ReLU networks of standard depth, post-activation placement often works slightly better. For very deep networks with residual connections, pre-activation batch normalization tends to improve gradient flow. The practical guidance is to use post-activation as the default and experiment with pre-activation if training is unstable.

The Smoothing Effect on the Optimization Landscape

To appreciate why batch normalization helps so much beyond just normalization, it helps to visualize what it does to the loss landscape. Without batch normalization, the loss surface for a deep network is highly irregular: it has many narrow valleys, sharp cliffs, and regions where the gradient changes direction dramatically over small steps. The same gradient descent step that works well in one region of this landscape may cause divergence in another.

Batch normalization smooths this landscape through two related effects. First, by keeping activations in a controlled range, it prevents the extreme activation magnitudes that create the sharpest features of the loss surface. Second, as the Santurkar et al. analysis showed, it reduces the magnitude of the Hessian of the loss (the matrix of second derivatives), which directly bounds how much the gradient can change between consecutive gradient steps. This bound, called the β\beta-smoothness of the loss function, determines how large a learning rate can safely be used: specifically, gradient descent is guaranteed to converge for step sizes up to 1/β1/\beta.

The practical implication is a wider range of usable learning rates. Learning rate tuning for deep networks is notoriously difficult: too small, and training is slow; too large, and training diverges. Batch normalization widens the range of learning rates that produce stable training, making the optimization procedure more forgiving. This is why the original batch normalization paper was able to train an Inception architecture with learning rates 14 times larger than the baseline, achieving the same validation accuracy in 14 times fewer training steps.

Out[5]:
Visualization
Two parabolic curves illustrating sharp vs smooth loss landscapes, with gradient descent step arrows on each curve.
Schematic comparison of loss landscape sharpness with and without batch normalization. Without batch normalization (blue), the loss surface has sharper curvature and requires smaller learning rates. With batch normalization (orange), the landscape is smoother, enabling larger learning rate steps that still converge reliably. The arrows represent gradient descent steps of the same size on each landscape.

Batch Size Sensitivity

Batch normalization has a significant and often overlooked weakness: it depends fundamentally on batch size. To estimate meaningful statistics, a batch must contain enough samples to represent the data distribution. When batch sizes are very small, typically fewer than 8 to 16 samples, the batch statistics are too noisy to be reliable.

The variance estimate, in particular, becomes unstable with small batches. Consider estimating the variance of a distribution from only 4 samples. The sample variance is an unbiased estimator, but its own variance (the variance of the estimator itself) is proportional to 2m1\frac{2}{m-1} times the square of the population variance. For m=4m = 4, this means the estimated variance fluctuates wildly from batch to batch, making the normalization highly inconsistent. Different batches may normalize the same input to very different values, creating an unstable training signal.

This creates practical problems in several important scenarios:

  • Memory-constrained training: When using very large models or high-resolution images, GPU memory may only accommodate batches of 2 to 4 samples. For example, training a 3D medical image segmentation network on high-resolution volumes may only allow batch sizes of 1 or 2 due to memory constraints.
  • Object detection: Architectures like Faster R-CNN operate on single images at a time during the region proposal phase, making batch normalization impractical.
  • Recurrent networks: Variable-length sequences complicate the definition of "a batch" for normalization purposes, since different sequences contribute different numbers of timesteps.
  • Online learning: Some applications require learning from single samples, which is completely incompatible with batch statistics.
  • Multi-GPU training with gradient accumulation: When gradients are accumulated over many small mini-batches before updating weights, the effective batch size seen by each batch normalization layer is still the small per-step batch size, not the accumulated size.

The sensitivity to batch size was a significant practical obstacle in many domains and directly motivated the development of alternative normalization schemes.

Alternatives to Batch Normalization

The batch-size dependency of batch normalization motivated a family of alternative normalization techniques that compute statistics differently. Understanding these alternatives clarifies what batch normalization does and when to prefer each variant.

This section covers four main alternatives: layer normalization, group normalization, instance normalization, and weight normalization, along with their respective domains of application.

Layer Normalization

Layer normalization (Ba et al., 2016) normalizes across the feature dimension for each sample independently, rather than across the batch dimension:

μ(i)=1Hj=1Hxi,j,σ2(i)=1Hj=1H(xi,jμ(i))2\mu^{(i)} = \frac{1}{H} \sum_{j=1}^{H} x_{i,j}, \quad \sigma^{2(i)} = \frac{1}{H} \sum_{j=1}^{H} (x_{i,j} - \mu^{(i)})^2

where HH is the number of features in the layer. Each sample's activations are normalized using statistics computed only from that sample's feature vector, making it completely independent of batch size.

Layer normalization can process a single sample at inference time without any special casing, because the statistics are computed from that single sample's feature vector. There is no distinction between training mode and inference mode: the normalization always uses the current input's own statistics. This simplicity is an advantage over batch normalization.

Layer normalization is particularly well-suited for recurrent and transformer architectures, where sequences vary in length and batch composition is irregular. The original Transformer paper by Vaswani et al. (2017) uses layer normalization throughout, and it has become the default normalization for language models. When you encounter BERT, GPT, or any large language model, layer normalization is doing the heavy lifting of maintaining stable activations. We will encounter layer normalization extensively in the chapters on self-attention and transformer architectures.

The one place where layer normalization underperforms batch normalization is large-batch image classification with CNNs. In this setting, the feature statistics vary substantially across the spatial dimensions (edges look different from backgrounds, for instance), and normalizing across all features of a single spatial position loses useful spatial structure. Batch normalization's approach of normalizing each channel across the batch preserves spatial structure in a way that layer normalization does not.

Instance Normalization

Instance normalization normalizes each sample and each channel independently, using statistics from that channel's spatial dimensions. For a feature map of height HH and width WW for sample ii and channel cc:

μi,c=1HWh=1Hw=1Wxi,c,h,w\mu_{i,c} = \frac{1}{HW} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{i,c,h,w}

This is the most "local" of the normalization schemes: statistics are computed within each image and within each feature channel separately. No information is shared between images or between channels.

This locality is precisely what makes instance normalization useful for style transfer tasks, where the goal is to separate an image's "content" from its "style." The style of an image is encoded in the channel statistics (means and variances of feature activations), and instance normalization explicitly removes these statistics, effectively stripping away the style. A style transfer network can then re-apply different style statistics through the learned γ\gamma and β\beta parameters, producing images with the content of one image rendered in the style of another.

Group Normalization

Group normalization (Wu and He, 2018) divides the feature channels into GG groups and normalizes within each group for each sample independently:

μi,g=1(C/G)Sfeatures in group gxi,j\mu_{i,g} = \frac{1}{(C/G) \cdot S} \sum_{\text{features in group } g} x_{i,j}

where CC is the number of channels and SS is the number of spatial elements. Group normalization is a generalization that interpolates between layer normalization and instance normalization: when G=1G = 1, every channel is in the same group and it becomes layer normalization; when G=CG = C, each channel is its own group and it becomes instance normalization.

Group normalization was designed specifically for computer vision tasks where batch sizes must be small due to high-resolution inputs. Wu and He showed that on ImageNet classification with a batch size of 2, group normalization significantly outperforms batch normalization (which degrades severely at this batch size) and is only slightly behind batch normalization at the standard batch size of 32. The key insight is that within each group of channels, there are enough activations to compute reliable statistics even with a single sample, because each channel contributes many spatial positions.

A common choice in practice is G=32G = 32 for networks with 256 or more channels, or G=8G = 8 for networks with 64 channels. The group size should be chosen so that each group contains a reasonable number of channels (at least 8 to 16) to enable stable statistics.

Weight Normalization

Weight normalization (Salimans and Kingma, 2016) takes a different approach entirely: instead of normalizing the activations, it normalizes the weight vectors. Each weight vector w\mathbf{w} is reparameterized as:

w=gvv\mathbf{w} = \frac{g}{\|\mathbf{v}\|} \mathbf{v}

where v\mathbf{v} is an un-normalized weight vector and gg is a scalar magnitude parameter. This decouples the direction of each weight vector from its magnitude, which stabilizes the gradient updates.

Weight normalization has different properties from activation normalization. It introduces no dependency between training samples and has no distinction between training and inference modes, making it suitable for online learning and recurrent networks where mini-batch dependencies are problematic. It is also computationally cheaper, as it does not require computing statistics over a batch. However, it does not smooth the loss landscape as aggressively as batch normalization, and it requires careful data-dependent initialization to work well.

Comparison

The normalization methods differ in which dimensions they compute statistics over:

  • Batch normalization: statistics over the batch dimension (for each feature independently)
  • Layer normalization: statistics over the feature dimension (for each sample independently)
  • Instance normalization: statistics over spatial dimensions (for each sample and channel independently)
  • Group normalization: statistics over spatial and channel-group dimensions (for each sample and group independently)
  • Weight normalization: statistics over the weight vector dimension (no activation statistics involved)

The choice depends on the architecture and task. For large-batch image classification with CNNs, batch normalization typically performs best. For NLP and sequence models with variable-length inputs, layer normalization is standard. For image generation and style transfer, instance normalization is common. For memory-constrained vision tasks, group normalization is the practical choice. For reinforcement learning and online learning scenarios, weight normalization is often preferred.

Out[6]:
Visualization
Grid diagram showing which tensor dimensions are normalized by batch norm, layer norm, instance norm, and group norm.
Comparison of normalization dimensions across four methods for a feature tensor with batch dimension N and channel dimension C. Each color region shows which elements share normalization statistics. Batch norm groups along N per channel; layer norm groups along C per sample; instance norm normalizes each sample-channel pair independently; group norm normalizes subsets of channels together per sample.

Batch Normalization in CNNs vs. NLP

The mechanics of batch normalization differ between convolutional neural networks and sequence models, because these architectures have fundamentally different data shapes and different notions of what constitutes a "feature."

Batch Normalization in CNNs

In a CNN, the output of a convolutional layer for a batch of NN images has shape (N,C,H,W)(N, C, H, W) where CC is the number of channels, HH the height, and WW the width of the feature map. Batch normalization normalizes each channel independently across the batch and spatial dimensions:

μc=1NHWn=1Nh=1Hw=1Wxn,c,h,w\mu_c = \frac{1}{N \cdot H \cdot W} \sum_{n=1}^{N} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{n,c,h,w}

This means every spatial location in the same channel of the same image contributes to the mean and variance. The intuition is that a channel in a CNN represents a specific learned feature detector, such as a particular edge orientation or texture, and all spatial locations where that feature appears should be normalized together. Whether the feature appears in the upper left or lower right of the image, it represents the same underlying pattern and should be treated consistently.

Each channel has its own γc\gamma_c and βc\beta_c parameters. For a convolutional layer with 256 output channels, batch normalization adds 512 learnable parameters (256×2256 \times 2), which is negligible compared to the weight matrix itself but still affects training dynamics.

For CNNs applied to image classification, batch normalization has been essential to achieving state-of-the-art results. ResNet, VGG, Inception, DenseNet, and virtually every other high-performing CNN architecture from 2015 onward uses batch normalization after its convolutional layers. The ability to train networks with dozens to hundreds of layers, which was impractical before batch normalization, directly enabled the advances in image recognition that followed.

Batch Normalization in NLP

For sequence models, the data has shape (N,T,H)(N, T, H) where TT is the sequence length and HH is the hidden dimension. Batch normalization across the batch and time dimensions would mix different timesteps and different sentences, which is conceptually problematic. The statistics of the token at position 1 in one sentence may be very different from the token at position 1 in another sentence (one might be a subject noun, the other an article or punctuation). Normalizing these together would conflate structurally different representations.

Sequences in a mini-batch often have different lengths. The padding tokens used to make sequences the same length would need to be excluded from the statistics computation, which complicates the implementation. And at inference time, a single sequence should produce deterministic outputs, but batch normalization in training mode would give different results depending on what other sequences happen to be in the same batch.

These issues collectively make batch normalization unsuitable for most NLP applications. Layer normalization, which normalizes across the feature dimension HH for each individual token position independently, is the standard choice. When you look at transformer code, each multi-head attention block and feedforward layer is followed by layer normalization, not batch normalization. This is not an accident but a deliberate design decision rooted in the differences between image and sequence data.

Batch Normalization as Regularization

An interesting side effect of batch normalization is regularization. Because normalization statistics are computed over the current mini-batch rather than the full dataset, they introduce a form of stochastic noise. Each mini-batch produces slightly different mean and variance estimates, which vary the normalization applied to each sample depending on what other samples appear in the same batch.

Consider a concrete example: sample AA appears in one batch alongside samples B,C,DB, C, D, and in another batch alongside samples E,F,GE, F, G. In the first batch, AA's activation might be slightly above the mean; in the second batch, it might be slightly below. The network sees AA normalized differently each time it appears during training. This variation forces the network to learn representations that are robust to small distributional perturbations, which is exactly the property that improves generalization.

This noise acts similarly to dropout: it prevents the network from relying too heavily on any specific activation pattern, improving generalization. In practice, networks trained with batch normalization often require less or even no dropout. The original batch normalization paper reported that the regularization effect of batch normalization was strong enough to remove the need for dropout in their Inception architecture, achieving better results on ImageNet without dropout than the baseline with dropout.

The strength of this regularization effect scales with the "noisiness" of the batch statistics, which in turn scales with the inverse of the batch size. Small batches produce noisier statistics and more regularization. Large batches produce cleaner statistics and less regularization. This creates a subtle tradeoff: increasing batch size improves the stability of batch normalization as a normalization technique, but simultaneously reduces its regularization benefit. Some researchers argue that this is why models trained with very large batch sizes (thousands of samples) sometimes generalize worse than those trained with moderate batch sizes, despite converging faster.

However, this regularization property disappears at inference time, when the stable running statistics are used instead of stochastic batch statistics. This is by design: at inference time, we want deterministic, stable predictions. The regularization effect was only ever intended as a training-time phenomenon.

Worked Example: Batch Norm By Hand

Let's trace through a single batch normalization step with a concrete mini-batch. Suppose we have a batch of 4 samples with activations for one feature dimension:

x1=2.0,x2=4.0,x3=3.0,x4=1.0x_1 = 2.0, \quad x_2 = 4.0, \quad x_3 = 3.0, \quad x_4 = 1.0

Step 1: Compute the batch mean.

μB=2.0+4.0+3.0+1.04=10.04=2.5\mu_{\mathcal{B}} = \frac{2.0 + 4.0 + 3.0 + 1.0}{4} = \frac{10.0}{4} = 2.5

Step 2: Compute the batch variance.

σB2=(2.02.5)2+(4.02.5)2+(3.02.5)2+(1.02.5)24\sigma^2_{\mathcal{B}} = \frac{(2.0-2.5)^2 + (4.0-2.5)^2 + (3.0-2.5)^2 + (1.0-2.5)^2}{4} =0.25+2.25+0.25+2.254=5.04=1.25= \frac{0.25 + 2.25 + 0.25 + 2.25}{4} = \frac{5.0}{4} = 1.25

Step 3: Normalize. Using ϵ=105\epsilon = 10^{-5}:

x^1=2.02.51.25+1050.51.1180.447\hat{x}_1 = \frac{2.0 - 2.5}{\sqrt{1.25 + 10^{-5}}} \approx \frac{-0.5}{1.118} \approx -0.447 x^21.342,x^30.447,x^41.342\hat{x}_2 \approx 1.342, \quad \hat{x}_3 \approx 0.447, \quad \hat{x}_4 \approx -1.342

Notice that the normalized values sum to zero and have unit variance: (0.447)2+(1.342)2+(0.447)2+(1.342)2=0.2+1.8+0.2+1.8=4.0(-0.447)^2 + (1.342)^2 + (0.447)^2 + (-1.342)^2 = 0.2 + 1.8 + 0.2 + 1.8 = 4.0, and 4.0/4=1.04.0 / 4 = 1.0. The normalization has precisely achieved its goal.

Step 4: Scale and shift. With initial parameters γ=1.0\gamma = 1.0 and β=0.0\beta = 0.0 (the default initialization):

yi=γx^i+β=1.0x^i+0.0=x^iy_i = \gamma \hat{x}_i + \beta = 1.0 \cdot \hat{x}_i + 0.0 = \hat{x}_i

Initially, the transformation is pure normalization. As training proceeds, γ\gamma and β\beta will shift away from these initial values to match the learned optimal distribution. If the downstream layer works best when inputs are scaled by 2 and shifted by 0.5, gradient descent will move γ2.0\gamma \to 2.0 and β0.5\beta \to 0.5 over the course of training.

What happens in the next training step? If a different batch is drawn with slightly different values, say mean 2.7 instead of 2.5, the normalized values will be different. Sample x1=2.0x_1 = 2.0 will be normalized to 2.02.7σ20.625\frac{2.0 - 2.7}{\sqrt{\sigma^2}} \approx -0.625 instead of 0.447-0.447. This is the stochastic noise of batch normalization. Over many batches, the network learns representations that are robust to this variability.

The figure below shows the effect of batch normalization on activation distributions.

Out[7]:
Visualization
Histogram of raw activations centered near 4.0.
Activation distribution before batch normalization. The feature has mean 4.0 and standard deviation 2.0, representing an arbitrary, uncontrolled distribution typical of deep network activations.
Histogram of normalized activations centered near 0.
After the normalize step: activations are recentered at zero with unit variance. All features share a common scale, regardless of their raw magnitude, which stabilizes gradient flow.
Histogram of scaled and shifted activations centered near 0.5.
After applying learned scale (gamma=1.5) and shift (beta=0.5): the network recovers a useful distribution. The gamma and beta parameters restore representational capacity that pure normalization removed.

Code Implementation

This section walks through implementing batch normalization from scratch in PyTorch, then shows how to use PyTorch's built-in nn.BatchNorm1d and nn.BatchNorm2d modules in a realistic network. The goal is to solidify your understanding of both the mechanics and the practical pitfalls.

Batch Norm from Scratch

Let's implement batch normalization from scratch to see exactly how the forward pass works, including the training/inference mode switch.

In[8]:
Code
import torch


class BatchNorm1dManual:
    """
    Manual batch normalization for 1D inputs.
    Demonstrates the training vs inference modes.
    """

    def __init__(self, num_features, eps=1e-5, momentum=0.1):
        self.num_features = num_features
        self.eps = eps
        self.momentum = momentum

        # Learnable parameters
        self.gamma = torch.ones(num_features)
        self.beta = torch.zeros(num_features)

        # Running statistics (updated during training, used during inference)
        self.running_mean = torch.zeros(num_features)
        self.running_var = torch.ones(num_features)

        self.training = True

    def forward(self, x):
        if self.training:
            # Compute batch statistics
            batch_mean = x.mean(dim=0)
            batch_var = x.var(dim=0, unbiased=False)  # Population variance

            # Normalize using batch statistics
            x_hat = (x - batch_mean) / torch.sqrt(batch_var + self.eps)

            # Update running statistics with exponential moving average
            self.running_mean = (
                1 - self.momentum
            ) * self.running_mean + self.momentum * batch_mean
            self.running_var = (
                1 - self.momentum
            ) * self.running_var + self.momentum * batch_var
        else:
            # Use running statistics for inference
            x_hat = (x - self.running_mean) / torch.sqrt(
                self.running_var + self.eps
            )

        # Scale and shift with learned parameters
        return self.gamma * x_hat + self.beta
Out[9]:
Console
Input activations:
[[2. 1. 3.]
 [4. 3. 1.]
 [3. 2. 4.]
 [1. 4. 2.]]

Input mean per feature: [2.5, 2.5, 2.5]
Input std per feature:  [1.291, 1.291, 1.291]

Output after batch norm (training mode):
[[-0.4472 -1.3416  0.4472]
 [ 1.3416  0.4472 -1.3416]
 [ 0.4472 -0.4472  1.3416]
 [-1.3416  1.3416 -0.4472]]

Output mean per feature: [0.0, 0.0, 0.0]
Output std per feature:  [1.1547, 1.1547, 1.1547]

The output shows that batch normalization brings the mean of each feature very close to zero and the standard deviation close to one. With the initial γ=1\gamma = 1 and β=0\beta = 0, the output is simply the normalized input.

Using PyTorch's Built-in Batch Norm

PyTorch provides nn.BatchNorm1d (for fully connected layers) and nn.BatchNorm2d (for convolutional layers). Let's see how to use them and inspect their internal state.

In[10]:
Code
# BatchNorm1d: for fully connected layers
# Input shape: (batch_size, num_features)
bn1d = nn.BatchNorm1d(num_features=128)

# BatchNorm2d: for convolutional layers
# Input shape: (batch_size, channels, height, width)
bn2d = nn.BatchNorm2d(num_features=64)  # 64 channels
Out[11]:
Console
BatchNorm1d: gamma (weight) shape = torch.Size([128]), beta (bias) shape = torch.Size([128])
  gamma initialized to all 1.0: True
  beta initialized to all 0.0:  True

BatchNorm2d: gamma (weight) shape = torch.Size([64]) (one per channel)

Running mean (first 3): [0.0, 0.0, 0.0]
Running var  (first 3): [1.0, 1.0, 1.0]

PyTorch initializes gamma (weight) to ones and beta (bias) to zeros, matching our manual implementation. The running statistics start at zero mean and unit variance.

Building a Network with Batch Normalization

Let's build a small fully connected classifier that incorporates batch normalization, and observe the effect on training stability compared to a network without it.

In[12]:
Code
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from torch.utils.data import DataLoader, TensorDataset

torch.manual_seed(42)
np.random.seed(42)

# Generate a synthetic classification dataset
X_np, y_np = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=10,
    n_redundant=5,
    n_classes=2,
    class_sep=0.8,
    random_state=42,
)

# Standardize inputs
scaler = StandardScaler()
X_np = scaler.fit_transform(X_np).astype(np.float32)
y_np = y_np.astype(np.int64)

# Split into train/test
split = 800
X_train = torch.tensor(X_np[:split])
y_train = torch.tensor(y_np[:split])
X_test = torch.tensor(X_np[split:])
y_test = torch.tensor(y_np[split:])

train_loader = DataLoader(
    TensorDataset(X_train, y_train), batch_size=64, shuffle=True
)
In[13]:
Code
# Define two architectures: with and without batch normalization
class MLPWithBN(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.BatchNorm1d(hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.BatchNorm1d(hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim),
        )

    def forward(self, x):
        return self.net(x)


class MLPWithoutBN(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim),
        )

    def forward(self, x):
        return self.net(x)


def train_model(model, loader, n_epochs=30, lr=0.01):
    optimizer = optim.SGD(model.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()
    losses = []
    for epoch in range(n_epochs):
        model.train()
        epoch_loss = 0.0
        for X_batch, y_batch in loader:
            optimizer.zero_grad()
            logits = model(X_batch)
            loss = criterion(logits, y_batch)
            loss.backward()
            optimizer.step()
            epoch_loss += loss.item()
        losses.append(epoch_loss / len(loader))
    return losses


# Train both models - batch norm allows a higher learning rate
model_bn = MLPWithBN(input_dim=20, hidden_dim=64, output_dim=2)
model_no_bn = MLPWithoutBN(input_dim=20, hidden_dim=64, output_dim=2)

losses_bn = train_model(model_bn, train_loader, n_epochs=30, lr=0.05)
losses_no_bn = train_model(model_no_bn, train_loader, n_epochs=30, lr=0.01)
In[14]:
Code
# Evaluate both models on the test set
def evaluate_model(model, X, y):
    model.eval()
    with torch.no_grad():
        logits = model(X)
        preds = logits.argmax(dim=1)
        accuracy = (preds == y).float().mean().item()
    return accuracy


acc_bn = evaluate_model(model_bn, X_test, y_test)
acc_no_bn = evaluate_model(model_no_bn, X_test, y_test)
Out[15]:
Console
Test accuracy WITH batch normalization:    0.935
Test accuracy WITHOUT batch normalization: 0.715

Final training loss WITH batch norm:    0.0820
Final training loss WITHOUT batch norm: 0.6064

Batch norm model trained with lr=0.05; plain model trained with lr=0.01
(Higher lr without batch norm would diverge)

The model with batch normalization achieves better accuracy and lower training loss, even though it used a higher learning rate (0.05 vs 0.01). Without batch normalization, using the higher learning rate would likely cause training to diverge. This demonstrates one of the most practically important effects of batch normalization: it allows more aggressive learning rates, which directly translate to faster convergence and better final performance within a fixed training budget.

The training curves below show how batch normalization enables faster convergence.

Out[16]:
Visualization
Line plot comparing training loss curves for models with and without batch normalization.
Training loss over 30 epochs for networks with and without batch normalization. The batch-normalized model (trained with lr=0.05) converges faster and to a lower final loss than the plain network (trained with lr=0.01). Using lr=0.05 without batch normalization would cause divergence, illustrating how batch normalization widens the stable learning rate range.

The training=True vs training=False Distinction

One of the most important practical aspects of batch normalization is correctly switching between training and inference modes. Let's verify how model.eval() changes behavior and understand why it matters.

In[17]:
Code
# Demonstrate the effect of switching to inference mode on batch norm behavior.
# We train briefly on a batch to warm up running statistics, then compare
# outputs between training mode and inference mode on the same single sample.
torch.manual_seed(42)
test_model = nn.Sequential(nn.Linear(5, 8), nn.BatchNorm1d(8), nn.ReLU())

# Pass a small training batch to warm up the running statistics
x_batch = torch.randn(16, 5)
test_model.train()
_ = test_model(x_batch)  # Updates running_mean and running_var

# Now test with a single sample in BOTH modes
x_single = torch.randn(1, 5)

# Training mode on a single sample raises ValueError in PyTorch.
# Use a 2-sample batch to approximate what training mode would do.
x_two = torch.cat([x_single, torch.randn(1, 5)], dim=0)
test_model.train()
out_train_approx = test_model(x_two)[0:1]  # Keep only the first sample's output

# Inference mode: uses stable running statistics, works with any batch size
test_model.eval()
out_inference = test_model(x_single)

outputs_match = torch.allclose(out_train_approx, out_inference, atol=1e-2)
Out[18]:
Console
Training mode output (first 4 values): [1. 0. 0. 1.]
Inference mode output (first 4 values): [0.     0.     0.     0.8766]

Outputs match (within tolerance of 0.01): False

Training mode uses live batch statistics, producing different outputs
depending on which other samples appear in the batch.
Inference mode uses fixed running statistics for consistent, deterministic predictions.
Always switch to inference mode before generating predictions!

With a batch size of 1 in training mode, batch normalization computes the variance of a single value, which is always zero. This produces the wrong result and PyTorch will raise an error. Switching to inference mode before generating predictions ensures the stable running statistics are used, which is always the correct behavior.

Key Parameters

The key parameters for nn.BatchNorm1d and nn.BatchNorm2d are:

  • num_features: The number of features (for BatchNorm1d) or channels (for BatchNorm2d). This determines the shape of the learnable γ\gamma and β\beta parameters.
  • eps: The small constant added to the variance for numerical stability. Default is 1e-5. Rarely needs tuning, but you might increase it to 1e-3 if you experience NaN losses due to very small variance estimates.
  • momentum: The momentum for updating running statistics. Default is 0.1 in PyTorch, meaning new batches contribute 10% to the running estimate. Use smaller values (e.g., 0.01) for very large datasets where you want the running statistics to update conservatively.
  • affine: If True (default), learnable γ\gamma and β\beta are included. If False, batch norm only normalizes without the scale and shift step. Setting affine=False is occasionally useful when you want normalization without additional parameters.
  • track_running_stats: If True (default), maintains running mean and variance for inference. If False, always uses batch statistics. This can be useful for fine-tuning scenarios where you want the normalization to adapt to a new domain's statistics.

Practical Considerations and Common Pitfalls

Working with batch normalization in real projects involves several practical considerations beyond the theory. Understanding these pitfalls can save significant debugging time.

Forgetting to Switch Modes

The most common batch normalization bug is forgetting to switch to inference mode before evaluation or prediction. When a model is in training mode and receives a small batch, the batch statistics can differ substantially from the running statistics, producing artificially worse metrics during evaluation. This is a particularly subtle bug because the model still produces outputs; they are just slightly wrong and inconsistent.

The correct pattern is:

In[29]:
Code
# Training loop
model.train()
for batch in train_loader:
    pass  # ... training step

# Evaluation loop
model.eval()
with torch.no_grad():
    for batch in val_loader:
        pass  # ... evaluation step

Batch Norm with Frozen Layers

When fine-tuning a pre-trained model, you may freeze some layers to prevent their weights from updating. Frozen batch normalization layers pose a subtle problem: even though their γ\gamma and β\beta parameters are frozen, in training mode they will still update their running statistics based on the new domain's data. If you need the running statistics to remain frozen (for example, to preserve the original domain's normalization), you must explicitly set the normalization layers to inference mode while training other parts of the network.

This is handled by switching specific submodules to inference mode rather than the whole model, or by iterating over the model's modules and switching BatchNorm layers explicitly. Libraries like HuggingFace Transformers use layer normalization partly because it avoids this complication entirely.

Small Batch Size Instability

When batch size falls below about 8, batch normalization's variance estimates become unreliable. You will see this as erratic training loss, poor convergence, or NaN values. The fix is to switch to a batch-size-independent normalization method. Group normalization with an appropriate number of groups is the standard solution for computer vision tasks. Layer normalization is the standard solution for sequence tasks.

If you must use batch normalization with small batches (for example, when fine-tuning a pre-trained model that uses it), Synchronized Batch Normalization across multiple GPUs can effectively increase the batch size seen by each normalization layer. If you have 4 GPUs each processing 4 samples, synchronized batch normalization computes statistics over all 16 samples, restoring the effective batch size to a reasonable range.

Limitations and Impact

Batch normalization changed what kinds of networks were trainable. Before its introduction, training networks with more than a dozen layers was extremely difficult, requiring careful initialization, small learning rates, and extensive hyperparameter tuning. After batch normalization, researchers could train networks with hundreds of layers. ResNet-152, with its 152 weight layers, would have been completely intractable without batch normalization. The ImageNet challenge winner in 2015 was a 152-layer residual network that relied fundamentally on batch normalization for stable training.

The impact extended beyond just enabling deeper networks. Batch normalization also reduced sensitivity to hyperparameter choices. Without it, the learning rate had to be tuned carefully for each architecture and dataset: too high and training diverged, too low and training was painfully slow. With batch normalization, a much wider range of learning rates produced stable, fast convergence. This reduced the cost of hyperparameter tuning and made deep learning accessible to practitioners without the patience for extensive grid search. The original paper showed that the same Inception architecture trained with batch normalization and a 14x larger learning rate achieved equivalent accuracy in 14 times fewer training steps, a dramatic practical improvement.

However, batch normalization carries significant limitations that have shaped the development of the field. The dependence on batch size is its most fundamental constraint. Small batches produce unreliable statistics, which is a practical problem in many domains. Memory-intensive tasks such as high-resolution image segmentation, video analysis, and multi-task learning with complex models often cannot accommodate the large batch sizes that batch normalization needs. This limitation directly motivated the development of layer normalization, group normalization, and instance normalization, each of which has become standard in its respective domain.

Another limitation is the gap between training and inference behavior. Because batch normalization computes different statistics during training and inference, there can be a distribution mismatch if the training data distribution differs from the inference distribution. This is particularly problematic in domain adaptation scenarios, where the test data comes from a different distribution than training data. The running statistics accumulated during training on the source domain may not accurately represent the target domain, leading to poor generalization. One research direction to address this is test-time batch normalization, where the running statistics are recomputed from a few batches of target-domain data before inference, but this adds complexity and latency.

Batch normalization also introduces a dependency between samples in the same mini-batch, which breaks the assumption that training samples are independent. This can cause issues in certain settings, such as contrastive learning, where the content of the batch matters for the training signal. In contrastive learning, the model learns by comparing pairs of similar and dissimilar samples. If batch normalization statistics depend on which samples appear together, the normalization inadvertently encodes information about batch composition, creating a confound that can harm the quality of the learned representations.

The introduction of transformer architectures, with their reliance on layer normalization, has significantly reduced the role of batch normalization in the most impactful models of the past decade. Large language models from BERT and GPT onward use layer normalization, not batch normalization. For practitioners in the NLP space, batch normalization is mostly a historical curiosity relevant mainly as context for understanding why layer normalization was chosen. For practitioners in computer vision, it remains the normalization of choice for image classification tasks where large batch sizes are feasible.

Despite these limitations and the rise of alternatives, batch normalization remains widely used in convolutional neural networks for image tasks, where large batch sizes are feasible and the technique's benefits are most pronounced. And more fundamentally, understanding batch normalization deeply teaches something important about what makes deep networks hard to train and what properties of the optimization landscape matter for stable, fast convergence. Those lessons carry forward into every alternative normalization scheme and every architecture decision you will encounter.

Summary

Batch normalization normalizes layer activations using mini-batch statistics during training, addressing internal covariate shift and dramatically improving the trainability of deep networks. The algorithm involves four steps: computing batch mean, computing batch variance, normalizing to zero mean and unit variance, then applying learned scale γ\gamma and shift β\beta parameters.

Key ideas to retain:

  • Internal covariate shift: Without normalization, each layer faces a constantly shifting input distribution as upstream weights update, requiring small learning rates and careful tuning. Batch normalization fixes this by explicitly normalizing layer inputs every forward pass.

  • Training vs inference: During training, batch normalization uses batch statistics that change with every mini-batch; during inference, it uses running statistics accumulated via exponential moving averages. Always switch to inference mode before generating predictions.

  • Learnable parameters: The γ\gamma and β\beta parameters allow the network to learn the optimal output scale and shift, preserving representational flexibility after normalization. This is what prevents normalization from being a straightjacket on the network's expressiveness.

  • Placement: Batch normalization is typically placed after the linear transformation and before or after the activation. For ReLU networks, post-activation placement often works slightly better. For very deep residual networks, pre-activation placement can improve gradient flow.

  • Batch size sensitivity: Batch normalization requires reasonably large batches (at least 16, ideally 32 or more) to produce reliable statistics. Small batches cause training instability and poor convergence.

  • Smooth loss landscape: Beyond normalization, batch normalization smooths the loss surface and reduces its Lipschitz constant, enabling larger, safer gradient steps. This is likely its primary mechanism of benefit.

  • Regularization effect: The stochasticity of batch statistics provides implicit regularization during training, reducing the need for explicit regularization techniques like dropout.

  • Alternatives: Layer normalization (default for NLP and transformers), group normalization (small-batch vision tasks), instance normalization (style transfer), and weight normalization each address batch normalization's limitations in specific domains.

In the next chapter, we explore dropout, another foundational regularization technique. Whereas batch normalization addresses training stability through normalization, dropout addresses overfitting by randomly deactivating neurons during training. The two techniques are often used together in modern architectures, and understanding their complementary roles helps clarify why each component is present in the networks you encounter.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about batch normalization.

Batch Normalization Quiz

Question 1 of 80 of 8 completed
What problem does batch normalization primarily address during deep network training?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025batchnormalization, author = {Michael Brenndoerfer}, title = {Batch Normalization: Stabilizing Deep Network Training}, year = {2025}, url = {https://mbrenndoerfer.com/writing/batch-normalization-deep-learning}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-15} }
APAAcademic
Michael Brenndoerfer (2025). Batch Normalization: Stabilizing Deep Network Training. Retrieved from https://mbrenndoerfer.com/writing/batch-normalization-deep-learning
MLAAcademic
Michael Brenndoerfer. "Batch Normalization: Stabilizing Deep Network Training." 2026. Web. September 15, 2026. <https://mbrenndoerfer.com/writing/batch-normalization-deep-learning>.
CHICAGOAcademic
Michael Brenndoerfer. "Batch Normalization: Stabilizing Deep Network Training." Accessed September 15, 2026. https://mbrenndoerfer.com/writing/batch-normalization-deep-learning.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Batch Normalization: Stabilizing Deep Network Training'. Available at: https://mbrenndoerfer.com/writing/batch-normalization-deep-learning (Accessed: September 15, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Batch Normalization: Stabilizing Deep Network Training. https://mbrenndoerfer.com/writing/batch-normalization-deep-learning

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.