共计 2340 个字符,预计需要花费 6 分钟才能阅读完成。
背景:前向传播与损失函数
神经网络的前向传播可以看作是一系列函数复合的过程。假设我们有一个单隐藏层网络,输入层到隐藏层的权重矩阵为 $W_1$,隐藏层到输出层的权重矩阵为 $W_2$,那么前向传播过程可以表示为:

$$
\begin{aligned}
h &= \sigma(W_1 x + b_1) \
y &= \text{softmax}(W_2 h + b_2)
\end{aligned}
$$
其中 $\sigma$ 是激活函数(如 sigmoid),$\text{softmax}$ 用于多分类问题。损失函数 $E$ 通常使用交叉熵损失:
$$
E = -\sum_i t_i \log(y_i)
$$
这里 $t_i$ 是真实标签的 one-hot 编码。我们的目标就是通过调整 $W_1,W_2,b_1,b_2$ 来最小化这个损失函数。
数学原理:链式法则推导
反向传播的核心就是链式法则。我们以输出层权重 $W_2$ 的梯度计算为例:
- 首先计算损失对网络输出的偏导:
$$
\frac{\partial E}{\partial y_i} = -\frac{t_i}{y_i}
$$
- 然后计算 softmax 的导数(这里省略推导过程):
$$
\frac{\partial y_j}{\partial z_k} = y_j(\delta_{jk} – y_k)
$$
- 结合这两部分,得到误差项 $\delta_o$:
$$
\delta_o = \frac{\partial E}{\partial z} = y – t
$$
- 最后得到 $W_2$ 的梯度:
$$
\frac{\partial E}{\partial W_2} = h^T \delta_o
$$
对于隐藏层权重 $W_1$,推导过程类似但更复杂,因为需要通过链式法则传播误差:
$$
\delta_h = (\delta_o W_2^T) \circ \sigma'(z_h)
$$
$$
\frac{\partial E}{\partial W_1} = x^T \delta_h
$$
其中 $\circ$ 表示逐元素相乘。
Python 实现
下面是用 NumPy 实现的单隐藏层 BP 算法核心代码:
import numpy as np
from typing import Tuple
def sigmoid(x: np.ndarray) -> np.ndarray:
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x: np.ndarray) -> np.ndarray:
return x * (1 - x)
class NeuralNetwork:
def __init__(self, input_size: int, hidden_size: int, output_size: int):
# 初始化权重
self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros(hidden_size)
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros(output_size)
def forward(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
# 前向传播
self.z1 = np.dot(x, self.W1) + self.b1
self.h = sigmoid(self.z1)
self.z2 = np.dot(self.h, self.W2) + self.b2
self.y = softmax(self.z2)
return self.h, self.y
def backward(self, x: np.ndarray, t: np.ndarray, lr: float) -> None:
# 反向传播
batch_size = x.shape[0]
# 输出层误差
delta_o = self.y - t
# 计算 W2 和 b2 的梯度
dW2 = np.dot(self.h.T, delta_o) / batch_size
db2 = np.sum(delta_o, axis=0) / batch_size
# 隐藏层误差
delta_h = np.dot(delta_o, self.W2.T) * sigmoid_derivative(self.h)
# 计算 W1 和 b1 的梯度
dW1 = np.dot(x.T, delta_h) / batch_size
db1 = np.sum(delta_h, axis=0) / batch_size
# 参数更新
self.W2 -= lr * dW2
self.b2 -= lr * db2
self.W1 -= lr * dW1
self.b1 -= lr * db1
避坑指南
- 梯度爆炸 :当梯度值变得非常大时,可以使用梯度裁剪:
def clip_gradients(grad: np.ndarray, max_norm: float) -> np.ndarray:
norm = np.linalg.norm(grad)
if norm > max_norm:
grad = grad * max_norm / norm
return grad
- ReLU 死亡神经元 :当使用 ReLU 激活函数时,某些神经元可能永远不激活。解决方法包括:
- 使用 LeakyReLU
- 合理初始化权重
- 使用较小的学习率
验证实验
在 MNIST 数据集上的测试步骤:
- 加载并预处理数据
- 初始化网络(如 784-128-10 结构)
- 训练过程中记录损失和准确率
- 对比不同学习率(如 0.1, 0.01, 0.001)的效果
典型的学习率对比结果可能显示:
– 过大的学习率(如 0.1)导致震荡或不收敛
– 过小的学习率(如 0.001)收敛过慢
– 适中的学习率(如 0.01)表现最佳
总结
通过本文的数学推导和代码实现,你应该已经掌握了 BP 算法的核心思想。在实际应用中,还需要考虑批量归一化、更复杂的网络结构等问题。建议下一步尝试实现多隐藏层网络,并观察不同激活函数的性能差异。
