BP神经网络反向传播算法:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

背景:前向传播与损失函数

神经网络的前向传播可以看作是一系列函数复合的过程。假设我们有一个单隐藏层网络,输入层到隐藏层的权重矩阵为 $W_1$,隐藏层到输出层的权重矩阵为 $W_2$,那么前向传播过程可以表示为:

BP 神经网络反向传播算法:从数学原理到 Python 实现

$$
\begin{aligned}
h &= \sigma(W_1 x + b_1) \
y &= \text{softmax}(W_2 h + b_2)
\end{aligned}
$$

其中 $\sigma$ 是激活函数(如 sigmoid),$\text{softmax}$ 用于多分类问题。损失函数 $E$ 通常使用交叉熵损失:

$$
E = -\sum_i t_i \log(y_i)
$$

这里 $t_i$ 是真实标签的 one-hot 编码。我们的目标就是通过调整 $W_1,W_2,b_1,b_2$ 来最小化这个损失函数。

数学原理:链式法则推导

反向传播的核心就是链式法则。我们以输出层权重 $W_2$ 的梯度计算为例:

  1. 首先计算损失对网络输出的偏导:

$$
\frac{\partial E}{\partial y_i} = -\frac{t_i}{y_i}
$$

  1. 然后计算 softmax 的导数(这里省略推导过程):

$$
\frac{\partial y_j}{\partial z_k} = y_j(\delta_{jk} – y_k)
$$

  1. 结合这两部分,得到误差项 $\delta_o$:

$$
\delta_o = \frac{\partial E}{\partial z} = y – t
$$

  1. 最后得到 $W_2$ 的梯度:

$$
\frac{\partial E}{\partial W_2} = h^T \delta_o
$$

对于隐藏层权重 $W_1$,推导过程类似但更复杂,因为需要通过链式法则传播误差:

$$
\delta_h = (\delta_o W_2^T) \circ \sigma'(z_h)
$$

$$
\frac{\partial E}{\partial W_1} = x^T \delta_h
$$

其中 $\circ$ 表示逐元素相乘。

Python 实现

下面是用 NumPy 实现的单隐藏层 BP 算法核心代码:

import numpy as np
from typing import Tuple

def sigmoid(x: np.ndarray) -> np.ndarray:
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x: np.ndarray) -> np.ndarray:
    return x * (1 - x)

class NeuralNetwork:
    def __init__(self, input_size: int, hidden_size: int, output_size: int):
        # 初始化权重
        self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros(hidden_size)
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros(output_size)

    def forward(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        # 前向传播
        self.z1 = np.dot(x, self.W1) + self.b1
        self.h = sigmoid(self.z1)
        self.z2 = np.dot(self.h, self.W2) + self.b2
        self.y = softmax(self.z2)
        return self.h, self.y

    def backward(self, x: np.ndarray, t: np.ndarray, lr: float) -> None:
        # 反向传播
        batch_size = x.shape[0]

        # 输出层误差
        delta_o = self.y - t

        # 计算 W2 和 b2 的梯度
        dW2 = np.dot(self.h.T, delta_o) / batch_size
        db2 = np.sum(delta_o, axis=0) / batch_size

        # 隐藏层误差
        delta_h = np.dot(delta_o, self.W2.T) * sigmoid_derivative(self.h)

        # 计算 W1 和 b1 的梯度
        dW1 = np.dot(x.T, delta_h) / batch_size
        db1 = np.sum(delta_h, axis=0) / batch_size

        # 参数更新
        self.W2 -= lr * dW2
        self.b2 -= lr * db2
        self.W1 -= lr * dW1
        self.b1 -= lr * db1

避坑指南

  1. 梯度爆炸 :当梯度值变得非常大时,可以使用梯度裁剪:
def clip_gradients(grad: np.ndarray, max_norm: float) -> np.ndarray:
    norm = np.linalg.norm(grad)
    if norm > max_norm:
        grad = grad * max_norm / norm
    return grad
  1. ReLU 死亡神经元 :当使用 ReLU 激活函数时,某些神经元可能永远不激活。解决方法包括:
  2. 使用 LeakyReLU
  3. 合理初始化权重
  4. 使用较小的学习率

验证实验

在 MNIST 数据集上的测试步骤:

  1. 加载并预处理数据
  2. 初始化网络(如 784-128-10 结构)
  3. 训练过程中记录损失和准确率
  4. 对比不同学习率(如 0.1, 0.01, 0.001)的效果

典型的学习率对比结果可能显示:
– 过大的学习率(如 0.1)导致震荡或不收敛
– 过小的学习率(如 0.001)收敛过慢
– 适中的学习率(如 0.01)表现最佳

总结

通过本文的数学推导和代码实现,你应该已经掌握了 BP 算法的核心思想。在实际应用中,还需要考虑批量归一化、更复杂的网络结构等问题。建议下一步尝试实现多隐藏层网络,并观察不同激活函数的性能差异。

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