zero_grad:None与零梯度为何更新不同

黎 浩然/ 19 10 月, 2026/ 大语言模型/LARGELANGUAGEMODEL/LLM, 教程/TUTORIAL, 计算机/COMPUTER/ 0 comments

zero_grad(set_to_none=True)与把梯度张量清成零,不只是内存写法不同。对PyTorch优化器,grad为None表示跳过该参数的本次step;grad是零张量则仍可能进入更新流程,权重衰减或历史动量可能继续改变参数。

文章目录
  1. 先区分没有梯度和数值为零
  2. 零新梯度,衰减仍可以执行
  3. 历史动量也能让零梯度移动参数
  4. 九项检查把参数和状态一起对照
  5. set_to_none=False不保证给所有参数创建零梯度
  6. 冻结参数要有明确的更新契约
  7. 用一次真实step做最小验收
  8. 别把状态差异压成一个零
  9. 来源与补发说明

因此,“本批没有给这个参数新梯度”“本批导数恰好为零”“参数已经冻结”是三个不同判断。下面用两条精确更新路径解释差异,再给出适用于条件分支和分阶段微调的状态核验方法。Read in English。

先区分没有梯度和数值为零

PyTorch 2.8的zero_grad说明明确区分None与零梯度:前者使优化器跳过该参数,后者按零梯度执行step。set_to_none=True之后,没有收到梯度的参数会保持grad为None。这里讨论该版本内置优化器的约定,不把它推广为所有自定义优化器的共同规则。

None是缺少梯度张量的状态;零张量则是一个存在、形状明确、元素为零的对象。一个参数参与了可微计算,但本批导数抵消为零,也可能得到零张量。反过来,没有参与本批路径时,若残留着以前清零后的张量,优化器看到的也不一定是None。

检查时应同时记录grad是否存在、范数或元素值,以及参数是否仍在优化器参数组中。只打印一个梯度范数,甚至把None先映射成0方便展示,会丢掉这篇最重要的状态差别。

grad=None:跳过本参数step
grad=0:进入更新路径
参数变化与状态变化分别核验
原创状态图:缺少梯度张量不等于张量数值为零。

零新梯度,衰减仍可以执行

AdamW定义把权重衰减与梯度矩估计分离。为隔离这个因素,先看没有历史矩贡献的教学步骤:θ=1,学习率η=0.1,衰减λ=0.1。若零梯度仍让参数进入step,仅衰减项就给出θ′=(1−ηλ)θ=0.99。

若该参数grad为None,按照上述跳过约定,该step不执行在这个参数上,θ仍为1。这不是在说所有零梯度步骤的AdamW结果必定是0.99;一旦存在历史一阶矩、二阶矩和步数,它们也会影响完整更新。

这个分离例子说明,清零并不等于“不对参数做任何事”。衰减是更新流程的一部分,不要求本批损失导数非零。若某层只在部分批次激活,None与零的选择可能影响它实际经历了多少次衰减,而不是只影响日志显示。

历史动量也能让零梯度移动参数

再看SGD的动量公式,限定已有动量缓冲区、无衰减、无dampening、无Nesterov:b′=μb+g,θ′=θ−ηb′。给定θ=1、b=2、μ=0.9、η=0.1,即使g=0,也有b′=1.8、θ′=0.82。

如果本次以None跳过该参数,这个教学状态保持θ=1、b=2。下一次重新激活时,从哪个缓冲区继续,取决于之前到底执行还是跳过了更新。只比较这一步的梯度数值,解释不了之后轨迹为什么不同。

学习率设为0也不是完整状态冻结的充分条件。在上述零梯度动量路径里,θ可以不动,但b仍从2变成1.8。参数没变化和优化器状态没变化,需要分别验证;它们对未来重新训练的含义不同。

隔离衰减:1 → 0.99
已有动量:θ 1 → 0.82,b 2 → 1.8
None跳过:θ 1、b 2保持
原创算例对照:两条独立教学路径,不是完整AdamW与SGD基准。

九项检查把参数和状态一起对照

下面两个小函数分别隔离衰减和已有动量的影响。它们不是完整SGD或AdamW实现,不含首次动量初始化、偏差修正、混合精度或张量后端。None分支按本文讨论的跳过约定保留状态。

# Exact teaching transitions, not complete SGD/AdamW implementations.
from fractions import Fraction as F

def decay_only(theta, grad, lr=F(1,10), wd=F(1,10)):
    if grad is None:
        return theta
    if grad != 0:
        raise ValueError("this toy handles zero gradients only")
    return theta*(1-lr*wd)

