Skip to main content
Create your own

Backpropagation from Scratch

Hello! Let's dive into the second lesson of our "Deep Neural Network Fundamentals" module.

Introduction

In our previous lesson, we successfully built a multi-layer perceptron (MLP) and implemented forward propagation. This gave our network the ability to take an input, like a handwritten digit, and produce a prediction. However, since we initialized the network's weights and biases randomly, these predictions are, for now, completely meaningless.

Today, we will answer the crucial question: How does a neural network learn from its mistakes?

The answer lies in the celebrated backpropagation algorithm. This lesson is dedicated to understanding, deriving, and implementing backpropagation from scratch. It is arguably the most important algorithm in deep learning, as it enables our networks to learn complex patterns from data. Backpropagation is essentially a highly efficient method for applying the chain rule from calculus to compute the gradient of the loss function with respect to every single weight and bias in the network. Once we have these gradients, we can use them to "nudge" our parameters in the right direction to improve the network's performance.

By the end of this lesson, you will be able to derive the backpropagation algorithm from first principles and implement it in Python to train your first neural network.

1. The Core Idea: Gradient Descent and the Chain Rule

Before diving into the math, let's establish the high-level goal. We have:

  1. A network that makes a prediction from an input .
  2. A loss function that measures how far our prediction is from the true label .

Our goal is to adjust the network's parameters (weights and biases ) to minimize this loss. The most common way to do this is with gradient descent. We calculate the gradient of the loss with respect to each parameter—a vector that points in the direction of the steepest ascent of the loss—and take a small step in the opposite direction.

Here, is the learning rate. The challenge is calculating the partial derivatives (e.g., ) for potentially millions of parameters in a deep network. This is where backpropagation comes in. It's an algorithm that computes these gradients efficiently by propagating the error signal backward through the network, from the output layer to the input layer.

2. Intuition and Calculus of Backpropagation

At its heart, backpropagation is a clever application of the chain rule. To build a strong intuition for this, there's no better resource than the one from 3Blue1Brown.

Backpropagation calculus | Deep Learning Chapter 4

This video provides a brilliant visual and conceptual walkthrough of backpropagation. It starts with a very simple network and gradually adds complexity, showing how a change in one weight ripples through the network to affect the final cost.

Please watch from 00:43 to 09:03. Focus on understanding: The Chain Rule in Action (00:43 - 06:43): How the derivative of the cost with respect to a weight (\partial C / \partial w^{(L)}) is broken down into a product of simpler derivatives ( rac{\partial C}{\partial a^{(L)}} rac{\partial a^{(L)}}{\partial z^{(L)}} rac{\partial z^{(L)}}{\partial w^{(L)}}). Propagating Backwards (06:43 - 09:03): The key idea that the sensitivity of the cost to a neuron's activation in a layer (\partial C / \partial a^{(L-1)}) can be calculated from the sensitivities in the next layer, allowing the process to be repeated backward through the network.

A More Formal Derivation

Now, let's formalize the concepts from the video using the notation from our previous lesson. The core of backpropagation is computing the gradient of the loss with respect to the parameters of each layer . This boils down to finding and .

Backpropagation in a Multi-Layer Perceptron
This diagram illustrates the core concept of backpropagation. After a forward pass computes the predictions and the loss, the algorithm computes gradients by propagating the error signal (purple arrows) backward from the output layer.

The chain rule tells us that to find the gradient with respect to a weight , we can write:

Let's break this down:

  1. : Since , the derivative of with respect to is simply .
  2. : This term is the error signal for layer , often denoted as . It tells us how the final loss is affected by the pre-activation values in layer .

The brilliance of backpropagation is how it computes for every layer recursively, starting from the last layer, .

  • For the Output Layer ():

    where is element-wise multiplication and is the derivative of the activation function of the output layer.

  • For any Hidden Layer ():
    The error at layer depends on the error from layer .

    This is the "propagation" rule: the error from the next layer () is multiplied by the weights connecting the layers () and then by the local gradient of the activation function.

Once we have for a given layer, the gradients for the parameters of that layer are straightforward:

(We sum the errors across all examples in the batch for the bias gradient).

For a detailed, step-by-step mathematical derivation, the following article is an excellent resource.

Backpropagation: Step-By-Step Derivation

The article 'Backpropagation: Step-By-Step Derivation' by Dr. Roi Yehoshua provides a clear and general derivation of the algorithm.

Please read the sections 'Backward Pass' and 'Gradient Descent'. Focus on: The introduction of the 'delta terms' (\delta). The derivation of the 'Delta Rule', which shows how \delta^{(l)} relates to \delta^{(l+1)}. How the deltas in the output layer are calculated for different loss/activation combinations. The final gradient descent update rule that uses these delta terms.

3. Implementing Backpropagation with NumPy

Now we'll translate this theory into code, continuing with the MLP we started in the last lesson. We'll follow the structure of the Samson Zhang video, which we also used for forward propagation.

Building a neural network FROM SCRATCH (no Tensorflow/Pytorch, just numpy & math)

Let's return to our from-scratch implementation. This part of the video explains the mathematical intuition behind the backward pass in the context of our specific 2-layer network.

Watch from 07:56 to 11:11. Pay close attention to: How the error at the output layer (dZ2) is calculated. The 'fancy bit of math' that propagates the error back to the first layer (dZ1). This is the delta rule in action! The final update equations for the weights and biases, which is the gradient descent step.

Step 3.1: The back_prop Function

The back_prop function will take the activations and parameters from the forward pass, along with the true labels, and compute the gradients for all weights and biases.

