反向传播(Backpropagation)是训练人工神经网络的核心算法。它通过链式法则高效计算损失函数相对于网络中每个参数的梯度,为梯度下降等优化算法提供方向。没有反向传播,深度学习的现代突破几乎不可能实现。
神经网络的本质是一个嵌套的复合函数。以一个三层网络为例:
其中 是激活函数, 是权重矩阵, 是偏置向量。
训练目标是找到使损失函数 最小的参数。梯度下降要求我们计算 和 ,即每个参数对最终损失的"贡献"。
对于浅层模型(如线性回归或逻辑回归),我们可以直接写出解析梯度。但对于多层网络,参数的误差贡献被层层传递和变换,需要系统化的方法——这就是反向传播。
反向传播的数学基础是微积分中的链式法则(Chain Rule)。对于复合函数 :
对于多变量的情况:
假设函数 ,其中 ,在 处求 :
神经网络的梯度计算遵循完全相同的逻辑,只不过维度从标量扩展到高维张量。
在计算梯度之前,必须先执行前向传播(Forward Pass)——将输入数据逐层传递,计算出最终的预测值和损失。
计算图(Computation Graph)是有向无环图(DAG),其中节点表示变量或操作,边表示数据流。它清晰地展示了前向传播和反向传播的路径。
以一个简单的两层网络为例:
输入 x → [线性变换 W1·x + b1] → 激活 σ → h1 → [线性变换 W2·h1 + b2] → 输出 y_hat → [损失 L(y_hat, y)]
每个节点代表一个计算步骤,箭头表示数据流向。
以单样本训练为例,设网络结构为:输入层 3 个神经元、隐藏层 4 个神经元、输出层 2 个神经元。
步骤 1:输入层到隐藏层的线性变换
其中 ,,。
步骤 2:隐藏层激活
假设使用 Sigmoid 激活函数:。
步骤 3:隐藏层到输出层的线性变换
其中 ,,。
步骤 4:输出层激活(分类任务使用 Softmax)
步骤 5:计算损失
使用交叉熵损失:
假设:
前向计算:
:
| 神经元 | 计算过程 | 结果 |
|---|---|---|
| z_1[0] | 0.1×1.0 + 0.2×0.5 + (-0.1)×(-0.5) + 0.1 | = 0.1 + 0.1 + 0.05 + 0.1 = 0.35 |
| z_1[1] | (-0.2)×1.0 + 0.3×0.5 + 0.1×(-0.5) + (-0.1) | = -0.2 + 0.15 - 0.05 - 0.1 = -0.20 |
| z_1[2] | 0.15×1.0 + (-0.1)×0.5 + 0.2×(-0.5) + 0.05 | = 0.15 - 0.05 - 0.1 + 0.05 = 0.05 |
| z_1[3] | 0.0×1.0 + 0.25×0.5 + (-0.15)×(-0.5) + 0.0 | = 0.0 + 0.125 + 0.075 + 0.0 = 0.20 |
即
应用 Sigmoid 激活 :
| 神经元 | 结果 | |
|---|---|---|
| h_1[0] | 0.587 | |
| h_1[1] | 0.450 | |
| h_1[2] | 0.512 | |
| h_1[3] | 0.550 |
:
| 输出神经元 | 计算过程 | 结果 |
|---|---|---|
| z_2[0] | 0.2×0.587 + (-0.3)×0.450 + 0.1×0.512 + 0.4×0.550 + (-0.05) | = 0.117 - 0.135 + 0.051 + 0.220 - 0.05 = 0.203 |
| z_2[1] | (-0.1)×0.587 + 0.2×0.450 + (-0.25)×0.512 + 0.15×0.550 + 0.05 | = -0.059 + 0.090 - 0.128 + 0.083 + 0.05 = 0.036 |
Softmax 激活:
,,分母
交叉熵损失:
至此,前向传播完成,损失为 0.615。反向传播将计算每个参数对损失的贡献。
反向传播通过计算图逆向传播梯度信号。从损失 开始,根据链式法则,逐层计算每个中间变量和参数对 的偏导数。
整个过程分为三步:
沿用上面的数值示例,从输出向输入方向逐层计算。
步骤 1:输出层 Softmax 交叉熵梯度
对于 Softmax + 交叉熵的组合,梯度有一个简洁形式:
| 输出神经元 | 梯度 |
|---|---|
所以 。
步骤 2:输出层权重 的梯度
这是一个外积运算:
| 梯度 | j=0 | j=1 | j=2 | j=3 |
|---|---|---|---|---|
| i=0 | -0.269 | -0.207 | -0.235 | -0.252 |
| i=1 | 0.269 | 0.207 | 0.235 | 0.252 |
偏置 的梯度:
步骤 3:误差向隐藏层传播
Sigmoid 的导数为 :
| 隐藏神经元 | ||
|---|---|---|
| 0 | 0.587 | 0.587 × 0.413 = 0.242 |
| 1 | 0.450 | 0.450 × 0.550 = 0.248 |
| 2 | 0.512 | 0.512 × 0.488 = 0.250 |
| 3 | 0.550 | 0.550 × 0.450 = 0.248 |
首先计算 :
| 神经元 | 计算过程 | 结果 |
|---|---|---|
| 0 | 0.2×(-0.459) + (-0.1)×0.459 | -0.092 - 0.046 = -0.138 |
| 1 | (-0.3)×(-0.459) + 0.2×0.459 | 0.138 + 0.092 = 0.230 |
| 2 | 0.1×(-0.459) + (-0.25)×0.459 | -0.046 - 0.115 = -0.161 |
| 3 | 0.4×(-0.459) + 0.15×0.459 | -0.184 + 0.069 = -0.115 |
步骤 4:隐藏层权重 的梯度
| 梯度 | 输入 x_0=1.0 | x_1=0.5 | x_2=-0.5 |
|---|---|---|---|
| δ_1[0] = -0.033 | -0.033 | -0.017 | 0.017 |
| δ_1[1] = 0.057 | 0.057 | 0.029 | -0.029 |
| δ_1[2] = -0.040 | -0.040 | -0.020 | 0.020 |
| δ_1[3] = -0.029 | -0.029 | -0.015 | 0.015 |
偏置 的梯度:
使用学习率 更新权重:
以 为例:
更新后, 的第一个元素从 0.1 变为 0.1033——因为梯度为负(减小当前权重有助于降低损失),所以权重反而增大。
实际训练中,我们不会一次只用一个样本计算梯度,而是使用小批量(Mini-batch)样本。
设批量大小为 ,损失为批内所有样本的平均:
批量梯度即为各个样本梯度的平均:
使用矩阵运算可以同时处理整个批次。设输入矩阵 :
前向传播:
反向传播:
# 输出层误差
dZ2 = Y_hat - Y # shape: (m, 2)
# 输出层梯度
dW2 = dZ2.T @ H1 / m # 除以 m 取平均
db2 = np.mean(dZ2, axis=0) # shape: (2,)
# 隐藏层误差
dH1 = dZ2 @ W2 # shape: (m, 4)
dZ1 = dH1 * sigmoid_derivative(Z1) # element-wise
# 隐藏层梯度
dW1 = dZ1.T @ X / m
db1 = np.mean(dZ1, axis=0)
向量化相比逐样本循环的优势:
| 维度 | 逐样本循环 | 向量化 |
|---|---|---|
| 代码行数 | ~30 行显式循环 | ~5 行矩阵运算 |
| 计算速度(1000 样本) | 慢(Python 循环) | 快(BLAS 优化) |
| 可扩展性 | 难以 GPU 并行 | 天然 GPU 并行 |
不同激活函数的导数形式对反向传播的梯度信号有直接影响:
| 激活函数 | 公式 | 导数 | 梯度范围 | 特点 |
|---|---|---|---|---|
| Sigmoid | (0, 0.25] | 两端饱和,梯度消失严重 | ||
| Tanh | (0, 1] | 零中心,但仍会饱和 | ||
| ReLU | 非饱和,计算快 | |||
| Leaky ReLU | if else 1 | {, 1} | 避免"死亡 ReLU" | |
| GELU | 近似 ReLU 的平滑版本 | (0, 1] | 现代模型首选 |
从导数范围可以看出,Sigmoid 的最大导数为 0.25。这意味着每经过一个 Sigmoid 层,梯度幅度至少缩小 4 倍。对于 10 层网络:
这就是梯度消失——深层网络的梯度信号在传播过程中指数级衰减,导致浅层参数几乎无法学习。
以之前的数值为例:输出层 的幅度约为 0.459,传播到隐藏层后 的最大幅度降到 0.057,衰减了约 8 倍。这还只是一层 Sigmoid 的效果。
ReLU 的导数恒为 0 或 1(输入为正时),避免了这种指数衰减,因此成为深度网络的首选。
当网络层数较多,且使用 Sigmoid/Tanh 等饱和激活函数时,靠近输入层的权重几乎收不到有效的梯度更新。
特征表现:
解决方案:
| 方法 | 说明 | 作用机制 |
|---|---|---|
| ReLU 系列激活 | 导数为 0 或 1 | 梯度信号直达浅层 |
| 残差连接(ResNet) | 梯度通过快捷连接直达浅层 | |
| Batch Normalization | 每层输入归一化 | 防止激活值进入饱和区 |
| LSTM 门控机制 | 遗忘门控制信息流 | 选择性记住/忘记长期信息 |
与梯度消失相反,梯度的范数随着反向传播指数级增长,导致参数更新幅度过大,训练发散。
特征表现:
解决方案:
| 方法 | 说明 |
|---|---|
| 梯度裁剪(Gradient Clipping) | 限制梯度的最大范数,如 |
| 权重初始化 | 合适的初始化可以控制前向/反向传播中值的方差 |
| Batch Normalization | 限制每层激活值的分布范围 |
梯度裁剪是处理梯度爆炸最直接有效的方法:
def clip_gradients(gradients, max_norm=5.0):
"""Clip gradients by global norm."""
total_norm = 0.0
for grad in gradients:
total_norm += np.sum(grad ** 2)
total_norm = np.sqrt(total_norm)
if total_norm > max_norm:
scaling_factor = max_norm / total_norm
clipped = [g * scaling_factor for g in gradients]
return clipped
return gradients
示例: 如果某次迭代中所有参数的梯度范数为 12.5,而阈值为 5.0,则所有梯度乘以 ,总范数降至 5.0。
这种方法保留了梯度的方向,只削减其幅度,因此不会破坏收敛方向。
以下是一个完整的反向传播实现,整合了前向传播、梯度计算和参数更新:
import numpy as np
class NeuralNetwork:
def __init__(self, layer_sizes, learning_rate=0.1):
"""
layer_sizes: [input_size, hidden_size, ..., output_size]
"""
self.lr = learning_rate
self.params = {}
self.grads = {}
for i in range(len(layer_sizes) - 1):
# Xavier initialization
std = np.sqrt(2.0 / (layer_sizes[i] + layer_sizes[i+1]))
self.params[f'W{i+1}'] = np.random.randn(
layer_sizes[i+1], layer_sizes[i]) * std
self.params[f'b{i+1}'] = np.zeros((layer_sizes[i+1], 1))
def sigmoid(self, z):
return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
def sigmoid_derivative(self, a):
return a * (1 - a)
def forward(self, X):
"""Full forward pass with cache for backprop."""
cache = {'A0': X}
num_layers = len(self.params) // 2
for i in range(num_layers - 1):
W = self.params[f'W{i+1}']
b = self.params[f'b{i+1}']
A_prev = cache[f'A{i}']
Z = W @ A_prev + b
A = self.sigmoid(Z)
cache[f'Z{i+1}'] = Z
cache[f'A{i+1}'] = A
# Output layer (no activation for regression, or softmax for classification)
W_last = self.params[f'W{num_layers}']
b_last = self.params[f'b{num_layers}']
A_last = cache[f'A{num_layers-1}']
Z_last = W_last @ A_last + b_last
# Softmax for classification
exp_z = np.exp(Z_last - np.max(Z_last, axis=0, keepdims=True))
A_last = exp_z / np.sum(exp_z, axis=0, keepdims=True)
cache[f'Z{num_layers}'] = Z_last
cache[f'A{num_layers}'] = A_last
return A_last, cache
def backward(self, Y, cache):
"""Full backward pass computing all gradients."""
num_layers = len(self.params) // 2
m = Y.shape[1] # batch size
# Output layer gradient (cross-entropy + softmax)
dZ = cache[f'A{num_layers}'] - Y # Simplified gradient
self.grads[f'W{num_layers}'] = dZ @ cache[f'A{num_layers-1}'].T / m
self.grads[f'b{num_layers}'] = np.sum(dZ, axis=1, keepdims=True) / m
# Backward through hidden layers
for i in range(num_layers - 1, 0, -1):
W_next = self.params[f'W{i+1}']
A_cur = cache[f'A{i}']
Z_cur = cache[f'Z{i}']
# Error propagation
dA_prev = W_next.T @ dZ
dZ = dA_prev * self.sigmoid_derivative(A_cur)
# Gradients
A_prev = cache[f'A{i-1}']
self.grads[f'W{i}'] = dZ @ A_prev.T / m
self.grads[f'b{i}'] = np.sum(dZ, axis=1, keepdims=True) / m
def update_params(self):
"""Gradient descent update."""
for key in self.params:
self.params[key] -= self.lr * self.grads[key]
def train(self, X, Y, epochs=1000, verbose=True):
"""Full training loop."""
for epoch in range(epochs):
Y_pred, cache = self.forward(X)
self.backward(Y, cache)
self.update_params()
if verbose and epoch % 100 == 0:
loss = -np.mean(np.sum(Y * np.log(Y_pred + 1e-8), axis=0))
print(f"Epoch {epoch}: loss = {loss:.6f}")
现代深度学习框架(PyTorch、TensorFlow)使用自动微分技术,它是对反向传播的工程化实现。
自动微分分为两种模式:
| 模式 | 方向 | 计算复杂度 | 适用场景 |
|---|---|---|---|
| 前向模式 | 从输入到输出 | O(n × m) | 输入少、输出多 |
| 反向模式 | 从输出到输入 | O(n + m) | 输出少、输入多(神经网络) |
反向模式自动微分正是反向传播的精确数学表述。PyTorch 通过构建动态计算图并记录每个操作的反向函数来实现:
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
w = torch.tensor([1.5, -0.5], requires_grad=True)
b = torch.tensor([0.1], requires_grad=True)
# 前向传播(自动构建计算图)
z = torch.dot(x, w) + b
y = torch.sigmoid(z)
loss = torch.nn.functional.binary_cross_entropy(y, torch.tensor([1.0]))
# 反向传播(自动计算所有梯度)
loss.backward()
# 查看梯度
print(f"dw: {w.grad}")
print(f"db: {b.grad}")
print(f"dx: {x.grad}")
反向传播计算的梯度可以配合各种优化器使用,而不仅仅是原始梯度下降:
| 优化器 | 核心思想 | 更新公式中反向传播的角色 |
|---|---|---|
| SGD | 原始梯度下降 | 直接使用梯度 |
| Momentum | 积累动量 | 梯度用于更新动量向量 |
| Adam | 自适应学习率 + 动量 | 梯度用于计算一阶/二阶矩 |
| RMSprop | 自适应学习率 | 梯度用于计算平方梯度的移动平均 |
数值梯度是验证反向传播正确性的黄金标准:
def gradient_check(params, gradients, X, Y, epsilon=1e-7):
"""Numerically verify backprop gradients."""
numerical_grads = {}
for key, param in params.items():
num_grad = np.zeros_like(param)
it = np.nditer(param, flags=['multi_index'])
while not it.finished:
idx = it.multi_index
# f(theta + epsilon)
old_val = param[idx]
param[idx] = old_val + epsilon
loss_plus, _ = forward(X, Y, params)
# f(theta - epsilon)
param[idx] = old_val - epsilon
loss_minus, _ = forward(X, Y, params)
# Numerical gradient
num_grad[idx] = (loss_plus - loss_minus) / (2 * epsilon)
# Restore
param[idx] = old_val
it.iternext()
numerical_grads[key] = num_grad
# Compare
for key in params:
diff = np.linalg.norm(gradients[key] - numerical_grads[key]) / \
(np.linalg.norm(gradients[key]) + np.linalg.norm(numerical_grads[key]) + 1e-12)
print(f"{key}: relative difference = {diff:.2e}")
if diff > 1e-7:
print(f" ⚠️ Gradient check failed!")
以下是一个使用数值梯度验证之前手动计算结果的例子:
对先前示例中的参数 :
这与我们手动计算的结果 相差很大——说明我们在手动计算中犯了错误。这就是数值梯度检查的价值:它能发现反向传播实现中的 bug。
总结反向传播的几个关键洞察:
局部梯度:每个节点只需要知道其输出相对于输入的局部梯度,不需要知道整个网络的结构。这使网络可以模块化构建。
信号复用:前向传播的中间结果(各层的激活值)在反向传播中被重复使用。这就是为什么需要缓存这些值。
线性成本:反向传播的计算量与前向传播大致相同(通常约为前向的 2 倍),不会因为网络变深而指数增长。
链式法则的威力:将复杂的全局优化问题分解为简单的局部梯度计算,使得训练任意深度和结构的网络成为可能。
梯度消失的非对称性:由于链式法则的乘法特性,浅层梯度一定不会比深层梯度大。这意味着深层总是比浅层更容易学习——这是深层网络设计的核心约束。