def momentum_step(theta, buffer, grad, lr=F(1,10), mu=F(9,10)):
    if grad is None:
        return theta,buffer
    next_buffer=mu*buffer+grad
    return theta-lr*next_buffer,next_buffer

theta=F(1)
assert decay_only(theta,None)==1
assert decay_only(theta,F(0))==F(99,100)
assert decay_only(theta,F(0),wd=F(0))==1
assert momentum_step(theta,F(2),None)==(F(1),F(2))
assert momentum_step(theta,F(2),F(0))==(F(41,50),F(9,5))
assert momentum_step(theta,F(0),F(0))==(F(1),F(0))
assert momentum_step(theta,F(2),F(0),lr=F(0))==(F(1),F(9,5))
a,b=momentum_step(theta,F(2),F(0))
assert momentum_step(a,b,F(0))==(F(329,500),F(81,50))
assert decay_only(theta,None)!=decay_only(theta,F(0))
print("9 missing-versus-zero gradient checks passed")

本地九项检查通过,包括None、零梯度、零衰减、无历史动量、零学习率和连续两个零梯度步骤。第二个动量步骤得到θ=0.658、b=1.62,说明状态变化会延续到后续迭代。精确分数避免舍入掩盖差异,但没有测量真实训练效果。

set_to_none=False不保证给所有参数创建零梯度

该版本zero_grad实现只对已经存在的梯度进入清理路径。一个从未得到梯度、当前grad就是None的参数,不能仅凭调用set_to_none=False就假定它一定获得了新建零张量。本文的对照前提是优化器实际看到的状态分别为None和零,而不是只根据调用参数猜结果。

对条件分支,先运行一个确实激活该参数的批次,让它产生梯度,再分别清成None或零;下一批让分支不激活,并比较step前后。这样才能观察“过去有梯度、本批没有梯度”的差别。只用一个从未激活的全新参数,可能得到两条都为None的路径,误以为没有语义差异。

生产训练中还要确认前一批梯度没有残留。若需要梯度累积,就不能每个微批随意清空;若不需要累积,就应保证更新单元开始时状态符合约定。清梯度边界和是否跳过参数,是相关但不同的两件事。

这里的跳过以单个参数为单位。其他参数仍可能更新,全局学习率调度器或外部计数也可能独立推进;保留本参数的优化器状态,不等于暂停整个训练循环。

冻结参数要有明确的更新契约

“冻结”可能指不记录新梯度、暂时不被优化器更新、或者连同历史状态一起保持不变。先选择项目需要哪一种,再决定requires_grad、梯度清理和参数组如何管理。不能把某一个开关当成所有含义的替代。

例如更改requires_grad后,如果旧grad仍存在且参数仍由优化器管理,必须检查step实际会读到什么。它是否应该移出参数组、是否保留动量以便恢复、是否重置历史状态,都应作为分阶段训练方案明确定义,而不是在排错时临时猜测。

恢复训练也不能只对照参数权重。若需要重现更新轨迹,应核对优化器状态、学习率计划和参与更新的参数集合。此前AdamW与L2的区别讨论更新项含义,本篇补充的是:这些项究竟有没有在某个参数上执行。

用一次真实step做最小验收

在项目支持的环境,准备两个相同模型与相同优化器状态副本。先确认同一参数已有历史状态,再分别构造None与零梯度,运行一次step,比较参数值与所有相关状态。不要只比较loss,因为本步根本不需要改变前向loss就能出现更新差异。

随后加上项目实际使用的foreach或fused路径、AMP、参数共享与条件分支,逐项验证。自定义优化器尤其要读其grad为None时的实现。这篇没有运行这些后端验收,所以不会声称九项算术断言已经证明你的训练栈行为。

测试报告可以直接保留“是否进入step、θ前后、状态前后、grad是否None、配置版本”五类信息。它们比“这层梯度为0,所以应该不变”更能说明发生了什么。

别把状态差异压成一个零

选择set_to_none应同时考虑更新语义与实际实现,而不只看少写了多少内存。对条件激活参数,先决定未参与批次是否应该衰减或推进状态,再检查优化器实际契约。不存在脱离目标训练方案、永远正确的一种清梯度写法。

若这组算例和状态对照帮你解释了“梯度为零,参数却还在变”,欢迎通过文末支持区自愿支持。把缺少梯度与零梯度分开记录,是让分阶段训练更容易复现的一步。

来源与补发说明

AdamW and zero_grad、SGD

依据原计划时段前已有的PyTorch 2.8版本定义核对。推导、算术代码和两幅图为原创教学内容。计划文章日期为2026年10月19日23:30,实际公开时间另行核验报告。未运行PyTorch训练、分布式进程或GPU性能测试。

支持

如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*