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

1次阅读
没有评论

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

image.webp

为什么需要反向传播?

假设我们有一个最简单的神经网络,只有一个神经元,输入是 $x$,权重是 $w$,偏置是 $b$,激活函数是 sigmoid。我们的目标是让这个神经元的输出尽可能接近真实的标签 $y$。

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

训练神经网络的核心就是调整 $w$ 和 $b$,使得损失函数 $L$ 最小化。那么问题来了:我们怎么知道 $w$ 和 $b$ 应该往哪个方向调整呢?这就是梯度下降法要解决的问题——计算损失函数对参数的梯度。

链式法则:反向传播的核心

反向传播算法的核心就是链式法则。让我们从一个简单的例子开始理解。

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

那么 $z$ 对 $x$ 的导数就是:
$$ \frac{dz}{dx} = \frac{dz}{dg} \cdot \frac{dg}{dx} $$

这就是链式法则的基本形式。在神经网络中,我们需要计算损失函数对每一层参数的梯度,而这个过程就是层层应用链式法则。

单个神经元的前向传播与反向传播

让我们用一个具体的例子来说明。考虑一个简单的神经元:

  1. 输入 $x$ 乘以权重 $w$,加上偏置 $b$,得到 $z = w \cdot x + b$
  2. 通过 sigmoid 激活函数,得到 $a = \sigma(z)$
  3. 计算损失函数,比如平方误差 $L = \frac{1}{2}(a – y)^2$

现在我们需要计算 $L$ 对 $w$ 和 $b$ 的梯度。

反向传播的数学推导

让我们一步步计算:

  1. 首先计算 $L$ 对 $a$ 的梯度:
    $$ \frac{\partial L}{\partial a} = a – y $$

  2. 然后计算 $a$ 对 $z$ 的梯度(sigmoid 函数的导数):
    $$ \frac{\partial a}{\partial z} = \sigma(z)(1 – \sigma(z)) = a(1 – a) $$

  3. 接着计算 $z$ 对 $w$ 和 $b$ 的梯度:
    $$ \frac{\partial z}{\partial w} = x $$
    $$ \frac{\partial z}{\partial b} = 1 $$

  4. 最后,应用链式法则:
    $$ \frac{\partial L}{\partial w} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial w} = (a – y) \cdot a(1 – a) \cdot x $$
    $$ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial b} = (a – y) \cdot a(1 – a) $$

Python 实现

现在让我们用 Python 来实现这个计算过程。我们将定义一个简单的计算图节点类,然后实现前向传播和反向传播。

import numpy as np

class Node:
    """计算图节点类"""
    def __init__(self, value=None):
        self.value = value      # 节点的值
        self.grad = 0           # 节点的梯度
        self.children = []      # 子节点
        self.op = None          # 操作类型

    def forward(self):
        """前向传播"""
        pass

    def backward(self, grad=None):
        """反向传播"""
        if grad is not None:
            self.grad = grad
        else:
            self.grad = 1.0  # 默认梯度为 1(输出节点)# 根据操作类型计算子节点的梯度
        if self.op == 'add':
            self.children[0].backward(self.grad)
            self.children[1].backward(self.grad)
        elif self.op == 'mul':
            self.children[0].backward(self.grad * self.children[1].value)
            self.children[1].backward(self.grad * self.children[0].value)
        elif self.op == 'sigmoid':
            sig = self.value
            self.children[0].backward(self.grad * sig * (1 - sig))

# 定义 sigmoid 函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 前向传播计算
def forward(x, w, b):
    # 创建节点
    x_node = Node(x)
    w_node = Node(w)
    b_node = Node(b)

    # 计算 z = w*x + b
    mul_node = Node()
    mul_node.op = 'mul'
    mul_node.children = [w_node, x_node]
    mul_node.value = w * x

    add_node = Node()
    add_node.op = 'add'
    add_node.children = [mul_node, b_node]
    add_node.value = w * x + b

    # 计算 a = sigmoid(z)
    a_node = Node()
    a_node.op = 'sigmoid'
    a_node.children = [add_node]
    a_node.value = sigmoid(w * x + b)

    return a_node, add_node, mul_node, w_node, b_node, x_node

# 反向传播计算
def backward(a_node, y):
    # 计算损失函数 L = 0.5*(a - y)^2
    L = 0.5 * (a_node.value - y)**2

    # 计算 dL/da = a - y
    dL_da = a_node.value - y

    # 开始反向传播
    a_node.backward(dL_da)

    # 获取各个参数的梯度
    return a_node.children[0].grad, a_node.children[0].children[1].grad, a_node.children[0].children[0].children[0].grad

# 测试
x = 2.0
w = 0.5
b = 1.0
y = 0.8  # 真实值

a_node, _, _, w_node, b_node, x_node = forward(x, w, b)
dL_dz, dL_db, dL_dw = backward(a_node, y)

print(f"预测值: {a_node.value:.4f}")
print(f"w 的梯度: {dL_dw:.4f}")
print(f"b 的梯度: {dL_db:.4f}")

常见错误与避坑指南

  1. 矩阵维度问题 :
  2. 初学者经常混淆矩阵乘法的维度。记住:如果 $W$ 是 $m \times n$ 矩阵,$X$ 必须是 $n \times k$ 矩阵,结果才是 $m \times k$ 矩阵。
  3. 在反向传播时,梯度的维度必须和参数的维度一致。

  4. 激活函数导数 :

  5. 每个激活函数的导数不同,要特别注意。比如 ReLU 在 $x=0$ 时的导数通常是 0 或 1(根据实现方式)。
  6. sigmoid 函数的导数可以用 $\sigma(z)(1-\sigma(z))$ 表示,这样计算效率更高。

  7. 初始化问题 :

  8. 参数初始化不当可能导致梯度消失或爆炸。比如 sigmoid 函数的输入太大或太小都会导致梯度接近于 0。
  9. 常用的初始化方法有 Xavier 初始化、He 初始化等。

扩展到全连接层

理解了单个神经元后,扩展到全连接层就相对容易了。全连接层其实就是多个神经元的组合,每个神经元都有自己的一组权重和偏置。在反向传播时,我们需要计算损失函数对每个权重和偏置的梯度。

关键点是:

  1. 前向传播时,输入 $x$ 与权重矩阵 $W$ 相乘,加上偏置 $b$,得到 $z = Wx + b$
  2. 反向传播时,$\frac{\partial L}{\partial W} = \frac{\partial L}{\partial z} \cdot x^T$
  3. $\frac{\partial L}{\partial b} = \frac{\partial L}{\partial z}$(因为 $\frac{\partial z}{\partial b} = I$)

验证与总结

为了验证我们的实现是否正确,可以用自动微分库(如 PyTorch 或 TensorFlow)计算相同的例子,看看结果是否一致。

理解反向传播和链式法则是掌握深度学习的关键。虽然现代深度学习框架帮我们自动完成了这些计算,但了解背后的原理能帮助我们更好地调试模型、理解模型的行为。

希望这篇文章能帮助你理解反向传播和链式法则的原理。下一步,你可以尝试实现一个完整的全连接神经网络,并观察不同超参数对训练过程的影响。

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