BP反向传播算法计算题实战:从数学推导到Python实现

1次阅读
没有评论

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

image.webp

为什么需要反向传播?

神经网络的核心是通过调整权重来最小化损失函数。想象一个小朋友学写字:
1. 先尝试写一个字(前向传播)
2. 对照字帖发现哪里不对(计算损失)
3. 根据错误调整握笔姿势(反向传播更新权重)

BP 反向传播算法计算题实战:从数学推导到 Python 实现

传统机器学习中,我们可以直接计算梯度。但当网络层数增多时:
– 参数数量呈指数级增长
– 手动计算梯度变得不可能
– 反向传播通过链式法则实现高效梯度计算

数学推导:剥洋葱般的链式法则

以单隐层网络为例,设:
– 输入层到隐层权重 $W^{(1)}$,偏置 $b^{(1)}$
– 隐层到输出层权重 $W^{(2)}$,偏置 $b^{(2)}$
– 使用 Sigmoid 激活 $\sigma(z)=\frac{1}{1+e^{-z}}$
– 损失函数为 MSE:$L=\frac{1}{2}(y-\hat{y})^2$

关键推导步骤

  1. 输出层梯度计算:
    $$
    \frac{\partial L}{\partial W^{(2)}} = \underbrace{(\hat{y}-y)}{\text{ 损失梯度}} \cdot \underbrace{\sigma'(z^{(2)})}
    $$}} \cdot \underbrace{a^{(1)}}_{\text{ 上游激活}

  2. 隐层梯度计算(链式法则展开):
    $$
    \frac{\partial L}{\partial W^{(1)}} = \left[\sum (\hat{y}-y) \cdot \sigma'(z^{(2)}) \cdot W^{(2)}\right] \cdot \sigma'(z^{(1)}) \cdot x
    $$

  3. 参数更新规则:
    $$
    W \leftarrow W – \eta \cdot \frac{\partial L}{\partial W}
    $$

Python 实现:手把手编码

import numpy as np

class TwoLayerNet:
    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 sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    def sigmoid_deriv(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):
        # 输出层梯度
        dL_da2 = self.a2 - y
        da2_dz2 = self.sigmoid_deriv(self.a2)
        dz2_dW2 = self.a1

        # 隐层梯度
        dz2_da1 = self.W2
        da1_dz1 = self.sigmoid_deriv(self.a1)
        dz1_dW1 = x

        # 参数更新
        self.W2 -= learning_rate * np.dot(dz2_dW2.T, dL_da2 * da2_dz2)
        self.b2 -= learning_rate * np.sum(dL_da2 * da2_dz2, axis=0)

        hidden_error = np.dot(dL_da2 * da2_dz2, dz2_da1.T)
        self.W1 -= learning_rate * np.dot(dz1_dW1.T, hidden_error * da1_dz1)
        self.b1 -= learning_rate * np.sum(hidden_error * da1_dz1, axis=0)

常见计算陷阱

维度不匹配急救包

  • 典型错误:(256, 128) * (64, 10)
  • 调试技巧:
  • 打印每层输出的 shape
  • 记住矩阵乘法规则 (m,n) @ (n,p) = (m,p)

梯度消失诊断

当出现以下情况时:
– 浅层权重几乎不更新
– 损失函数下降极其缓慢
– 使用 ReLU 代替 Sigmoid
– 尝试 Xavier 初始化

MNIST 实战演示

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)
X = mnist.data / 255.0
y = np.eye(10)[mnist.target.astype(int)]

net = TwoLayerNet(784, 64, 10)
for epoch in range(10):
    # 随机批次训练
    for i in range(0, len(X), 64):
        batch_X = X[i:i+64]
        batch_y = y[i:i+64]

        pred = net.forward(batch_X)
        net.backward(batch_X, batch_y, 0.1)

    # 验证集准确率
    val_pred = net.forward(X_test)
    acc = np.mean(np.argmax(val_pred,1) == np.argmax(y_test,1))
    print(f"Epoch {epoch}: {acc:.3f}")

三个思考题

  1. 如果输出层使用线性激活函数,反向传播公式会发生什么变化?
  2. 当 batch_size= 1 时,参数更新会有什么特点?
  3. 如何修改代码实现 L2 正则化?

通过这个实现过程,最让我惊讶的是反向传播中梯度计算的对称美——每一层的梯度计算其实遵循相同的模式,只是不断叠加链式法则。建议读者可以尝试用不同激活函数(如 ReLU)重新实现,观察训练效果的变化。

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