4. 固定学习率:优缺点分析,适用场景与分子生成实验
聊完了学习率的基础概念,咱们得先看最简单、最原始的策略——固定学习率。你别看它“简单”,在分子生成这个领域里,有时候最朴素的方法反而最管用。我见过不少新手,一上来就搞各种花哨的调度器,最后调了半天,效果还不如一个恒定的学习率来得稳。
说白了,固定学习率就是整个训练过程中,学习率从头到尾都保持不变。听起来很无脑对吧?但这种“无脑”背后,其实藏着很深的工程哲学。
4.1 优缺点分析
先说说优点。固定学习率最大的好处,就是稳定、可复现。你想想看,如果每次跑实验,学习率都在变,那出了 Bug 你到底是怀疑模型结构有问题,还是怀疑学习率调度没写对?
我个人习惯,在验证一个新的分子生成架构时,一定会先用固定学习率跑一轮基线。这样我就能排除掉学习率变化带来的干扰,专心看模型本身能不能学得动。
核心优点可以归纳成这样几点:
- 超参空间小:你只需要调一个初始值,不需要管衰减率、步长、周期这些东西。
- 训练轨迹可预测:损失曲线的下降速度是均匀的,便于判断模型是否真的在收敛。
- 实现极其简单:不会因为调度器 Bug 导致训练中断或爆炸。
但缺点也很明显。说实话,固定学习率基本上是一种“牺牲上限换下限”的策略。
注意:我曾经在一个生成小分子的 GAN 项目里吃过亏。固定学习率训到一半,生成器损失突然开始震荡。查了两天才发现,模型已经进入了一个陡峭的局部极小值,固定学习率太大,根本落不下去。换成衰减策略后,问题立刻解决。
下面这个表格可以帮你快速对比:
| 维度 | 优势 | 劣势 |
|---|---|---|
| 收敛速度 | 前期稳定,不会失控 | 后期可能过慢,浪费算力 |
| 泛化性能 | 不容易跳过好解 | 容易卡在局部最优 |
| 调参难度 | 仅一个超参:初始值 | 初始值选错,全盘皆输 |
| 鲁棒性 | 对噪声不那么敏感 | 无法自适应梯度变化 |
4.2 适用场景
那么,什么时候该用固定学习率?我根据自己的项目经验,总结了三个典型的适用场景:
- 基线实验:新架构、新损失的验证阶段。用固定学习率快速跑出基线,比什么都重要。
- 小数据量微调:比如你有一个预训练的分子生成模型,想在下游小数据集上微调。固定学习率能防止灾难性遗忘。
- 生成模型训练的早期阶段:像 VAE、Flow-based 模型,初期梯度方差很大。固定一个比较小的学习率,先让模型稳定下来,再换调度策略。
我的技巧:如果你不确定该用多大学习率,可以先从 3e-4 开始试。这是分子生成领域里一个比较“万金油”的值。跑 10 个 epoch,看看损失曲线。如果震荡剧烈,砍到 1e-4;如果下降太慢,提到 5e-4。固定学习率的好处就是,试错成本极低。
4.3 在分子生成中的实验
咱们拿一个具体的分子生成任务来说。假设你正在用 GAN 生成 类药小分子(SMILES 序列生成)。数据集是 ZINC-250k,模型是一个简单的 LSTM + 全连接判别器。我们来看看固定学习率到底表现如何。
实验配置长这样:
# 超参数配置
BATCH_SIZE = 256
EPOCHS = 200
LATENT_DIM = 128
# 生成器学习率(固定)
GEN_LR = 1e-4
# 判别器学习率(固定)
DISC_LR = 2e-4
# 优化器
gen_optimizer = torch.optim.Adam(generator.parameters(), lr=GEN_LR)
disc_optimizer = torch.optim.Adam(discriminator.parameters(), lr=DISC_LR)
你可能会问,为什么判别器学习率要比生成器大一倍?
嗯,这个问题很关键。在分子生成的 GAN 里,判别器需要快速分辨真实分子和生成分子,它的任务更“急迫”。生成器则要慢慢学,太激进容易模式坍塌。我在项目中试过把两个学习率调成一样的,结果生成器很快就“摆烂”了——只输出几个固定的化学式。
跑了 200 个 epoch 后,我们记录了关键的指标:
| 指标 | 固定学习率 (GEN 1e-4 / DISC 2e-4) |
|---|---|
| Valid SMILES 比例 | 86.3% |
| Unique 分子比例 | 54.7% |
| Novelty 新颖性 | 42.1% |
| 训练时间(200 epoch) | 约 2.5 小时 |
实验结论:固定学习率虽然在新颖性上不算最好(毕竟后期探索能力受限),但 Valid 比例很高,说明生成器的化学语法学得非常扎实。对于需要高化学有效性的场景,比如药物分子库预筛选,固定学习率是一个相当稳妥的选择。
下面是这个实验的核心代码片段。我建议你直接拿去跑一下,看看自己项目里是不是也符合这个规律。
import torch
import torch.optim as optim
# 假设 generator 和 discriminator 已经定义好了
gen_optimizer = optim.Adam(generator.parameters(), lr=1e-4)
disc_optimizer = optim.Adam(discriminator.parameters(), lr=2e-4)
# 训练循环(简化版)
for epoch in range(200):
for real_smiles_batch in dataloader:
# 1. 训练判别器
noise = torch.randn(BATCH_SIZE, LATENT_DIM)
fake_smiles = generator(noise)
disc_real_loss = criterion(discriminator(real_smiles_batch), real_label)
disc_fake_loss = criterion(discriminator(fake_smiles.detach()), fake_label)
disc_loss = disc_real_loss + disc_fake_loss
disc_optimizer.zero_grad()
disc_loss.backward()
disc_optimizer.step()
# 2. 训练生成器
noise = torch.randn(BATCH_SIZE, LATENT_DIM)
fake_smiles = generator(noise)
gen_loss = criterion(discriminator(fake_smiles), real_label)
gen_optimizer.zero_grad()
gen_loss.backward()
gen_optimizer.step()
# 每 10 个 epoch 打印一次指标
if epoch % 10 == 0:
valid_ratio = compute_valid_ratio(generator)
print(f"Epoch {epoch} | Valid: {valid_ratio:.2f}")
你会发现,代码里没有 `scheduler.step()`,没有 `lr_scheduler` 模块。这就是固定学习率最大的优势——干净的代码,干净的逻辑。
为了帮你更直观地理解固定学习率在分子生成训练中的位置,我画了一张图:
你看这张图,整个流程非常直。没有分叉,没有调度决策。固定学习率就像一个任劳任怨的老黄牛,虽然不惊艳,但绝不会中途给你整出幺蛾子。在做分子生成这种高维、离散、极其不稳定的任务时,这种确定性,本身就是一种宝贵的资源。