共计 3230 个字符,预计需要花费 9 分钟才能阅读完成。
从 MNIST 分类看 BP 网络的价值
手写数字识别(MNIST)是理解 BP 网络的经典场景。每个 28×28 像素的图像被展平成 784 维输入向量,我们需要一个能自动学习特征并分类的网络。传统线性模型无法处理像素间的复杂关系,而具有隐藏层的 BP 网络通过非线性变换实现了这一点。

数学推导:链式法则与权重更新
计算图视角下的反向传播
假设三层网络结构为:输入层 $\mathbf{x}$、隐藏层 $\mathbf{h} = \sigma(\mathbf{W}_1\mathbf{x}+\mathbf{b}_1)$、输出层 $\mathbf{y} = \text{softmax}(\mathbf{W}_2\mathbf{h}+\mathbf{b}_2)$,损失函数为交叉熵 $E$。关键推导步骤如下:
-
输出层梯度:
$$\frac{\partial E}{\partial \mathbf{W}_2} = \frac{\partial E}{\partial \mathbf{y}} \cdot \frac{\partial \mathbf{y}}{\partial (\mathbf{W}_2\mathbf{h})} \cdot \frac{\partial (\mathbf{W}_2\mathbf{h})}{\partial \mathbf{W}_2} = (\mathbf{y} – \mathbf{t}) \otimes \mathbf{h}^T$$
其中 $\mathbf{t}$ 为 one-hot 标签 -
隐藏层梯度:
$$\frac{\partial E}{\partial \mathbf{W}_1} = \frac{\partial E}{\partial \mathbf{h}} \cdot \frac{\partial \mathbf{h}}{\partial (\mathbf{W}_1\mathbf{x})} \cdot \frac{\partial (\mathbf{W}_1\mathbf{x})}{\partial \mathbf{W}_1}$$
展开后为:
$$\mathbf{W}_2^T(\mathbf{y} – \mathbf{t}) \odot \sigma'(\mathbf{W}_1\mathbf{x}) \otimes \mathbf{x}^T$$
NumPy 实现关键代码
import numpy as np
class ThreeLayerNet:
def __init__(self, input_size, hidden_size, output_size):
# 初始化权重(He 初始化)self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2/input_size)
self.b1 = np.zeros(hidden_size)
self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2/hidden_size)
self.b2 = np.zeros(output_size)
def forward(self, x):
# 前向传播维度变化: (batch, input) -> (batch, hidden) -> (batch, output)
self.z1 = x @ self.W1 + self.b1 # 注意广播机制
self.h = np.maximum(0, self.z1) # ReLU 激活
self.z2 = self.h @ self.W2 + self.b2
self.y = self._softmax(self.z2)
return self.y
def backward(self, x, t, lr):
batch_size = x.shape[0]
# 输出层梯度
dy = (self.y - t) / batch_size # 交叉熵 +softmax 的联合梯度
dW2 = self.h.T @ dy
db2 = np.sum(dy, axis=0)
# 隐藏层梯度(利用前向传播缓存的 z1)dh = dy @ self.W2.T
dz1 = dh * (self.z1 > 0) # ReLU 导数
dW1 = x.T @ dz1
db1 = np.sum(dz1, axis=0)
# 参数更新(带学习率衰减)self.W2 -= lr * dW2
self.b2 -= lr * db2
self.W1 -= lr * dW1
self.b1 -= lr * db1
优化技巧与避坑指南
激活函数选择
-
Sigmoid 问题 :当输入绝对值较大时梯度接近 0(梯度消失),且计算含指数运算较慢
$$\sigma'(z) = \sigma(z)(1-\sigma(z))$$ -
ReLU 优势 :正向区间梯度为 1,避免梯度消失,计算速度快
$$\text{ReLU}'(z) = \begin{cases} 1 & z > 0 \ 0 & \text{otherwise} \end{cases}$$
批量归一化实现
class BatchNorm:
def __init__(self, dim, eps=1e-5):
self.gamma = np.ones(dim)
self.beta = np.zeros(dim)
self.eps = eps
def forward(self, x, train=True):
if train:
self.mu = np.mean(x, axis=0)
self.sigma2 = np.var(x, axis=0)
x_hat = (x - self.mu) / np.sqrt(self.sigma2 + self.eps)
self.cache = (x, x_hat)
return self.gamma * x_hat + self.beta
else:
# 测试时使用移动平均
return self.gamma * (x - self.moving_mu) / np.sqrt(self.moving_sigma2 + self.eps) + self.beta
超参数设置经验
-
隐藏层神经元数量:通常取输入输出层大小的几何平均数,例如 MNIST 中可设为 $\sqrt{784 \times 10} \approx 88$
-
梯度检查实现(数值梯度 vs 解析梯度):
def gradient_check(x, t, net, param_name, epsilon=1e-7): param = getattr(net, param_name) grad = getattr(net, 'd'+param_name) # 反向传播得到的梯度 for i in range(min(10, param.size)): # 随机检查部分维度 idx = np.unravel_index(i, param.shape) orig = param[idx] param[idx] = orig + epsilon loss_plus = net.loss(x, t) param[idx] = orig - epsilon loss_minus = net.loss(x, t) numeric_grad = (loss_plus - loss_minus) / (2*epsilon) param[idx] = orig # 恢复原值 diff = np.abs(numeric_grad - grad[idx]) if diff > 1e-5: print(f'梯度检查失败:{param_name} 维度 {idx},差异 {diff:.2e}') -
学习率与 batch size:大 batch 需要更大学习率,推荐线性缩放规则(batch 扩大 k 倍时 lr 乘以 k)
思考题解析
当输出层采用交叉熵损失 +softmax 时,求导会出现神奇简化:
$$\frac{\partial E}{\partial z_i} = \sum_j \frac{\partial E}{\partial y_j} \frac{\partial y_j}{\partial z_i} = y_i – t_i$$
这是因为 $\frac{\partial E}{\partial y_j} = -\frac{t_j}{y_j}$,而 softmax 导数为 $\frac{\partial y_j}{\partial z_i} = y_j(\delta_{ij} – y_i)$,二者相乘后所有项相互抵消,最终得到简洁形式。
