Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Batch Normalization: Stabilizing Neural Network Training

Recommended resources

Hello! Welcome to the final lesson in our "Neural Network Foundations" module.

In our previous lesson, we implemented dropout, a powerful regularization technique to combat overfitting. We now have a tool to prevent our models from memorizing the training data. Today, we turn our attention to another fundamental challenge in training deep networks: the stability and speed of the training process itself.

As a network trains, the weights in the early layers are constantly updated. This causes the distribution of the activations—the inputs to the subsequent layers—to continuously shift. This phenomenon is called internal covariate shift. Imagine a later layer trying to learn a function while the statistical properties (like the mean and standard deviation) of its inputs are always changing. It's like trying to hit a moving target. This instability forces us to use small learning rates, slowing down training.

Today's lesson addresses this problem head-on. Our learning outcome is to implement batch normalization and explain its effect on stabilizing the training process.

1. The Problem: Internal Covariate Shift

Let's visualize the problem. Many activation functions, like the sigmoid or tanh, have "saturated" regions where their gradient is close to zero. If the inputs to these functions fall into these regions, the gradients vanish, and learning grinds to a halt. Internal covariate shift can push the activations into these undesirable regions as the network trains.

Batch Normalization and Internal Covariate Shift
This image illustrates internal covariate shift. The top panels show how a layer's inputs (pink circles) can shift into the saturated (flat) regions of a sigmoid activation function, causing vanishing gradients. The bottom-right panel shows the goal: to keep the inputs (orange circles) in the active, non-saturated region.

The core idea of Batch Normalization (BN), introduced in 2015, is elegantly simple: if the shifting distributions are the problem, why not explicitly force the inputs to each layer to have a stable distribution throughout training?


2. The Core Idea of Batch Normalization

Batch Normalization normalizes the output of a previous layer before it's fed into the next one. For each mini-batch during training, and for each feature/neuron independently, it performs the following steps:

Batch Normalization Process Flow
This flowchart shows the key steps of Batch Normalization during training. For a given batch, it calculates the mean and standard deviation, normalizes the data, and then applies a learnable scale and shift. It also maintains running averages of the mean and standard deviation for later use during inference.

This process ensures that the inputs to the next layer are always centered around a mean of 0 and have a standard deviation of 1, regardless of what's happening in the preceding layers.

To get a better sense of why this is so effective, let's start with a video that outlines the main benefits.

Batch Normalization - EXPLAINED!

The CodeEmporium channel provides a great high-level overview of the three main benefits of Batch Normalization.

Watch from 01:16 to 05:54. Focus on understanding the three key advantages discussed: increased training speed, reduced importance of weight initialization, and the regularization effect.


3. Forward Pass: The Mechanics

While forcing the activations to a standard Gaussian distribution (mean 0, variance 1) seems like a good idea for stability, it might be too restrictive. What if the optimal representation for a layer actually requires a different mean and variance?

To solve this, Batch Normalization introduces two learnable parameters per feature:

  1. A scale parameter, (gamma), initialized to 1.
  2. A shift parameter, (beta), initialized to 0.

After normalization, the layer's output is scaled by and shifted by .

Here, is the normalized activation. Since and are learnable, the network can decide the optimal mean and variance for each layer's inputs. In a worst-case scenario, it can learn and , effectively undoing the normalization and recovering the original activations. This means BN can't hurt the network's representational power.

Batch Normalization: Stabilizing Deep Network Training

The article 'Batch Normalization: Stabilizing Deep Network Training' provides a fantastic step-by-step breakdown of the forward pass, including the mathematics and the crucial role of gamma and beta. The visualizations are particularly helpful.

Please read the sections titled 'Batch Statistics Computation' and 'Learnable Scale and Shift Parameters'. Focus on: The formulas for calculating the batch mean (μ) and variance (σ²). The normalization formula, including the small epsilon (ε) for numerical stability. The explanation of why gamma (γ) and beta (β) are necessary to restore the network's representational capacity. Pay attention to the visualizations showing their effect.

