深度学习入门:从零实现BP算法及其关键细节解析

1次阅读
没有评论

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

image.webp

背景:为什么需要 BP 算法?

神经网络训练的本质是通过调整权重参数,使得模型输出尽可能接近真实值。这个过程需要两个关键步骤:

深度学习入门:从零实现 BP 算法及其关键细节解析

  1. 前向传播 :输入数据经过层层计算得到预测值
  2. 反向传播 :根据预测误差,从输出层反向计算各层的参数更新量

BP 算法正是解决第二个问题的核心方法,它通过链式法则高效计算所有参数的梯度。

数学原理:链式法则的直观理解

想象一个三明治的制作流程:

  1. 买面包 → 涂抹黄油 → 加配料 → 成品
  2. 如果成品太咸,我们需要知道:
  3. 咸度多少来自配料?
  4. 多少来自黄油?
  5. 面包的选择有没有影响?

链式法则就是这种 ” 责任追溯 ” 的数学表达。在神经网络中,我们用计算图表示这种依赖关系:

 输入 X → 隐藏层 H → 输出 Y → 损失 L 

计算梯度时的核心公式:

$$\frac{\partial L}{\partial W} = \frac{\partial L}{\partial Y} \cdot \frac{\partial Y}{\partial H} \cdot \frac{\partial H}{\partial W}$$

关键痛点:梯度消失问题

当使用 Sigmoid 激活函数时,由于其导数最大值为 0.25,多层连乘会导致:

$$\frac{\partial L}{\partial W_{early}} ≈ 0.25^n \cdot \frac{\partial L}{\partial W_{late}}$$

这意味着:

  • 浅层网络的权重几乎得不到有效更新
  • 网络越深,问题越严重
  • 表现为训练早期 loss 下降缓慢甚至停滞

代码实现:NumPy 版 BP 算法

import numpy as np
from matplotlib import pyplot as plt

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

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

    def forward(self, X):
        self.hidden = self.sigmoid(X @ self.W1 + self.b1)  # (N, hidden_size)
        self.output = self.sigmoid(self.hidden @ self.W2 + self.b2)  # (N, output_size)
        return self.output

    def backward(self, X, y, lr=0.1):
        # 输出层梯度
        d_output = (self.output - y) * self.output * (1 - self.output)  # (N, output_size)

        # 隐藏层梯度(注意梯度消失!)d_hidden = (d_output @ self.W2.T) * self.hidden * (1 - self.hidden)  # (N, hidden_size)

        # 参数更新
        self.W2 -= lr * self.hidden.T @ d_output  # (hidden_size, output_size)
        self.b2 -= lr * d_output.mean(axis=0)
        self.W1 -= lr * X.T @ d_hidden  # (input_size, hidden_size)
        self.b1 -= lr * d_hidden.mean(axis=0)

# 示例训练流程
net = TwoLayerNet(2, 4, 1)
loss_history = []

for epoch in range(1000):
    # 假设 X,y 是训练数据
    output = net.forward(X)
    loss = ((output - y)**2).mean()
    net.backward(X, y, lr=0.1/(1+epoch*0.01))  # 学习率衰减
    loss_history.append(loss)

plt.plot(loss_history)
plt.xlabel('Epoch')
plt.ylabel('Loss')

优化技巧实践

Xavier 初始化

通过调整初始化范围,使各层输出的方差保持一致:

# 对于第 l 层,初始化标准差为 1/sqrt(n_{l-1})
self.W = np.random.randn(input_dim, output_dim) * np.sqrt(1/input_dim)

学习率衰减

随着训练进行逐渐减小学习步伐:

initial_lr = 0.1
def get_lr(epoch):
    return initial_lr / (1 + epoch*0.01)

避坑指南:梯度校验

手动实现的梯度计算容易出错,可以通过数值方法验证:

def grad_check(net, X, y, param='W1', epsilon=1e-7):
    original = getattr(net, param).copy()
    grad = np.zeros_like(original)

    # 数值计算梯度
    for i in range(original.shape[0]):
        for j in range(original.shape[1]):
            # 正向扰动
            net.W1[i,j] += epsilon
            loss_plus = ((net.forward(X) - y)**2).mean()

            # 负向扰动
            net.W1[i,j] -= 2*epsilon
            loss_minus = ((net.forward(X) - y)**2).mean()

            # 中心差分
            grad[i,j] = (loss_plus - loss_minus) / (2*epsilon)
            net.W1[i,j] = original[i,j]  # 恢复原值

    # 与 BP 结果对比
    net.forward(X)
    net.backward(X, y, lr=0)
    bp_grad = getattr(net, param)

    return np.allclose(grad, bp_grad, atol=1e-5)

扩展思考

自动微分 vs 手动实现

  • 手动实现 :
  • 优点:深入理解算法细节
  • 缺点:易出错,修改网络结构需要重写

  • 自动微分(如 PyTorch):

  • 优点:自动计算梯度,支持动态计算图
  • 缺点:隐藏实现细节,可能成为 ” 黑箱 ”

用 ReLU 解决梯度消失

尝试将代码中的 Sigmoid 替换为:

def relu(x):
    return np.maximum(0, x)

观察训练速度和最终效果的变化。ReLU 的梯度在正区间恒为 1,避免了连乘导致的梯度衰减。

结语

实现 BP 算法就像学习骑自行车——刚开始需要关注每一个踏板动作,但掌握原理后,你可以更专注于路线规划(网络结构设计)。建议读者:

  1. 先用这个小例子理解每个变量的维度变化
  2. 尝试增加网络深度观察梯度变化
  3. 用不同激活函数比较训练效果

当你能直观地 ” 看到 ” 梯度在网络中的流动时,就真正掌握了深度学习的核心引擎。

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