第二章 学习率调优:让GNN收敛不再“撞墙”
学习率,说白了就是模型每次更新参数时迈的步子大小。步子太大容易扯着蛋——损失震荡不收敛;步子太小又像蜗牛爬,训到天荒地老。我在调GNN时踩过最深的坑,就是学习率没选对,模型直接原地爆炸。
这一章咱们就聊聊学习率那些事。我会结合自己的实战经验,把余弦退火、warmup、自适应学习率这些招数掰开揉碎了讲清楚。
2.1 学习率对GNN收敛的影响
先看个直观的例子。假设你在训练一个3层的GCN,节点分类任务。学习率设成0.1,loss曲线是这样的:
Epoch 1: loss = 2.3456
Epoch 2: loss = 1.9876
Epoch 3: loss = 1.6543
...
Epoch 50: loss = 0.4321
看起来还行对吧?但如果你把学习率改成0.5:
Epoch 1: loss = 2.3456
Epoch 2: loss = 5.6789 ← 炸了
Epoch 3: loss = 12.3456 ← 彻底炸了
为什么会这样?GNN的梯度传播路径长,多层叠加后梯度本身就敏感。学习率一高,参数更新直接飞出可行域。
核心结论:GNN对学习率比普通CNN更敏感。我建议初始学习率设在0.001~0.01之间,具体看你的图规模和数据噪声程度。
反过来,学习率太小也有问题。我记得有一次调一个异构图模型,学习率设成1e-5,跑了200个epoch loss才降了0.1。那叫一个折磨。
2.2 余弦退火调度
余弦退火,名字听着高大上,其实原理很简单——让学习率按照余弦函数周期性地衰减。刚开始大步探索,后期小步精调。
公式长这样:
η_t = η_min + 0.5 * (η_max - η_min) * (1 + cos(t / T * π))
其中t是当前epoch,T是总epoch数。η_max是初始学习率,η_min是最小学习率。
我项目中常用的配置:
import torch.optim.lr_scheduler as scheduler
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
cosine_scheduler = scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # 周期长度
eta_min=1e-6 # 最小学习率
)
for epoch in range(100):
train_one_epoch()
cosine_scheduler.step()
我的经验:T_max一般设为总epoch数的1/2到1/3。比如你打算训200个epoch,T_max设100左右效果最好。太长会导致后期学习率降得太慢,太短又浪费了前期探索能力。
余弦退火有个变种叫余弦退火重启(CosineAnnealingWarmRestarts)。它会周期性地把学习率重置到初始值,帮助模型跳出局部最优。我在处理大规模图数据时特别喜欢用这个——图数据往往有很多局部极小点,重启一下往往能发现更好的解。
2.3 Warmup策略
Warmup,就是让学习率从0慢慢升到目标值。为什么需要这个?
嗯,这里要注意。GNN刚开始训练时,参数是随机初始化的,梯度方向很不稳定。如果一上来就用大学习率,模型很容易被带偏。就像你刚学开车,不可能直接上高速吧?
我曾经在一个分子性质预测任务上吃过亏。模型前10个epoch loss降得飞快,我还以为捡到宝了。结果到第20个epoch,loss突然反弹,怎么调都回不去。后来加了warmup,问题就解决了。
常见的warmup实现:
def warmup_cosine_scheduler(optimizer, warmup_epochs, total_epochs, lr_max):
def lambda_rule(epoch):
if epoch < warmup_epochs:
return epoch / warmup_epochs # 线性上升
else:
# 余弦衰减
progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs)
return 0.5 * (1 + cos(progress * pi))
return scheduler.LambdaLR(optimizer, lr_lambda=lambda_rule)
避坑指南:我曾经把warmup epoch设得太长(总epoch的50%),结果模型一直在低学习率下徘徊,收敛速度反而变慢了。一般warmup占5%~10%的总epoch就够了。
2.4 自适应学习率方法
手动调学习率太累了,对吧?自适应方法就是让优化器自己决定每一步该迈多大。
GNN领域最常用的三个自适应优化器:
| 优化器 | 特点 | 适用场景 |
|---|---|---|
| Adam | 自适应学习率 + 动量 | 大多数GNN任务,默认选择 |
| AdamW | Adam + 权重衰减解耦 | 需要正则化的场景,防止过拟合 |
| RAdam | 修正Adam的方差问题 | 小批量训练,梯度噪声大时 |
我个人习惯先用Adam跑基线。如果发现验证集loss震荡厉害,就换成RAdam。RAdam在训练初期更稳定,因为它修正了Adam的方差估计偏差。
代码示例:
# Adam
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999))
# AdamW
optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
# RAdam
from torch.optim import RAdam
optimizer = RAdam(model.parameters(), lr=0.001)
小技巧:自适应优化器虽然省事,但别忘了配合学习率调度器。我通常的做法是:Adam + 余弦退火 + warmup,这个组合在90%的GNN任务上都能取得不错的效果。
2.5 本章知识体系
下面这张图总结了学习率调优的核心逻辑:
说白了,学习率调优没有银弹。不同数据集、不同GNN结构,最优配置都不一样。我的建议是:先用Adam跑几个epoch看看loss趋势,再决定要不要加warmup和余弦退火。别一上来就搞花里胡哨的调度策略,先把基础打牢。