PyTorch 学习率实战:从零理解衰减策略与调度器
副标题:手把手带你排掉"学习率定死"的坑------用下山类比讲透过大过小的判断,StepLR / 余弦退火 / Warmup 怎么选,以及 scheduler.step() 到底该放哪
你有没有过这种抓狂:模型换了优化器、加了正则化、连 batch_size 都调了,loss 还是不理想。最后发现------问题出在那个最不起眼的旋钮:学习率 。太大,loss 在天上飞、甚至直接 NaN;太小,loss 趴着纹丝不动,跑了一晚上跟没跑一样。
学习率,是训练里最敏感、也最常被误用 的超参数。前面第 12 篇的梯度下降公式 θ = θ − α ∇ L \theta = \theta - \alpha \nabla L θ=θ−α∇L 里,那个 α \alpha α 就是它。这一课,我们专门把 α \alpha α 怎么设、训练中怎么"变",一次讲透。我会用"下山找谷底"的比喻串起全程。
一、学习率是什么:下山时的"步子大小"
把训练想象成蒙眼下山找最低点(loss 最小处) 。你每走一步,靠脚下的坡度(梯度)判断往哪走,而学习率就是这一步迈多大。
- 步子太大:一脚跨过谷底,在坡两边来回蹦,永远落不准最低点;再大点,直接飞出山崖(loss 爆炸 / NaN)。
- 步子太小:安全是安全,但一小时才挪一厘米,天黑都下不到山脚(loss 几乎不动)。
- 步子刚好:稳稳朝着谷底走,最后在最低处精修到位。
于是聪明人会这么干:一开始离谷底远,大步快走;快到了,换成小碎步精修。这就是"学习率衰减"的全部哲学。
💡 一句话定位:学习率控制每次参数更新的步子大小。前期大步快走探路,后期小步精修收敛------这就是所有衰减策略的核心逻辑。
二、学习率过大 vs 过小:看 loss 曲线说话
判断学习率合不合适,最直观的办法就是看 loss 曲线。把训练日志画出来,四种典型形态对号入座:

| 症状 | 可能原因 | 处理 |
|---|---|---|
| loss 剧烈震荡 | 学习率太大 | 降 10 倍 |
| loss 变 NaN | 学习率太大 | 降 10 倍 + 梯度裁剪 |
| loss 先降后升 | 学习率偏大 | 降 2~5 倍 |
| loss 几乎不动 | 学习率太小 | 升 10 倍 |
| loss 平稳下降 | 合适 | 保持,必要时后期衰减 |
各优化器的经验起点
| 优化器 | 入门起点 | 常用范围 |
|---|---|---|
| SGD | 0.01 |
0.001 ~ 0.1 |
| Adam | 0.001 |
0.0001 ~ 0.01 |
| AdamW(大模型微调) | 0.00002 |
0.00001 ~ 0.0001 |
python
import torch
# ❌ 太大:loss 爆炸
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
# ✅ 合适:loss 平稳下降
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# ❌ 太小:loss 趴着不动
optimizer = torch.optim.Adam(model.parameters(), lr=0.00001)
🕳️ 直觉补充:学习率没有"绝对正确值",只有"相对合适区间"。它和 batch_size、模型规模都耦合------batch 越大、模型越深,通常能承受更大的 lr。所以经验值只是起点,最终还得用 loss 曲线来裁定。
三、为什么需要衰减:训练的两个阶段
训练天然分两段:
- 前期 :参数离最优解还远,需要大步快走 → 大学习率
- 后期 :参数已经接近最优解,需要小步精修 → 小学习率
如果全程用大学习率,后期参数会在谷底附近来回震荡 ,永远精确不下来(就像你大步跨过最低点,左右横跳)。如果全程用小学习率,前期又慢得让人想砸电脑。所以必须衰减------前期大、后期小,平滑过渡。
四、常见衰减策略(附代码与适用场景)
PyTorch 把衰减逻辑封装在 torch.optim.lr_scheduler 里。记住一点:调度器是优化器的"副驾驶",它只负责改 optimizer 里的 lr,不负责更新参数。下面逐个看。
1. StepLR:等间隔衰减(最直白)
每隔固定 step_size 个 epoch,学习率乘一次 gamma。
python
from torch.optim.lr_scheduler import StepLR
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
scheduler = StepLR(optimizer, step_size=10, gamma=0.5)
# 每 10 个 epoch,lr = lr * 0.5:0.1 → 0.05 → 0.025 ...
for epoch in range(30):
train(...)
scheduler.step() # 每个 epoch 结束后调用一次
| 参数 | 含义 |
|---|---|
step_size |
每隔多少 epoch 衰减一次 |
gamma |
衰减系数(0.5 表示减半) |

