Designing Machine Learning ApplicationsDesigning Machine Learning Applications
Home
Discus
Home
Discus
  • Contents
  • Preface

    • About the Author
    • About DMLA
  • Mathematical Foundations

    • Linear Algebra

      • Vector Basics
      • Matrix Basics
    • Calculus

      • Limits, Derivatives, and Differentials
      • Multivariate Functions and Composite Function Differentiation
    • Statistics and Probability

      • Probability Basics
      • Statistical Inference
  • Classical Statistical Learning

    • Linear Models

      • Linear Regression
      • Logistic Regression
      • Regularization and Generalized Linear Models
    • Bayesian Methods

      • Naive Bayes
      • Bayesian Network
      • EM Algorithm
    • Support Vector Machines

      • Support Vector Machine
      • Kernel Trick
    • Decision Trees and Ensembles

      • Decision Trees
      • Random Forest
      • Boosting
    • Unsupervised Learning

      • Clustering
      • Dimensionality Reduction
  • Neural Networks and Deep Learning

    • Neural Network Architectures

      • Fundamentals of Neural Networks
      • Linear Perceptron
      • Multi-Layer Perceptron
      • Forward Propagation
      • Backpropagation
      • Activation Functions and Loss Functions
    • Optimization

      • Gradient Descent
      • Adaptive Optimizers
    • Deep Network Stability

      • Weight Initialization
      • Dropout Regularization
      • Batch Normalization
    • Convolutional Neural Networks

      • CNN Basics
      • AlexNet and the CNN Revival
      • VGG and GoogLeNet
      • ResNet Residual Network
      • Lab: AlexNet Image Classification
    • Generative Models

      • Variational Autoencoder
      • Generative Adversarial Network
      • Lab: DCGAN Image Generation
    • Sequence Models

      • Word Embedding and Representation Learning
      • RNN Fundamentals
      • LSTM and GRU Gating Mechanisms
      • Seq2Seq Sequence Mapping
      • Lab: LSTM Poetry Generation
  • The Language Model Singularity

    • Transformer Architecture

      • Transformer Fundamentals
      • Transformer Evolution and Variants
      • Language Models and Tokenization
      • Lab: Transformer Model Training
    • Pretraining and Fine-Tuning

      • Pretraining Data Engineering
      • Scaling Laws
      • Distributed Training Infrastructure
      • Supervised Fine-Tuning
      • Lab: SFT Model Conversation
    • Alignment Training

      • Reinforcement Learning from Human Feedback
      • Evolution of Alignment Methods
      • Lab: DPO Alignment Training
    • Reasoning Capabilities

      • Chain of Thought and Reasoning Models
      • Test-Time Compute Scaling
      • Inference Efficiency Optimization
      • Lab: LLM Inference Optimization
    • Multimodal Fusion and Safety

      • Multimodal Large Language Models
      • Model Evaluation and Safety
      • Lab: VLM Training
  • AI Infrastructure and Engineering

    • Model Serving

      • Inference Service Architecture
      • Request Scheduling and Batching
      • GPU Resource Management
      • Lab: Deploying LLM Inference Service
    • MLOps Practices

      • Data Versioning
      • Experiment Tracking and Model Registry
      • Hyperparameter Optimization
      • Model Performance Monitoring
      • Drift Detection
  • Agentic Application Systems

    • Vector Retrieval and RAG

      • Embedding and Vector Retrieval
      • Retrieval Quality Evaluation and Optimization
      • Retrieval-Augmented Generation
      • Lab: Building a Knowledge Base Q&A System
    • Building Agent Applications

      • From LLM to Agent
      • Tool Use
      • Planning and Reasoning
      • Memory Systems
      • Agent Collaboration and Communication
      • Orchestration and Fault Tolerance
      • Lab: Research Agent Collaboration System
  • Appendix

    • Building the Sandbox Environment
    • NumPy Practice

      • Data Processing Practice
      • Calculus Computation Practice
      • Probability and Statistics Practice

Backpropagation

In the previous chapter, we deeply explored the forward propagation process where signals travel from the input layer through each layer of neurons layer by layer to the output layer. Forward propagation answers how a neural network computes inference results, but the problem of how the network learns through training remains unsolved — that is, how the network's parameters (weights and biases) are determined. The answer to this problem is the Backpropagation Algorithm, the core mechanism of neural network training, regarded as one of the most important algorithm inventions in the field of deep learning.

In 1986, Geoffrey Hinton proposed the backpropagation algorithm in his paper Learning representations by back-propagating errors published in Nature, applying it to the training of multi-layer neural networks. After its introduction, this algorithm became a key milestone in the revival of neural networks. It was this breakthrough that made the training of multi-layer neural networks theoretically feasible, laying the foundation for the subsequent deep learning revolution.

Backpropagation solved a difficult problem then known as the Credit Assignment Problem: when the network produces an incorrect output, how do we determine which among hundreds or even thousands of parameters should be adjusted, and by how much? Backpropagation computes the gradient of the loss function with respect to the parameters of each layer, precisely transmitting the error signal from the output end backward to every layer and every parameter, telling the network whose contribution is large and who should be adjusted by how much.

This chapter will introduce the mathematical foundation of backpropagation (the chain rule), the backpropagation process through the lens of computation graphs, the detailed derivation of gradient computation, and computational complexity analysis. This chapter involves a significant amount of mathematical derivation and is one of the more challenging chapters in this book. However, understanding backpropagation is key to mastering the principles of neural network training and an essential step on the path to deep learning.

Mathematical Foundation of Backpropagation

To understand backpropagation, one must first grasp two prerequisite concepts: the chain rule from calculus, and the signal flow process of forward propagation. Let us review the mathematical essence of a neural network: a neural network is essentially a nested composite function. Data begins at the input layer, passes through the linear transformation and activation function of each layer in sequence, and finally reaches the output layer. Therefore, if we want to adjust a certain parameter to reduce the prediction error, computing how much that parameter should be adjusted essentially requires tracing backward from the output value of this composite function to infer how the parameter should change — this is where the chain rule comes in.

Recall the mathematical expression of a multi-layer network structure: F(x)=fK∘fK−1∘⋯∘f1(x)F(\mathbf{x}) = f^K \circ f^{K-1} \circ \cdots \circ f^1(\mathbf{x})F(x)=fK∘fK−1∘⋯∘f1(x), a KKK-layer composite function where each layer function fff includes a linear transformation (z=Wh+bz = \mathbf{W} \mathbf{h} + \mathbf{b}z=Wh+b) and a nonlinear activation (a=σ(z)a = \sigma(z)a=σ(z)). Let us start with the simplest case, considering the relationship between the input value and the loss along a single path. Suppose a neuron has an input value xxx, a pre-activation value z=wx+bz = wx + bz=wx+b, an activation value a=σ(z)a = \sigma(z)a=σ(z), and the gap (loss) between the sample label (true value) and the function output (predicted value) is lll. According to the chain rule, the derivative of the loss with respect to the input value is:

