共计 1900 个字符,预计需要花费 5 分钟才能阅读完成。
引言:神经网络训练概述
神经网络通过调整权重来最小化损失函数,这个过程依赖于梯度下降。反向传播(Backpropagation, BP)是高效计算梯度的核心算法,其本质是链式法则的递归应用。本文将剥开 BP 的数学外壳,用代码实现揭示其运作机制。

数学基础:链式法则详解
链式法则告诉我们:复合函数的导数等于各层导数的乘积。对于神经网络,假设损失函数为 L,第 l 层输出为 a^l,则有:
$$
\frac{\partial L}{\partial w^l} = \frac{\partial L}{\partial a^l} \cdot \frac{\partial a^l}{\partial z^l} \cdot \frac{\partial z^l}{\partial w^l}
$$
其中 z^l = w^l a^{l-1} + b^l。这个连乘过程像多米诺骨牌,误差信号从输出层逐层向前传递。
反向传播算法逐步解析
1. 前向传播阶段
记录每层的线性输出 (z) 和激活输出(a),为反向计算准备数据。
2. 误差反向传播
从输出层开始:
- 计算输出层误差:δ^L = ∇_a L ⊙ σ'(z^L)
- 逐层回传:δ^l = (w^{l+1})^T δ^{l+1} ⊙ σ'(z^l)
- 参数梯度:∇_w L = δ^l (a^{l-1})^T, ∇_b L = δ^l
其中⊙表示逐元素乘,σ’ 是激活函数导数。
代码实现(带注释)
import numpy as np
class NeuralNetwork:
def __init__(self, layers):
self.weights = [np.random.randn(y, x) for x, y in zip(layers[:-1], layers[1:])]
self.biases = [np.random.randn(y, 1) for y in layers[1:]]
def forward(self, x):
"""前向传播并缓存计算中间值"""
self.activations = [x]
self.zs = []
for w, b in zip(self.weights, self.biases):
z = np.dot(w, self.activations[-1]) + b
self.zs.append(z)
self.activations.append(self.sigmoid(z))
return self.activations[-1]
def backward(self, x, y):
"""反向传播计算梯度"""
# 初始化梯度容器
nabla_w = [np.zeros_like(w) for w in self.weights]
nabla_b = [np.zeros_like(b) for b in self.biases]
# 输出层误差
delta = (self.activations[-1] - y) * self.sigmoid_prime(self.zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, self.activations[-2].T)
# 逐层反向传播
for l in range(2, len(self.weights)+1):
delta = np.dot(self.weights[-l+1].T, delta) * self.sigmoid_prime(self.zs[-l])
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, self.activations[-l-1].T)
return nabla_w, nabla_b
def sigmoid(self, z):
return 1/(1+np.exp(-z))
def sigmoid_prime(self, z):
return self.sigmoid(z)*(1-self.sigmoid(z))
性能分析与优化
计算复杂度分析
- 前向传播:O(∑{l=1}^L nn_l)
- 反向传播:相同量级
优化策略
- 批量计算:矩阵运算代替循环,利用 GPU 并行
- 激活函数选择:ReLU 比 sigmoid 计算更简单
- 梯度检查:用数值梯度验证实现正确性
避坑指南
- 梯度消失:
- 现象:深层网络早期层梯度接近 0
-
解决:使用 ReLU、残差连接、批归一化
-
初始化陷阱:
- 错误:全零初始化导致对称性破坏
-
正确:使用 He/Xavier 初始化
-
数值不稳定:
- 现象:NaN 值出现
- 检查:激活函数定义域、log 计算保护
总结与思考题
- 如果使用线性激活函数 σ(z)=z,反向传播会发生什么变化?
- 如何修改代码实现 mini-batch 梯度下降?
- 为什么二阶导数(Hessian 矩阵)在 BP 中很少使用?
通过拆解 BP 的数学本质和实现细节,我们更能理解深度学习框架的底层逻辑。建议读者尝试扩展代码实现 dropout、动量法等高级特性。
正文完
