共计 3006 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点
对于刚接触神经网络的开发者来说,反向传播算法(Backpropagation)往往是最难啃的骨头之一。主要困难集中在以下几个地方:

- 矩阵求导的复杂性 :神经网络中的权重和偏置都是矩阵形式,如何正确求导容易让人困惑
- 链式法则的多层嵌套 :误差需要从输出层逐层反向传播,涉及多层复合函数求导
- 符号系统的混乱 :不同资料使用的变量命名不一致,导致理解困难
数学推导(单隐层网络示例)
网络结构定义
设网络结构为:
– 输入层:$x \in \mathbb{R}^n$
– 隐层:$h = \sigma(W_1x + b_1)$,其中 $W_1 \in \mathbb{R}^{m \times n}$, $b_1 \in \mathbb{R}^m$
– 输出层:$\hat{y} = \sigma(W_2h + b_2)$,其中 $W_2 \in \mathbb{R}^{k \times m}$, $b_2 \in \mathbb{R}^k$
– 损失函数:$L = \frac{1}{2}||y – \hat{y}||^2$
反向传播分步推导
- 输出层梯度计算
$$\frac{\partial L}{\partial W_2} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_2} \cdot \frac{\partial z_2}{\partial W_2}$$
其中 $z_2 = W_2h + b_2$,具体展开为:
- $\frac{\partial L}{\partial \hat{y}} = \hat{y} – y$
- $\frac{\partial \hat{y}}{\partial z_2} = \sigma'(z_2)$(激活函数导数)
- $\frac{\partial z_2}{\partial W_2} = h^T$
最终得到:
$$\frac{\partial L}{\partial W_2} = (\hat{y} – y) \odot \sigma'(z_2) \cdot h^T$$
- 隐层梯度计算
$$\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial h} \cdot \frac{\partial h}{\partial z_1} \cdot \frac{\partial z_1}{\partial W_1}$$
其中 $z_1 = W_1x + b_1$,具体展开为:
- $\frac{\partial L}{\partial h} = W_2^T \cdot [(\hat{y} – y) \odot \sigma'(z_2)]$
- $\frac{\partial h}{\partial z_1} = \sigma'(z_1)$
- $\frac{\partial z_1}{\partial W_1} = x^T$
最终得到:
$$\frac{\partial L}{\partial W_1} = [W_2^T \cdot (\hat{y} – y) \odot \sigma'(z_2)] \odot \sigma'(z_1) \cdot x^T$$
Python 代码实现
import numpy as np
# Sigmoid 激活函数及其导数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
# 网络参数初始化
input_size = 3
hidden_size = 4
output_size = 1
W1 = np.random.randn(hidden_size, input_size)
W2 = np.random.randn(output_size, hidden_size)
b1 = np.zeros((hidden_size, 1))
b2 = np.zeros((output_size, 1))
# 训练数据
X = np.random.randn(input_size, 100) # 100 个样本
y = np.random.randn(output_size, 100)
# 训练循环
learning_rate = 0.1
for epoch in range(1000):
# 前向传播
z1 = np.dot(W1, X) + b1
h = sigmoid(z1)
z2 = np.dot(W2, h) + b2
y_pred = sigmoid(z2)
# 计算损失
loss = 0.5 * np.sum((y_pred - y)**2)
# 反向传播
# 输出层梯度
dL_dy = y_pred - y
dy_dz2 = sigmoid_derivative(y_pred)
dz2_dW2 = h.T
dW2 = np.dot(dL_dy * dy_dz2, dz2_dW2)
db2 = np.sum(dL_dy * dy_dz2, axis=1, keepdims=True)
# 隐层梯度
dL_dh = np.dot(W2.T, dL_dy * dy_dz2)
dh_dz1 = sigmoid_derivative(h)
dz1_dW1 = X.T
dW1 = np.dot(dL_dh * dh_dz1, dz1_dW1)
db1 = np.sum(dL_dh * dh_dz1, axis=1, keepdims=True)
# 参数更新
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
if epoch % 100 == 0:
print(f'Epoch {epoch}, Loss: {loss}')
避坑指南
- 梯度爆炸 / 消失问题
- 使用合适的权重初始化方法(如 Xavier 初始化)
- 考虑使用 ReLU 等非饱和激活函数
-
实施梯度裁剪(gradient clipping)
-
学习率设置
- 初始学习率建议从 0.1 开始尝试
- 考虑使用学习率衰减策略
-
可以尝试自适应优化器(如 Adam)
-
梯度检查
- 实现数值梯度检查来验证反向传播的正确性
def gradient_check(W, f, epsilon=1e-7): grad_approx = np.zeros_like(W) for i in range(W.size): original = W.flat[i] W.flat[i] = original + epsilon loss_plus = f() W.flat[i] = original - epsilon loss_minus = f() grad_approx.flat[i] = (loss_plus - loss_minus) / (2 * epsilon) W.flat[i] = original return grad_approx
延伸思考
-
与自动微分的关系
现代深度学习框架(如 PyTorch、TensorFlow)都实现了自动微分,背后的原理正是反向传播算法。理解手工推导有助于更好地使用这些框架。 -
不同激活函数的影响
- Sigmoid:导数范围为 (0, 0.25),容易导致梯度消失
- ReLU:缓解梯度消失,但存在 ” 死亡神经元 ” 问题
-
LeakyReLU:解决了 ReLU 的神经元死亡问题
-
更复杂的网络结构
对于深层网络,反向传播的链式法则会涉及更多层的连乘,需要特别注意梯度流动问题。
总结
通过本文的数学推导和代码实现,我们完整地走过了 BP 神经网络的核心算法——反向传播的每一个步骤。理解这些基础原理对于后续学习更复杂的神经网络架构至关重要。建议读者可以尝试:
- 手工推导一个三层的网络
- 实现不同激活函数的版本
- 用真实数据集测试代码效果
记住,反向传播本质上就是链式法则的巧妙应用,只要掌握了这个核心思想,各种变体网络的反向传播都不再是难题。