∂l∂x=∂l∂a⋅∂a∂z⋅∂z∂x\frac{\partial l}{\partial x} = \frac{\partial l}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial x}∂x∂l​=∂a∂l​⋅∂z∂a​⋅∂x∂z​

Since the input and output of each layer in a neural network are not single values but vectors composed of multiple neurons, the chain rule must be extended to the multivariate case. Let the activation values of the k−1k-1k−1-th layer (i.e., the input values to the next kkk-th layer) be ak−1∈Rn\mathbf{a}^{k-1} \in \mathbb{R}^nak−1∈Rn, the pre-activation values of the kkk-th layer be zk∈Rm\mathbf{z}^k \in \mathbb{R}^mzk∈Rm, the activation values be ak∈Rm\mathbf{a}^k \in \mathbb{R}^mak∈Rm, and the loss be l∈Rl \in \mathbb{R}l∈R. Then the partial derivative of the loss with respect to each input value of that layer is:

∂l∂aik−1=∑j=1m∂l∂ajk⋅∂ajk∂zjk⋅∂zjk∂aik−1\frac{\partial l}{\partial a^{k-1}_i} = \sum_{j=1}^{m} \frac{\partial l}{\partial a^k_j} \cdot \frac{\partial a^k_j}{\partial z^k_j} \cdot \frac{\partial z^k_j}{\partial a^{k-1}_i}∂aik−1​∂l​=j=1∑m​∂ajk​∂l​⋅∂zjk​∂ajk​​⋅∂aik−1​∂zjk​​

The term ∑j=1m\sum_{j=1}^{m}∑j=1m​ in the formula sums up the contributions from all influence paths, because the iii-th neuron of the k−1k-1k−1-th layer (with activation value aik−1a^{k-1}_iaik−1​) simultaneously affects all mmm neurons of the kkk-th layer. The overall formula can be understood as: the total rate of change of the loss with respect to a particular activation value equals the sum of the rates of change along all downstream paths. Since the vector ak−1\mathbf{a}^{k-1}ak−1 consists of multiple components, the combined influence of all neurons in a single layer can be expressed concisely in matrix form:

∂l∂ak−1=∂l∂ak⋅∂ak∂zk⋅∂zk∂ak−1\frac{\partial l}{\partial \mathbf{a}^{k-1}} = \frac{\partial l}{\partial \mathbf{a}^k} \cdot \frac{\partial \mathbf{a}^k}{\partial \mathbf{z}^k} \cdot \frac{\partial \mathbf{z}^k}{\partial \mathbf{a}^{k-1}}∂ak−1∂l​=∂ak∂l​⋅∂zk∂ak​⋅∂ak−1∂zk​

Additional Note

To demonstrate the consistency of representation across three forms — scalar (single neuron), matrix (single layer), and matrix chain product (multi-layer) — the multiplication here uses numerator layout (Jacobian form), where gradients are row vectors. In practice, the common convention in machine learning uses denominator layout (Hessian form), where gradients are column vectors. In that case, a transpose is needed to ensure dimensional compatibility:

∂l∂ak−1=(∂zk∂ak−1)T(∂ak∂zk)T∂l∂ak\frac{\partial l}{\partial \mathbf{a}^{k-1}} = \left(\frac{\partial \mathbf{z}^k}{\partial \mathbf{a}^{k-1}}\right)^T \left(\frac{\partial \mathbf{a}^k}{\partial \mathbf{z}^k}\right)^T \frac{\partial l}{\partial \mathbf{a}^k}∂ak−1∂l​=(∂ak−1∂zk​)T(∂zk∂ak​)T∂ak∂l​

Next, we extend the single-layer case to a multi-layer network and examine the derivative of the loss with respect to changes in the input x\mathbf{x}x. Using a three-layer network as an example, the forward information propagation path is as follows:

Figure: Forward propagation signal flow in a three-layer neural network

Suppose we want to compute the gradient of the loss function lll with respect to the input vector x\mathbf{x}x. From the perspective of forward propagation, a change in x\mathbf{x}x affects z1\mathbf{z}^1z1, which in turn affects a1\mathbf{a}^1a1, then z2\mathbf{z}^2z2, a2\mathbf{a}^2a2, z3\mathbf{z}^3z3, a3\mathbf{a}^3a3, and finally the loss function lll. This influence chain spans three complete network layers. Applying the multivariate chain rule, the gradient propagates layer by layer along this chain:

∂l∂x=∂l∂a3⋅∂a3∂z3⋅∂z3∂a2⋅∂a2∂z2⋅∂z2∂a1⋅∂a1∂z1⋅∂z1∂x\frac{\partial l}{\partial \mathbf{x}} = \frac{\partial l}{\partial \mathbf{a}^3} \cdot \frac{\partial \mathbf{a}^3}{\partial \mathbf{z}^3} \cdot \frac{\partial \mathbf{z}^3}{\partial \mathbf{a}^2} \cdot \frac{\partial \mathbf{a}^2}{\partial \mathbf{z}^2} \cdot \frac{\partial \mathbf{z}^2}{\partial \mathbf{a}^1} \cdot \frac{\partial \mathbf{a}^1}{\partial \mathbf{z}^1} \cdot \frac{\partial \mathbf{z}^1}{\partial \mathbf{x}}∂x∂l​=∂a3∂l​⋅∂z3∂a3​⋅∂a2∂z3​⋅∂z2∂a2​⋅∂a1∂z2​⋅∂z1∂a1​⋅∂x∂z1​

This formula indicates that the gradient transmitted to the first layer's input equals the chain product of the derivatives of all subsequent layers. This is the core of backpropagation: the gradient propagates backward from the output layer to the input layer along the computation chain, and at each layer, the gradient is multiplied by the derivative of that layer's activation function and the derivative of its linear transformation. This process runs in the opposite direction of forward propagation, hence the name backpropagation.

