梯度下降详解

什么是梯度下降

梯度下降(Gradient Descent)是机器学习和深度学习中最核心的优化算法。它的直觉非常简单:想象你站在一座山上,周围一片浓雾,你看不到全貌,但你能感受到脚下地面的倾斜方向。为了到达山谷(损失函数的最小值),你每一步都沿着当前位置最陡峭的下坡方向走一小步。这就是梯度下降的核心思想——通过反复计算损失函数关于参数的梯度(即方向导数),不断更新参数,使损失函数逐步减小,最终找到最优(或近似最优)的参数组合。

几乎所有的神经网络训练都依赖梯度下降及其变体。理解梯度下降的原理、不同变体的特点以及学习率的调节策略,是深度学习工程师的必备技能。本指南将从数学基础到 PyTorch 实战,全面讲解梯度下降的方方面面。

数学基础

梯度 (Gradient)

梯度是多元函数对各个变量的偏导数组成的向量,它指向函数值增长最快的方向。对于函数 f(x1, x2, ..., xn),梯度定义为:

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

梯度的方向是函数值增长最快的方向,而负梯度方向就是函数值下降最快的方向。梯度下降正是沿着负梯度方向更新参数。

参数更新规则

梯度下降的核心更新公式非常简洁:

θ = θ - α · ∇J(θ)

其中 θ 是模型参数,α 是学习率(learning rate),J(θ) 是损失函数,∇J(θ) 是损失函数关于参数的梯度。每次迭代都用当前梯度乘以学习率来更新参数。

学习率 α 的影响

学习率是梯度下降中最关键的超参数:

学习率太小:每步更新极小,收敛速度极慢,训练需要非常长的时间,而且容易卡在局部最小值附近。
学习率太大:更新步幅过大,可能跳过最优解,损失值来回震荡甚至发散到无穷大(loss 变成 NaN)。
合适的学习率:损失函数平稳下降,既不会太慢也不会震荡。实践中通常从 0.001 开始尝试,再根据训练曲线调整。

梯度下降的三种类型

批量梯度下降 (Batch Gradient Descent)

每次更新使用全部训练数据计算梯度。梯度估计准确、更新方向稳定,但当数据量很大时计算成本极高,每一步都需要遍历整个数据集。无法进行在线学习,且容易卡在鞍点。

for epoch in range(n_epochs): grad = compute_gradient(X_all, y_all, theta) # 使用全部数据 theta = theta - lr * grad

随机梯度下降 (Stochastic Gradient Descent, SGD)

每次更新仅使用一个样本计算梯度。更新频率极高,能快速逃离局部最小值,支持在线学习。但梯度估计噪声大,损失曲线震荡剧烈,收敛路径不稳定。

for epoch in range(n_epochs): np.random.shuffle(data) for xi, yi in zip(X, y): grad = compute_gradient(xi, yi, theta) # 单个样本 theta = theta - lr * grad

小批量梯度下降 (Mini-batch Gradient Descent)

实践中最常用的方法。每次使用一个小批量(通常 32-256 个样本)计算梯度。兼具批量方法的稳定性和随机方法的高效性,能充分利用 GPU 并行计算。这也是 PyTorch、TensorFlow 默认的训练方式。

for epoch in range(n_epochs): for X_batch, y_batch in dataloader: # batch_size=64 grad = compute_gradient(X_batch, y_batch, theta) theta = theta - lr * grad

三种方法对比

方法速度稳定性内存占用适用场景
批量 GD慢(每步代价高)高(梯度准确)高(需加载全部数据)小数据集、凸优化
随机 GD (SGD)快(每步代价低)低(噪声大)低(单样本)在线学习、大数据流
小批量 GD最优(GPU 并行)中等(平衡)中等(一个 batch)深度学习标准方法

优化器详解

原始的 SGD 存在收敛慢、容易震荡等问题。研究者们提出了多种改进优化器,它们通过引入动量、自适应学习率等机制来加速和稳定训练过程。以下是最常用的优化器。

SGD with Momentum(带动量的 SGD)