适用:不知道该何时衰减、只想简单定期砍一刀的场景。缺点是"砍"得太生硬,曲线不连续。
2. MultiStepLR:指定里程碑衰减(更可控)
只在你指定的 milestones 处衰减,其余时间不变。
python
from torch.optim.lr_scheduler import MultiStepLR
scheduler = MultiStepLR(optimizer, milestones=[50, 130, 200], gamma=0.1)
# 在第 50、130、200 个 epoch 各乘以 0.1
适用:你心里大概知道"在哪些节点该降"(比如 ImageNet 训练常在第 30、60、90 轮降),比 StepLR 更贴合人工经验。
3. ExponentialLR:指数衰减(最平滑但降得快)
每个 epoch 都乘一次 gamma,连续平滑下降。
python
from torch.optim.lr_scheduler import ExponentialLR
scheduler = ExponentialLR(optimizer, gamma=0.99)
# 每个 epoch: lr = lr * 0.99
适用 :想要平滑衰减、且不在乎前期降得偏快的情况。注意 gamma 要接近 1(如 0.99),否则几十步就归零了。
4. CosineAnnealingLR:余弦退火(现代最爱)
按余弦曲线平滑下降,前期慢、中期快、后期又慢,像正弦的一半。
核心公式(学习率随 epoch 变化):
η t = η min + 1 2 ( η max − η min ) ( 1 + cos ( t T max π ) ) \eta_t = \eta_{\min} + \frac{1}{2}(\eta_{\max} - \eta_{\min})\left(1 + \cos\left(\frac{t}{T_{\max}}\pi\right)\right) ηt=ηmin+21(ηmax−ηmin)(1+cos(Tmaxtπ))
其中 η max \eta_{\max} ηmax 是初始 lr, η min \eta_{\min} ηmin 是 eta_min, T max T_{\max} Tmax 是周期长度。
python
from torch.optim.lr_scheduler import CosineAnnealingLR
scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=0)
# T_max:整个余弦周期长度(常设成总 epoch 数)
# eta_min:最小学习率(一般设 0 或很小的值)

⚠️ 为什么余弦退火这么流行? 它的下降节奏和人类调参的直觉高度吻合:前期 lr 还高时慢慢降(给你时间大步探路),中期快速滑落(加速收敛),后期贴近 0 时又放缓(精修不震荡)。现代深度学习(尤其是 Transformer 训练)几乎默认用它。
推荐:CosineAnnealingLR 是现代深度学习的首选衰减策略,实践效果通常优于生硬的 StepLR。
五、标准使用模板(含打印当前 lr)
python
import torch
import torch.nn as nn
from torch.optim.lr_scheduler import CosineAnnealingLR
model = nn.Sequential(nn.Linear(100, 50), nn.ReLU(), nn.Linear(50, 10))
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
scheduler = CosineAnnealingLR(optimizer, T_max=50) # 周期 = 总 epoch 数
for epoch in range(50):
model.train()
for x, y in train_loader:
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step() # ① 优化器更新参数
scheduler.step() # ② 调度器更新学习率(一个 epoch 调一次)
# 打印当前学习率(注意从 param_groups 里取,别硬编码)
current_lr = optimizer.param_groups[0]['lr']
print(f"Epoch {epoch+1:02d}, lr={current_lr:.6f}, loss={loss.item():.4f}")

关键 :scheduler.step() 放在每个 epoch 训练结束后,不是每个 batch 后(详见第七节错误 1)。
六、Warmup:大模型训练的"先热身"
大模型训练(BERT、GPT 等)常加 Warmup :前几个 epoch 学习率从 0(或很小)线性升到目标值,再开始衰减。
为什么? 大模型初期参数还没稳定,直接用大学习率会一脚把精心初始化好的权重踢飞。先小步热身、让训练"进入状态",再大步走、最后衰减精修------三段式最稳。
python
from torch.optim.lr_scheduler import LinearLR, SequentialLR
# Warmup(前 5 个 epoch 线性升到目标 lr)+ Cosine(之后退火到 0)
warmup = LinearLR(optimizer, start_factor=0.1, total_iters=5)
cosine = CosineAnnealingLR(optimizer, T_max=45)
scheduler = SequentialLR(optimizer, [warmup, cosine], milestones=[5])
# milestones=[5]:第 5 个 epoch 前用 warmup,之后切到 cosine

