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

1次阅读
没有评论

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

image.webp

问题引入:为什么我的神经网络不收敛?

刚开始尝试用 BP 神经网络拟合 MNIST 数据集时,我遇到了两个经典问题:

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

  1. 梯度消失:当网络层数增加到 5 层时,模型完全停止学习,训练损失几乎不变。检查梯度发现前几层的权重更新幅度小于 1e-6
  2. 过拟合 :在 3 层网络中使用高学习率(0.1) 时,训练准确率很快达到 98%,但测试集准确率卡在 92%,差距随时间不断拉大

数学原理:反向传播的矩阵视角

设网络第 $l$ 层的权重矩阵为 $W^l$,输入为 $a^{l-1}$,则前向传播可表示为:

$$ z^l = W^l a^{l-1} + b^l $$
$$ a^l = \sigma(z^l) $$

对于交叉熵损失函数 $L$,反向传播时梯度计算遵循链式法则:

$$ \frac{\partial L}{\partial W^l} = \frac{\partial L}{\partial z^l} \cdot \frac{\partial z^l}{\partial W^l} = \delta^l \cdot (a^{l-1})^T $$

其中误差项 $\delta^l$ 的递推公式为:

$$ \delta^{l} = (W^{l+1})^T \delta^{l+1} \odot \sigma'(z^l) $$

这就是梯度消失的根源——当使用 Sigmoid 激活时,$\sigma'(z^l)$ 的最大值仅为 0.25,多层连乘会导致梯度指数级衰减。

Python 实现核心代码

1. 网络初始化(He 初始化)

def initialize_parameters(layer_dims):
    params = {}
    for l in range(1, len(layer_dims)):
        # He 初始化:适应 ReLU 的 sqrt(2/n)缩放
        params['W'+str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1]) * np.sqrt(2/layer_dims[l-1])
        params['b'+str(l)] = np.zeros((layer_dims[l], 1))
    return params

2. 前向传播(含 ReLU 激活)

def forward_prop(X, params):
    caches = []
    A = X
    L = len(params) // 2

    for l in range(1, L):
        A_prev = A
        Z = np.dot(params['W'+str(l)], A_prev) + params['b'+str(l)]
        A = np.maximum(0, Z)  # ReLU
        caches.append((A_prev, Z))

    # 输出层用 Sigmoid
    ZL = np.dot(params['W'+str(L)], A) + params['b'+str(L)]
    AL = 1/(1+np.exp(-ZL))
    caches.append((A, ZL))
    return AL, caches

3. 反向传播(含梯度裁剪)

def backward_prop(AL, Y, caches, clip_threshold=5.0):
    grads = {}
    L = len(caches)
    dZL = AL - Y  # 交叉熵的梯度

    # 梯度裁剪
    dZL = np.clip(dZL, -clip_threshold, clip_threshold)

    for l in reversed(range(L)):
        A_prev, Z = caches[l]
        m = A_prev.shape[1]

        if l == L-1:  # 输出层
            dW = np.dot(dZL, A_prev.T) / m
            db = np.sum(dZL, axis=1, keepdims=True) / m
        else:  # 隐藏层
            dA = np.dot(params['W'+str(l+2)].T, dZ)
            dZ = np.array(dA, copy=True)
            dZ[Z <= 0] = 0  # ReLU 梯度
            dW = np.dot(dZ, A_prev.T) / m
            db = np.sum(dZ, axis=1, keepdims=True) / m

        grads['dW'+str(l+1)] = dW
        grads['db'+str(l+1)] = db
    return grads

实验对比:MNIST 数据集表现

配置 训练准确率 测试准确率 备注
3 层网络 lr=0.1 98.2% 92.1% 明显过拟合
5 层网络 lr=0.01 89.3% 88.7% 梯度消失
3 层网络 +Dropout 96.5% 94.8% 缓解过拟合
5 层网络 +BN 97.1% 95.3% 解决梯度消失

避坑指南

  1. 梯度裁剪阈值:一般设置在 3.0-10.0 之间,可通过观察梯度直方图调整
  2. BN 层位置:应放在激活函数前,即:全连接 → BN → ReLU
  3. 早停法实现:当验证集损失连续 5 个 epoch 不下降时终止训练

延伸阅读

  1. PyTorch 的实现会更简洁,主要区别在于:
  2. 自动求导机制无需手动实现反向传播
  3. nn.Module 提供了更灵活的层组合方式
  4. 进阶技巧:
  5. 学习率预热(Warmup)
  6. SWA(随机权重平均)
  7. 自监督预训练

通过这个纯 NumPy 实现,相信你对神经网络的核心机制有了更直观的理解。接下来可以用 PyTorch/TensorFlow 等框架来验证这些概念,会发现它们本质上在做相同的事情,只是包装得更易用了。

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