共计 3449 个字符,预计需要花费 9 分钟才能阅读完成。
为什么需要反向传播?
假设我们有一个最简单的神经网络,只有一个神经元,输入是 $x$,权重是 $w$,偏置是 $b$,激活函数是 sigmoid。我们的目标是让这个神经元的输出尽可能接近真实的标签 $y$。

训练神经网络的核心就是调整 $w$ 和 $b$,使得损失函数 $L$ 最小化。那么问题来了:我们怎么知道 $w$ 和 $b$ 应该往哪个方向调整呢?这就是梯度下降法要解决的问题——计算损失函数对参数的梯度。
链式法则:反向传播的核心
反向传播算法的核心就是链式法则。让我们从一个简单的例子开始理解。
假设有一个复合函数:
$$ z = f(g(x)) $$
那么 $z$ 对 $x$ 的导数就是:
$$ \frac{dz}{dx} = \frac{dz}{dg} \cdot \frac{dg}{dx} $$
这就是链式法则的基本形式。在神经网络中,我们需要计算损失函数对每一层参数的梯度,而这个过程就是层层应用链式法则。
单个神经元的前向传播与反向传播
让我们用一个具体的例子来说明。考虑一个简单的神经元:
- 输入 $x$ 乘以权重 $w$,加上偏置 $b$,得到 $z = w \cdot x + b$
- 通过 sigmoid 激活函数,得到 $a = \sigma(z)$
- 计算损失函数,比如平方误差 $L = \frac{1}{2}(a – y)^2$
现在我们需要计算 $L$ 对 $w$ 和 $b$ 的梯度。
反向传播的数学推导
让我们一步步计算:
-
首先计算 $L$ 对 $a$ 的梯度:
$$ \frac{\partial L}{\partial a} = a – y $$ -
然后计算 $a$ 对 $z$ 的梯度(sigmoid 函数的导数):
$$ \frac{\partial a}{\partial z} = \sigma(z)(1 – \sigma(z)) = a(1 – a) $$ -
接着计算 $z$ 对 $w$ 和 $b$ 的梯度:
$$ \frac{\partial z}{\partial w} = x $$
$$ \frac{\partial z}{\partial b} = 1 $$ -
最后,应用链式法则:
$$ \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}")
常见错误与避坑指南
- 矩阵维度问题 :
- 初学者经常混淆矩阵乘法的维度。记住:如果 $W$ 是 $m \times n$ 矩阵,$X$ 必须是 $n \times k$ 矩阵,结果才是 $m \times k$ 矩阵。
-
在反向传播时,梯度的维度必须和参数的维度一致。
-
激活函数导数 :
- 每个激活函数的导数不同,要特别注意。比如 ReLU 在 $x=0$ 时的导数通常是 0 或 1(根据实现方式)。
-
sigmoid 函数的导数可以用 $\sigma(z)(1-\sigma(z))$ 表示,这样计算效率更高。
-
初始化问题 :
- 参数初始化不当可能导致梯度消失或爆炸。比如 sigmoid 函数的输入太大或太小都会导致梯度接近于 0。
- 常用的初始化方法有 Xavier 初始化、He 初始化等。
扩展到全连接层
理解了单个神经元后,扩展到全连接层就相对容易了。全连接层其实就是多个神经元的组合,每个神经元都有自己的一组权重和偏置。在反向传播时,我们需要计算损失函数对每个权重和偏置的梯度。
关键点是:
- 前向传播时,输入 $x$ 与权重矩阵 $W$ 相乘,加上偏置 $b$,得到 $z = Wx + b$
- 反向传播时,$\frac{\partial L}{\partial W} = \frac{\partial L}{\partial z} \cdot x^T$
- $\frac{\partial L}{\partial b} = \frac{\partial L}{\partial z}$(因为 $\frac{\partial z}{\partial b} = I$)
验证与总结
为了验证我们的实现是否正确,可以用自动微分库(如 PyTorch 或 TensorFlow)计算相同的例子,看看结果是否一致。
理解反向传播和链式法则是掌握深度学习的关键。虽然现代深度学习框架帮我们自动完成了这些计算,但了解背后的原理能帮助我们更好地调试模型、理解模型的行为。
希望这篇文章能帮助你理解反向传播和链式法则的原理。下一步,你可以尝试实现一个完整的全连接神经网络,并观察不同超参数对训练过程的影响。