Imagine a water pipe extending from the mountaintop to the valley, with water flowing in the forward propagation direction (from mountaintop to valley). If we measure a pressure problem at the valley and want to find which section of the pipe on the mountaintop is faulty, we must trace backward along the pipe, measuring the changes in water pressure (partial derivatives) from the bottom up. Backpropagation is like this reverse tracing process: starting from the error signal at the output end, it traces backward along the computation chain to determine the responsibility (gradient) of each component (each layer's parameters).

Backpropagation Through the Lens of Computation Graphs

The mathematical derivation above uses the chain rule, but in actual programming implementations, deep learning frameworks (such as TensorFlow, PyTorch) do not directly manipulate these complex formulas. Instead, just as they compute forward propagation, they use computation graphs to carry out backpropagation. A computation graph decomposes the forward propagation process of a neural network into a series of basic operation nodes, where each node is responsible for a single simple operation (such as matrix multiplication, activation function, addition), and data flows from input to output along the edges. When handling backpropagation, the direction of information flow is reversed. This design allows gradient computation to be automated — the developer only needs to define the forward propagation computation process, and the framework automatically derives the backward gradient computation. This is the power and convenience of modern deep learning frameworks. Below, we illustrate this using the computation graph of information flowing through a single neuron:

Figure: Computation graph of a simple neuron (forward propagation direction)

During forward propagation, data flows from left to right: xxx and www are multiplied to obtain w⋅xw·xw⋅x, then bbb is added to obtain zzz, passing through the activation function σ\sigmaσ to obtain aaa, which is compared with the true label yyy to compute the loss lll. During backpropagation, gradients flow from right to left along the same computation graph, but the traversal direction is opposite to forward propagation. This can be illustrated by the following diagram (in practice, it is the same computation graph as forward propagation, just traversed in reverse; here we split it into two diagrams for clarity):

Figure: Computation graph of a simple neuron (backpropagation direction)

  1. Output Layer Gradient: The gradient of the loss lll with respect to the output aaa is ∂l∂a\frac{\partial l}{\partial a}∂a∂l​. Using the squared error loss as an example, loss=12(a−y)2loss = \frac{1}{2}(a - y)^2loss=21​(a−y)2, this gradient is (a−y)(a - y)(a−y).
  2. Activation Function Backpropagation: The gradient is multiplied by the derivative of the activation function σ′(z)\sigma'(z)σ′(z) to obtain the gradient of the loss with respect to the pre-activation value zzz: ∂l∂z=∂l∂a⋅σ′(z)\frac{\partial l}{\partial z} = \frac{\partial l}{\partial a} \cdot \sigma'(z)∂z∂l​=∂a∂l​⋅σ′(z). This step also applies the chain rule, transmitting the gradient from the output end of the activation value to its input end.
  3. Linear Transformation Backpropagation: The pre-activation value is z=w⋅x+bz = w \cdot x + bz=w⋅x+b, therefore:
    • Gradient of the loss with respect to the weight www: ∂l∂w=∂l∂z⋅∂z∂w=∂l∂z⋅x\frac{\partial l}{\partial w} = \frac{\partial l}{\partial z} \cdot \frac{\partial z}{\partial w} = \frac{\partial l}{\partial z} \cdot x∂w∂l​=∂z∂l​⋅∂w∂z​=∂z∂l​⋅x
    • Gradient of the loss with respect to the bias bbb: ∂l∂b=∂l∂z⋅∂z∂b=∂l∂z⋅1=∂l∂z\frac{\partial l}{\partial b} = \frac{\partial l}{\partial z} \cdot \frac{\partial z}{\partial b} = \frac{\partial l}{\partial z} \cdot 1 = \frac{\partial l}{\partial z}∂b∂l​=∂z∂l​⋅∂b∂z​=∂z∂l​⋅1=∂z∂l​
    • Gradient of the loss with respect to the input xxx: ∂l∂x=∂l∂z⋅∂z∂x=∂l∂z⋅w\frac{\partial l}{\partial x} = \frac{\partial l}{\partial z} \cdot \frac{\partial z}{\partial x} = \frac{\partial l}{\partial z} \cdot w∂x∂l​=∂z∂l​⋅∂x∂z​=∂z∂l​⋅w, this gradient is passed upstream to continue backpropagation.

The entire computation process is like tracing backward through the computation graph: starting from the final loss value, tracing in reverse along each edge, and distributing the gradient to each parameter node according to the chain rule. Each node only needs to know how to compute its own local gradient (the derivative of its output with respect to its input), then multiply the gradient received from upstream by the local gradient, and pass the result to downstream nodes. Once the neuron knows the gradients of its parameters (weight www and bias bbb), it has the direction for parameter adjustment and can update the parameters via gradient descent: wnew←w−η∂l∂ww^{new} \leftarrow w - \eta \frac{\partial l}{\partial w}wnew←w−η∂w∂l​, bnew←b−η∂l∂bb^{new} \leftarrow b - \eta \frac{\partial l}{\partial b}bnew←b−η∂b∂l​.

Gradient Computation

After analyzing the computation graph of a single neuron, we have a preliminary understanding of the gradient backpropagation process. Next, we will delve deeper into the details, deriving the gradient computation process for an entire multi-layer neural network using a specific example with a chosen loss function and activation function. Suppose the network has KKK layers, the loss function is cross-entropy loss, the output layer uses the Softmax activation function, and the hidden layers use the Sigmoid activation function — the most common configuration for classification tasks. To facilitate the subsequent derivation, we first establish some notation:

  • zk=Wkak−1+bk\mathbf{z}^k = \mathbf{W}^k \mathbf{a}^{k-1} + \mathbf{b}^kzk=Wkak−1+bk: Pre-activation value of the kkk-th layer, i.e., the result of the linear transformation.
  • ak=σk(zk)\mathbf{a}^k = \sigma^k(\mathbf{z}^k)ak=σk(zk): Activation value of the kkk-th layer, i.e., the output after passing through the activation function.
  • δk=∂l∂zk\delta^k = \frac{\partial l}{\partial \mathbf{z}^k}δk=∂zk∂l​: Error signal of the kkk-th layer, representing the gradient of the loss function with respect to the pre-activation values of that layer. The error signal δk\delta^kδk is the core of backpropagation derivation; it tells us how much the pre-activation values of that layer should be adjusted to reduce the loss.

Output Layer Gradient

Following the same approach as the single neuron derivation, we first compute the gradient of the output layer (the KKK-th layer). For the Softmax + Cross-Entropy combination, the gradient of the output layer has an elegantly simplified form — one of the most delightful mathematical coincidences in neural networks. Suppose the output layer has III neurons (corresponding to III classes), and the output of the Softmax function is:

aiK=eziK∑j=1IezjKa_i^K = \frac{e^{z_i^K}}{\sum_{j=1}^{I} e^{z_j^K}}aiK​=∑j=1I​ezjK​eziK​​

Here, ziKz_i^KziK​ is the pre-activation value (raw output of the linear transformation) of the iii-th neuron in the output layer, and aiKa_i^KaiK​ is the predicted probability of the iii-th class after the Softmax transformation. The cross-entropy loss is:

l=−∑i=1Iyilog⁡aiKl = -\sum_{i=1}^{I} y_i \log a_i^Kl=−i=1∑I​yi​logaiK​

where yiy_iyi​ is the One-Hot encoding of the true label (the correct class is 1, all other classes are 0). log⁡aiK\log a_i^KlogaiK​ is the logarithm of the predicted probability — the closer the probability is to 1, the closer the logarithm is to 0. The overall loss equals the negative logarithm of the predicted probability of the correct class: the lower the probability, the greater the loss. The partial derivative of lll with respect to ziKz_i^KziK​ is (see the derivation in the Exercises section):

∂l∂ziK=aiK−yi\frac{\partial l}{\partial z_i^K} = a_i^K - y_i∂ziK​∂l​=aiK​−yi​
(1)

Mathematically, the gradient of Softmax + Cross-Entropy is remarkably concise — it is simply the predicted probability minus the true label. This means the error signal of the output layer can be obtained through a simple subtraction without any differentiation computation: the error signal δK\delta^KδK is aK−y\mathbf{a}^K - \mathbf{y}aK−y.

Hidden Layer Gradient Propagation

Once the error signal of the output layer is computed, it needs to be transmitted layer by layer to the hidden layers. Let the error signal of the kkk-th hidden layer be δk\delta^kδk, and the gradient transmitted from the k+1k+1k+1-th layer be δk+1\delta^{k+1}δk+1. δk\delta^kδk is the gradient of the loss function with respect to the pre-activation values of that layer. According to the chain rule, by multiplying by the derivative of the activation function σ′(z)\sigma'(z)σ′(z), the gradient is transmitted from the output end of the activation value to its input end, yielding the error signal for each hidden layer:

δk=∂l∂zk=∂l∂ak⋅∂ak∂zk=∂l∂ak⋅σ′(zk)\delta^k = \frac{\partial l}{\partial \mathbf{z}^k} = \frac{\partial l}{\partial \mathbf{a}^k} \cdot \frac{\partial \mathbf{a}^k}{\partial \mathbf{z}^k}= \frac{\partial l}{\partial \mathbf{a}^k} \cdot \sigma'(\mathbf{z}^k)δk=∂zk∂l​=∂ak∂l​⋅∂zk∂ak​=∂ak∂l​⋅σ′(zk)
(2)

The term ∂l∂ak\frac{\partial l}{\partial \mathbf{a}^k}∂ak∂l​ in the formula can be obtained from the error signal δk+1\delta^{k+1}δk+1 transmitted from the k+1k+1k+1-th layer:

∂l∂ak=∂l∂zk+1⋅∂zk+1∂ak=(Wk+1)Tδk+1\frac{\partial l}{\partial \mathbf{a}^k} = \frac{\partial l}{\partial \mathbf{z}^{k+1}} \cdot \frac{\partial \mathbf{z}^{k+1}}{\partial \mathbf{a}^k} = (\mathbf{W}^{k+1})^T \delta^{k+1}∂ak∂l​=∂zk+1∂l​⋅∂ak∂zk+1​=(Wk+1)Tδk+1

Here, ∂l∂zk+1\frac{\partial l}{\partial \mathbf{z}^{k+1}}∂zk+1∂l​ is precisely the error signal δk+1\delta^{k+1}δk+1 of the previous layer, which, when iterated back to the output layer (see (1)), becomes aK−y\mathbf{a}^K - \mathbf{y}aK−y. ∂zk+1∂ak\frac{\partial \mathbf{z}^{k+1}}{\partial \mathbf{a}^k}∂ak∂zk+1​ is the derivative of the next layer's pre-activation values with respect to this layer's activation values. Since zk+1=Wk+1ak+bk+1\mathbf{z}^{k+1} = \mathbf{W}^{k+1} \mathbf{a}^k + \mathbf{b}^{k+1}zk+1=Wk+1ak+bk+1, its partial derivative with respect to ak\mathbf{a}^kak equals the weight matrix Wk+1\mathbf{W}^{k+1}Wk+1. As mentioned earlier, the common convention in machine learning uses denominator layout, where gradients are column vectors, requiring a transpose to ensure dimensional compatibility. To satisfy the inner dimension matching requirement for matrix multiplication, the transpose of the weight matrix (Wk+1)T(\mathbf{W}^{k+1})^T(Wk+1)T is multiplied with the error signal from the previous layer. From the perspective of signal propagation, this operation is analogous to signal scaling: forward propagation uses Wk+1\mathbf{W}^{k+1}Wk+1 to amplify the signal, while backpropagation uses the transpose (Wk+1)T(\mathbf{W}^{k+1})^T(Wk+1)T to scale the gradient back down. Substituting this expression into the hidden layer error signal (see (2)) gives:

δk=(Wk+1)Tδk+1⋅σ′(zk)\delta^k = (\mathbf{W}^{k+1})^T \delta^{k+1} \cdot \sigma'(\mathbf{z}^k)δk=(Wk+1)Tδk+1⋅σ′(zk)
(3)

This is the propagation formula for the hidden layer error signal: the error signal of a hidden layer can be obtained by multiplying the error signal from the previous layer by the transpose of this layer's weight matrix, and then multiplying by the derivative of this layer's activation function. An analogy of signal attenuation can help understand this derivation: the error signal δk+1\delta^{k+1}δk+1 arrives from downstream, is scaled by the inverse mapping of the weight matrix ((Wk+1)T(\mathbf{W}^{k+1})^T(Wk+1)T), and then adjusted in strength by the activation function derivative (σ′(zk)\sigma'(\mathbf{z}^k)σ′(zk)), ultimately yielding this layer's error signal δk\delta^kδk. If the activation function derivative is very small (e.g., Sigmoid at its extremes), the error signal is significantly attenuated — this is the root cause of the vanishing gradient problem discussed later.

Parameter Gradient Computation

With the error signal δk\delta^kδk (see (3)) in hand, we can compute the gradient of the parameters of the kkk-th layer. These gradients tell us how much the weights and biases should be adjusted to reduce the loss. According to the chain rule, the weight gradient is:

∂l∂Wk=∂l∂zk⋅∂zk∂Wk\frac{\partial l}{\partial \mathbf{W}^k} = \frac{\partial l}{\partial z^k} \cdot \frac{\partial z^k}{\partial \mathbf{W}^k}∂Wk∂l​=∂zk∂l​⋅∂Wk∂zk​

Since ∂l∂zk=δk\frac{\partial l}{\partial z^k} = \delta^k∂zk∂l​=δk and ∂zk∂Wk=ak−1\frac{\partial z^k}{\partial \mathbf{W}^k} = a^{k-1}∂Wk∂zk​=ak−1, we have:

∂l∂Wk=∂l∂zk⋅∂zk∂Wk=δk(ak−1)T\frac{\partial l}{\partial \mathbf{W}^k} = \frac{\partial l}{\partial z^k} \cdot \frac{\partial z^k}{\partial \mathbf{W}^k} = \delta^k (\mathbf{a}^{k-1})^T∂Wk∂l​=∂zk∂l​⋅∂Wk∂zk​=δk(ak−1)T

In the formula, δk(ak−1)T\delta^k (\mathbf{a}^{k-1})^Tδk(ak−1)T is the outer product of two vectors, with dimensions nk×nk−1n_k \times n_{k-1}nk​×nk−1​, exactly matching the dimensions of the weight matrix. The conclusion of the overall formula is that the weight gradient equals the outer product of the error signal and the input signal. From a geometric perspective, the outer product δk(ak−1)T\delta^k (\mathbf{a}^{k-1})^Tδk(ak−1)T is a kind of correlation matrix. If a certain input signal ajk−1a_j^{k-1}ajk−1​ is strong (large absolute value) and the corresponding neuron's error δik\delta_i^kδik​ is also large, then the gradient of weight WijkW_{ij}^kWijk​ is large — this weight contributes significantly to the error and needs a substantial adjustment. Conversely, if the input signal is weak or the error is small, the gradient is small, and the adjustment is correspondingly minor.

Similarly, we compute the bias gradient. According to the chain rule:

∂l∂bk=∂l∂zk⋅∂zk∂bk\frac{\partial l}{\partial b^k} = \frac{\partial l}{\partial z^k} \cdot \frac{\partial z^k}{\partial b^k}∂bk∂l​=∂zk∂l​⋅∂bk∂zk​

Since ∂l∂zk=δk\frac{\partial l}{\partial z^k} = \delta^k∂zk∂l​=δk and ∂zk∂bk=1\frac{\partial z^k}{\partial b^k} = 1∂bk∂zk​=1 (the derivative of the bias bkb^kbk with respect to itself is 1, independent of other terms), we have:

∂l∂bk=∂l∂zk⋅∂zk∂bk=δk\frac{\partial l}{\partial b^k} = \frac{\partial l}{\partial z^k} \cdot \frac{\partial z^k}{\partial b^k} = \delta^k∂bk∂l​=∂zk∂l​⋅∂bk∂zk​=δk

The conclusion is very clear and concise: the bias gradient is directly equal to the error signal. The bias acts independently on each neuron and does not depend on the input signal, so its gradient is simply the error signal δik\delta_i^kδik​ of that neuron, without influence from other factors. This differs from the weight gradient, where the strength of the input signal must be considered (reflected through the outer product), whereas the bias gradient reflects only the magnitude of the error itself.

Gradient Computation for Batched Samples

The above derivation applies to a single sample in a multi-layer network. In actual training, we do not compute gradients using just one sample; instead, we use batch computation. Batch computation not only leverages the parallel computing power of GPUs to significantly improve training efficiency, but also takes into account that single-sample gradients may have large random fluctuations, while the average gradient over an appropriately sized batch is more stable and better reflects the overall pattern of the data. Therefore, we finally extend the computation process to the case of batched samples. Let the batch size be mmm, the pre-activation matrix of the kkk-th layer be Zk∈Rnk×m\mathbf{Z}^k \in \mathbb{R}^{n_k \times m}Zk∈Rnk​×m, and the activation matrix be Ak∈Rnk×m\mathbf{A}^k \in \mathbb{R}^{n_k \times m}Ak∈Rnk​×m. Each column of the matrix corresponds to one sample. The batch error signal matrix is:

Δk=∂L∂Zk=[δ1kδ2k⋯δmk]∈Rnk×m\Delta^k = \frac{\partial L}{\partial \mathbf{Z}^k} = \begin{bmatrix} \delta^k_1 & \delta^k_2 & \cdots & \delta^k_m \end{bmatrix} \in \mathbb{R}^{n_k \times m}Δk=∂Zk∂L​=[δ1k​​δ2k​​⋯​δmk​​]∈Rnk​×m

Here, Δk\Delta^kΔk is the error signal matrix, with each column being the error signal vector δik∈Rnk\delta^k_i \in \mathbb{R}^{n_k}δik​∈Rnk​ of one sample. When computing parameter gradients, we average over the batch dimension:

δˉk=1m∑i=1mδik∈Rnk\bar{\delta}^k = \frac{1}{m} \sum_{i=1}^{m} \delta^k_i \in \mathbb{R}^{n_k}δˉk=m1​i=1∑m​δik​∈Rnk​

The weight gradient is ∂L∂Wk=1mΔk(Ak−1)T\frac{\partial L}{\partial \mathbf{W}^k} = \frac{1}{m} \Delta^k (\mathbf{A}^{k-1})^T∂Wk∂L​=m1​Δk(Ak−1)T, and the bias gradient is ∂L∂bk=δˉk\frac{\partial L}{\partial \mathbf{b}^k} = \bar{\delta}^k∂bk∂L​=δˉk. The meaning of the entire formula is that the batch gradient equals the average of the gradients of individual samples.

Computational Complexity Analysis

Understanding the computational complexity of backpropagation has significant practical value for estimating training time, designing network architectures, and optimizing hardware utilization. This section analyzes the complexity comparison between forward propagation and backpropagation, memory overhead, and numerical stability issues.

In terms of time complexity, the time complexity of backpropagation is of the same order as forward propagation — contrary to the intuitive expectation that backpropagation, which computes gradients for all parameters, should be more complex. This is an important and surprising conclusion, meaning that the computational cost of one training iteration (one forward pass and one backward pass) is roughly twice that of a forward pass alone, remaining within the same order of magnitude. This can be demonstrated through a simple derivation. Suppose the network has KKK layers, the kkk-th layer has nkn_knk​ neurons, and the total number of parameters in the entire network is P=∑k=1K(nk⋅nk−1+nk)P = \sum_{k=1}^{K} (n_k \cdot n_{k-1} + n_k)P=∑k=1K​(nk​⋅nk−1​+nk​).

  • In forward propagation, the computation per layer primarily consists of the matrix multiplication Wkak−1\mathbf{W}^k \mathbf{a}^{k-1}Wkak−1, with time complexity O(nk⋅nk−1)O(n_k \cdot n_{k-1})O(nk​⋅nk−1​), and the activation function computation, with time complexity O(nk)O(n_k)O(nk​). Thus, the total time complexity for a single sample in a single forward pass is Tforward=O(P)T_{forward} = O(P)Tforward​=O(P).
  • In backpropagation, the computation per layer includes error signal propagation (Wk+1)Tδk+1(\mathbf{W}^{k+1})^T \delta^{k+1}(Wk+1)Tδk+1, with time complexity O(nk+1⋅nk)O(n_{k+1} \cdot n_k)O(nk+1​⋅nk​), and parameter gradient computation δk(ak−1)T\delta^k (\mathbf{a}^{k-1})^Tδk(ak−1)T, with time complexity O(nk⋅nk−1)O(n_k \cdot n_{k-1})O(nk​⋅nk−1​). Thus, the total time complexity for a single sample in a single backward pass is also Tbackward=O(P)T_{backward} = O(P)Tbackward​=O(P).

From this, we conclude that the time complexity of backpropagation is comparable to that of forward propagation, both being O(P)O(P)O(P).

In terms of space complexity, backpropagation requires storing intermediate results from forward propagation (pre-activation values zk\mathbf{z}^kzk and activation values ak\mathbf{a}^kak) for gradient computation. This incurs a certain memory overhead (hereafter, we collectively refer to CPU memory and GPU memory as memory). Let the batch size be BBB, and the total amount of intermediate results stored across all layers be M=B⋅∑k=1K2nkM = B \cdot \sum_{k=1}^{K} 2 n_kM=B⋅∑k=1K​2nk​, where the factor 222 accounts for storing two values per neuron — the pre-activation value zikz_i^kzik​ and the activation value aika_i^kaik​. The overall formula shows that memory usage equals the batch size times twice the sum of neurons in each layer.

For large networks and batch training, memory overhead can become a bottleneck. For example, a 10-layer network with 1000 neurons per layer and a batch size of 1000 requires storing approximately 1000×10×1000×2=201000 \times 10 \times 1000 \times 2 = 201000×10×1000×2=20 million floating-point numbers. At FP32 precision, this amounts to about 80 MB of memory, and this is only the storage cost for intermediate training results, not accounting for the model parameters themselves. Modern machine learning frameworks employ various optimization strategies to reduce memory usage, such as:

  • Gradient Checkpointing: Stores intermediate results for only a subset of layers, recomputing the rest when needed. This is a compute-for-memory strategy suitable for scenarios where memory is limited but compute resources are abundant.
  • Memory Reuse: Releases intermediate result memory immediately after computing gradients, reducing peak memory usage.
  • Mixed Precision: Stores intermediate results using lower precision (e.g., FP16), halving memory usage while maintaining basic computational accuracy.

Backpropagation Algorithm in Practice

The following code implements the complete backpropagation process, verifies the correctness of gradient computation, and visualizes the transmission of error signals through the network. From the four visualization charts produced by the experiment, we can observe the trends of loss and gradients during training. If training proceeds smoothly, the loss should monotonically decrease and the gradients should attenuate layer by layer, indicating that the model loss gradually converges toward stability. However, in practice, training stability is a major engineering challenge that we will discuss separately later.

import numpy as np
import matplotlib.pyplot as plt

class NeuralNetworkBP:
    """
    Complete backpropagation implementation
    
    Supports multi-layer networks with multiple activation functions
    """
    def __init__(self, layer_sizes, activations, learning_rate=0.01):
        """
        Parameters:
        layer_sizes : list of int
            Number of neurons in each layer
        activations : list of str
            Activation function type for each layer
        learning_rate : float
            Learning rate
        """
        self.layer_sizes = layer_sizes
        self.activations = activations
        self.lr = learning_rate
        self.num_layers = len(layer_sizes) - 1
        
        # Initialize weights and biases
        np.random.seed(42)
        self.weights = []
        self.biases = []
        
        for i in range(self.num_layers):
            # He initialization (suitable for ReLU)
            if activations[i] == 'relu':
                scale = np.sqrt(2.0 / layer_sizes[i])
            else:
                scale = np.sqrt(1.0 / layer_sizes[i])
            
            w = np.random.randn(layer_sizes[i+1], layer_sizes[i]) * scale
            b = np.zeros((layer_sizes[i+1], 1))
            self.weights.append(w)
            self.biases.append(b)
        
        # Store intermediate results and gradient history
        self.activations_cache = []
        self.pre_activations_cache = []
        self.gradients_history = []
        self.loss_history = []
    
    def _apply_activation(self, Z, activation_name):
        """Apply activation function"""
        if activation_name == 'sigmoid':
            Z = np.clip(Z, -500, 500)
            return 1 / (1 + np.exp(-Z))
        elif activation_name == 'relu':
            return np.maximum(0, Z)
        elif activation_name == 'tanh':
            return np.tanh(Z)
        elif activation_name == 'softmax':
            Z_shifted = Z - np.max(Z, axis=0, keepdims=True)
            exp_Z = np.exp(Z_shifted)
            return exp_Z / np.sum(exp_Z, axis=0, keepdims=True)
        elif activation_name == 'linear':
            return Z
        else:
            raise ValueError(f"Unknown activation: {activation_name}")
    
    def _activation_derivative(self, Z, A, activation_name):
        """Compute activation function derivative"""
        if activation_name == 'sigmoid':
            return A * (1 - A)
        elif activation_name == 'relu':
            return (Z > 0).astype(float)
        elif activation_name == 'tanh':
            return 1 - A ** 2
        elif activation_name == 'linear':
            return np.ones_like(Z)
        else:
            raise ValueError(f"Derivative not implemented for: {activation_name}")
    
    def forward(self, X):
        """Forward propagation"""
        self.activations_cache = [X]
        self.pre_activations_cache = []
        
        A = X
        for i in range(self.num_layers):
            Z = self.weights[i] @ A + self.biases[i]
            self.pre_activations_cache.append(Z)
            A = self._apply_activation(Z, self.activations[i])
            self.activations_cache.append(A)
        
        return A
    
    def backward(self, Y):
        """Backpropagation"""
        m = Y.shape[1]  # Number of samples
        gradients = {'weights': [], 'biases': []}
        
        # Output layer error signal
        if self.activations[-1] == 'softmax':
            # Simplified gradient for Softmax + Cross-Entropy
            delta = self.activations_cache[-1] - Y
        else:
            # Other activation functions: cross-entropy loss gradient is -Y/A
            delta = (-Y / self.activations_cache[-1]) * \
                    self._activation_derivative(
                        self.pre_activations_cache[-1],
                        self.activations_cache[-1],
                        self.activations[-1]
                    )
        
        # Backward propagation layer by layer
        for i in range(self.num_layers - 1, -1, -1):
            # Compute parameter gradients
            dW = delta @ self.activations_cache[i].T / m
            db = np.sum(delta, axis=1, keepdims=True) / m
            
            gradients['weights'].insert(0, dW)
            gradients['biases'].insert(0, db)
            
            # Propagate to the previous layer (except the input layer)
            if i > 0:
                delta_prev = self.weights[i].T @ delta
                delta = delta_prev * self._activation_derivative(
                    self.pre_activations_cache[i-1],
                    self.activations_cache[i],
                    self.activations[i-1]
                )
        
        self.gradients_history.append(gradients)
        return gradients
    
    def compute_loss(self, Y_pred, Y_true):
        """Compute cross-entropy loss"""
        eps = 1e-15
        Y_pred = np.clip(Y_pred, eps, 1 - eps)
        return -np.mean(np.sum(Y_true * np.log(Y_pred), axis=0))
    
    def update_parameters(self, gradients):
        """Update parameters"""
        for i in range(self.num_layers):
            self.weights[i] -= self.lr * gradients['weights'][i]
            self.biases[i] -= self.lr * gradients['biases'][i]
    
    def train(self, X, Y, epochs=100):
        """Train the network"""
        for epoch in range(epochs):
            # Forward propagation
            Y_pred = self.forward(X)
            
            # Compute loss
            loss = self.compute_loss(Y_pred, Y)
            self.loss_history.append(loss)
            
            # Backpropagation
            gradients = self.backward(Y)
            
            # Update parameters
            self.update_parameters(gradients)
        
        return self
    
    def predict(self, X):
        """Predict"""
        Y_pred = self.forward(X)
        return np.argmax(Y_pred, axis=0)


# Experiment: Backpropagation process visualization
print("=" * 60)
print("Experiment: Backpropagation Process Visualization")
print("=" * 60)

# Create a three-layer network
layer_sizes = [2, 16, 8, 3]  # Input 2, two hidden layers, output 3 classes
activations = ['relu', 'relu', 'softmax']
nn = NeuralNetworkBP(layer_sizes, activations, learning_rate=0.5)

print(f"Network Structure: {' -> '.join(map(str, layer_sizes))}")
print(f"Activation Functions: {activations}")
print(f"Total Parameters: {sum(w.size + b.size for w, b in zip(nn.weights, nn.biases))}")
print()

# Generate training data
np.random.seed(123)
m = 100  # Number of samples
X = np.random.randn(2, m)

# Generate three-class labels
Y_indices = np.random.randint(0, 3, m)
Y = np.zeros((3, m))
for i, idx in enumerate(Y_indices):
    Y[idx, i] = 1

# Train the network
nn.train(X, Y, epochs=200)

print(f"Training Complete, Final Loss: {nn.loss_history[-1]:.4f}")
print()

# Visualize the backpropagation process
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Chart 1: Loss curve
ax1 = axes[0, 0]
ax1.plot(nn.loss_history, color='#3498db', linewidth=2)
ax1.set_xlabel('Iteration', fontsize=11)
ax1.set_ylabel('Cross Entropy Loss', fontsize=11)
ax1.set_title('Loss During Training', fontsize=12)
ax1.grid(True, alpha=0.3)

# Chart 2: Gradient norm trend
ax2 = axes[0, 1]
gradient_norms = []
for grad in nn.gradients_history:
    total_norm = 0
    for w_grad in grad['weights']:
        total_norm += np.linalg.norm(w_grad)
    for b_grad in grad['biases']:
        total_norm += np.linalg.norm(b_grad)
    gradient_norms.append(total_norm)

ax2.plot(gradient_norms, color='#e74c3c', linewidth=2)
ax2.set_xlabel('Iteration', fontsize=11)
ax2.set_ylabel('Total Gradient Norm', fontsize=11)
ax2.set_title('Gradient Trend', fontsize=12)
ax2.grid(True, alpha=0.3)

# Chart 3: Gradient distribution per layer (last iteration)
ax3 = axes[1, 0]
last_gradients = nn.gradients_history[-1]
layer_names = ['Layer 1 Weights', 'Layer 1 Biases', 'Layer 2 Weights', 'Layer 2 Biases', 'Layer 3 Weights', 'Layer 3 Biases']
layer_values = []

for i in range(nn.num_layers):
    layer_values.append(np.abs(last_gradients['weights'][i]).mean())
    layer_values.append(np.abs(last_gradients['biases'][i]).mean())

colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']
bars = ax3.bar(range(len(layer_values)), layer_values, color=colors, alpha=0.7)
ax3.set_xticks(range(len(layer_values)))
ax3.set_xticklabels(layer_names, fontsize=9)
ax3.set_ylabel('Mean Absolute Gradient', fontsize=11)
ax3.set_title('Gradient Distribution per Layer (Last Iteration)', fontsize=12)
ax3.grid(True, alpha=0.3, axis='y')

# Chart 4: Error signal propagation visualization
ax4 = axes[1, 1]

# Simulate one pass of error signal propagation
nn.forward(X[:, :5])  # Use 5 samples
nn.backward(Y[:, :5])

# Plot the norm of error signals across layers
delta_norms = []
for i in range(nn.num_layers):
    if i == nn.num_layers - 1:
        # Output layer: directly compute error signal
        delta = nn.activations_cache[-1] - Y[:, :5]
        delta_norm = np.linalg.norm(delta)
        delta_norms.append(delta_norm)
    else:
        # Hidden layer: approximate error signal via gradient norm
        grad_norm = np.linalg.norm(nn.gradients_history[-1]['weights'][i])
        delta_norms.append(grad_norm)

# Since we didn't store intermediate deltas, we approximate with gradient norm
delta_approx = [np.linalg.norm(g) for g in nn.gradients_history[-1]['weights']]
ax4.plot(range(nn.num_layers), delta_approx[::-1], 'o-', color='#2ecc71',
         linewidth=2, markersize=8, label='Approximate Error Signal Norm')
ax4.set_xlabel('Layer Index (Output to Input)', fontsize=11)
ax4.set_ylabel('Error Signal Norm', fontsize=11)
ax4.set_title('Error Signal Backpropagation', fontsize=12)
ax4.invert_xaxis()  # Backpropagation direction: from right (output layer) to left (input layer)
ax4.legend()
ax4.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
plt.close()
Click Run to execute code. Click the code area to edit.

Summary

This chapter has provided a detailed introduction to the principles and implementation of the backpropagation algorithm, including the mathematical foundation of the chain rule, the backpropagation process through the lens of computation graphs, the detailed derivation of gradient computation, and computational complexity analysis. Backpropagation is the core algorithm of neural network training; it solves the credit assignment problem for multi-layer networks by transmitting the error signal from the output layer backward to each layer, computing the gradients for parameter updates. Understanding backpropagation lays a solid foundation for subsequent study of activation functions, loss functions, optimization algorithms, and more. The next chapter will introduce activation functions, exploring the characteristics of different activation functions and their impact on gradient propagation.

Exercises

  1. Suppose a neural network uses the Sigmoid activation function f(z)=11+e−zf(z) = \frac{1}{1+e^{-z}}f(z)=1+e−z1​. Prove that its derivative is f′(z)=f(z)(1−f(z))f'(z) = f(z)(1-f(z))f′(z)=f(z)(1−f(z)). Analyze the maximum value of the Sigmoid derivative and its impact on gradient propagation.

    Reference Answer

    Proof of Sigmoid derivative:

    Let f(z)=11+e−z=ez1+ezf(z) = \frac{1}{1+e^{-z}} = \frac{e^z}{1+e^z}f(z)=1+e−z1​=1+ezez​

    Differentiate f(z)f(z)f(z):

    f′(z)=ddz(11+e−z)=e−z(1+e−z)2f'(z) = \frac{d}{dz}\left(\frac{1}{1+e^{-z}}\right) = \frac{e^{-z}}{(1+e^{-z})^2}f′(z)=dzd​(1+e−z1​)=(1+e−z)2e−z​

    Note that 1−f(z)=1−11+e−z=e−z1+e−z1 - f(z) = 1 - \frac{1}{1+e^{-z}} = \frac{e^{-z}}{1+e^{-z}}1−f(z)=1−1+e−z1​=1+e−ze−z​

    Therefore:

    f′(z)=f(z)⋅(1−f(z))=11+e−z⋅e−z1+e−z=e−z(1+e−z)2f'(z) = f(z) \cdot (1 - f(z)) = \frac{1}{1+e^{-z}} \cdot \frac{e^{-z}}{1+e^{-z}} = \frac{e^{-z}}{(1+e^{-z})^2}f′(z)=f(z)⋅(1−f(z))=1+e−z1​⋅1+e−ze−z​=(1+e−z)2e−z​

    Analysis of maximum derivative:

    f′(z)=f(z)(1−f(z))f'(z) = f(z)(1-f(z))f′(z)=f(z)(1−f(z)). Let f(z)=tf(z) = tf(z)=t, then f′(z)=t(1−t)f'(z) = t(1-t)f′(z)=t(1−t).

    When t=0.5t = 0.5t=0.5 (i.e., z=0z=0z=0), f′(z)=0.5×0.5=0.25f'(z) = 0.5 \times 0.5 = 0.25f′(z)=0.5×0.5=0.25, which is the maximum value of the derivative.

    When zzz is very large (f(z)≈1f(z) \approx 1f(z)≈1) or very small (f(z)≈0f(z) \approx 0f(z)≈0), the derivative approaches 0.

    Impact on gradient propagation:

    In backpropagation, each time the gradient passes through a Sigmoid activation layer, it is multiplied by f′(z)f'(z)f′(z) (maximum 0.25). This means the gradient decays layer by layer:

    • After 1 Sigmoid layer: at most 25% of the gradient remains
    • After 2 layers: at most 25%×25%=6.25%25\% \times 25\% = 6.25\%25%×25%=6.25% remains
    • After 10 layers: at most (0.25)10≈0.0001%(0.25)^{10} \approx 0.0001\%(0.25)10≈0.0001% remains

    This is the root cause of the vanishing gradient problem. When deep networks use the Sigmoid activation function, the gradients of the earlier layers become nearly 0, and the parameters cannot be updated effectively.

  2. Explain why the gradient of Softmax + Cross-Entropy ∂l∂zL=aL−y\frac{\partial l}{\partial \mathbf{z}^L} = \mathbf{a}^L - \mathbf{y}∂zL∂l​=aL−y is so concise. What is the practical significance of this simplification?

    Reference Answer

    Reason for simplification:

    This simplification arises from the special combination of properties of Softmax and Cross-Entropy.

    Let the Softmax output be ak=ezk∑jezja_k = \frac{e^{z_k}}{\sum_j e^{z_j}}ak​=∑j​ezj​ezk​​ and the Cross-Entropy loss be l=−∑kyklog⁡akl = -\sum_k y_k \log a_kl=−∑k​yk​logak​.

    Directly compute ∂l∂zk\frac{\partial l}{\partial z_k}∂zk​∂l​:

    ∂l∂zk=∑j∂l∂aj⋅∂aj∂zk\frac{\partial l}{\partial z_k} = \sum_j \frac{\partial l}{\partial a_j} \cdot \frac{\partial a_j}{\partial z_k}∂zk​∂l​=j∑​∂aj​∂l​⋅∂zk​∂aj​​

    Where:

    • ∂l∂aj=−yjaj\frac{\partial l}{\partial a_j} = -\frac{y_j}{a_j}∂aj​∂l​=−aj​yj​​
    • ∂aj∂zk=aj(δjk−ak)\frac{\partial a_j}{\partial z_k} = a_j(\delta_{jk} - a_k)∂zk​∂aj​​=aj​(δjk​−ak​) (δjk\delta_{jk}δjk​ is the Kronecker delta)

    Substituting:

    ∂l∂zk=∑j−yjaj⋅aj(δjk−ak)=−∑jyj(δjk−ak)\frac{\partial l}{\partial z_k} = \sum_j -\frac{y_j}{a_j} \cdot a_j(\delta_{jk} - a_k) = -\sum_j y_j(\delta_{jk} - a_k)∂zk​∂l​=j∑​−aj​yj​​⋅aj​(δjk​−ak​)=−j∑​yj​(δjk​−ak​)
    =−∑jyjδjk+∑jyjak=−yk+ak∑jyj= -\sum_j y_j \delta_{jk} + \sum_j y_j a_k = -y_k + a_k \sum_j y_j=−j∑​yj​δjk​+j∑​yj​ak​=−yk​+ak​j∑​yj​

    Since ∑jyj=1\sum_j y_j = 1∑j​yj​=1 (One-Hot encoding), we obtain:

    ∂l∂zk=ak−yk\frac{\partial l}{\partial z_k} = a_k - y_k∂zk​∂l​=ak​−yk​

    Practical significance:

    1. Computationally efficient: No need to explicitly compute the Jacobian matrix of Softmax (size K×KK \times KK×K); simply compute the difference between the predicted probability and the true label.
    2. Numerically stable: Computing the Jacobian matrix of Softmax involves aj(δjk−ak)a_j(\delta_{jk} - a_k)aj​(δjk​−ak​), which can cause numerical issues when aka_kak​ is very small. The simplified formula avoids these complex computations.
    3. Intuitive gradient: The error signal ak−yka_k - y_kak​−yk​ intuitively represents the "prediction error." When the prediction is correct, ak≈yka_k \approx y_kak​≈yk​ and the gradient is near 0; when the prediction is wrong, the gradient points in the direction of correction.
    4. Avoids vanishing gradients: When Softmax is used alone, the output layer gradient can be very small. However, when combined with Cross-Entropy, the gradient is always proportional to the prediction error, avoiding vanishing gradients.

    This is why classification problems almost always use the Softmax + Cross-Entropy combination: the gradient computation is concise and efficient.

  3. The computational complexity of backpropagation is comparable to that of forward propagation. What implications does this have for hardware design?

    Reference Answer The fact that backpropagation complexity is comparable to forward propagation means that hardware optimizations for matrix operations can simultaneously improve both training and inference efficiency. Modern AI hardware (GPUs, TPUs) is designed precisely around matrix multiplication.
    1. Matrix operations are core: The main computation in both forward and backward propagation is matrix multiplication. GPUs should optimize matrix computation capabilities.

    2. Memory bandwidth matters: Backpropagation requires reading intermediate results stored during forward propagation, making memory bandwidth a potential bottleneck. GPUs should have high-bandwidth memory (e.g., HBM).

    3. Specialized accelerators: Since forward and backward propagation have comparable complexity, dedicated hardware can be designed to optimize both. The matrix multiplication acceleration units in TPUs are designed precisely for this purpose.

    4. Operator fusion: Combine linear combination, activation function, and gradient computation into a single operation to reduce memory access. Modern GPUs and frameworks support operator fusion.

    5. Memory optimization: Backpropagation requires storing intermediate results for all layers. Memory reuse mechanisms or gradient checkpointing techniques can be designed to reduce memory usage.

    6. Parallel computation: Computations across different layers and different samples can be parallelized. In batch processing, the forward and backward passes of mmm samples can be executed in parallel.

Words: 5,917
Updated 2026-08-10
Last Updated:
Contributors: icyfenix, Claude
Prev
Forward Propagation
Next
Activation Functions and Loss Functions