bp反向传播算法入门指南:从数学原理到Python实现

1次阅读
没有评论

共计 2235 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

为什么需要反向传播?

刚入门神经网络时,最让我头疼的就是:那么多参数(Weights 和 Biases)该怎么调整?比如一个简单的 3 层网络,输入层 10 个节点,隐藏层 20 个节点,输出层 2 个节点,光是权重矩阵就有 (10×20)+(20×2)=240 个参数!如果靠瞎猜来调整这些参数,估计训练到宇宙末日也完不成。

bp 反向传播算法入门指南:从数学原理到 Python 实现

链式法则:反向传播的数学核心

反向传播的精髓在于链式法则(Chain Rule)。举个具体例子,假设我们有如下单隐藏层网络:

 输入层 → 隐藏层(Sigmoid 激活)→ 输出层(线性输出)

用数学表示前向传播过程:

  1. 隐藏层输入:$h_{in} = XW_1 + b_1$
  2. 隐藏层输出:$h_{out} = \sigma(h_{in})$
  3. 最终输出:$y_{pred} = h_{out}W_2 + b_2$

当使用均方误差损失函数时,我们需要计算 $\frac{\partial Loss}{\partial W_1}$。根据链式法则,这个梯度可以拆解为:

$$
\frac{\partial Loss}{\partial W_1} = \frac{\partial Loss}{\partial y_{pred}} \cdot \frac{\partial y_{pred}}{\partial h_{out}} \cdot \frac{\partial h_{out}}{\partial h_{in}} \cdot \frac{\partial h_{in}}{\partial W_1}
$$

具体到每一步的计算(假设 batch_size=1):

  • $\frac{\partial Loss}{\partial y_{pred}} = 2(y_{pred} – y_{true})$(形状:1×2)
  • $\frac{\partial y_{pred}}{\partial h_{out}} = W_2^T$(形状:2×20)
  • $\frac{\partial h_{out}}{\partial h_{in}} = \sigma'(h_{in})$(形状:1×20)
  • $\frac{\partial h_{in}}{\partial W_1} = X^T$(形状:10×1)

最终通过矩阵相乘得到 $\frac{\partial Loss}{\partial W_1}$ 的形状是 10×20,与 $W_1$ 本身的形状一致。

Python 实现详解

下面是用 NumPy 实现的完整代码,关键部分都加了形状变化的注释:

import numpy as np

# 网络参数初始化
input_size = 10
hidden_size = 20
output_size = 2

W1 = np.random.randn(input_size, hidden_size) * 0.01  # 形状:10×20
b1 = np.zeros((1, hidden_size))                      # 形状:1×20
W2 = np.random.randn(hidden_size, output_size) * 0.01 # 形状:20×2
b2 = np.zeros((1, output_size))                      # 形状:1×2

# 前向传播
def forward(X):
    h_in = np.dot(X, W1) + b1        # 形状:1×20
    h_out = 1 / (1 + np.exp(-h_in))  # Sigmoid 激活
    y_pred = np.dot(h_out, W2) + b2  # 形状:1×2
    return h_in, h_out, y_pred

# 反向传播
def backward(X, y_true, h_in, h_out, y_pred):
    batch_size = X.shape[0]

    # 输出层梯度
    dLoss = 2 * (y_pred - y_true)              # 形状:1×2
    dW2 = np.dot(h_out.T, dLoss) / batch_size  # 形状:20×2
    db2 = np.sum(dLoss, axis=0) / batch_size   # 形状:1×2

    # 隐藏层梯度
    dh_out = np.dot(dLoss, W2.T)               # 形状:1×20
    dh_in = dh_out * h_out * (1 - h_out)       # Sigmoid 导数
    dW1 = np.dot(X.T, dh_in) / batch_size      # 形状:10×20
    db1 = np.sum(dh_in, axis=0) / batch_size   # 形状:1×20

    return dW1, db1, dW2, db2

# 参数更新(学习率 lr=0.1)def update_params(dW1, db1, dW2, db2, lr=0.1):
    global W1, b1, W2, b2
    W1 -= lr * dW1
    b1 -= lr * db1
    W2 -= lr * dW2
    b2 -= lr * db2

避坑指南

在实际实现时,新手常会遇到这些问题:

  1. 梯度爆炸 / 消失 :当网络较深时,梯度可能指数级增大或减小。解决方法:
  2. 使用 ReLU 等改进的激活函数替代 Sigmoid
  3. 采用梯度裁剪(Gradient Clipping)
  4. 合理的权重初始化(如 He 初始化)

  5. 学习率设置不当

  6. 可以从 0.01~0.1 开始尝试
  7. 更高级的优化器(Adam、RMSProp)能自动调整

  8. 数值不稳定

  9. 避免除零问题(如在交叉熵损失中加 epsilon)
  10. 使用对数空间计算(如 log_softmax)

延伸思考

尝试改进上面的基础实现:

  1. 如何修改代码支持 mini-batch 训练?提示:注意 batch 维度的矩阵运算
  2. 如果隐藏层改用 ReLU 激活,反向传播的计算需要做哪些修改?

通过这个练习,你会更深刻地理解:反向传播本质上是一套巧妙的梯度分发系统,它让神经网络能够高效地自我修正。建议亲手敲一遍代码,观察中间变量的形状变化,这比看十遍理论都管用!

正文完
 0
评论(没有评论)