BP神经网络误差反向传播推导:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

背景痛点

对于刚接触神经网络的开发者来说,反向传播算法(Backpropagation)往往是最难啃的骨头之一。主要困难集中在以下几个地方:

BP 神经网络误差反向传播推导:从数学原理到 Python 实现

  • 矩阵求导的复杂性 :神经网络中的权重和偏置都是矩阵形式,如何正确求导容易让人困惑
  • 链式法则的多层嵌套 :误差需要从输出层逐层反向传播,涉及多层复合函数求导
  • 符号系统的混乱 :不同资料使用的变量命名不一致,导致理解困难

数学推导(单隐层网络示例)

网络结构定义

设网络结构为:
– 输入层:$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$

反向传播分步推导

  1. 输出层梯度计算

$$\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$$

  1. 隐层梯度计算

$$\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}')

避坑指南

  1. 梯度爆炸 / 消失问题
  2. 使用合适的权重初始化方法(如 Xavier 初始化)
  3. 考虑使用 ReLU 等非饱和激活函数
  4. 实施梯度裁剪(gradient clipping)

  5. 学习率设置

  6. 初始学习率建议从 0.1 开始尝试
  7. 考虑使用学习率衰减策略
  8. 可以尝试自适应优化器(如 Adam)

  9. 梯度检查

  10. 实现数值梯度检查来验证反向传播的正确性
    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

延伸思考

  1. 与自动微分的关系
    现代深度学习框架(如 PyTorch、TensorFlow)都实现了自动微分,背后的原理正是反向传播算法。理解手工推导有助于更好地使用这些框架。

  2. 不同激活函数的影响

  3. Sigmoid:导数范围为 (0, 0.25),容易导致梯度消失
  4. ReLU:缓解梯度消失,但存在 ” 死亡神经元 ” 问题
  5. LeakyReLU:解决了 ReLU 的神经元死亡问题

  6. 更复杂的网络结构
    对于深层网络,反向传播的链式法则会涉及更多层的连乘,需要特别注意梯度流动问题。

总结

通过本文的数学推导和代码实现,我们完整地走过了 BP 神经网络的核心算法——反向传播的每一个步骤。理解这些基础原理对于后续学习更复杂的神经网络架构至关重要。建议读者可以尝试:

  1. 手工推导一个三层的网络
  2. 实现不同激活函数的版本
  3. 用真实数据集测试代码效果

记住,反向传播本质上就是链式法则的巧妙应用,只要掌握了这个核心思想,各种变体网络的反向传播都不再是难题。

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