从零推导bp反向传播算法:新手入门指南与数学实现

1次阅读
没有评论

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

image.webp

神经网络基础与问题定义

当我们构建一个最简单的三层神经网络(输入层、隐藏层、输出层)时,前向传播过程可以表示为:

从零推导 bp 反向传播算法:新手入门指南与数学实现

$$
\begin{aligned}
z^{(2)} &= W^{(1)}x + b^{(1)} \
a^{(2)} &= \sigma(z^{(2)}) \
z^{(3)} &= W^{(2)}a^{(2)} + b^{(2)} \
a^{(3)} &= \sigma(z^{(3)})
\end{aligned}
$$

其中 $\sigma(\cdot)$ 是 sigmoid 激活函数,$W$ 和 $b$ 分别代表权重矩阵和偏置项。我们需要定义一个损失函数(这里采用均方误差):

$$
J = \frac{1}{2}(y – a^{(3)})^2
$$

链式法则的逐层应用

输出层梯度计算

首先计算输出层权重 $W^{(2)}$ 的梯度:

$$
\frac{\partial J}{\partial W^{(2)}} = \frac{\partial J}{\partial a^{(3)}} \cdot \frac{\partial a^{(3)}}{\partial z^{(3)}} \cdot \frac{\partial z^{(3)}}{\partial W^{(2)}}
$$

展开各项导数:

  1. $\frac{\partial J}{\partial a^{(3)}} = -(y – a^{(3)})$
  2. $\frac{\partial a^{(3)}}{\partial z^{(3)}} = \sigma'(z^{(3)}) = a^{(3)}(1-a^{(3)})$
  3. $\frac{\partial z^{(3)}}{\partial W^{(2)}} = a^{(2)}$

最终得到:

$$
\delta^{(3)} = (a^{(3)} – y) \cdot a^{(3)}(1-a^{(3)})
$$

$$
\frac{\partial J}{\partial W^{(2)}} = \delta^{(3)} a^{(2)T}
$$

隐藏层梯度计算

对于隐藏层权重 $W^{(1)}$,梯度计算需要继续反向传播:

$$
\frac{\partial J}{\partial W^{(1)}} = \frac{\partial J}{\partial a^{(3)}} \cdot \frac{\partial a^{(3)}}{\partial z^{(3)}} \cdot \frac{\partial z^{(3)}}{\partial a^{(2)}} \cdot \frac{\partial a^{(2)}}{\partial z^{(2)}} \cdot \frac{\partial z^{(2)}}{\partial W^{(1)}}
$$

其中新增的导数为:

  1. $\frac{\partial z^{(3)}}{\partial a^{(2)}} = W^{(2)}$
  2. $\frac{\partial a^{(2)}}{\partial z^{(2)}} = \sigma'(z^{(2)}) = a^{(2)}(1-a^{(2)})$
  3. $\frac{\partial z^{(2)}}{\partial W^{(1)}} = x$

因此隐藏层误差项为:

$$
\delta^{(2)} = (W^{(2)T} \delta^{(3)}) \odot a^{(2)}(1-a^{(2)})
$$

Python 完整实现

import numpy as np

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

    def sigmoid(self, z):
        return 1 / (1 + np.exp(-z))

    def sigmoid_derivative(self, z):
        s = self.sigmoid(z)
        return s * (1 - s)

    def forward(self, X):
        self.z2 = np.dot(X, self.W1) + self.b1
        self.a2 = self.sigmoid(self.z2)
        self.z3 = np.dot(self.a2, self.W2) + self.b2
        self.a3 = self.sigmoid(self.z3)
        return self.a3

    def backward(self, X, y, learning_rate):
        m = X.shape[0]  # 样本数量

        # 输出层误差
        delta3 = (self.a3 - y) * self.sigmoid_derivative(self.z3)
        dW2 = np.dot(self.a2.T, delta3)
        db2 = np.sum(delta3, axis=0, keepdims=True)

        # 隐藏层误差
        delta2 = np.dot(delta3, self.W2.T) * self.sigmoid_derivative(self.z2)
        dW1 = np.dot(X.T, delta2)
        db1 = np.sum(delta2, axis=0)

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

    def train(self, X, y, epochs, learning_rate):
        for i in range(epochs):
            output = self.forward(X)
            self.backward(X, y, learning_rate)

避坑指南

梯度消失问题

当使用 sigmoid 激活函数时,其导数的最大值仅为 0.25(在输入为 0 时)。这意味着在深层网络中,梯度会随着反向传播层层衰减,导致靠前的层几乎无法更新,这种现象称为梯度消失。解决方案包括:

  • 使用 ReLU 等具有恒定梯度的激活函数
  • 采用残差连接(ResNet)
  • 使用 Batch Normalization

学习率选择

学习率过大容易震荡不收敛,过小则训练缓慢。建议策略:

  1. 初始尝试 0.01 或 0.001
  2. 使用学习率衰减策略
  3. 考虑自适应优化器(Adam 等)

权重初始化

全零初始化会导致神经元对称性问题。推荐方法:

  • Xavier 初始化:$W \sim N(0, \sqrt{2/(n_{in} + n_{out})})$
  • He 初始化(ReLU 适用):$W \sim N(0, \sqrt{2/n_{in}})$

思考题

  1. 如何修改代码实现 ReLU 激活函数?需要注意哪些问题?
  2. 批量训练(Batch)与在线训练(Online)在实现上有什么区别?各自的优缺点是什么?
  3. 二阶优化算法(如 L -BFGS)相比梯度下降有什么理论优势?为什么在深度学习中不常见?

总结

通过本文的推导和实现,我们完成了以下目标:
1. 从数学原理层面理解了反向传播的链式法则应用
2. 实现了完整的神经网络训练流程
3. 掌握了实践中常见的调参技巧

建议读者尝试用不同数据集测试代码,观察不同超参数对训练过程的影响,这是理解算法行为的最佳方式。

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