BP神经网络拟合实战:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

问题引入:从拟合失败案例说起

刚接触神经网络时,我尝试用 BP 网络拟合一个简单的正弦函数。理论上单隐层网络就能逼近任意函数,但实际训练中常遇到两种典型问题:

BP 神经网络拟合实战:从数学原理到 Python 实现

  • 梯度消失:当网络层数较深时,损失函数对前面层参数的梯度会指数级衰减。例如用 Sigmoid 激活函数时,其导数最大值为 0.25,经过 5 层传播后梯度最多缩小到原来的 0.00098

  • 过拟合:在 MNIST 数据集上,一个 10 层网络在训练集准确率达到 99% 后,测试集准确率反而从 98% 下降到 85%,这就是典型的过拟合现象

数学原理:反向传播的本质

BP 算法的核心是链式求导法则。设损失函数为 $L$,对于第 $l$ 层的参数 $W^l$,其梯度计算为:

$$
\frac{\partial L}{\partial W^l} = \frac{\partial L}{\partial z^{l+1}} \cdot \frac{\partial z^{l+1}}{\partial W^l}
$$

其中 $z^{l+1}=W^l a^l + b^l$,$a^l$ 是第 $l$ 层输出。具体推导过程:

  1. 输出层梯度:
    $$
    \delta^L = \nabla_a L \odot \sigma'(z^L)
    $$
  2. 隐藏层梯度(反向传播):
    $$
    \delta^l = ((W^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)
    $$
  3. 参数梯度:
    $$
    \frac{\partial L}{\partial W^l} = \delta^l (a^{l-1})^T
    $$

Python 实现核心代码

import numpy as np

class NeuralNetwork:
    def __init__(self, layers):
        # He 初始化
        self.weights = [np.random.randn(y, x)*np.sqrt(2/x) 
                        for x,y in zip(layers[:-1], layers[1:])]
        self.biases = [np.zeros((y,1)) for y in layers[1:]]

    def forward(self, x):
        a = x
        for w, b in zip(self.weights, self.biases):
            z = np.dot(w, a) + b
            a = 1/(1+np.exp(-z))  # Sigmoid
        return a

    def backprop(self, x, y):
        # 前向传播缓存中间值
        activations = [x]
        zs = []
        for w, b in zip(self.weights, self.biases):
            z = np.dot(w, activations[-1]) + b
            zs.append(z)
            activations.append(1/(1+np.exp(-z)))

        # 反向传播
        delta = (activations[-1] - y) * activations[-1] * (1-activations[-1])
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w[-1] = np.dot(delta, activations[-2].T)
        nabla_b[-1] = delta

        for l in range(2, len(self.weights)+1):
            delta = np.dot(self.weights[-l+1].T, delta) * \
                    activations[-l] * (1-activations[-l])
            nabla_w[-l] = np.dot(delta, activations[-l-1].T)
            nabla_b[-l] = delta
        return nabla_w, nabla_b

MNIST 实验对比

网络结构 学习率 训练准确率 测试准确率
[784, 30, 10] 0.1 92.3% 91.8%
[784, 100, 10] 0.1 95.7% 94.2%
[784, 30, 10] 0.01 89.1% 88.9%

避坑指南

  1. 梯度裁剪:当梯度范数超过阈值时进行缩放

    max_norm = 5.0
    total_norm = np.sqrt(sum(np.sum(g**2) for g in gradients))
    if total_norm > max_norm:
        gradients = [g*(max_norm/total_norm) for g in gradients]

  2. Batch Normalization:应在激活函数前插入

    FC -> BN -> ReLU  # 正确顺序
    FC -> ReLU -> BN  # 错误顺序

  3. 早停法实现:当验证集误差连续 5 个 epoch 不下降时终止训练

延伸阅读

  • 使用 PyTorch 实现相同功能代码量减少 60%
  • 残差连接 (ResNet) 可以彻底解决梯度消失问题
  • 自适应优化器 (Adam) 通常比手动调参效果更好

通过这次实践,我深刻理解了神经网络的黑箱运作机制。建议初学者一定要亲手实现一遍 BP 算法,这对理解深度学习框架底层原理大有裨益。

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