BP神经网络之BP算法手写推导:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

前向传播的数学原理

神经网络的前向传播可以看作是多层复合函数的计算过程。以单隐层网络为例,设输入层为 $\mathbf{x}$,隐藏层输出为 $\mathbf{h}$,最终输出为 $\mathbf{\hat{y}}$,则计算过程如下:

BP 神经网络之 BP 算法手写推导:从数学原理到 Python 实现

  1. 隐藏层计算:
    $$\mathbf{z}^{(1)} = \mathbf{W}^{(1)}\mathbf{x} + \mathbf{b}^{(1)}$$
    $$\mathbf{h} = \sigma(\mathbf{z}^{(1)})$$

  2. 输出层计算:
    $$\mathbf{z}^{(2)} = \mathbf{W}^{(2)}\mathbf{h} + \mathbf{b}^{(2)}$$
    $$\mathbf{\hat{y}} = \text{softmax}(\mathbf{z}^{(2)})$$

其中 $\sigma$ 表示 sigmoid 激活函数,softmax 用于多分类问题。

反向传播的链式法则推导

反向传播的核心是计算损失函数对权重和偏置的偏导数。以均方误差损失 $L = \frac{1}{2}(y-\hat{y})^2$ 为例:

  1. 输出层权重梯度:
    $$\frac{\partial L}{\partial W_{ij}^{(2)}} = \frac{\partial L}{\partial \hat{y}i} \frac{\partial \hat{y}_i}{\partial z_i^{(2)}} \frac{\partial z_i^{(2)}}{\partial W_i)\cdot h_j$$}^{(2)}} = (\hat{y}_i-y_i)\cdot \hat{y}_i(1-\hat{y

  2. 隐藏层权重梯度:
    $$\frac{\partial L}{\partial W_{jk}^{(1)}} = \sum_i \frac{\partial L}{\partial \hat{y}i} \frac{\partial \hat{y}_i}{\partial z_i^{(2)}} \frac{\partial z_i^{(2)}}{\partial h_j} \frac{\partial h_j}{\partial z_j^{(1)}} \frac{\partial z_j^{(1)}}{\partial W$$}^{(1)}

Python 实现核心代码

import numpy as np

class NeuralNetwork:
    def __init__(self, input_size, hidden_size, output_size):
        # 初始化权重(使用较小随机值)self.W1 = np.random.randn(hidden_size, input_size) * 0.01
        self.b1 = np.zeros((hidden_size, 1))
        self.W2 = np.random.randn(output_size, hidden_size) * 0.01
        self.b2 = np.zeros((output_size, 1))

    def forward(self, x):
        # 前向传播
        self.z1 = np.dot(self.W1, x) + self.b1
        self.h = 1 / (1 + np.exp(-self.z1))  # sigmoid 激活
        self.z2 = np.dot(self.W2, self.h) + self.b2
        self.y_hat = np.exp(self.z2) / np.sum(np.exp(self.z2))  # softmax
        return self.y_hat

    def backward(self, x, y, learning_rate):
        # 反向传播
        m = x.shape[1]  # 样本数

        # 输出层梯度
        dz2 = self.y_hat - y
        dW2 = np.dot(dz2, self.h.T) / m
        db2 = np.sum(dz2, axis=1, keepdims=True) / m

        # 隐藏层梯度
        dz1 = np.dot(self.W2.T, dz2) * (self.h * (1 - self.h))
        dW1 = np.dot(dz1, x.T) / m
        db1 = np.sum(dz1, axis=1, keepdims=True) / m

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

常见问题与优化

梯度消失问题

当使用 sigmoid 激活函数时,其导数 $\sigma'(x) = \sigma(x)(1-\sigma(x))$ 最大值仅为 0.25,在深层网络中梯度会指数级减小。解决方案:

  1. 使用 ReLU 激活函数:$\text{ReLU}(x) = \max(0, x)$
  2. 加入批量归一化(BatchNorm)层
  3. 使用残差连接(ResNet)

参数初始化技巧

  1. 权重初始化过大会导致梯度爆炸
  2. 推荐使用 Xavier 初始化:$W \sim N(0, \sqrt{\frac{2}{n_{in}+n_{out}}})$

学习率调整

实现学习率衰减可提升后期训练稳定性:

learning_rate = initial_lr * (1. / (1. + decay_rate * epoch))

梯度检查实现

数值梯度检查是验证反向传播正确性的有效方法:

def gradient_check(x, y, epsilon=1e-7):
    # 计算解析梯度
    nn.forward(x)
    nn.backward(x, y, learning_rate=0, update=False)

    # 对每个参数进行数值梯度近似
    for param, dparam in zip([nn.W1, nn.b1, nn.W2, nn.b2], 
                            [nn.dW1, nn.db1, nn.dW2, nn.db2]):
        for i in range(param.shape[0]):
            for j in range(param.shape[1]):
                original = param[i,j]

                # 计算 f(x+epsilon)
                param[i,j] = original + epsilon
                loss_plus = nn.compute_loss(x, y)

                # 计算 f(x-epsilon)
                param[i,j] = original - epsilon
                loss_minus = nn.compute_loss(x, y)

                # 恢复原值
                param[i,j] = original

                # 数值梯度
                grad_approx = (loss_plus - loss_minus) / (2 * epsilon)

                # 比较数值梯度和解析梯度
                assert np.abs(dparam[i,j] - grad_approx) < 1e-5

延伸思考

  1. 如何修改代码实现带动量的随机梯度下降(Momentum SGD)?
  2. 如果改用交叉熵损失函数,反向传播公式会有哪些变化?
  3. 尝试将 sigmoid 激活函数替换为 LeakyReLU,观察训练效果差异
  4. 如何实现早停(Early Stopping)机制防止过拟合?

通过这次推导和实现,我深刻理解了 BP 算法的数学本质。建议读者在理解基本原理后,可以尝试用 PyTorch 等框架实现相同功能,对比理解自动微分的实现原理。

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