Training vs. Inference

A crucial detail of Batch Normalization is that it behaves differently during training and inference.

  • During training, we normalize using the current mini-batch's mean and variance. This is fine because we process data in batches.
  • During inference (testing), we might want to process a single example. Calculating a "batch" mean and variance for a single sample is meaningless. Furthermore, we want our model's output to be deterministic for a given input, not dependent on other examples in a hypothetical batch.

The solution is to calculate an exponential moving average of the batch means and variances during training. These running statistics provide a stable estimate of the entire training set's statistics. At inference time, we use these fixed running averages to normalize the inputs.

Let's watch a detailed walkthrough of this process.

Building makemore Part 3: Activations & Gradients, BatchNorm

Andrej Karpathy's 'makemore' series provides an excellent, in-depth look at implementing Batch Normalization from scratch. We will focus on the parts covering the forward pass mechanics, including the critical distinction between training and inference.

Please watch the following two segments: From 00:42:23 to 00:47:44: This covers the step-by-step implementation of the forward pass, including the introduction of the trainable gain (gamma) and bias (beta) parameters. From 00:53:57 to 01:00:56: This segment explains the problem of inference and demonstrates how to use running mean and standard deviation to solve it.


4. Implementation from Scratch

Now let's consolidate everything into a Python class. We will implement the forward pass with two modes: 'train' and 'test' (or 'eval').

Below is a NumPy-based implementation of the BatchNorm forward pass. Note how it mirrors the logic from the resources you've just studied.

import numpy as np

class BatchNorm1d:
    def __init__(self, num_features, eps=1e-5, momentum=0.9):
        # Learnable parameters
        self.gamma = np.ones(num_features)  # scale
        self.beta = np.zeros(num_features) # shift

        # Parameters for inference mode
        self.running_mean = np.zeros(num_features)
        self.running_var = np.ones(num_features)
        
        # Hyperparameters
        self.eps = eps
        self.momentum = momentum
        self.training = True # Controls mode
        
        # Cache for backward pass
        self.cache = None

    def forward(self, x):
        # x is the input, shape (batch_size, num_features)
        
        if self.training:
            # 1. Calculate batch mean and variance
            batch_mean = np.mean(x, axis=0)
            batch_var = np.var(x, axis=0)

            # 2. Normalize the batch
            x_hat = (x - batch_mean) / np.sqrt(batch_var + self.eps)

            # 3. Update running statistics for inference
            self.running_mean = self.momentum * self.running_mean + (1 - self.momentum) * batch_mean
            self.running_var = self.momentum * self.running_var + (1 - self.momentum) * batch_var
            
            # Store values needed for backward pass
            self.cache = (x, x_hat, batch_mean, batch_var, self.gamma)
        else:
            # At inference, use running statistics
            x_hat = (x - self.running_mean) / np.sqrt(self.running_var + self.eps)
        
        # 4. Scale and shift
        out = self.gamma * x_hat + self.beta
        return out

5. Backward Pass: The Gradients

The backward pass for Batch Normalization is more complex than for other layers we've seen. This is because the output for each sample in a batch depends not only on its corresponding input but on all other samples in the batch through the shared batch mean and variance.

This inter-dependency means the gradient calculation must account for how a single input affects the loss through all outputs in the batch. While the derivation is an excellent exercise in applying the chain rule, we will focus on the intuition and the final implementation.

Batch Normalization: Stabilizing Deep Network Training

To understand how the gradients are derived, we'll return to the 'Batch Normalization: Stabilizing Deep Network Training' article. It provides a clear, mathematical derivation of the backward pass.

Read the section 'Gradient Flow Through Batch Normalization'. You don't need to memorize the derivation, but try to follow the logic of how the gradients for gamma, beta, and the input x are computed. Pay attention to the final implementation in the batch_norm_backward function.

