BP神经网络推导入门:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

理解 BP 神经网络的核心:反向传播

反向传播(Backpropagation,BP)是神经网络训练的基石。对于初学者来说,理解其数学推导过程往往是第一个难关。本文将从最基础的单层感知机开始,逐步拆解 BP 算法的实现细节。

BP 神经网络推导入门:从数学原理到 Python 实现

1. 从单层感知机开始

假设我们有一个最简单的单层神经网络(感知机),其输出为:

$$ y = \sigma(wx + b) $$

其中 $\sigma$ 是激活函数(如 Sigmoid),$w$ 是权重,$b$ 是偏置。

对于二分类问题,我们使用交叉熵损失函数:

$$ L = -[y_{true} \log(y) + (1-y_{true}) \log(1-y)] $$

计算损失对权重 $w$ 的梯度:

  1. 首先计算损失对输出 $y$ 的偏导:
    $$ \frac{\partial L}{\partial y} = -\frac{y_{true}}{y} + \frac{1-y_{true}}{1-y} $$

  2. 然后计算 $y$ 对 $z=wx+b$ 的偏导(Sigmoid 函数的导数为 $\sigma'(z)=\sigma(z)(1-\sigma(z))$):
    $$ \frac{\partial y}{\partial z} = y(1-y) $$

  3. 最后计算 $z$ 对 $w$ 的偏导:
    $$ \frac{\partial z}{\partial w} = x $$

通过链式法则,三者相乘得到最终梯度:

$$ \frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial z} \cdot \frac{\partial z}{\partial w} = (y-y_{true})x $$

这个简单例子展示了链式法则在梯度计算中的核心作用。

2. 扩展到多层神经网络

对于多层网络,我们需要从输出层逐层反向计算梯度。以一个 3 层网络(输入层 - 隐藏层 - 输出层)为例:

  • 前向传播过程:
    $$ z^{[1]} = W^{[1]}x + b^{[1]} $$
    $$ a^{[1]} = \sigma(z^{[1]}) $$
    $$ z^{[2]} = W^{[2]}a^{[1]} + b^{[2]} $$
    $$ a^{[2]} = \sigma(z^{[2]}) $$

  • 反向传播梯度计算(以输出层为例):
    $$ \frac{\partial L}{\partial W^{[2]}} = \frac{\partial L}{\partial a^{[2]}} \cdot \frac{\partial a^{[2]}}{\partial z^{[2]}} \cdot \frac{\partial z^{[2]}}{\partial W^{[2]}} $$
    $$ = (a^{[2]} – y) \cdot a^{[1]T} $$

3. Python 完整实现

以下是用 NumPy 实现的完整代码:

import numpy as np

class NeuralNetwork:
    def __init__(self, input_size, hidden_size, output_size):
        # 权重初始化(使用 Xavier 初始化)self.W1 = np.random.randn(hidden_size, input_size) * np.sqrt(1./input_size)
        self.b1 = np.zeros((hidden_size, 1))
        self.W2 = np.random.randn(output_size, hidden_size) * np.sqrt(1./hidden_size)
        self.b2 = np.zeros((output_size, 1))

    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.Z1 = np.dot(self.W1, X) + self.b1
        self.A1 = self.sigmoid(self.Z1)
        self.Z2 = np.dot(self.W2, self.A1) + self.b2
        self.A2 = self.sigmoid(self.Z2)
        return self.A2

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

        # 输出层梯度
        dZ2 = self.A2 - y
        dW2 = (1/m) * np.dot(dZ2, self.A1.T)
        db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True)

        # 隐藏层梯度
        dZ1 = np.dot(self.W2.T, dZ2) * self.sigmoid_derivative(self.Z1)
        dW1 = (1/m) * np.dot(dZ1, X.T)
        db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True)

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

4. 梯度检查

梯度检查是验证反向传播实现是否正确的重要手段:

def gradient_check(nn, X, y, epsilon=1e-7):
    # 计算反向传播得到的梯度
    nn.forward(X)
    nn.backward(X, y)

    # 对每个参数进行数值梯度近似
    for param, grad_name in [(nn.W1, 'dW1'), (nn.b1, 'db1'), (nn.W2, 'dW2'), (nn.b2, 'db2')]:
        param_shape = param.shape
        grad = getattr(nn, grad_name)

        # 对每个元素进行数值梯度计算
        for i in range(min(10, param.size)):  # 检查前 10 个元素
            idx = np.unravel_index(i, param_shape)
            original_value = param[idx]

            # 计算 f(theta + epsilon)
            param[idx] = original_value + epsilon
            cost_plus = np.mean((nn.forward(X) - y)**2)

            # 计算 f(theta - epsilon)
            param[idx] = original_value - epsilon
            cost_minus = np.mean((nn.forward(X) - y)**2)

            # 恢复原值
            param[idx] = original_value

            # 数值梯度
            numeric_grad = (cost_plus - cost_minus)/(2*epsilon)

            # 比较
            backprop_grad = grad[idx]
            diff = abs(numeric_grad - backprop_grad)

            print(f'{grad_name}[{idx}]: numeric {numeric_grad:.6f}, backprop {backprop_grad:.6f}, diff {diff:.6f}')

5. 训练注意事项

  • 学习率选择
  • 从较大的学习率(如 0.1)开始尝试,如果损失震荡则减小
  • 可以尝试学习率衰减策略

  • 梯度消失问题

  • 深层网络中,Sigmoid 函数的导数最大为 0.25,多次连乘会导致梯度指数级减小
  • 表现为浅层网络权重几乎不更新
  • 解决方案:使用 ReLU 等激活函数、残差连接、批量归一化等

  • 权重初始化

  • 避免全零初始化,这会导致对称性问题
  • Xavier 初始化适合 Sigmoid/Tanh
  • He 初始化适合 ReLU

6. 思考与扩展

尝试修改代码实现 ReLU 激活函数及其导数:
1. ReLU 函数:$f(x) = \max(0, x)$
2. 其导数为:$f'(x) = 1$ if $x > 0$ else $0$

需要修改哪些部分?反向传播的计算会受到什么影响?这能解决梯度消失问题吗?

通过本文的学习,你应该已经掌握了 BP 神经网络的核心数学原理和实现方法。下一步可以尝试扩展网络深度、添加正则化项,或实现更复杂的网络结构。

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