动量法借鉴物理学中"惯性"的概念:梯度更新不仅取决于当前梯度,还会累积之前的更新方向。这使得参数在一致的梯度方向上加速,在震荡方向上减速,就像一个小球滚下山坡会逐渐加速一样。动量系数 β 通常设为 0.9。

v = β · v + α · ∇J(θ) θ = θ - v
import torch.optim as optim optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 训练循环 for batch in dataloader: optimizer.zero_grad() loss = criterion(model(batch_x), batch_y) loss.backward() optimizer.step()

RMSprop(均方根传播)

RMSprop 为每个参数维护一个梯度平方的指数移动平均,用于自适应地缩放学习率。对于梯度较大的参数,有效学习率会自动缩小;对于梯度较小的参数,有效学习率会增大。这解决了 Adagrad 学习率单调递减的问题。

cache = β · cache + (1 - β) · (∇J)² θ = θ - α · ∇J / √(cache + ε)
optimizer = optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99, eps=1e-8)

Adam(自适应矩估计)

Adam 是目前最流行的优化器,它结合了 Momentum(一阶矩估计)和 RMSprop(二阶矩估计)的优点。Adam 为每个参数同时维护梯度的均值(动量)和方差(自适应学习率),并通过偏差修正来消除初始化偏差。默认参数 (β₁=0.9, β₂=0.999, ε=1e-8) 在大多数情况下表现良好。

m = β₁ · m + (1 - β₁) · ∇J (一阶矩 / 动量) v = β₂ · v + (1 - β₂) · (∇J)² (二阶矩 / 自适应率) m̂ = m / (1 - β₁ᵗ) (偏差修正) v̂ = v / (1 - β₂ᵗ) (偏差修正) θ = θ - α · m̂ / (√v̂ + ε)
optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-8)

AdamW(解耦权重衰减)

AdamW 修复了 Adam 中 L2 正则化实现不正确的问题。在标准 Adam 中,权重衰减(weight decay)被加入到梯度中再进行自适应缩放,导致正则化效果被削弱。AdamW 将权重衰减从梯度更新中解耦出来,直接在参数上施加衰减,正则化效果更好。AdamW 已成为训练 Transformer 和大型语言模型的标准选择。

Adam 步骤同上,但权重衰减独立施加: θ = θ - α · m̂ / (√v̂ + ε) - α · λ · θ (其中 λ 是权重衰减系数)
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)

优化器对比

优化器自适应学习率动量权重衰减典型用途
SGDL2凸优化基线
SGD + MomentumL2CNN 训练(ResNet 等)
RMSpropL2RNN / 非平稳目标
AdamL2(耦合)通用默认选择
AdamW解耦Transformer / LLM 训练

学习率调度策略

固定学习率很少是最优选择。训练初期需要较大的学习率快速探索,后期则需要较小的学习率精细调整。以下是常用的学习率调度策略。

固定学习率 (Constant)

最简单的策略,整个训练过程使用同一个学习率。适合小模型和简单任务,但大多数情况下不是最优。

optimizer = optim.Adam(model.parameters(), lr=0.001) # 无需 scheduler,学习率始终为 0.001

阶梯衰减 (Step Decay)

每隔固定的 epoch 数将学习率乘以一个衰减因子(如 0.1)。简单直观,在 CNN 训练中广泛使用(如 ResNet 在第 30、60、90 个 epoch 衰减学习率)。

scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) for epoch in range(100): train_one_epoch() scheduler.step() # 第 30, 60, 90 epoch 时 lr 乘以 0.1

余弦退火 (Cosine Annealing)

学习率按余弦函数从初始值平滑衰减到最小值(接近 0),曲线平滑自然。在训练中后期减速更加渐进,是目前最流行的调度策略之一。

scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6) for epoch in range(100): train_one_epoch() scheduler.step()

预热 (Warmup)

训练开始时使用极小的学习率,在前几个 epoch 线性增大到目标学习率,然后再开始衰减。预热可以防止模型在训练初期因为随机初始化的参数产生过大的梯度更新而不稳定。Transformer 训练几乎都使用 Warmup。

