📚 课程目录

第2课:梯度下降与反向传播

📖 本课概述

梯度下降是深度学习的核心优化算法,反向传播是高效计算梯度的关键。本课深入理解梯度的数学本质,手动推导反向传播,并通过PyTorch验证。

🧮 一、梯度(Gradient)

1.1 什么是梯度

梯度是多元函数偏导数组成的向量,指向函数值增长最快的方向。梯度下降就是沿着梯度的反方向移动,从而最小化函数值。

对于函数 f(x₁, x₂, ..., xₙ),梯度定义为:

∇f = (∂f/∂x₁, ∂f/∂x₂, ..., ∂f/∂xₙ)

梯度下降更新规则:

x ← x - η · ∇f(x)

1.2 PyTorch自动求导

PyTorch通过计算图链式法则自动计算梯度:


import torch

# 手动计算梯度
x = torch.tensor([2.0], requires_grad=True)
y = x**2 + 3*x + 1  # y = x² + 3x + 1
y.backward()
print(f"函数: y = x² + 3x + 1")
print(f"x = {x.item():.1f} 时, y = {y.item():.1f}")
print(f"dy/dx = 2x + 3 = {x.grad.item():.1f}")
print(f"验证: 2*{x.item():.1f} + 3 = {2*x.item()+3:.1f}")

# 多变量梯度
print("\n=== 多变量梯度 ===")
w1 = torch.tensor([1.0], requires_grad=True)
w2 = torch.tensor([2.0], requires_grad=True)
loss = w1**2 + w2**2 + w1*w2
loss.backward()
print(f"函数: L = w1² + w2² + w1*w2")
print(f"w1=1.0, w2=2.0 时, L={loss.item():.1f}")
print(f"∂L/∂w1 = 2w1 + w2 = {w1.grad.item():.1f}")
print(f"∂L/∂w2 = 2w2 + w1 = {w2.grad.item():.1f}")
🟢 运行结果 — 梯度计算 ✅验证通过 函数: y = x² + 3x + 1 x = 2.0 时, y = 11.0 dy/dx = 2x + 3 = 7.0 验证: 2*2.0 + 3 = 7.0 === 多变量梯度 === 函数: L = w1² + w2² + w1*w2 w1=1.0, w2=2.0 时, L=7.0 ∂L/∂w1 = 2w1 + w2 = 4.0 ∂L/∂w2 = 2w2 + w1 = 5.0

🔄 二、反向传播(Backpropagation)

2.1 计算图与链式法则

反向传播的核心是链式法则。对于复合函数 y = f(g(x)):

dy/dx = dy/dg · dg/dx

对于神经网络:L = Loss(f₂(f₁(x)))

∂L/∂W₁ = ∂L/∂f₂ · ∂f₂/∂f₁ · ∂f₁/∂W₁

2.2 前向传播 vs 反向传播

阶段方向计算内容
前向传播输入→输出各层输出值
反向传播输出→输入各层梯度值
💡 计算图有静态图(TensorFlow 1.x)和动态图(PyTorch)两种。PyTorch的动态图更直观、更易调试,每次前向传播都构建新的计算图。

2.3 反向传播实现


import torch
import torch.nn as nn

# 手动实现反向传播 vs PyTorch自动求导
torch.manual_seed(42)

# 2层网络
X = torch.randn(4, 3)
y = torch.randn(4, 1)

# 方式1:PyTorch自动求导
model = nn.Sequential(
    nn.Linear(3, 4),
    nn.Sigmoid(),
    nn.Linear(4, 1)
)

loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

print("=== 梯度下降训练过程 ===")
for epoch in range(0, 501, 100):
    optimizer.zero_grad()
    output = model(X)
    loss = loss_fn(output, y)
    loss.backward()
    optimizer.step()
    
    # 打印梯度范数
    grad_norms = []
    for p in model.parameters():
        if p.grad is not None:
            grad_norms.append(f"{p.grad.norm().item():.6f}")
    
    print(f"Epoch {epoch:3d}: loss={loss.item():.6f}, grad_norms=[{', '.join(grad_norms)}]")

# 查看最终参数
print("\n=== 最终模型参数 ===")
for name, param in model.named_parameters():
    print(f"{name}: shape={param.shape}, norm={param.data.norm().item():.4f}")
🟢 运行结果 — 反向传播训练 ✅验证通过 === 梯度下降训练过程 === Epoch 0: loss=1.915635, grad_norms=[0.249424, 0.208081, 1.537386, 1.277153] Epoch 100: loss=1.875599, grad_norms=[0.244931, 0.196072, 1.465403, 1.215505] Epoch 200: loss=1.839239, grad_norms=[0.241006, 0.184939, 1.397031, 1.156853] Epoch 300: loss=1.806207, grad_norms=[0.237584, 0.174605, 1.332082, 1.101046] Epoch 400: loss=1.776186, grad_norms=[0.234604, 0.165001, 1.270381, 1.047940] Epoch 500: loss=1.748891, grad_norms=[0.232014, 0.156064, 1.211762, 0.997402] === 最终模型参数 === 0.weight: shape=torch.Size([4, 3]), norm=0.9994 0.bias: shape=torch.Size([4]), norm=0.5446 2.weight: shape=torch.Size([1, 4]), norm=0.6221 2.bias: shape=torch.Size([1]), norm=0.3156

📉 三、梯度下降的变体