💡 入门提示 :普通小项目不需要 Warmup,它主要是为了解决"大模型初始不稳"的问题。但知道业界大模型训练有这回事,对你读论文、看开源代码很有帮助。
七、三个高频错误(收藏备用)
错误 1:scheduler.step() 放错位置
python
# ❌ 放在 batch 循环里:一个 epoch 里被调用几百次,学习率瞬间衰减没
for epoch in range(50):
for x, y in train_loader:
...
optimizer.step()
scheduler.step() # 每 batch 都调!学习率崩了
# ✅ 放在 epoch 循环里:每个 epoch 只调一次
for epoch in range(50):
for x, y in train_loader:
...
optimizer.step()
scheduler.step() # 每个 epoch 调一次
⚠️ 记忆口诀 :
optimizer.step()在 batch 里(更新参数),scheduler.step()在 epoch 后(调整 lr)。它俩频率差了一个数量级。
错误 2:只调学习率,不看 loss 曲线
盲目调 lr 没意义。训练时务必记录 loss、画出来看趋势------眼睛比直觉靠谱。
python
losses = []
for epoch in range(50):
for x, y in train_loader:
...
losses.append(loss.item())
import matplotlib.pyplot as plt
plt.plot(losses)
plt.xlabel('step')
plt.ylabel('loss')
plt.show()
错误 3:学习率衰减太早
前 10 个 epoch loss 还在快速下降,你就急着衰减学习率,结果训练提前停滞------相当于还没走下山就蹲下了。一般要等前期 loss 进入"平台期"、下降变慢时,再开始衰减。
核心要点小结
- 学习率 = 下山步子大小:太大 → loss 震荡 / NaN;太小 → loss 不动;合适 → 平稳下降。
- 经验起点 :Adam
1e-3、SGD0.01、大模型微调1e-5级,但最终用 loss 曲线裁定。 - 衰减逻辑:前期大步快走探路,后期小步精修收敛。
- 调度器是优化器的"副驾驶" :只改 lr,不更新参数;
scheduler.step()放 epoch 循环里,不是 batch 循环里。 - CosineAnnealingLR 是现代首选:下降节奏(慢---快---慢)最贴合调参直觉;StepLR 生硬、ExponentialLR 降太快。
- 大模型训练加 Warmup:先小步热身(线性升 lr)再退火,避免初始大 lr 踢飞权重。
- 训练时画 loss 曲线:用眼睛判断学习率是否合适,别盲调。
动手思考题
StepLR(step_size=10, gamma=0.1),初始lr=0.1,第 25 个 epoch 时学习率是多少?(提示:数一数第 10、20 个 epoch 各衰减了几次)- 训练时 loss 先正常下降,到 epoch 30 突然变成 NaN,最可能是什么原因?请列出你的排查步骤(至少 3 步)。
- 写一段训练配置:Adam 优化器,
lr=0.001,前 5 个 epoch 做 Warmup,之后用余弦退火到 0,总共 50 个 epoch。(提示:用LinearLR+CosineAnnealingLR+SequentialLR) - 小实验:把上面的配置跑一个小数据集,分别用 StepLR 和 CosineAnnealingLR,对比两者最终的 val_acc 和收敛平滑度,把你的曲线贴到评论区,我们一起看哪种衰减更香 🔥
下一课解决训练的另一个基础设施------GPU 训练时,模型和数据怎么放到同一块设备上 ,以及那个让人头大的 Expected all tensors to be on the same device 报错怎么破。
📚 关于本系列
本文是 「AI 学习路线 · 阶段四:PyTorch 深度学习基础」 系列中的一篇。所有文章在我的个人博客上都有 可交互动画 + 完整学习路线 版本,建议配合食用 👇
🔗 在博客上阅读本文原版(含可交互组件、公式动画)
🗺️ 查看完整 AI 学习路线 (从 0 到进阶,持续更新)
觉得有帮助的话,欢迎去博客点个收藏 ⭐,你的支持是我更新的最大动力!