PyTorch C++ 优化器 torch::optim 实战指南:从 SGD/Adam 到学习率调度
torch::optim 是 PyTorch C++ 前端(LibTorch)中与 Python 侧 torch.optim 一一对应的优化算法模块,为用 C++ 训练神经网络提供参数更新、梯度清零与状态管理的一整套能力。本文以官方 C++ 文档为主体,逐层讲解优化器基类架构、SGD / Adam / AdamW / RMSprop / Adagrad / LBFGS 的适用场景与配置参数,并串联 StepLR 等学习率调度器与完整训练循环,帮助读者在 C++ 侧实现从模型定义到收敛的全流程训练代码,同时结合仓库头文件给出源码级印证。
torch::optim:C++ 训练循环中的"优化器入口"
在 docs/cpp/source/api/optim/index.md 中,torch::optim 被定位为训练神经网络所使用的优化算法集合——这些优化器基于反向传播计算出的梯度更新模型参数,从而最小化损失函数。官方文档明确给出了它的三类典型使用时机:
- 用梯度下降训练神经网络时:优化器负责把
loss.backward()产出的梯度真正作用到参数上; - 需要不同优化策略时:可以根据任务在 SGD、Adam 等之间切换,而不改动模型与训练循环结构;
- 实现学习率调度(learning rate schedule)时:
torch::optim同时提供StepLR、ExponentialLR、ReduceLROnPlateau等调度器,按轮次或验证指标动态调整学习率。
基本用法可以浓缩为下面这个循环骨架(即官方文档给出的范式):
#include <torch/torch.h>
// 创建模型与优化器
auto model = std::make_shared<Net>();
auto optimizer = torch::optim::Adam(
model->parameters(),
torch::optim::AdamOptions(1e-3));
// 训练循环
for (auto& batch : *data_loader) {
optimizer.zero_grad(); // 清空梯度
auto loss = loss_fn(model->forward(batch.data), batch.target);
loss.backward(); // 反向传播计算梯度
optimizer.step(); // 按梯度更新参数
}
zero_grad() → backward() → step() 的循环是理解整个 torch::optim 模块的主线:先清零上次累积的梯度,再前向计算损失并反向传播,最后让优化器依据自身算法(含动量、自适应学习率等内部状态)执行一次参数更新。
头文件与源码布局
官方文档将优化器相关的头文件划分如下,这些文件正是本仓库 torch::optim 全部实现的实际载体:
| 头文件 | 作用 |
|---|---|
torch/csrc/api/include/torch/optim.h |
主优化头,集中导出全部优化器 |
torch/csrc/api/include/torch/optim/optimizer.h |
优化器基类与参数组/状态/选项基础设施 |
torch/csrc/api/include/torch/optim/sgd.h |
SGD 优化器及其选项 |
torch/csrc/api/include/torch/optim/adam.h |
Adam 优化器及其选项 |
在仓库 torch/csrc/api/include/torch/optim/ 目录下,可以看到比文档列出的更完整的实现清单:除上述文件外,还有 adamw.h(AdamW)、adagrad.h(Adagrad)、rmsprop.h(RMSprop)、lbfgs.h(LBFGS)、serialize.h(优化器状态序列化)以及 schedulers/ 子目录(lr_scheduler.h、step_lr.h、reduce_on_plateau_scheduler.h)。
与之配套的文档还有四个分篇,后续小节将逐一展开其核心内容:
优化器基类架构:Optimizer / Options / ParamGroup / ParamState
文档指出:所有优化器都继承自 Optimizer 基类,由它统一提供参数更新、梯度清零与状态管理能力。阅读 optimizer.h 可以确认这套设计的四个核心类型,以及它们各自承担的职责。
Optimizer 基类:统一训练接口
从源码看,Optimizer 类公开的核心能力包括:
| 成员 | 职责(源码依据) |
|---|---|
step(LossClosure closure = nullptr) |
纯虚函数,执行一次参数更新;LossClosure = std::function<Tensor()>,LBFGS 等需要闭包重新计算损失 |
zero_grad(bool set_to_none = true) |
清零所有参数梯度,默认采用"置 None"语义以节省内存 |
add_param_group() / add_parameters() |
动态添加参数组或参数向量 |
parameters() / size() |
返回首组参数引用与参数总数 |
param_groups() |
访问优化器持有的全部参数组 |
state() |
返回 ska::flat_hash_map<void*, std::unique_ptr<OptimizerParamState>>,以参数指针为键保存各优化算法的运行时状态(如动量缓冲) |
defaults() |
全局默认选项 |
save() / load() |
将优化器状态序列化到 OutputArchive / 从 InputArchive 恢复 |
值得注意的实现细节:源码删除了 Optimizer 的拷贝构造与拷贝赋值,并在注释中明确说明——若需复制一个优化器,应使用 state_dict / load_state_dict 式 API(即 save/load),而不是直接拷贝对象,因为优化器内部持有大量可变状态。
OptimizerOptions:选项的克隆与继承
OptimizerOptions 是所有优化器选项(SGDOptions、AdamOptions、AdamWOptions、AdagradOptions、RMSpropOptions、LBFGSOptions)的抽象基类,提供 clone()、序列化以及最核心的 get_lr() / set_lr(double) ——学习率调度器正是通过 set_lr 来改写各参数组学习率的。
紧随其后的模板类 OptimizerCloneableOptions<Derived> 则实现了参数组级选项继承:当用户为某个参数组只设置部分字段(例如 AdamOptions().weight_decay(0.1))时,未显式设置的字段会从优化器全局默认值继承,显式设置的字段被保留。其实现采用 SFINAE 探测 lr()、momentum()、weight_decay()、betas()、eps() 等访问器是否存在,再通过"与构造函数默认值比较"来区分字段是否被用户显式赋值(见 optimizer.h 中 _merge_by_comparison 的算法注释)。
OptimizerParamGroup:不同参数使用不同超参
OptimizerParamGroup 把一组参数与一组(可能不同的)选项绑定起来,便于对网络不同部分施以不同学习率或正则强度。其内部持有 std::vector<Tensor> params_ 与 std::unique_ptr<OptimizerOptions> options_,拷贝构造时会通过 options().clone() 深拷贝选项,保证 std::vector<OptimizerParamGroup> 可安全存储。
OptimizerParamState:算法运行时状态
OptimizerParamState 是每个参数"动量缓冲、一阶/二阶矩估计"等运行时状态的抽象基类,提供 clone() 与 serialize()。模板类 OptimizerCloneableParamState<Derived> 通过克隆派生类完成状态复制。例如 AdamParamState 就保存了 step(迭代计数)、exp_avg(一阶矩)、exp_avg_sq(二阶矩)与 max_exp_avg_sq(AMSGrad 最大二阶矩),见 adam.h。
如何选择优化器
官方文档从"模型架构、数据集与训练需求"三个维度,给出一张简明选型对照表(下表完整保留文档内容):
| 优化器 | 最适合 | 权衡 |
|---|---|---|
| SGD + Momentum | CNN、已被充分理解的问题、可精细调节超参的场景 | 学习率需要精心调节;通常能达到最佳最终精度 |
| Adam / AdamW | 通用任务、Transformer、快速原型验证 | 开箱即用效果好;使用 weight decay 时优先选 AdamW |
| RMSprop | RNN、非平稳目标函数 | 适合循环架构;能处理变化剧烈的梯度尺度 |
| Adagrad | 稀疏数据(NLP、Embedding) | 学习率随时间单调下降;适合低频特征 |
| LBFGS | 小模型、微调、凸问题 | 内存开销大;必须提供 closure 闭包函数 |
梯度下降类优化器:SGD
分篇文档 gradient_descent.md 指出,这类优化器基于梯度下降并可选动量等增强,是神经网络训练的基础,适合愿意投入时间精细调参的场景。经典的 SGD 配合动量经过妥善调节后,往往是卷积神经网络(ResNet、VGG 等)取得最高最终精度的选择。
主要超参与经验取值:
lr:学习率,CNN 常用 0.01–0.1;momentum:动量系数,典型值 0.9,加速收敛并抑制震荡;weight_decay:L2 正则系数;nesterov:是否启用 Nesterov 动量,常能进一步改善收敛。
对照 sgd.h 可以看到 SGDOptions 的完整字段与默认值:momentum = 0、dampening = 0、weight_decay = 0、nesterov = false。构造函数中还有一组 TORCH_CHECK 前置校验,其中特别值得注意的一条约束是:启用 Nesterov 动量要求 momentum > 0 且 dampening == 0,否则构造会直接抛出异常——这解释了为何使用 Nesterov 时通常需要显式 .dampening(0) 或保持默认。
官方示例(适用于 CNN 训练):
// 标准 SGD + Momentum —— CNN 训练常用配置
auto optimizer = torch::optim::SGD(
model->parameters(),
torch::optim::SGDOptions(0.01) // 学习率
.momentum(0.9) // 动量系数
.weight_decay(1e-4) // L2 正则
.nesterov(true)); // Nesterov 动量
自适应学习率优化器:Adam、AdamW、RMSprop、Adagrad
分篇文档 adaptive.md 统一介绍了根据历史梯度信息为每个参数自动适配学习率的一族算法。它们通常需要更少的超参调节,适用范围广。
Adam:默认的"开箱即用"选择
Adam(Adaptive Moment Estimation)融合了 RMSprop 与动量的思想,为每个参数维护自适应学习率。特别适合 Transformer 等注意力模型、快速原型迭代、以及不便做大规模超参搜索的通用任务。
其 AdamOptions 在 adam.h 中定义的字段与默认值为:
lr:默认 1e-3,文档给出常用区间 1e-3~1e-4;betas:默认{0.9, 0.999},一/二阶矩的指数衰减系数;eps:默认 1e-8,数值稳定项;weight_decay:默认 0,注意其施加方式与 SGD 不同(在 Adam 中与梯度耦合);amsgrad:默认 false,是否启用 AMSGrad 变体。
源码构造函数会对 lr >= 0、eps >= 0、weight_decay >= 0 做校验,并对两个 beta 强制 0 <= beta < 1。
// 标准 Adam 配置
auto optimizer = torch::optim::Adam(
model->parameters(),
torch::optim::AdamOptions(1e-3) // 学习率
.betas({0.9, 0.999}) // 动量项
.eps(1e-8) // 数值稳定
.weight_decay(0)); // L2 惩罚
// Transformer 常用:较低学习率 + warmup 配合
auto optimizer = torch::optim::Adam(
model->parameters(),
torch::optim::AdamOptions(1e-4)
.betas({0.9, 0.98})); // Transformer 中常将 β2 设为 0.98
AdamW:解耦权重衰减的 Adam
AdamW 修复了 Adam 中 weight decay 与梯度更新耦合的缺陷,把权重衰减直接作用于权重,行为更接近 SGD 中的 L2 正则。文档给出的两条更新公式清晰区分了两者:
- Adam:
weight = weight - lr * (grad + weight_decay * weight) - AdamW:
weight = weight - lr * grad - lr * weight_decay * weight
对比 adamw.h 可以发现,AdamWOptions 的默认 weight_decay 是 1e-2(而非 Adam 的 0),其余字段默认值与 Adam 一致(lr = 1e-3、betas = {0.9, 0.999}、eps = 1e-8、amsgrad = false),其校验逻辑也与 Adam 相同。
// AdamW 解耦权重衰减 —— Transformer 训练首选
auto optimizer = torch::optim::AdamW(
model->parameters(),
torch::optim::AdamWOptions(1e-4)
.betas({0.9, 0.999})
.weight_decay(0.01)); // 解耦后的正则项
RMSprop:面向 RNN 与变化梯度
RMSprop 通过近期梯度均方根的滑动平均来归一化学习率,特别适合 RNN/LSTM 与目标函数非平稳、梯度尺度波动大的问题,也适用于在线学习场景。分篇文档给出的关键参数有 lr(典型 1e-3~1e-2)、alpha(平滑常数,默认 0.99)、可选的 momentum,以及 centered(启用方差中心化版本)。
// RMSprop 用于 RNN 训练
auto optimizer = torch::optim::RMSprop(
model->parameters(),
torch::optim::RMSpropOptions(1e-3)
.alpha(0.99) // 平滑常数
.momentum(0.9) // 可选动量
.centered(true)); // 按方差归一化
Adagrad:稀疏特征的天然选择
Adagrad 依据梯度平方的累积和适配学习率:频繁更新的参数获得更小学习率,低频更新参数获得更大学习率,因此非常适合 NLP 稀疏特征、低频更新的 Embedding 层、以及用户/物品特征稀疏的推荐系统。官方同时提醒其局限:学习率单调递减可能导致训练过早停滞,长训练任务建议改用 Adam 或 RMSprop。
// Adagrad 用于稀疏 NLP 特征
auto optimizer = torch::optim::Adagrad(
model->parameters(),
torch::optim::AdagradOptions(0.01)
.lr_decay(0) // 学习率衰减
.weight_decay(0)
.initial_accumulator_value(0));
二阶优化器:LBFGS
分篇文档 second_order.md 介绍,二阶方法利用曲率信息(Hessian 或其近似)获得更优的更新步。LBFGS 是其中代表——一种以梯度历史近似逆 Hessian 的拟牛顿法,在光滑、近似凸的损失曲面上收敛速度可显著快于一阶方法。
适用场景为:内存无虞的小模型、预训练模型微调、凸或近凸问题、以及全批量(full-batch)而非 mini-batch训练。
对照 lbfgs.h,LBFGSOptions 的字段与默认值如下:
lr:默认 1(文档建议 LBFGS 常取 1.0,与一阶方法量级不同);max_iter:每次 step 的最大迭代次数,默认 20;max_eval:默认nullopt,构造函数会自动取max_iter * 5 / 4;tolerance_grad:默认 1e-7,梯度范数收敛判据;tolerance_change:默认 1e-9,变化量收敛判据;history_size:保留的过去梯度/步长数量,默认 100;line_search_fn:可选的行搜索函数名,默认nullopt。
源码中还硬性约束:LBFGS 不支持逐参数选项(参数组),参数组数量必须为 1。与一阶优化器最关键的区别是:step() 必须接收一个重新计算损失并反向传播的闭包。
auto optimizer = torch::optim::LBFGS(
model->parameters(),
torch::optim::LBFGSOptions(1.0)
.max_iter(20)
.history_size(10));
// LBFGS 需要一个每次迭代重算损失与梯度的闭包
for (int epoch = 0; epoch < num_epochs; ++epoch) {
auto closure = [&]() {
optimizer.zero_grad();
auto output = model->forward(data);
auto loss = loss_fn(output, target);
loss.backward();
return loss;
};
optimizer.step(closure);
}
学习率调度器:让训练后期更精细
分篇文档 schedulers.md 指出,训练中动态调整学习率往往能改善收敛与最终精度,常见策略包括:
- 步进衰减(Step decay):每隔 N 个 epoch 将学习率乘一个因子;
- 指数衰减(Exponential decay):每个 epoch 都乘以
gamma; - 余弦退火(Cosine annealing):沿余弦曲线平滑下降;
- Warmup:训练初期逐步抬高学习率。
LRScheduler 基类:持有优化器引用的调度中枢
调度器的实现载体位于 torch/csrc/api/include/torch/optim/schedulers/lr_scheduler.h。从源码看,LRScheduler 构造时持有一个 Optimizer& 引用(因此文档注释特别强调:调度器生命周期内必须保证优化器存活),对外暴露统一的 step();受保护接口 get_lrs() 由子类实现——它返回一个按参数组逐一给出新学习率的 vector(常规用法是全部元素相同);内部通过 set_optimizer_lrs() 与优化器的 set_lr() 通道完成实际写入,并维护 step_count_ 记录步数。调度器目录下还有 step_lr.h 与 reduce_on_plateau_scheduler.h 两个具体实现。
StepLR:最常用的一档多段衰减
每 step_size 个 epoch 将学习率乘以 gamma。官方示例演示了经典的"每 30 轮降 10 倍"方案:
auto optimizer = torch::optim::SGD(
model->parameters(),
torch::optim::SGDOptions(0.1));
// 每 30 个 epoch 学习率降为 1/10
auto scheduler = torch::optim::StepLR(
optimizer,
/*step_size=*/30,
/*gamma=*/0.1);
for (int epoch = 0; epoch < 90; ++epoch) {
train_one_epoch(model, optimizer, data_loader);
scheduler.step();
// 学习率轨迹:0.1 (epochs 0-29), 0.01 (30-59), 0.001 (60-89)
}
ExponentialLR:逐轮平滑衰减
每轮把学习率乘以 gamma,衰减比 StepLR 更平滑,但学习率下降相对偏慢:
auto optimizer = torch::optim::Adam(
model->parameters(),
torch::optim::AdamOptions(1e-3));
// 每个 epoch 学习率降为 95%
auto scheduler = torch::optim::ExponentialLR(
optimizer,
/*gamma=*/0.95);
for (int epoch = 0; epoch < num_epochs; ++epoch) {
train_one_epoch(model, optimizer, data_loader);
scheduler.step();
}
ReduceLROnPlateau:按验证指标响应式降学习率
ReduceLROnPlateauScheduler 的调度策略不遵循固定曲线,而是当某个监控指标(如验证损失)停止改善时再降低学习率,因此适合"让调度器响应验证损失而非机械计时"的场景。在使用时,需在监控指标更新后触发对应 step 逻辑。
组合起来:带调度器的完整训练示例
分篇文档最后给出了一个自包含的可运行示例,完整覆盖"模型 → 优化器 → 调度器 → 损失函数 → 训练循环 → 每轮打印损失与学习率"的链路,并演示了通过 scheduler.get_last_lr()[0] 读取当前学习率的用法:
#include <torch/torch.h>
struct Net : torch::nn::Module {
Net() {
fc1 = register_module("fc1", torch::nn::Linear(784, 256));
fc2 = register_module("fc2", torch::nn::Linear(256, 10));
}
torch::Tensor forward(torch::Tensor x) {
x = torch::relu(fc1->forward(x.view({-1, 784})));
return fc2->forward(x);
}
torch::nn::Linear fc1{nullptr}, fc2{nullptr};
};
int main() {
// 创建模型
auto model = std::make_shared<Net>();
// 创建优化器(带权重衰减)
auto optimizer = torch::optim::AdamW(
model->parameters(),
torch::optim::AdamWOptions(1e-3)
.weight_decay(0.01));
// 学习率调度器:每 10 个 epoch 减半
auto scheduler = torch::optim::StepLR(optimizer, 10, 0.5);
// 损失函数
auto loss_fn = torch::nn::CrossEntropyLoss();
// 训练循环
for (int epoch = 0; epoch < 30; ++epoch) {
model->train();
double epoch_loss = 0.0;
for (auto& batch : *train_loader) {
optimizer.zero_grad();
auto output = model->forward(batch.data);
auto loss = loss_fn(output, batch.target);
loss.backward();
optimizer.step();
epoch_loss += loss.item<double>();
}
scheduler.step();
std::cout << "Epoch " << epoch
<< " Loss: " << epoch_loss
<< " LR: " << scheduler.get_last_lr()[0]
<< std::endl;
}
return 0;
}
该示例同时体现了本文的核心要点:优化器只负责"怎么走",调度器负责"每步走多大",二者在每一轮结束处(epoch 级)以 scheduler.step() 桥接。初学者把它作为模板替换数据集、网络结构与损失函数,即可快速得到一套可调参的 C++ 训练管线。
进阶:参数组与状态序列化
参数组:为不同层定制超参
OptimizerParamGroup 允许对同一优化器中的不同参数子集使用差异化选项。结合 OptimizerCloneableOptions 的继承语义,可以写出"只指定部分字段、其余继承默认值"的代码——这与 Python 侧 optimizer = Adam([{'params': params1, 'weight_decay': 0.1}], lr=0.01) 的行为对齐:显式给出的 weight_decay = 0.1 被保留,未给出的 lr = 0.01 从全局默认继承(机制说明见 optimizer.h 的类注释)。
断点续训:save / load 优化器状态
Optimizer 基类提供了 save(serialize::OutputArchive&) 与 load(serialize::InputArchive&)(见 optimizer.h),配合 torch::serialize 与头文件 serialize.h 可将参数组选项与各参数状态(动量缓冲、矩估计等)持久化,是实现"保存检查点、中断后续训"的关键。源码注释还明确了序列化语义与 Python API 严格一致:选项中的 std::nullopt 会被写入归档;参数状态中"未定义张量 / std::nullopt"等价于 Python 侧的缺失键,因此不会被序列化。若需完整复制一个优化器,官方同样推荐走归档导出/导入而非对象拷贝(拷贝构造已被删除)。
小结
PyTorch C++ 前端通过 torch::optim 把 Python 侧成熟的优化器体系完整移植到 C++:统一基类与"Options / ParamGroup / ParamState"四件套保证了各算法接口一致、状态可序列化;SGD、Adam、AdamW、RMSprop、Adagrad 与 LBFGS 覆盖了从 CNN、RNN、Transformer 到稀疏特征与全批量优化等主流训练形态;而 LRScheduler 家族(StepLR、ExponentialLR、ReduceLROnPlateau)让学习率策略可以像 Python 侧一样方便地接入训练循环。需要更细粒度资料时,可继续查阅同目录下的 梯度下降篇、自适应算法篇、二阶方法篇 与 调度器篇,或直接深入 torch/csrc/api/include/torch/optim/ 下的头文件研读实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00