A crucial simplification occurs when using Softmax activation with Cross-Entropy Loss (which is standard for multi-class classification). The derivative of the loss with respect to the final pre-activation, , simplifies beautifully to:

where is the matrix of predicted probabilities and is the one-hot encoded matrix of true labels. This avoids calculating the complex derivatives of Softmax and the loss function separately.

We also need the derivative of our hidden layer activation, ReLU:

Now, we can write our back_prop function.

# Based on the video's implementation

def one_hot(Y):
    # Creates a one-hot encoded matrix of the labels
    one_hot_Y = np.zeros((Y.size, Y.max() + 1))
    one_hot_Y[np.arange(Y.size), Y] = 1
    one_hot_Y = one_hot_Y.T
    return one_hot_Y

def deriv_ReLU(Z):
    # Returns 1 for Z > 0, 0 otherwise
    return Z > 0

def back_prop(Z1, A1, Z2, A2, W2, X, Y):
    m = Y.size # Number of training examples
    one_hot_Y = one_hot(Y)
    
    # Layer 2 gradients
    dZ2 = A2 - one_hot_Y
    dW2 = 1 / m * dZ2.dot(A1.T)
    db2 = 1 / m * np.sum(dZ2, axis=1, keepdims=True)
    
    # Layer 1 gradients
    dZ1 = W2.T.dot(dZ2) * deriv_ReLU(Z1)
    dW1 = 1 / m * dZ1.dot(X.T)
    db1 = 1 / m * np.sum(dZ1, axis=1, keepdims=True)
    
    return dW1, db1, dW2, db2

The back_prop function implements the equations we derived. It starts from the output layer (dZ2) and works backward to compute all gradients.

Step 3.2: The update_params Function

This function is simple: it takes the current parameters and the computed gradients and performs the gradient descent step.

def update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, alpha):
    W1 = W1 - alpha * dW1
    b1 = b1 - alpha * db1
    W2 = W2 - alpha * dW2
    b2 = b2 - alpha * db2
    return W1, b1, W2, b2

This function applies the gradient descent update rule to each parameter.

You can follow along with the video's implementation of these functions.

Building a neural network FROM SCRATCH (no Tensorflow/Pytorch, just numpy & math)

Let's see the code implementation for the backpropagation and update steps.

Watch from 18:56 to 24:56. This section covers: The one_hot encoding function. The implementation of back_prop, including the deriv_ReLU function. The implementation of update_params. The beginning of the main gradient_descent training loop that ties everything together.

Test your understanding!

In our back_prop function, we calculate dZ1 = W2.T.dot(dZ2) * deriv_ReLU(Z1).
In terms of our formal derivation, which part of the equation does each part of the code correspond to?
(Here, ).

Show answer
  • W2.T: This corresponds to .
  • dZ2: This is the error signal from the next layer, .
  • W2.T.dot(dZ2): This corresponds to the term .
  • *: This is the element-wise multiplication, .
  • deriv_ReLU(Z1): This is the derivative of the activation function for layer 1, .

The code is a direct implementation of the backpropagation rule for a hidden layer.

A Deeper Look: The Computational Graph

Your computer science background gives you a unique advantage in understanding a deeper, more fundamental view of backpropagation. Modern frameworks like PyTorch and TensorFlow don't just see a "network"; they see a computational graph. Every operation (addition, multiplication, tanh, etc.) is a node in this graph.

Forward propagation is the process of evaluating this graph. Backpropagation is simply applying the chain rule recursively over this graph, starting from the final output node.

The article below implements a Value object that mimics this behavior. It's a fantastic way to truly grasp how automatic differentiation (autograd) engines work from the ground up.

A Python introduction to neural networks and backpropagation

This article, 'A Python introduction to neural networks and backpropagation' by Vivek Sriram, builds a tiny autograd engine from scratch. It's an optional but highly recommended read that will give you a profound understanding of what loss.backward() actually does in PyTorch.

I encourage you to read from the section 'The ‘Value’ Class' through to 'A simple neural network'. Pay attention to: How the Value class stores not just data, but also its 'children' nodes and the operation that created it. The _backward method inside each operation (like __add__ and __mul__), which defines how to locally propagate the gradient. The final backward() method, which uses a topological sort to call all the _backward functions in the correct order—this is backpropagation on a graph. How a full MLP is built and trained using these Value objects.

This perspective connects the high-level math of backpropagation directly to the low-level implementation details found in professional deep learning frameworks.

Conclusion

Fantastic work! Today, you've conquered one of the most fundamental and powerful concepts in deep learning. By deriving and implementing backpropagation, you've built a neural network that can actually learn from data.

Key Takeaways:

  • Backpropagation is an efficient algorithm for computing the gradient of the loss function with respect to all network parameters.
  • It works by applying the chain rule recursively, starting from the output layer and moving backward.
  • The algorithm consists of a forward pass (to compute activations and loss) and a backward pass (to compute gradients).
  • The core of the backward pass is the recursive calculation of the error signals () for each layer.
  • Once gradients are computed, we use an optimization algorithm like gradient descent to update the network's weights and biases, moving the network toward a state of lower loss.

Preview of the next lesson:
Now that we have a working training pipeline, we can start refining our network. In the next lesson, we will take a closer look at the tools in our toolbox: the activation functions. We'll compare the properties and use cases of common functions like Sigmoid, Tanh, ReLU, and Leaky ReLU, and understand why the choice of activation function is so important for training deep networks.

Can't find a good explanation? Sign up and we'll make it for you

Sign up