深度学习入门:从零理解bp反向传播链式法则的实现原理

1次阅读
没有评论

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

image.webp

为什么需要反向传播?

当我们训练神经网络时,核心目标是找到一组参数(权重和偏置),使得网络的预测输出尽可能接近真实值。这个过程通常通过最小化损失函数来实现,而梯度下降算法就是调整这些参数的经典方法。

深度学习入门:从零理解 bp 反向传播链式法则的实现原理

但是,神经网络往往包含大量参数,手动计算每个参数的梯度几乎是不可能的任务。这就是反向传播算法发挥作用的地方——它提供了一种高效计算这些梯度的方法。

链式法则:反向传播的数学基础

反向传播的核心是链式法则,这是微积分中的一个基本原理。让我们从一个简单的例子开始理解。

假设有一个复合函数:
$$ y = f(g(x)) $$

根据链式法则,y 对 x 的导数为:
$$ \frac{dy}{dx} = \frac{dy}{dg} \cdot \frac{dg}{dx} $$

在神经网络中,这个原理被扩展到更复杂的计算图中。每个节点的梯度都可以表示为下游梯度与本节点局部梯度的乘积。

单个神经元的计算示例

考虑一个简单的神经元,它有两个输入 x₁和 x₂,对应的权重为 w₁和 w₂,偏置为 b,使用 sigmoid 激活函数。

前向传播过程:
1. 计算加权和:
$$ z = w_1x_1 + w_2x_2 + b $$
2. 应用激活函数:
$$ a = \sigma(z) = \frac{1}{1+e^{-z}} $$

现在,假设我们使用平方误差作为损失函数:
$$ L = \frac{1}{2}(a – y)^2 $$
其中 y 是真实值。

反向传播需要计算损失函数对各参数的梯度。让我们一步步推导:

  1. 首先计算损失对激活输出的梯度:
    $$ \frac{\partial L}{\partial a} = a – y $$

  2. 然后计算激活输出对加权和的梯度:
    $$ \frac{\partial a}{\partial z} = \sigma(z)(1-\sigma(z)) = a(1-a) $$

  3. 接着计算加权和对各权重的梯度:
    $$ \frac{\partial z}{\partial w_1} = x_1 $$
    $$ \frac{\partial z}{\partial w_2} = x_2 $$
    $$ \frac{\partial z}{\partial b} = 1 $$

  4. 最后,应用链式法则组合这些梯度:
    $$ \frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial w_1} $$
    $$ \frac{\partial L}{\partial w_2} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial w_2} $$
    $$ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial b} $$

Python 实现

下面我们用 Python 实现这个简单的神经元,包括前向和反向传播过程:

import numpy as np

class Neuron:
    def __init__(self, input_size):
        # 初始化权重和偏置
        self.weights = np.random.randn(input_size)
        self.bias = np.random.randn()

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

    def forward(self, inputs):
        # 前向传播
        self.inputs = inputs
        self.z = np.dot(self.weights, inputs) + self.bias
        self.a = self.sigmoid(self.z)
        return self.a

    def backward(self, dL_da):
        # 反向传播
        da_dz = self.a * (1 - self.a)  # sigmoid 导数
        dL_dz = dL_da * da_dz

        # 计算各参数的梯度
        self.dw = dL_dz * self.inputs
        self.db = dL_dz

        # 返回对输入的梯度(用于更深的网络)dz_dinputs = self.weights
        dL_dinputs = dL_dz * dz_dinputs
        return dL_dinputs

# 示例使用
neuron = Neuron(2)
x = np.array([0.5, -0.3])
y_true = 1

# 前向传播
y_pred = neuron.forward(x)

# 计算损失对输出的梯度
dL_da = y_pred - y_true

# 反向传播
dL_dinputs = neuron.backward(dL_da)

print(f"权重梯度: {neuron.dw}")
print(f"偏置梯度: {neuron.db}")

常见错误与避坑指南

  1. 矩阵维度不匹配
  2. 问题:在多层网络中,梯度计算的维度容易出错
  3. 解决:始终检查矩阵乘法的维度关系,必要时转置

  4. 忘记激活函数的导数

  5. 问题:只计算了损失对加权和的梯度,漏掉了激活函数
  6. 解决:明确写出每一步的链式法则,确保不跳过任何环节

  7. 批量处理时梯度累加错误

  8. 问题:在批处理中,梯度应该是样本梯度的平均值而非总和
  9. 解决:注意除以 batch_size

扩展思考

理解了单个神经元后,可以尝试扩展到全连接层。主要区别在于:
1. 权重矩阵代替了权重向量
2. 需要考虑多个神经元的梯度聚合
3. 矩阵运算的维度关系更复杂

建议尝试实现一个简单的两层网络,加深对反向传播的理解。

验证正确性

为了确保我们的实现正确,可以与自动微分库(如 PyTorch)的结果对比:

import torch

# 使用相同的初始值
w = torch.tensor(neuron.weights, requires_grad=True)
b = torch.tensor(neuron.bias, requires_grad=True)
x_torch = torch.tensor(x)
y_torch = torch.tensor(y_true)

# PyTorch 计算
z_torch = torch.dot(w, x_torch) + b
a_torch = torch.sigmoid(z_torch)
loss = 0.5 * (a_torch - y_torch)**2
loss.backward()

print(f"PyTorch 权重梯度: {w.grad}")
print(f"PyTorch 偏置梯度: {b.grad}")

通过这样的验证,可以确认我们的手动实现是否正确。

总结

反向传播是深度学习训练的核心算法,而链式法则是其数学基础。通过从简单的神经元入手,逐步理解梯度是如何在网络中反向流动的,可以帮助我们更好地设计和调试神经网络。

记住,反向传播不是魔法,而是精心设计的梯度计算流程。当遇到问题时,回到基本原理,一步步推导,往往能找到解决方案。

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