让我们通过一个具体实例来演示反向传播 (backpropagation)算法。如前所述,训练神经网络 (neural network)需要调整其权重 (weight)和偏置 (bias)以最小化损失函数 (loss function)。反向传播提供了一种高效方法来计算损失函数相对于网络中每个参数 (parameter)的梯度,这正是梯度下降 (gradient descent)进行这些调整所必需的。
我们将使用一个非常简单的网络:一个输入神经元、一个包含两个神经元的隐藏层和一个输出神经元。这个网络足够小,可以手动追踪所有计算。
1. 网络设置与前向传播
让我们定义我们的简单网络:
- 输入: x=0.5
- 隐藏层(2个神经元): Sigmoid 激活函数 (activation function)。
- 输出层(1个神经元): Sigmoid 激活函数。
- 权重 (weight)和偏置 (bias)(随机初始化):
- 输入到隐藏层: w1=0.1, w2=0.2
- 隐藏层偏置: bh1=0.1, bh2=0.1
- 隐藏层到输出层: w3=0.3, w4=0.4
- 输出层偏置: bo=0.1
- 目标输出: y=1.0
- 损失函数 (loss function): 均方误差 (MSE): L=21(y^−y)2。(我们使用 21 是为了方便,因为它的导数会抵消掉指数 2)。
- 激活函数: Sigmoid: σ(z)=1+e−z1。它的导数是 σ′(z)=σ(z)(1−σ(z))。
让我们逐步执行前向传播计算:
a. 输入到隐藏层:
- 隐藏神经元 1 的加权和: zh1=w1⋅x+bh1=(0.1⋅0.5)+0.1=0.05+0.1=0.15
- 隐藏神经元 1 的激活值: h1=σ(zh1)=σ(0.15)≈0.5374
- 隐藏神经元 2 的加权和: zh2=w2⋅x+bh2=(0.2⋅0.5)+0.1=0.1+0.1=0.20
- 隐藏神经元 2 的激活值: h2=σ(zh2)=σ(0.20)≈0.5498
b. 隐藏层到输出层:
- 输出神经元的加权和: zo=w3⋅h1+w4⋅h2+bo=(0.3⋅0.5374)+(0.4⋅0.5498)+0.1
zo≈0.1612+0.2199+0.1=0.4811
- 最终预测(输出激活值): y^=σ(zo)=σ(0.4811)≈0.6180
c. 计算损失:
- 损失: L=21(y^−y)2=21(0.6180−1.0)2=21(−0.3820)2≈21(0.1459)≈0.0730
我们的网络目前预测值为 0.6180,这与目标值 1.0 相差甚远,导致损失为 0.0730。现在,让我们使用反向传播 (backpropagation)来确定如何调整权重和偏置以减小此损失。
2. 反向传播 (backpropagation):计算梯度
主要思路是利用微积分中的链式法则来计算每个权重 (weight) w 的 ∂w∂L(偏置 (bias) b 同理)。我们从末端(损失)开始,反向进行计算。
a. 输出层权重 (w3,w4) 和偏置 (bo) 的梯度
我们需要 ∂w3∂L、∂w4∂L 和 ∂bo∂L。让我们使用链式法则分解 ∂w3∂L:
∂w3∂L=∂y^∂L⋅∂zo∂y^⋅∂w3∂zo
让我们计算每个部分:
-
∂y^∂L: 损失相对于预测值的导数。
L=21(y^−y)2⟹∂y^∂L=(y^−y)=0.6180−1.0=−0.3820
-
∂zo∂y^: 输出激活函数 (activation function)相对于其输入 (zo) 的导数。由于 y^=σ(zo),导数是 σ′(zo)=σ(zo)(1−σ(zo))。
∂zo∂y^=y^(1−y^)=0.6180⋅(1−0.6180)=0.6180⋅0.3820≈0.2361
-
∂w3∂zo: 输出加权和相对于 w3 的导数。
zo=w3h1+w4h2+bo⟹∂w3∂zo=h1≈0.5374
现在,将它们组合起来:
∂w3∂L=(−0.3820)⋅(0.2361)⋅(0.5374)≈−0.0485
w4 同理:唯一的区别是链式法则中的最后一项。
∂w4∂zo=h2≈0.5498
∂w4∂L=∂y^∂L⋅∂zo∂y^⋅∂w4∂zo=(−0.3820)⋅(0.2361)⋅(0.5498)≈−0.0496
对于输出偏置 bo:
∂bo∂zo=1
∂bo∂L=∂y^∂L⋅∂zo∂y^⋅∂bo∂zo=(−0.3820)⋅(0.2361)⋅(1)≈−0.0902
通常,前两项会合并为输出层的单个“delta”项:δo=∂y^∂L⋅∂zo∂y^≈−0.0902。这样,梯度就简单地表示为 ∂w3∂L=δo⋅h1、∂w4∂L=δo⋅h2 和 ∂bo∂L=δo。
b. 隐藏层权重 (w1,w2) 和偏置 (bh1,bh2) 的梯度
现在我们需要将误差反向传播到隐藏层。我们需要 ∂w1∂L、∂w2∂L、∂bh1∂L 和 ∂bh2∂L。让我们专注于 w1。
链式法则的路径更长:
∂w1∂L=∂y^∂L⋅∂zo∂y^⋅∂h1∂zo⋅∂zh1∂h1⋅∂w1∂zh1
我们可以重用 δo=∂y^∂L⋅∂zo∂y^≈−0.0902。
让我们计算新部分:
-
∂h1∂zo: 输出加权和相对于第一个隐藏神经元激活的导数。
zo=w3h1+w4h2+bo⟹∂h1∂zo=w3=0.3
-
∂zh1∂h1: 第一个隐藏神经元激活相对于其输入的导数。h1=σ(zh1),因此导数是 σ′(zh1)=h1(1−h1)。
∂zh1∂h1=0.5374⋅(1−0.5374)=0.5374⋅0.4626≈0.2486
-
∂w1∂zh1: 第一个隐藏神经元加权和相对于 w1 的导数。
zh1=w1x+bh1⟹∂w1∂zh1=x=0.5
现在,将所有项组合起来得到 ∂w1∂L:
∂w1∂L=(δo)⋅(∂h1∂zo)⋅(∂zh1∂h1)⋅(∂w1∂zh1)
∂w1∂L=(−0.0902)⋅(0.3)⋅(0.2486)⋅(0.5)≈−0.00337
w2 同理:误差贡献流经 h2。
∂w2∂L=∂y^∂L⋅∂zo∂y^⋅∂h2∂zo⋅∂zh2∂h2⋅∂w2∂zh2
- ∂h2∂zo=w4=0.4
- ∂zh2∂h2=h2(1−h2)=0.5498⋅(1−0.5498)=0.5498⋅0.4502≈0.2475
- ∂w2∂zh2=x=0.5
组合它们:
∂w2∂L=(δo)⋅(∂h2∂zo)⋅(∂zh2∂h2)⋅(∂w2∂zh2)
∂w2∂L=(−0.0902)⋅(0.4)⋅(0.2475)⋅(0.5)≈−0.00446
对于隐藏层偏置 bh1 和 bh2:链式法则的最后部分变为 ∂bh1∂zh1=1 和 ∂bh2∂zh2=1。
∂bh1∂L=(δo)⋅(∂h1∂zo)⋅(∂zh1∂h1)⋅(1)
∂bh1∂L=(−0.0902)⋅(0.3)⋅(0.2486)⋅(1)≈−0.00673
∂bh2∂L=(δo)⋅(∂h2∂zo)⋅(∂zh2∂h2)⋅(1)
∂bh2∂L=(−0.0902)⋅(0.4)⋅(0.2475)⋅(1)≈−0.00891
我们也可以为隐藏层定义 delta 项:δh1=δo⋅∂h1∂zo⋅∂zh1∂h1 和 δh2=δo⋅∂h2∂zo⋅∂zh2∂h2。那么梯度就是 ∂w1∂L=δh1⋅x、∂w2∂L=δh2⋅x、∂bh1∂L=δh1 和 ∂bh2∂L=δh2。
3. 流程图示
我们可以使用计算图来表示这些计算。节点代表值(输入、权重 (weight)、中间结果、损失),边代表操作。反向传播 (backpropagation)通过对其出边流入的梯度求和来计算每个节点的梯度。
一个计算图,显示了我们简单网络的前向传播计算。反向传播通过从损失(L)开始反向遍历此图来计算梯度。实心箭头表示前向计算流;在反向传播期间,梯度沿这些路径的反方向流动。
4. 权重 (weight)更新
梯度计算完成后,我们可以使用梯度下降 (gradient descent)规则更新权重和偏置 (bias)。假设学习率 η=0.1。
- w1new=w1−η∂w1∂L=0.1−(0.1⋅−0.00337)≈0.1003
- w2new=w2−η∂w2∂L=0.2−(0.1⋅−0.00446)≈0.2004
- bh1new=bh1−η∂bh1∂L=0.1−(0.1⋅−0.00673)≈0.1007
- bh2new=bh2−η∂bh2∂L=0.1−(0.1⋅−0.00891)≈0.1009
- w3new=w3−η∂w3∂L=0.3−(0.1⋅−0.0485)≈0.3049
- w4new=w4−η∂w4∂L=0.4−(0.1⋅−0.0496)≈0.4050
- bonew=bo−η∂bo∂L=0.1−(0.1⋅−0.0902)≈0.1090
仅一步之后,参数 (parameter)已在预期能减少损失的方向上进行了微调 (fine-tuning),以便在下一次使用相同输入的前向传播中降低损失。
5. PyTorch 中的反向传播 (backpropagation)
手动计算这些梯度具有指导意义,但对于大型网络来说很快就会变得不切实际。像 PyTorch 这样的深度学习 (deep learning)框架使用自动微分(autograd)来自动化此过程。以下是设置相同计算的方法:
import torch
# --- 设置 ---
# 使用 FloatTensor 进行计算
dtype = torch.float
# 对于这个简单例子,使用 CPU
device = torch.device("cpu")
# 定义输入、目标和参数的张量
# requires_grad=True 告诉 PyTorch 跟踪操作以进行梯度计算
x = torch.tensor([[0.5]], device=device, dtype=dtype)
y = torch.tensor([[1.0]], device=device, dtype=dtype)
w1 = torch.tensor([[0.1]], device=device, dtype=dtype, requires_grad=True)
w2 = torch.tensor([[0.2]], device=device, dtype=dtype, requires_grad=True)
bh1 = torch.tensor([[0.1]], device=device, dtype=dtype, requires_grad=True)
bh2 = torch.tensor([[0.1]], device=device, dtype=dtype, requires_grad=True)
# 稍后将隐藏层权重和偏置组合用于矩阵乘法
# 对于这个简单例子,我们像手动计算一样保持它们分离
w3 = torch.tensor([[0.3]], device=device, dtype=dtype, requires_grad=True)
w4 = torch.tensor([[0.4]], device=device, dtype=dtype, requires_grad=True)
bo = torch.tensor([[0.1]], device=device, dtype=dtype, requires_grad=True)
# --- 前向传播 ---
z_h1 = x @ w1.t() + bh1 # 使用 @ 进行矩阵乘法(尽管这里是 1x1)
h1 = torch.sigmoid(z_h1)
z_h2 = x @ w2.t() + bh2
h2 = torch.sigmoid(z_h2)
# 组合隐藏层激活(形成隐藏层输出向量)
# h_combined = torch.cat((h1, h2), dim=1) # 需要 w_ho 作为矩阵 [2,1]
# 为了直接比较,手动计算 z_o
z_o = h1 @ w3.t() + h2 @ w4.t() + bo
y_hat = torch.sigmoid(z_o)
# --- 损失计算 ---
# 使用内置的 MSELoss 更好,但为了比较这里手动计算
loss = 0.5 * (y_hat - y).pow(2)
loss = loss.sum() # 通常对批次中的损失取平均,这里只是标量损失求和
print(f"Input x: {x.item():.4f}")
print(f"Target y: {y.item():.4f}")
print(f"h1: {h1.item():.4f}, h2: {h2.item():.4f}")
print(f"Prediction y_hat: {y_hat.item():.4f}")
print(f"Loss: {loss.item():.4f}")
# --- 反向传播(自动微分)---
# 计算所有 requires_grad=True 的张量的梯度
loss.backward()
# --- 检查梯度 ---
# 梯度存储在张量的 .grad 属性中
print("\n--- Gradients ---")
print(f"dL/dw1: {w1.grad.item():.5f} (Manual: -0.00337)")
print(f"dL/dw2: {w2.grad.item():.5f} (Manual: -0.00446)")
print(f"dL/dbh1: {bh1.grad.item():.5f} (Manual: -0.00673)")
print(f"dL/dbh2: {bh2.grad.item():.5f} (Manual: -0.00891)")
print(f"dL/dw3: {w3.grad.item():.5f} (Manual: -0.04850)") # 由于精度问题略有差异
print(f"dL/dw4: {w4.grad.item():.5f} (Manual: -0.04957)") # 由于精度问题略有差异
print(f"dL/dbo: {bo.grad.item():.5f} (Manual: -0.09019)") # 由于精度问题略有差异
# 注意:手动计算使用了较少的小数位,导致微小差异。
# PyTorch 使用更高精度。
这一实践演示了反向传播如何细致地应用链式法则,以找到每个权重 (weight)和偏置 (bias)对总损失的贡献。尽管框架处理实现细节,但理解这一流程对于诊断训练问题和设计有效的网络架构非常重要。这些计算出的梯度信息随后被馈送到优化算法(如 SGD、Adam 或 RMSprop,将在后续讨论)中,以迭代地改进模型。