3.1 不同学习率的影响


import torch
import torch.nn as nn
import numpy as np

# 可视化梯度下降轨迹(1D)
torch.manual_seed(0)
x_start = 5.0
lr_values = [0.01, 0.1, 0.5]

def f(x):
    return x**2

def df(x):
    return 2*x

print("=== 不同学习率的梯度下降轨迹 ===")
print("目标函数: f(x) = x², 最小值在 x=0")
print()

for lr in lr_values:
    x = x_start
    trajectory = [x]
    for step in range(20):
        grad = df(x)
        x = x - lr * grad
        trajectory.append(x)
    traj_str = " → ".join([f"{v:.3f}" for v in trajectory[:8]])
    print(f"lr={lr}: {traj_str} ... → {trajectory[-1]:.6f}")
    
# 梯度消失 vs 梯度爆炸演示
print("\n=== 梯度消失/爆炸演示 ===")
depths = [5, 10, 20, 50]
for depth in depths:
    x = torch.tensor([1.0])
    for i in range(depth):
        w = torch.tensor([0.9], requires_grad=True)
        x = w * x + 0.1
    x.backward()
    print(f"深度={depth:2d}: 最终输出={x.item():.8f}, 初始梯度估计~{0.9**depth:.8f}")
🟢 运行结果 — 学习率对比与梯度消失/爆炸 ✅验证通过 === 不同学习率的梯度下降轨迹 === 目标函数: f(x) = x², 最小值在 x=0 lr=0.01: 5.000 → 4.900 → 4.802 → 4.706 → 4.612 → 4.520 → 4.429 → 4.341 ... → 3.338040 lr=0.1: 5.000 → 4.000 → 3.200 → 2.560 → 2.048 → 1.638 → 1.311 → 1.049 ... → 0.057646 lr=0.5: 5.000 → 0.000 → 0.000 → 0.000 → 0.000 → 0.000 → 0.000 → 0.000 ... → 0.000000 === 梯度消失/爆炸演示 === 深度= 5: 最终输出=1.00000000, 初始梯度估计~0.59049000 深度=10: 最终输出=1.00000000, 初始梯度估计~0.34867844 深度=20: 最终输出=1.00000000, 初始梯度估计~0.12157665 深度=50: 最终输出=1.00000000, 初始梯度估计~0.00515378

3.2 梯度下降三种形式

变体每次用多少数据优点缺点
批量梯度下降(BGD)全部数据稳定收敛慢,内存大
随机梯度下降(SGD)1个样本快,可逃脱局部最优震荡剧烈
小批量SGDbatch_size个平衡速度和稳定性需调batch_size

3.3 梯度消失与梯度爆炸

在深层网络中,梯度通过链式法则连乘,容易导致:

⚠️ 梯度消失/爆炸是深度学习训练中最常见的问题之一。后续课程将学习解决方案:BatchNorm、残差连接、梯度裁剪等。

🔬 四、深入计算图

4.1 静态图 vs 动态图

特性静态图(TF1.x)动态图(PyTorch)
构建时机先定义后运行运行时构建
调试困难容易(可用pdb)
优化全局优化机会多优化空间较小
灵活性低(条件循环困难)高(原生Python)
部署方便(图导出)需TorchScript转换

4.2 梯度累积技巧

当GPU内存不足以容纳大batch时:

1. 将大batch分成多个小batch

2. 前向+反向累积梯度(不更新参数)

3. 累积N个小batch后,执行一步参数更新

等效batch_size = micro_batch × accumulation_steps

4.3 高级优化技巧

学习率预热(Warmup):训练初期用小学习率,逐步增大到目标值。防止初期参数变化太剧烈。

梯度裁剪(Gradient Clipping):当梯度范数超过阈值时,按比例缩小。防止梯度爆炸。

if ‖g‖ > max_norm: g = g × (max_norm / ‖g‖)

PyTorch: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

📖 五、延伸阅读

💡 推荐资源:

📝 练习

练习1:手动推导

对于网络 y = σ(w₂·σ(w₁x + b₁) + b₂),手动推导 ∂L/∂w₁ 的表达式。

练习2:梯度裁剪

在PyTorch中实现梯度裁剪(torch.nn.utils.clip_grad_norm_),观察对训练稳定性的影响。

练习3:计算图可视化

使用torchviz库可视化一个简单网络的计算图,理解前向和反向传播的流程。

💡 六、实践技巧总结

6.1 梯度调试方法

如何验证你的梯度计算是否正确?使用数值梯度对比:

数值梯度: ∂f/∂xᵢ ≈ (f(x+εeᵢ) - f(x-εeᵢ)) / (2ε)

ε通常取1e-5,如果解析梯度与数值梯度的差 < 1e-7,则正确

6.2 常见梯度问题与解决

问题症状解决方案
梯度消失损失不下降,参数不更新ReLU、BatchNorm、残差连接
梯度爆炸损失突然变为NaN梯度裁剪、降低学习率
梯度死区部分参数梯度恒为0LeakyReLU、调整初始化
梯度震荡损失上下波动不收敛降低学习率、增加动量

6.3 反向传播的内存优化

标准反向传播需要存储前向传播的所有中间值(O(depth)内存)。优化方法:

🏆

成就解锁:反向传播师

你已经掌握了深度学习的核心引擎——梯度下降和反向传播。
理解梯度,就理解了神经网络如何学习!