共计 3337 个字符,预计需要花费 9 分钟才能阅读完成。
背景痛点
全连接神经网络(Fully Connected Neural Networks)是深度学习的基础结构,而反向传播(Backpropagation,BP)算法是其训练的核心。为什么需要反向传播?简单来说,神经网络通过调整权重(weights)来最小化预测误差,而反向传播就是计算这些权重应该如何调整的高效方法。

初学者常见的误区包括:
- 混淆前向传播与反向传播:前向传播用于计算预测值,反向传播则用于计算梯度(即权重的调整方向)。两者目的不同,但紧密相关。
- 忽视梯度消失问题:在深层网络中,梯度可能会在反向传播过程中逐渐变小甚至消失,导致浅层网络的权重几乎不更新。这是深层网络训练的一大挑战。
数学原理
反向传播的核心是链式法则(Chain Rule)。假设我们有一个简单的 3 层网络(输入层、隐藏层、输出层),损失函数为均方误差(MSE)。对于权重 (w_{ij}^{(l)})(第 (l) 层的第 (i) 个神经元到第 (j) 个神经元的连接权重),其梯度计算如下:
-
输出层梯度:
[
\frac{\partial E}{\partial w_{ij}^{(2)}} = \frac{\partial E}{\partial a_j^{(2)}} \cdot \frac{\partial a_j^{(2)}}{\partial z_j^{(2)}} \cdot \frac{\partial z_j^{(2)}}{\partial w_{ij}^{(2)}}
]
其中,(a_j^{(2)})是输出层的激活值,(z_j^{(2)})是输出层的加权输入。 -
隐藏层梯度:
[
\frac{\partial E}{\partial w_{ij}^{(1)}} = \sum_k \frac{\partial E}{\partial a_k^{(2)}} \cdot \frac{\partial a_k^{(2)}}{\partial z_k^{(2)}} \cdot \frac{\partial z_k^{(2)}}{\partial a_j^{(1)}} \cdot \frac{\partial a_j^{(1)}}{\partial z_j^{(1)}} \cdot \frac{\partial z_j^{(1)}}{\partial w_{ij}^{(1)}}
]
这里体现了链式法则的逐层传递特性。
优化器对比
-
SGD(随机梯度下降):
[
w_{ij} \leftarrow w_{ij} – \eta \cdot \frac{\partial E}{\partial w_{ij}}
]
其中,(\eta)是学习率。 -
Momentum(动量法):
[
v_{ij} \leftarrow \beta v_{ij} + (1 – \beta) \frac{\partial E}{\partial w_{ij}}
]
[
w_{ij} \leftarrow w_{ij} – \eta v_{ij}
]
动量法通过引入速度变量(v_{ij}),减少了训练过程中的震荡。
代码实现
以下是一个用 NumPy 实现的 3 层神经网络,包含正向传播、反向传播和学习率衰减机制。
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros((1, output_size))
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(self, x):
return x * (1 - x)
def forward(self, X):
self.z1 = np.dot(X, self.W1) + self.b1
self.a1 = self.sigmoid(self.z1)
self.z2 = np.dot(self.a1, self.W2) + self.b2
self.a2 = self.sigmoid(self.z2)
return self.a2
def backward(self, X, y, learning_rate):
m = X.shape[0]
# 输出层梯度
dE_da2 = (self.a2 - y) / m
da2_dz2 = self.sigmoid_derivative(self.a2)
dz2_dW2 = self.a1
dW2 = np.dot(dz2_dW2.T, dE_da2 * da2_dz2)
db2 = np.sum(dE_da2 * da2_dz2, axis=0, keepdims=True)
# 隐藏层梯度
dE_da1 = np.dot(dE_da2 * da2_dz2, self.W2.T)
da1_dz1 = self.sigmoid_derivative(self.a1)
dz1_dW1 = X
dW1 = np.dot(dz1_dW1.T, dE_da1 * da1_dz1)
db1 = np.sum(dE_da1 * da1_dz1, axis=0, keepdims=True)
# 更新权重
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
避坑指南
梯度检查(Gradient Checking)
梯度检查是验证反向传播实现是否正确的重要手段。具体步骤如下:
-
对每个参数(\theta),计算数值梯度:
[
\frac{J(\theta + \epsilon) – J(\theta – \epsilon)}{2\epsilon}
]
其中,(\epsilon)通常取(10^{-7})。 -
比较数值梯度与反向传播计算的梯度,两者的相对误差应小于(10^{-7})。
隐藏层神经元数量与学习率
- 隐藏层神经元数量:通常从较少的神经元(如 64 或 128)开始,逐步增加直至性能不再显著提升。
- 学习率:初始值通常设为 0.01 或 0.001,并通过学习率衰减(如每 10 轮乘以 0.9)逐步调整。
输出层梯度计算的常见错误
- 忘记除以样本数:在批量训练时,梯度应对所有样本取平均,否则可能导致梯度爆炸。
- 激活函数导数错误:例如,Sigmoid 的导数应为(a(1-a)),而非(1-a)。
性能验证
在 MNIST 数据集上测试不同 batch size 的收敛速度:
- batch size=32:收敛快,但波动较大。
- batch size=256:收敛慢,但更稳定。
可视化损失函数曲线时,若曲线出现以下形态,需注意:
– 剧烈震荡:学习率可能过高。
– 长时间不下降:学习率可能过低或网络结构不合理。
延伸思考
-
尝试 ReLU 激活函数:ReLU(Rectified Linear Unit)可以缓解梯度消失问题,其梯度为:
[
\frac{\partial \text{ReLU}(z)}{\partial z} = \begin{cases}
1 & \text{if} z > 0 \
0 & \text{otherwise}
\end{cases}
] -
挑战题:Mini-batch 训练
修改代码以支持 Mini-batch 训练,核心逻辑是: - 将数据集划分为多个小批次(batches)。
- 对每个批次计算梯度并更新权重。
def train_mini_batch(X, y, batch_size=32, epochs=100):
for epoch in range(epochs):
for i in range(0, X.shape[0], batch_size):
X_batch = X[i:i+batch_size]
y_batch = y[i:i+batch_size]
nn.forward(X_batch)
nn.backward(X_batch, y_batch, learning_rate)
总结
本文从数学原理到代码实现,详细讲解了 BP 反向传播网络的核心机制。通过实践和避坑指南,希望能帮助初学者更快掌握这一关键技术。未来的学习方向可以包括更复杂的网络结构(如 CNN、RNN)和优化算法(如 Adam)。
