误差逆传播算法-最成功的神经网络学习算法
原理
对每个训练样例,BP算法执行以下操作:
先将输入示例提供给输入层神经元,然后逐层将新号前传,直到产生输出层的结果;
然后计算输出层的误差,再将误差逆向传播至隐藏神经元;
最后根据隐层神经元的误差来对连接权和阈值进行调整。
该迭代过程循环进行,直到达到某些停止条件为止。
1. 引言:为什么BP算法如此重要
反向传播(Backpropagation,BP)算法是训练前馈神经网络最核心的算法。它由 Rumelhart、Hinton 和 Williams 于 1986 年在 Nature 上系统性地提出,但这并非一蹴而就——早在 1960 年代,控制论领域就用到了类似的链式法则思想;1970 年代,Werbos 在博士论文中首次将反向模式自动微分应用于神经网络。
BP的本质是:利用链式法则(Chain Rule),高效地计算损失函数对网络中每一个可学习参数(权重和偏置)的偏导数(梯度)。有了这些梯度,我们就能用梯度下降法(或它的变体)来更新参数,使损失函数逐步减小。
为什么叫”反向传播”?因为梯度的计算方向与信号的前向传播方向相反——从输出层开始,逐层向输入层传播误差信号。
BP算法的核心贡献在于计算效率。对于一个具有 N 层、每层 M 个神经元的网络,如果使用数值微分来计算梯度,每计算一个参数的梯度需要两次前向传播,总复杂度为 O(M^2 * N)。而BP算法利用链式法则和动态规划的思想,共享中间计算结果,将总复杂度降为 O(M * N)(即一次前向传播 + 一次反向传播)。
2. 数学符号与网络结构定义
在深入推导之前,我们先建立严格的符号系统。考虑一个 L 层的全连接神经网络(输入层算作第 0 层,不计入 L):
2.1 符号约定
| 符号 | 含义 |
|---|---|
| L | 网络层数(不包含输入层) |
| n_l | 第 l 层的神经元数目 |
| W^[l] | 第 l-1 层到第 l 层的权重矩阵,维度 (n_l, n_{l-1}) |
| b^[l] | 第 l 层的偏置向量,维度 (n_l, 1) |
| z^[l] | 第 l 层的加权输入(激活前),维度 (n_l, 1) |
| a^[l] | 第 l 层的激活输出,维度 (n_l, 1) |
| a^[0] = x | 输入层的激活值,即输入特征 |
| a^[L] = ŷ | 网络的最终输出(预测值) |
| g^l | 第 l 层的激活函数 |
| E | 损失函数(误差函数) |
| δ^[l] | 第 l 层的误差信号(error signal) |
| ⊙ | 逐元素乘法(Hadamard product) |
2.2 损失函数
本文主要使用二次损失函数(均方误差,MSE)作为示例:
$$E = \frac{1}{2} \sum_{j=1}^{n_L} (a_j^{[L]} - y_j)^2 = \frac{1}{2} \|\hat{y} - y\|_2^2$$
对于分类问题,更常用交叉熵损失(Cross-Entropy Loss)。对于二分类(sigmoid输出):
$$E = -[y \log \hat{y} + (1-y) \log(1-\hat{y})]$$
对于多分类(softmax输出):
$$E = -\sum_{j=1}^{n_L} y_j \log \hat{y}_j$$
其中 y_j 是 one-hot 编码的真实标签。
3. 前向传播(Forward Propagation)
前向传播就是将输入信号逐层传递,最终产生预测结果的过程。
3.1 逐层计算
对于第 l 层(l = 1, 2, …, L):
步骤1:计算加权输入
$$z^{[l]} = W^{[l]} \cdot a^{[l-1]} + b^{[l]}$$
写成逐神经元的形式:
$$z_j^{[l]} = \sum_{k=1}^{n_{l-1}} W_{jk}^{[l]} \cdot a_k^{[l-1]} + b_j^{[l]}$$
步骤2:应用激活函数
$$a^{[l]} = g^{[l]}(z^{[l]})$$
3.2 伪代码
def forward_pass(X, parameters): |
3.3 三层网络具体示例
以 n_0 = 3(输入特征数),n_1 = 4(隐藏层神经元数),n_2 = 2(输出层神经元数)为例:
隐藏层(l=1):
Z1 = W1 @ X + b1 # shape: (4, 1) |
输出层(l=2):
Z2 = W2 @ A1 + b2 # shape: (2, 1) |
4. 反向传播推导(Backward Propagation)
这是本文最核心的部分。我们将使用链式法则,从输出层开始,逐层向后推导梯度。
4.1 输出层的误差信号 δ^[L]
定义第 l 层的误差信号为损失函数对该层加权输入的偏导数:
$$\delta^{[l]} = \frac{\partial E}{\partial z^{[l]}}$$
对于输出层(l = L),以 MSE 损失为例:
$$E = \frac{1}{2} \sum_{j} (a_j^{[L]} - y_j)^2$$
首先计算 ∂E/∂a^[L]:
$$\frac{\partial E}{\partial a_j^{[L]}} = a_j^{[L]} - y_j$$
即:
$$\frac{\partial E}{\partial a^{[L]}} = \hat{y} - y$$
现在利用链式法则求 δ^[L] = ∂E/∂z^[L]:
$$\delta_j^{[L]} = \frac{\partial E}{\partial z_j^{[L]}} = \frac{\partial E}{\partial a_j^{[L]}} \cdot \frac{\partial a_j^{[L]}}{\partial z_j^{[L]}} = (a_j^{[L]} - y_j) \cdot g'^{[L]}(z_j^{[L]})$$
写成向量形式:
$$\boxed{\delta^{[L]} = (\hat{y} - y) \odot g'^{[L]}(z^{[L]})}$$
重要:如果输出层使用 sigmoid 激活 + 交叉熵损失,可以证明 δ^[L] 有非常简洁的形式:
$$\delta^{[L]} = \hat{y} - y$$
这就是为什么分类任务中常使用 sigmoid + 交叉熵 或 softmax + 交叉熵的组合——它们能消除激活函数导数项,加速训练。
4.2 隐藏层的误差信号 δ^[l](l < L)
对于隐藏层,误差信号需要从后一层反向传播。核心递归关系:
$$\delta_j^{[l]} = \frac{\partial E}{\partial z_j^{[l]}} = \sum_{k=1}^{n_{l+1}} \frac{\partial E}{\partial z_k^{[l+1]}} \cdot \frac{\partial z_k^{[l+1]}}{\partial z_j^{[l]}}$$
关键在于 ∂z_k^{[l+1]} / ∂z_j^{[l]} 的计算。注意到:
$$z_k^{[l+1]} = \sum_{i=1}^{n_l} W_{ki}^{[l+1]} \cdot a_i^{[l]} + b_k^{[l+1]} = \sum_{i=1}^{n_l} W_{ki}^{[l+1]} \cdot g^{[l]}(z_i^{[l]}) + b_k^{[l+1]}$$
因此:
$$\frac{\partial z_k^{[l+1]}}{\partial z_j^{[l]}} = W_{kj}^{[l+1]} \cdot g'^{[l]}(z_j^{[l]})$$
代入递归式:
$$\delta_j^{[l]} = \sum_{k=1}^{n_{l+1}} \delta_k^{[l+1]} \cdot W_{kj}^{[l+1]} \cdot g'^{[l]}(z_j^{[l]}) = g'^{[l]}(z_j^{[l]}) \cdot \sum_{k=1}^{n_{l+1}} \delta_k^{[l+1]} W_{kj}^{[l+1]}$$
向量化形式:
$$\boxed{\delta^{[l]} = (W^{[l+1]})^T \cdot \delta^{[l+1]} \odot g'^{[l]}(z^{[l]})}$$
其中 (W^{[l+1]})^T 是 W^{[l+1]} 的转置。
4.3 权重梯度 ∂E/∂W^[l]
对于权重矩阵 W^[l] 中的每一个元素 W_{jk}^{[l]}(连接第 l-1 层的第 k 个神经元到第 l 层的第 j 个神经元):
$$\frac{\partial E}{\partial W_{jk}^{[l]}} = \frac{\partial E}{\partial z_j^{[l]}} \cdot \frac{\partial z_j^{[l]}}{\partial W_{jk}^{[l]}} = \delta_j^{[l]} \cdot a_k^{[l-1]}$$
向量化形式(考虑该层所有神经元的权重梯度):
$$\boxed{\frac{\partial E}{\partial W^{[l]}} = \delta^{[l]} \cdot (a^{[l-1]})^T}$$
维度验证:δ^[l] : (n_l, 1),(a^{[l-1]})^T : (1, n_{l-1}) → 结果 (n_l, n_{l-1}),与 W^[l] 的维度一致。
4.4 偏置梯度 ∂E/∂b^[l]
对于偏置 b_j^{[l]}:
$$\frac{\partial E}{\partial b_j^{[l]}} = \frac{\partial E}{\partial z_j^{[l]}} \cdot \frac{\partial z_j^{[l]}}{\partial b_j^{[l]}} = \delta_j^{[l]} \cdot 1 = \delta_j^{[l]}$$
因为 ∂z_j^{[l]}/∂b_j^{[l]} = 1(b_j 只是 z_j 的一个加项)。向量形式:
$$\boxed{\frac{\partial E}{\partial b^{[l]}} = \delta^{[l]}}$$
4.5 完整的反向传播算法伪代码
def backward_pass(y, cache, parameters): |
5. 矩阵/向量形式的高效计算
在实际实现中(如 NumPy、PyTorch、TensorFlow),所有计算都是批量处理的。下面给出考虑 mini-batch 的矩阵形式。
5.1 批量前向传播
假设 mini-batch 包含 m 个样本,输入矩阵 X 的维度是 (n_0, m)。则:
Z^{[l]} = W^{[l]} @ A^{[l-1]} + b^{[l]} # (n_l, m) |
注意 b^{[l]} 的维度是 (n_l, 1),在 NumPy 中会自动广播(broadcast)到 (n_l, m)。
5.2 批量反向传播
δ^{[L]} = (A^{[L]} - Y) ⊙ g'^{[L]}(Z^{[L]}) # (n_L, m) |
其中求和在样本维度上进行(axis=1),除以 m 得到平均梯度。
6. 激活函数及其导数分析
激活函数的选择对 BP 算法的性能影响巨大。以下是常用激活函数及其导数。
6.1 Sigmoid
$$\sigma(z) = \frac{1}{1 + e^{-z}}$$
导数:
$$\sigma'(z) = \sigma(z) \cdot (1 - \sigma(z))$$
性质:输出范围 (0, 1),导数最大值 0.25(在 z=0 处)。
def sigmoid(z): |
问题:当 |z| 较大时,σ(z) 饱和(趋于 0 或 1),σ’(z) 趋于 0,导致梯度消失。
6.2 Tanh(双曲正切)
$$\tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}$$
导数:
$$\tanh'(z) = 1 - \tanh^2(z)$$
性质:输出范围 (-1, 1),零中心化(zero-centered),缓解了 sigmoid 的非零中心问题。导数最大值为 1(在 z=0 处)。
def tanh_derivative(z): |
6.3 ReLU(Rectified Linear Unit)
$$\text{ReLU}(z) = \max(0, z)$$
导数:
$$\text{ReLU}'(z) = \begin{cases} 1 & \text{if } z > 0 \\ 0 & \text{if } z \leq 0 \end{cases}$$
def relu(z): |
优势:正区间梯度恒为 1,有效缓解梯度消失;计算简单高效。
问题(Dying ReLU):当 z ≤ 0 时梯度为 0,如果某个神经元对几乎所有输入都输出负值,该神经元的权重将永远不会更新——称为”神经元死亡”。这在学习率过大或偏置初始化不当时容易发生。
数值示例:假设某神经元经过 ReLU,初始偏置 b = -0.1,权重 W = [0.01, -0.02]。对于输入 x = [1, 2],z = 0.01×1 + (-0.02)×2 - 0.1 = -0.13 < 0,梯度为 0,参数不再更新。
6.4 LeakyReLU
$$\text{LeakyReLU}(z) = \max(\alpha z, z), \quad \alpha \text{ 通常取 } 0.01$$
导数:
$$\text{LeakyReLU}'(z) = \begin{cases} 1 & \text{if } z > 0 \\ \alpha & \text{if } z \leq 0 \end{cases}$$
α 是一个小的正数,保证了负区间的梯度不为零,解决了 Dying ReLU 问题。
6.5 ELU(Exponential Linear Unit)
$$\text{ELU}(z) = \begin{cases} z & \text{if } z > 0 \\ \alpha (e^z - 1) & \text{if } z \leq 0 \end{cases}$$
导数:
$$\text{ELU}'(z) = \begin{cases} 1 & \text{if } z > 0 \\ \text{ELU}(z) + \alpha & \text{if } z \leq 0 \end{cases}$$
ELU 的输出均值更接近零,收敛速度通常快于 LeakyReLU。代价是计算指数运算。
6.6 Swish(SiLU)
Google Brain 于 2017 年提出:
$$\text{Swish}(z) = z \cdot \sigma(\beta z)$$
其中 β 是可学习参数或固定常数(β=1 时即 SiLU)。
导数:
$$\text{Swish}'(z) = \beta \cdot \text{Swish}(z) + \sigma(\beta z) \cdot (1 - \beta \cdot \text{Swish}(z))$$
Swish 具有非单调的”凸起”形状,在深层网络上表现优于 ReLU。
6.7 激活函数对比表
| 函数 | 公式 | 输出范围 | 导数范围 | 零中心 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
| Sigmoid | 1/(1+e^{-z}) | (0,1) | (0, 0.25] | 否 | 平滑,可解释为概率 | 梯度消失,非零中心 |
| Tanh | (e^z-e^{-z})/(e^z+e^{-z}) | (-1,1) | (0, 1] | 是 | 零中心,比sigmoid更好 | 仍有饱和问题 |
| ReLU | max(0,z) | [0,∞) | {0,1} | 否 | 计算快,缓解梯度消失 | Dying ReLU |
| LeakyReLU | max(0.01z, z) | (-∞,∞) | {0.01,1} | 否 | 解决Dying ReLU | α需手动设定 |
| ELU | z if z>0 else α(e^z-1) | (-α,∞) | (0, 1] | 近似 | 输出均值接近零 | 计算指数 |
| Swish | z·σ(z) | (-~0.278,∞) | (0,~1.1) | 否 | 非单调,深层表现好 | 计算开销较大 |
经验法则:
- 隐藏层首选 ReLU 及其变体
- 二分类输出层用 Sigmoid
- 多分类输出层用 Softmax(组合)
- 回归输出层通常不用激活函数(线性输出)
7. 梯度消失与梯度爆炸
7.1 梯度消失(Vanishing Gradient)的数学根源
梯度消失是早期深度学习难以训练的主要原因。考虑一个深层网络,使用 sigmoid 作为激活函数。
根据链式法则,第 l 层的误差信号:
$$\delta^{[l]} = g'(z^{[l]}) \cdot (W^{[l+1]})^T \cdot \delta^{[l+1]}$$
展开到输出层:
$$\delta^{[l]} = \left( \prod_{i=l+1}^{L} g'(z^{[i]}) \cdot (W^{[i]})^T \right) \cdot (\hat{y} - y)$$
对于 sigmoid,导数最大值仅为 0.25。假设 |W| < 1,那么:
$$\|\delta^{[l]}\| \leq 0.25^{L-l} \cdot \|\hat{y} - y\|$$
当 L - l 较大(即接近输入层)时,误差信号呈指数级衰减——这就是梯度消失。
数值示例:对于 10 层 sigmoid 网络,如果每个 sigmoid 导数的平均值为 0.2,则梯度传播到第一层时约为原始梯度的 0.2^9 ≈ 5.12 × 10^{-7}。这意味着底层参数几乎不更新。
7.2 梯度爆炸(Exploding Gradient)
相反地,如果 |W| > 1(且激活函数导数也较大),梯度可能呈指数增长:
$$\|\delta^{[l]}\| \geq C^{L-l} \cdot \|\hat{y} - y\|, \quad C > 1$$
梯度爆炸会导致参数更新过大,损失函数发散(NaN)。
检测方法:
- 训练损失突然变为 NaN
- 梯度范数异常增大(例如 > 1000)
7.3 梯度裁剪(Gradient Clipping)
梯度爆炸的标准解决方案是梯度裁剪:
def clip_gradients(grads, max_norm=5.0): |
或者直接按值裁剪(value clipping):grads = np.clip(grads, -threshold, threshold)。
这是 RNN/LSTM 训练的标配操作。
8. 数值梯度检查(Numerical Gradient Checking)
BP 推导容易出错,数值梯度检查是验证反向传播实现正确性的重要工具。
8.1 双边有限差分法
对每个参数 θ_i,用中心差分近似梯度:
$$\frac{\partial E}{\partial \theta_i} \approx \frac{E(\theta_i + \epsilon) - E(\theta_i - \epsilon)}{2\epsilon}$$
其中 ε 通常取 10^{-7} 到 10^{-4} 之间的值。中心差分的截断误差为 O(ε^2),优于单边差分的 O(ε)。
8.2 实现代码
def numerical_gradient(f, theta, epsilon=1e-7): |
重要:由于需要对每个参数分别进行两次前向传播,数值梯度检查的总复杂度高达 O(参数量 × 前向传播)。因此只能用于小网络的验证,或检查部分参数是否为随机子集。
8.3 梯度比较
比较解析梯度和数值梯度时,使用相对误差:
$$\text{relative error} = \frac{\| \nabla_{analytical} - \nabla_{numerical} \|_2}{\| \nabla_{analytical} \|_2 + \| \nabla_{numerical} \|_2}$$
经验法则:
- relative error < 10^{-7}:非常好
- 10^{-7} < relative error < 10^{-5}:可能正确,可复查
- 10^{-5} < relative error < 10^{-3}:基本不满意,检查个别有问题的参数
- relative error > 10^{-3}:一定有 bug
常见陷阱:
- 必须在关闭 Dropout、BatchNorm 等随机性的情况下做梯度检查
- 注意正则化项(L1/L2)也需要计入梯度
- 使用 double precision(float64),避免浮点精度误差干扰
9. 优化器:BP梯度如何驱动参数更新
BP 计算出了梯度,而优化器决定了如何利用这些梯度来更新参数。
9.1 标准 SGD(Stochastic Gradient Descent)
$$\theta_{t+1} = \theta_t - \eta \cdot \nabla_\theta E(\theta_t)$$
η 是学习率。SGD 的特点是每个 mini-batch 的梯度都有噪声,这有助于逃离局部极小值,但也导致收敛路径震荡。
# 标准 SGD 更新步骤 |
9.2 Momentum(带动量的 SGD)
引入速度变量 v,累积历史梯度:
$$v_{t+1} = \gamma v_t + \eta \nabla_\theta E(\theta_t)$$
$$\theta_{t+1} = \theta_t - v_{t+1}$$
γ 通常取 0.9。类比物理:参数是在势能面上运动的粒子,梯度是力,动量是惯性。
解读:如果梯度方向持续一致,v 会累积增大,加速收敛;如果方向反复(震荡),v 会抵消,起到平滑作用。
def momentum_update(params, grads, velocities, lr=0.01, gamma=0.9): |
9.3 Nesterov Accelerated Gradient (NAG)
Nesterov 动量先沿着累积速度方向走一步,然后在”前瞻位置”计算梯度:
$$v_{t+1} = \gamma v_t + \eta \nabla_\theta E(\theta_t - \gamma v_t)$$
$$\theta_{t+1} = \theta_t - v_{t+1}$$
前瞻计算的梯度更准确地反映参数更新后的实际情况,通常比标准动量收敛更快。
9.4 AdaGrad(Adaptive Gradient)
为每个参数维护独立的学习率,根据历史梯度的平方和来自适应调整:
$$G_t = G_{t-1} + (\nabla_\theta E(\theta_t))^2$$
$$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \odot \nabla_\theta E(\theta_t)$$
优点:适合稀疏特征(每个特征收到不同的有效学习率)。
缺点:G_t 单调递增,学习率不断衰减,可能在达到最优点前就衰减到零。
9.5 RMSprop
解决 AdaGrad 学习率单调衰减的问题,用指数移动平均替代累加和:
$$G_t = \rho \cdot G_{t-1} + (1 - \rho) \cdot (\nabla_\theta E(\theta_t))^2$$
$$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \odot \nabla_\theta E(\theta_t)$$
ρ 通常取 0.9 到 0.999。这是 Hinton 在 Coursera 课程中提出的非正式算法。
9.6 Adam(Adaptive Moment Estimation)
融合了 Momentum 和 RMSprop 的思想,是目前最常用的优化器:
一阶矩估计(均值):
$$m_t = \beta_1 m_{t-1} + (1 - \beta_1) \nabla_\theta E(\theta_t)$$
二阶矩估计(未中心化的方差):
$$v_t = \beta_2 v_{t-1} + (1 - \beta_2) (\nabla_\theta E(\theta_t))^2$$
偏差校正(因为 m_0 = 0, v_0 = 0,初始步会有偏差):
$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}$$
参数更新:
$$\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$
默认超参数:η = 0.001, β_1 = 0.9, β_2 = 0.999, ε = 10^{-8}。
def adam_update(params, grads, m, v, t, lr=0.001, |
9.7 优化器对比
| 优化器 | 自适应学习率 | 动量 | 适用场景 |
|---|---|---|---|
| SGD | 否 | 否 | 简单任务,需要手动调节学习率 |
| SGD+Momentum | 否 | 是 | 大多数场景的好的 baseline |
| Nesterov | 否 | 是(前瞻) | 比 Momentum 略好 |
| AdaGrad | 是 | 否 | 稀疏特征(NLP/推荐系统) |
| RMSprop | 是 | 否 | RNN/LSTM 训练 |
| Adam | 是 | 是 | 默认首选,大多数场景效果好 |
| AdamW | 是 | 是 | Adam + 解耦权重衰减,现代标准 |
10. 批归一化(Batch Normalization)
BatchNorm 是 Ioffe 和 Szegedy 在 2015 年提出的技术,它极大地加速了深层网络的训练。理解 BN 的前向和反向传播对于掌握现代 BP 算法至关重要。
10.1 动机
训练过程中,前面层参数的更新会导致后面层输入分布不断变化——称为内部协变量偏移(Internal Covariate Shift)。BN 通过对每一层的激活值进行标准化来解决这个问题,使得每层都能学习在更加稳定的分布上。
10.2 前向传播
给定 mini-batch B = {x_1, x_2, …, x_m}(这里 x_i 是该层的激活值):
步骤1:计算 batch 均值和方差
$$\mu_B = \frac{1}{m} \sum_{i=1}^{m} x_i$$
$$\sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2$$
步骤2:标准化
$$\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}$$
步骤3:尺度变换和偏移(可学习参数 γ 和 β)
$$y_i = \gamma \cdot \hat{x}_i + \beta$$
注意 γ 和 β 是可学习的参数,使得网络可以在需要时恢复原始的分布(当 γ = σ_B, β = μ_B 时,y_i = x_i)。
10.3 反向传播推导
需要计算 ∂E/∂γ, ∂E/∂β, 以及最重要的 ∂E/∂x_i(传给前一层的梯度)。
已知上游梯度 ∂E/∂y_i。
∂E/∂γ 和 ∂E/∂β(直接计算):
$$\frac{\partial E}{\partial \gamma} = \sum_{i=1}^{m} \frac{\partial E}{\partial y_i} \cdot \hat{x}_i$$
$$\frac{\partial E}{\partial \beta} = \sum_{i=1}^{m} \frac{\partial E}{\partial y_i}$$
∂E/∂x_i(需要通过链式法则展开):
$$\frac{\partial E}{\partial x_i} = \frac{\partial E}{\partial \hat{x}_i} \cdot \frac{\partial \hat{x}_i}{\partial x_i} + \frac{\partial E}{\partial \mu_B} \cdot \frac{\partial \mu_B}{\partial x_i} + \frac{\partial E}{\partial \sigma_B^2} \cdot \frac{\partial \sigma_B^2}{\partial x_i}$$
这是因为 x_i 不仅直接影响 \hat{x}_i,还通过 μ_B 和 σ_B^2 间接影响所有 \hat{x}_j。
展开计算(省略详细推导):
$$\frac{\partial E}{\partial \hat{x}_i} = \frac{\partial E}{\partial y_i} \cdot \gamma$$
$$\frac{\partial E}{\partial \sigma_B^2} = \sum_{i=1}^{m} \frac{\partial E}{\partial \hat{x}_i} \cdot (x_i - \mu_B) \cdot \left(-\frac{1}{2}\right) \cdot (\sigma_B^2 + \epsilon)^{-3/2}$$
$$\frac{\partial E}{\partial \mu_B} = \left(\sum_{i=1}^{m} \frac{\partial E}{\partial \hat{x}_i} \cdot \frac{-1}{\sqrt{\sigma_B^2 + \epsilon}}\right) + \frac{\partial E}{\partial \sigma_B^2} \cdot \frac{-2}{m} \sum_{i=1}^{m} (x_i - \mu_B)$$
最终:
$$\frac{\partial E}{\partial x_i} = \frac{\partial E}{\partial \hat{x}_i} \cdot \frac{1}{\sqrt{\sigma_B^2 + \epsilon}}
+ \frac{\partial E}{\partial \sigma_B^2} \cdot \frac{2(x_i - \mu_B)}{m}
+ \frac{\partial E}{\partial \mu_B} \cdot \frac{1}{m}$$
10.4 训练 vs 推理
训练时使用 mini-batch 的统计量;推理时使用训练集上的移动平均:
$$\mu_{running} = \alpha \cdot \mu_{running} + (1 - \alpha) \cdot \mu_B$$
$$\sigma^2_{running} = \alpha \cdot \sigma^2_{running} + (1 - \alpha) \cdot \sigma^2_B$$
α 通常取 0.9 到 0.99。
11. 权重初始化策略
初始化对 BP 算法的效果有决定性影响——糟糕的初始化会导致梯度消失/爆炸,使网络无法训练。
11.1 Xavier(Glorot)初始化
适用于 sigmoid/tanh 激活函数。目标是保持各层激活值和梯度的方差一致。
假设输入维度为 fan_in,输出维度为 fan_out:
$$W_{ij} \sim U\left(-\sqrt{\frac{6}{fan\_in + fan\_out}}, \sqrt{\frac{6}{fan\_in + fan\_out}}\right)$$
或正态分布:
$$W_{ij} \sim \mathcal{N}\left(0, \sqrt{\frac{2}{fan\_in + fan\_out}}\right)$$
11.2 He(Kaiming)初始化
专为 ReLU 激活函数设计。ReLU 会使一半的激活值为零,因此方差被减半。He 初始化通过将方差加倍来补偿:
$$W_{ij} \sim \mathcal{N}\left(0, \sqrt{\frac{2}{fan\_in}}\right)$$
这是使用 ReLU 类激活时的标准选择。
# He 初始化实现 |
11.3 偏置初始化
偏置通常初始化为 0。但对于 ReLU 网络,常设为小的正数(如 0.01)以避免 Dying ReLU。
12. 学习率调度(Learning Rate Schedules)
学习率是 BP + SGD 中最重要的超参数。现代训练通常使用学习率调度策略。
| 策略 | 公式 | 说明 |
|---|---|---|
| Step decay | η = η_0 × γ^{floor(t/s)} | 每 s 轮衰减 γ 倍 |
| Exponential | η = η_0 × γ^t | 指数衰减 |
| Cosine | η = 0.5η_0 × (1 + cos(π×t/T)) | 余弦退火,常用 |
| Plateau | 监控验证损失,停滞时衰减 | 自适应 |
| OneCycleLR | 先升后降 | 超收敛 |
| Warmup | 前 k 步线性增长 | 大 batch 训练必需 |
Warmup 的重要性:在大 batch 训练或使用 Adam 时,初始阶段梯度方向不稳定,如果一开始就用大学习率,可能导致训练崩溃。Warmup 策略在前 1000~5000 步将学习率从 0 线性增加到 η_max。
13. 计算图视角与自动微分
现代深度学习框架(PyTorch、TensorFlow、JAX)并非”手动实现 BP”,而是基于计算图(Computational Graph)和自动微分(Autograd)机制。
13.1 计算图
神经网络的计算可以表示为一个有向无环图(DAG),其中:
- 节点表示操作(operation)或变量
- 边表示数据流
例如 z = Wx + b 的计算图:
x ────┐ |
13.2 反向模式自动微分
BP 本质上就是反向模式自动微分(Reverse-mode Automatic Differentiation)的一个特例。反向模式对于”多输入 → 单输出”(即损失函数)的场景特别高效,恰好与神经网络的训练需求匹配。
前向模式 vs 反向模式:
- 前向模式:适合 f: R^n → R^m 且 n << m(少输入,多输出)
- 反向模式:适合 f: R^n → R 且 n 很大(多输入,单输出),这正是神经网络的场景
13.3 PyTorch Autograd 示例
import torch |
loss.backward() 内部自动执行了完整的 BP 过程。
13.4 动态图 vs 静态图
| 特性 | PyTorch (动态图) | TensorFlow 1.x (静态图) |
|---|---|---|
| 图构建 | 每次前向传播动态构建 | 先定义图,后运行(define-and-run) |
| 调试 | 易于调试,可直接使用 Python 调试器 | 需要使用 tf.Session 和专用调试器 |
| 控制流 | 原生 Python if/for | 需要 tf.cond / tf.while_loop |
| 优化空间 | 优化空间较小 | 图层面可做更多优化 |
| 现代趋势 | PyTorch 2.0 引入 torch.compile 融合两者 | TensorFlow 2.x 转向 eager execution |
14. NumPy 实现:完整的 BP 训练示例
下面给出一个完整的 NumPy 实现,训练一个简单的多层网络进行二分类:
import numpy as np |
15. 训练实用技巧总结
15.1 数据预处理
- 标准化:使每个特征均值为 0,标准差为 1。
X = (X - mean) / std - 归一化:将特征缩放到 [0, 1] 或 [-1, 1] 区间
- 不标准化的后果:不同特征的尺度差异会导致权重更新的步长在不同方向差异巨大,优化路径呈 “之” 字形
15.2 梯度相关技巧
- 定期检查梯度范数:如果梯度持续很小(< 1e-7),可能存在梯度消失
- 如果损失函数不下降,试试增大学习率或更换优化器
- 使用
np.isnan(grad).any()检查梯度是否爆炸
15.3 调试 BP 的步进清单
- 先在极小的数据集(如 10 个样本)上训练,确保能过拟合(损失 → 0)
- 对随机数据(标签打乱)训练,损失应无法下降——这证明网络有足够容量且实现正确
- 用数值梯度检查验证手工推导的梯度
- 逐步增加网络深度,观察每一层的梯度范数
16. 面试高频问答
Q1: 请用白板推导 BP 算法中一个隐藏层神经元的权重梯度。要求写出完整的链式法则展开过程。
A: 以三层网络为例,隐藏层神经元 j 连接到输出层神经元 k 的权重 W_{kj}。根据 BP 算法,我们需要计算 ∂E/∂W_{kj}。
第一步:明确 E 是输出层激活值 a_k 的函数,而 a_k 依赖于 z_k,z_k 依赖于 W_{kj}:
∂E/∂W_{kj} = ∂E/∂a_k · ∂a_k/∂z_k · ∂z_k/∂W_{kj} |
第二步:分别计算三项:
- ∂E/∂a_k = a_k - y_k(MSE 损失)
- ∂a_k/∂z_k = g’(z_k)(激活函数导数)
- ∂z_k/∂W_{kj} = a_j(前一层激活值)
第三步:定义误差信号 δ_k = (a_k - y_k) · g’(z_k),则:
∂E/∂W_{kj} = δ_k · a_j |
写成矩阵形式就是 δ^{[2]} · (a^{[1]})^T。关键是理解为什么定义 δ = ∂E/∂z 作为中间变量——它使得整个反向传播可以递归地逐层计算,极大地简化了表达。
Q2: 为什么 sigmoid + MSE 的组合在深度网络中表现很差?sigmoid + 交叉熵又如何?请从数学角度分析。
A: sigmoid + MSE 的问题出在输出层的反向传播公式:
δ = (ŷ - y) ⊙ σ'(z) |
当 σ’(z) 很小时(即 z 的绝对值很大,sigmoid 进入饱和区),即使 (ŷ - y) 很大,δ 也会被 σ’(z) 缩小,导致梯度很小,参数更新缓慢。
而 sigmoid + 交叉熵损失的计算:
E = -[y log ŷ + (1-y) log(1-ŷ)] |
σ’(z) = ŷ(1-ŷ) 恰好与损失函数的导数中的项抵消,得到的 δ = ŷ - y 不包含 σ’(z) 因子。这样当预测错误时(ŷ 与 y 差异大),梯度就大,不会因为饱和而停止学习。这是数学上的精妙设计。
Q3: Batch Normalization 在训练和推理时的行为有什么不同?为什么?
A: 在训练时,BN 使用当前 mini-batch 的均值 μ_B 和方差 σ_B^2 进行标准化。这引入了随机性,起到了正则化效果(类似 Dropout)。
在推理(测试/部署)时,BN 使用训练过程中维护的移动平均 μ_running 和 σ^2_running 进行标准化。原因有二:
- 推理时可能没有 “batch” 的概念(单样本预测),无法计算 mini-batch 统计量
- 推理需要确定性输出,不能依赖当前输入的统计量
此外,BN 的可学习参数 γ 和 β 不受影响——它们在训练和推理时的行为完全一致。
Q4: 什么是 dying ReLU 问题?有哪些解决方案?
A: Dying ReLU 是指 ReLU 神经元对几乎所有输入输出负值,导致该神经元的梯度恒为零,权重永远不会再被更新的现象。常见原因包括:
- 学习率过大,权重被更新到使 z ≤ 0 的区域
- 偏置初始化过于负值
- 高学习率 + 大梯度导致参数 “跳入” 死亡区
解决方案:
- LeakyReLU/PReLU:负区间有非零梯度(如 α = 0.01),权重可以继续更新
- ELU:负区间平滑且输出均值接近零
- 更小的学习率:减少参数 “跳入” 死亡区的概率
- 偏置初始化为小的正数:如 b = 0.01,使初始 z > 0
- 使用 Adam 等自适应优化器:每个参数有独立的学习率,能更好地平衡
数学上,dying ReLU 的本质是 ReLU 在 z ≤ 0 时 ∂ReLU/∂z = 0,由 BP 的链式法则,这个零会传播给所有依赖它的权重:
∂E/∂W_j = δ_j · a_{prev} |
Q5: 请解释 Adam 优化器中的偏差校正(bias correction)是什么意思,为什么需要它?
A: Adam 维护两个指数移动平均:一阶矩估计 m_t 和二阶矩估计 v_t,初始化时均为零向量。
以 m_t 为例:
m_1 = β_1 · 0 + (1 - β_1) · g_1 = (1 - β_1) · g_1 |
展开后系数之和是 (1 - β_1) · (β_1 + 1),不等于 1(应为 1 才是有效的加权平均)。
实际上,数学期望 E[m_t] 不等于真实的一阶矩 E[g_t]。可以证明:
E[m_t] = E[g_t] · (1 - β_1^t) |
因此需要除以 (1 - β_1^t) 来校正:
m̂_t = m_t / (1 - β_1^t) |
当 t 很大时,β_1^t → 0,校正因子趋于 1,校正效果自动消失。这确保在训练的早期阶段(t 较小时),估计值是无偏的,避免初始步长的严重低估导致收敛缓慢。
实践中,不使用偏差校正的 Adam 在训练初期步长会非常小,可能需要数十步才能 “加速” 到正常步长。偏差校正使得 Adam 从第一步起就以合理的步长前进。

