继承 torch.optim.Optimizer 时必须重写 step() 方法,因其为纯虚函数,否则调用即抛 NotImplementedError;需确保 step() 是带 self 的实例方法,正确初始化 param_groups,并在更新参数前检查 p.grad 是否为 None 且用 torch.no_grad() 包裹。

继承 torch.optim.Optimizer 时必须重写 step() 方法
不重写 step(),你的优化器根本不会更新参数——PyTorch 的 Optimizer 基类里 step() 是纯虚方法,调用会直接抛 NotImplementedError。
常见错误现象:定义类后一调用 optimizer.step() 就报错 NotImplementedError: step() method must be overridden,不是漏写了,就是写了但没在当前类作用域下(比如缩进错、写在了 if 里、或被装饰器意外拦截)。
实操建议:
-
step()必须是实例方法,带self参数,且不能加@staticmethod或@classmethod - 务必在
__init__中调用super().__init__(params, defaults),否则param_groups不生效 - 别忘了在
step()开头加loss = None判断(如果支持传loss),这是和torch.optim.SGD等保持行为一致的关键
param_groups 是参数容器,不是原始参数列表
你拿到的 self.param_groups 是一个 list,每个元素是 dict,含 'params'(Parameter 对象的 iterator)、'lr'、'weight_decay' 等键。直接对 param_groups[0]['params'] 迭代,才是访问真实模型参数的正确方式。
立即学习“Python免费学习笔记(深入)”;
容易踩的坑:
- 误以为
self.param_groups是 flat 参数列表,试图for p in self.param_groups:—— 实际上会遍历 group 字典,不是参数 - 手动修改
p.data时没调用p.grad检查是否为None,导致NoneType错误;PyTorch 允许梯度为None(如冻结层),必须显式跳过 - 在
step()中改动param_groups[i]['lr']只影响后续 step,不影响本次计算——学习率是在取p.grad后才用于更新的
自定义更新逻辑要绕开 torch.no_grad() 自动管理
PyTorch 的 step() 默认不在 torch.no_grad() 上下文中运行,但你的更新操作(比如 p.data -= lr * p.grad)必须禁止梯度追踪,否则会污染计算图、爆显存、甚至训练崩溃。
所以不能依赖“外部已关梯度”,得自己包一层:
with torch.no_grad():
for group in self.param_groups:
lr = group['lr']
for p in group['params']:
if p.grad is None:
continue
# 这里写你的更新公式,例如:
p.data.add_(p.grad, alpha=-lr)
性能影响:torch.no_grad() 开销极小,但漏掉它会导致 p.data 被加入计算图,后续反向传播时尝试对非叶子节点求导,报 RuntimeError: leaf variable has been moved into the graph interior。
调试时优先检查 p.grad 和 p.requires_grad
自定义优化器跑不通,80% 的问题出在梯度没传进来,而不是 step 写错了。别急着改算法,先确认基础链路通不通。
快速验证步骤:
- 在
step()开头加print([p.grad is not None for group in self.param_groups for p in group['params']]) - 检查模型参数是否都设了
requires_grad=True(尤其加载预训练权重后常被设为False) - 确认 loss 是标量且调用了
loss.backward(),且没有在 backward 前清空过grad - 避免在
forward中使用torch.no_grad()或.detach()断掉了梯度流
兼容性注意:PyTorch 2.0+ 对 param_groups 结构更敏感,如果手动往 group 里塞非标准 key(比如 'my_flag'),某些高阶 API(如 torch.compile)可能忽略或报 warning,但一般不影响 step 执行。
真正复杂的是多参数组 + 不同更新规则 + 梯度裁剪 + 学习率预热混合时的状态同步——这时候每个 group 的 state 字典怎么初始化、何时更新、要不要跨 group 共享缓冲区,才是容易被忽略的点。


