scheduler = optim.lr_scheduler.LinearLR(optimizer, start_factor=0.01, total_iters=5) # 前 5 个 epoch: lr 从 0.01*base_lr 线性增长到 base_lr # 组合使用: warmup + cosine scheduler1 = optim.lr_scheduler.LinearLR(optimizer, start_factor=0.01, total_iters=5) scheduler2 = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=95) scheduler = optim.lr_scheduler.SequentialLR(optimizer, [scheduler1, scheduler2], milestones=[5])

OneCycleLR

Super-convergence 策略:学习率先从小值升到最大值,再降回极小值,整个过程在一个训练周期内完成。由 Leslie Smith 提出,可以使用比常规方法大 10 倍的学习率,显著加快收敛速度。

scheduler = optim.lr_scheduler.OneCycleLR( optimizer, max_lr=0.01, total_steps=len(dataloader) * n_epochs, pct_start=0.3, anneal_strategy='cos' ) for epoch in range(n_epochs): for batch in dataloader: train_step(batch) scheduler.step() # 注意:OneCycleLR 每个 batch 调用一次

从零实现梯度下降(线性回归)

用纯 NumPy 实现一个完整的梯度下降训练线性回归模型,帮助理解底层原理:

import numpy as np # 生成数据: y = 3x + 7 + noise np.random.seed(42) X = 2 * np.random.rand(100, 1) y = 3 * X + 7 + np.random.randn(100, 1) * 0.5 # 添加偏置项 x0=1 X_b = np.c_[np.ones((100, 1)), X] # shape: (100, 2) # 超参数 lr = 0.1 n_epochs = 1000 m = len(X_b) # 随机初始化参数 [bias, weight] theta = np.random.randn(2, 1) # 梯度下降 for epoch in range(n_epochs): predictions = X_b @ theta errors = predictions - y gradients = (2 / m) * X_b.T @ errors theta = theta - lr * gradients print(f"bias = {theta[0, 0]:.4f}, weight = {theta[1, 0]:.4f}") # 输出约: bias = 7.0, weight = 3.0

常见问题与陷阱

梯度消失 (Vanishing Gradients)

在深层网络中,梯度通过链式法则反向传播时会逐层相乘。如果每层的梯度小于 1,经过几十层后梯度会指数级缩小,接近于零。前面的层几乎不更新,网络无法学习深层特征。常见于使用 sigmoid/tanh 激活函数的深层网络。解决方案包括:使用 ReLU 激活函数、BatchNorm、残差连接(ResNet)、合适的权重初始化(He/Xavier)。

梯度爆炸 (Exploding Gradients)

与梯度消失相反,如果每层的梯度大于 1,反向传播后梯度会指数级增大,导致参数更新过大,损失值变成 NaN。常见于 RNN 处理长序列时。解决方案包括:梯度裁剪(gradient clipping)、使用 LSTM/GRU 代替 vanilla RNN、合适的权重初始化、降低学习率。

# PyTorch 梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

鞍点 (Saddle Points)

鞍点是梯度为零但既不是最小值也不是最大值的点——在某些方向上是极小值,在其他方向上是极大值。在高维空间中,鞍点的数量远多于局部最小值。SGD 的随机性和动量机制有助于逃离鞍点,这也是随机方法优于批量方法的原因之一。

局部最小值 (Local Minima)

非凸损失函数可能有多个局部最小值,梯度下降可能收敛到非全局最优的局部最小值。然而近年来的研究表明,在高维深度学习中,大多数局部最小值的损失值与全局最小值非常接近,因此局部最小值在实践中不如过去认为的那么严重。更大的挑战通常是鞍点和平坦区域。

学习率过高 / 过低

学习率过高:训练一开始 loss 就剧烈震荡或直接飙升到 NaN。参数更新步幅太大,跳过了最优解,甚至跳出了损失函数的合理区域。遇到这种情况应立即将学习率降低 10 倍。

