第二章 学习率调优:让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收敛速度 余弦退火调度 Warmup策略 自适应方法 梯度敏感 多层传播 周期衰减 重启机制 线性上升 稳定梯度 Adam/AdamW 最佳实践:Adam + 余弦退火 + Warmup

说白了,学习率调优没有银弹。不同数据集、不同GNN结构,最优配置都不一样。我的建议是:先用Adam跑几个epoch看看loss趋势,再决定要不要加warmup和余弦退火。别一上来就搞花里胡哨的调度策略,先把基础打牢。


专注资料整理