共计 2805 个字符,预计需要花费 8 分钟才能阅读完成。
为什么需要反向传播?
当我们训练神经网络时,核心目标是找到一组参数(权重和偏置),使得网络的预测输出尽可能接近真实值。这个过程通常通过最小化损失函数来实现,而梯度下降算法就是调整这些参数的经典方法。

但是,神经网络往往包含大量参数,手动计算每个参数的梯度几乎是不可能的任务。这就是反向传播算法发挥作用的地方——它提供了一种高效计算这些梯度的方法。
链式法则:反向传播的数学基础
反向传播的核心是链式法则,这是微积分中的一个基本原理。让我们从一个简单的例子开始理解。
假设有一个复合函数:
$$ 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 是真实值。
反向传播需要计算损失函数对各参数的梯度。让我们一步步推导:
-
首先计算损失对激活输出的梯度:
$$ \frac{\partial L}{\partial a} = a – y $$ -
然后计算激活输出对加权和的梯度:
$$ \frac{\partial a}{\partial z} = \sigma(z)(1-\sigma(z)) = a(1-a) $$ -
接着计算加权和对各权重的梯度:
$$ \frac{\partial z}{\partial w_1} = x_1 $$
$$ \frac{\partial z}{\partial w_2} = x_2 $$
$$ \frac{\partial z}{\partial b} = 1 $$ -
最后,应用链式法则组合这些梯度:
$$ \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}")
常见错误与避坑指南
- 矩阵维度不匹配
- 问题:在多层网络中,梯度计算的维度容易出错
-
解决:始终检查矩阵乘法的维度关系,必要时转置
-
忘记激活函数的导数
- 问题:只计算了损失对加权和的梯度,漏掉了激活函数
-
解决:明确写出每一步的链式法则,确保不跳过任何环节
-
批量处理时梯度累加错误
- 问题:在批处理中,梯度应该是样本梯度的平均值而非总和
- 解决:注意除以 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}")
通过这样的验证,可以确认我们的手动实现是否正确。
总结
反向传播是深度学习训练的核心算法,而链式法则是其数学基础。通过从简单的神经元入手,逐步理解梯度是如何在网络中反向流动的,可以帮助我们更好地设计和调试神经网络。
记住,反向传播不是魔法,而是精心设计的梯度计算流程。当遇到问题时,回到基本原理,一步步推导,往往能找到解决方案。