学习率过低:loss 下降极其缓慢,训练几百个 epoch 后 loss 仍然很高。模型在搜索空间中缓慢蠕动,可能需要成千上万个 epoch 才能收敛。遇到这种情况应将学习率增大 3-10 倍,或使用学习率预热策略。

实战调参技巧

1. 从 Adam 开始:如果不确定用什么优化器,先用 Adam (lr=1e-3) 或 AdamW (lr=1e-3, weight_decay=0.01)。它们在大多数任务上都有不错的表现。
2. 学习率查找器:使用 Learning Rate Finder(从极小的 lr 指数增长到极大的 lr,记录每个 lr 对应的 loss),找到 loss 下降最快的区间,将最大学习率设为该区间的 1/10。
3. Batch Size 与学习率:增大 batch size 时按比例增大学习率(线性缩放法则)。例如 batch_size 从 32 翻倍到 64,学习率也翻倍。但 batch_size 超过 8192 时这个法则可能不再适用。
4. 梯度裁剪是保险:即使不出现梯度爆炸,设置 max_norm=1.0 的梯度裁剪也几乎没有负面影响,却能有效防止偶尔出现的异常大梯度。
5. 余弦退火 + 预热是万金油:对大多数任务来说,5-10 个 epoch 的线性预热 + 余弦退火到最小学习率,是一个非常稳健的学习率调度方案。
6. 监控梯度范数:训练时记录每步的梯度范数(gradient norm)。如果梯度范数突然飙升,说明训练不稳定;如果持续为零,说明存在梯度消失。
7. SGD + Momentum 可能更好:虽然 Adam 收敛更快,但 SGD + Momentum + 学习率调度在 CNN(如 ResNet、EfficientNet)上通常能达到更高的最终精度,只是需要更仔细的调参。
8. 权重衰减别忘记:几乎所有的训练都应该使用权重衰减(weight decay),典型值为 1e-4 到 0.1。AdamW 的解耦权重衰减效果优于 Adam 的 L2 正则化。

常见问题 (FAQ)

Q: Adam 和 SGD 到底该选哪个?

如果追求快速收敛和简单调参,选 Adam/AdamW;如果追求最高精度且愿意花时间调参,选 SGD + Momentum + 学习率调度。在 NLP/Transformer 任务中,AdamW 几乎是唯一选择;在 CV/CNN 任务中,SGD + Momentum 仍然是竞赛冠军方案的主流。

Q: 学习率 warmup 有多重要?

在大模型和大 batch size 训练中非常重要。随机初始化的参数在训练初期会产生不稳定的梯度,如果直接使用大学习率更新,模型可能直接崩溃。Warmup 让模型在初期用小学习率"热身",等参数进入合理区间后再加速训练。对于 Transformer,省略 warmup 经常导致训练完全失败。

Q: batch size 越大越好吗?

不一定。大 batch size 可以更好地利用 GPU 并行性,单位时间处理更多数据,但过大的 batch size 反而可能降低模型的泛化能力(sharp minima 问题)。实践中 32-512 是常用范围,超大 batch 训练需要配合特殊的学习率策略(如 LARS、LAMB)。受 GPU 显存限制时,可以用梯度累积(gradient accumulation)来模拟大 batch。

Q: 损失不下降应该怎么排查?

按以下顺序排查:1) 检查学习率是否合适(先尝试 1e-3);2) 检查数据和标签是否正确(用小数据集过拟合测试);3) 检查损失函数是否匹配任务(分类用 CrossEntropy,回归用 MSE);4) 检查是否忘记调用 optimizer.zero_grad();5) 检查梯度范数是否正常(非零非 NaN);6) 简化模型,确认基本训练流程无误后再增加复杂度。

Q: 梯度下降只能用于可微分函数吗?

标准梯度下降需要损失函数关于参数可微分。对于不可微的操作(如 argmax、离散采样),可以使用替代方法:直通估计器(Straight-Through Estimator)、Gumbel-Softmax 重参数化、REINFORCE 策略梯度等。在实践中,ReLU 在 0 处不可微,但 PyTorch 默认将其梯度设为 0,不影响训练。