Here is the implementation of the backward method, which you can add to our BatchNorm1d class. It uses the values stored in self.cache from the forward pass.

    def backward(self, dout):
        # dout is the upstream gradient from the next layer
        x, x_hat, batch_mean, batch_var, gamma = self.cache
        N, D = x.shape

        # Gradient of learnable parameters (dgamma, dbeta)
        self.dbeta = np.sum(dout, axis=0)
        self.dgamma = np.sum(dout * x_hat, axis=0)

        # Gradient with respect to the input x (dx)
        # This is the most complex part of the backprop
        
        # 1. Gradient of loss w.r.t. normalized output
        dx_hat = dout * gamma
        
        # 2. Gradient of loss w.r.t. variance
        dvar = np.sum(dx_hat * (x - batch_mean) * -0.5 * (batch_var + self.eps)**(-1.5), axis=0)
        
        # 3. Gradient of loss w.r.t. mean
        dmean = np.sum(dx_hat * -1 / np.sqrt(batch_var + self.eps), axis=0) + \
                dvar * np.sum(-2 * (x - batch_mean), axis=0) / N
        
        # 4. Gradient of loss w.r.t. input x
        dx = (dx_hat / np.sqrt(batch_var + self.eps)) + \
             (dvar * 2 * (x - batch_mean) / N) + \
             (dmean / N)
        
        return dx
Test your understanding!

When applying Batch Normalization, why is the bias term in the preceding Linear (or Dense) layer often omitted? For example, in PyTorch, you might see nn.Linear(in, out, bias=False) followed by nn.BatchNorm1d(out).

Show answer

The bias term in the preceding linear layer becomes redundant because its effect is cancelled out by the Batch Normalization layer. The linear layer adds a bias , but the very next step in BN is to calculate the mean of the batch and subtract it. Any constant bias added to all samples will be immediately removed during this centering step. The BN layer's own learnable shift parameter, , serves the purpose of providing the necessary bias for the layer. Omitting the linear layer's bias is a small optimization that removes unnecessary parameters.


6. Mapping to PyTorch

Understanding the from-scratch implementation gives you a huge advantage when using high-level frameworks like PyTorch. The concepts map directly. Our BatchNorm1d class is a simplified version of PyTorch's nn.BatchNorm1d.

Building makemore Part 3: Activations & Gradients, BatchNorm

To tie everything together and prepare for our next module, let's see how our from-scratch concepts map to a real-world PyTorch implementation. Andrej Karpathy does a masterful job of this.

Watch from 01:00:56 to 01:14:09. This is a crucial section that covers: The redundancy of the bias term in the preceding layer. How to stack layers in a typical architecture (Linear -> BatchNorm -> Activation). A detailed look at the nn.BatchNorm1d layer in PyTorch and how its parameters (num_features, eps, momentum, affine) correspond directly to what we've just implemented.


Conclusion

Congratulations on completing the "Neural Network Foundations" module! You've gone from the fundamentals of backpropagation to implementing some of the most important optimizers and regularizers used in modern deep learning.

Key Takeaways from this Lesson:

  • Problem: Internal Covariate Shift destabilizes and slows down training by constantly changing the input distributions to layers.
  • Solution: Batch Normalization standardizes the inputs to each layer for every mini-batch, forcing them to have a mean of 0 and a variance of 1.
  • Representational Power: Learnable scale () and shift () parameters are added to allow the network to learn the optimal distribution for each layer.
  • Training vs. Inference: BN uses batch statistics during training but relies on fixed running averages of mean and variance for deterministic output during inference.
  • Effect: BN stabilizes training, which allows for much higher learning rates, faster convergence, and acts as a mild regularizer.

Preview of the Next Module:
You have built an incredible foundation by implementing these concepts from scratch. You now understand not just what these layers do, but how they work under the hood.

In our next module, "Applied Deep Learning in PyTorch," we will leverage this deep understanding to build and train models more efficiently using the powerful and optimized building blocks provided by PyTorch. We'll start by building our first feedforward network in PyTorch and training it on a real classification task.