共计 1986 个字符,预计需要花费 5 分钟才能阅读完成。
问题引入:从拟合失败案例说起
刚接触神经网络时,我尝试用 BP 网络拟合一个简单的正弦函数。理论上单隐层网络就能逼近任意函数,但实际训练中常遇到两种典型问题:

-
梯度消失:当网络层数较深时,损失函数对前面层参数的梯度会指数级衰减。例如用 Sigmoid 激活函数时,其导数最大值为 0.25,经过 5 层传播后梯度最多缩小到原来的 0.00098
-
过拟合:在 MNIST 数据集上,一个 10 层网络在训练集准确率达到 99% 后,测试集准确率反而从 98% 下降到 85%,这就是典型的过拟合现象
数学原理:反向传播的本质
BP 算法的核心是链式求导法则。设损失函数为 $L$,对于第 $l$ 层的参数 $W^l$,其梯度计算为:
$$
\frac{\partial L}{\partial W^l} = \frac{\partial L}{\partial z^{l+1}} \cdot \frac{\partial z^{l+1}}{\partial W^l}
$$
其中 $z^{l+1}=W^l a^l + b^l$,$a^l$ 是第 $l$ 层输出。具体推导过程:
- 输出层梯度:
$$
\delta^L = \nabla_a L \odot \sigma'(z^L)
$$ - 隐藏层梯度(反向传播):
$$
\delta^l = ((W^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)
$$ - 参数梯度:
$$
\frac{\partial L}{\partial W^l} = \delta^l (a^{l-1})^T
$$
Python 实现核心代码
import numpy as np
class NeuralNetwork:
def __init__(self, layers):
# He 初始化
self.weights = [np.random.randn(y, x)*np.sqrt(2/x)
for x,y in zip(layers[:-1], layers[1:])]
self.biases = [np.zeros((y,1)) for y in layers[1:]]
def forward(self, x):
a = x
for w, b in zip(self.weights, self.biases):
z = np.dot(w, a) + b
a = 1/(1+np.exp(-z)) # Sigmoid
return a
def backprop(self, x, y):
# 前向传播缓存中间值
activations = [x]
zs = []
for w, b in zip(self.weights, self.biases):
z = np.dot(w, activations[-1]) + b
zs.append(z)
activations.append(1/(1+np.exp(-z)))
# 反向传播
delta = (activations[-1] - y) * activations[-1] * (1-activations[-1])
nabla_w = [np.zeros(w.shape) for w in self.weights]
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w[-1] = np.dot(delta, activations[-2].T)
nabla_b[-1] = delta
for l in range(2, len(self.weights)+1):
delta = np.dot(self.weights[-l+1].T, delta) * \
activations[-l] * (1-activations[-l])
nabla_w[-l] = np.dot(delta, activations[-l-1].T)
nabla_b[-l] = delta
return nabla_w, nabla_b
MNIST 实验对比
| 网络结构 | 学习率 | 训练准确率 | 测试准确率 |
|---|---|---|---|
| [784, 30, 10] | 0.1 | 92.3% | 91.8% |
| [784, 100, 10] | 0.1 | 95.7% | 94.2% |
| [784, 30, 10] | 0.01 | 89.1% | 88.9% |
避坑指南
-
梯度裁剪:当梯度范数超过阈值时进行缩放
max_norm = 5.0 total_norm = np.sqrt(sum(np.sum(g**2) for g in gradients)) if total_norm > max_norm: gradients = [g*(max_norm/total_norm) for g in gradients] -
Batch Normalization:应在激活函数前插入
FC -> BN -> ReLU # 正确顺序 FC -> ReLU -> BN # 错误顺序 -
早停法实现:当验证集误差连续 5 个 epoch 不下降时终止训练
延伸阅读
- 使用 PyTorch 实现相同功能代码量减少 60%
- 残差连接 (ResNet) 可以彻底解决梯度消失问题
- 自适应优化器 (Adam) 通常比手动调参效果更好
通过这次实践,我深刻理解了神经网络的黑箱运作机制。建议初学者一定要亲手实现一遍 BP 算法,这对理解深度学习框架底层原理大有裨益。
正文完
