torch.where:输出正常,梯度为何仍是NaN

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

损失是有限值,梯度仍可能出现NaN,先检查掩码放在了哪里。先对整批数据除法,再挑出有效结果,只改变最终使用哪些值,不会自动让前面已经发生的除零从计算路径消失。修复应从危险运算之前开始,而不是只把最终输出变好看。

文章目录
  1. 前向选中的值,不等于整条路径都有效
  2. where选值,不是Python的惰性分支
  3. 乘掩码,连前向都未必清干净
  4. 先筛输入,或先保护操作数
  5. 十二项检查,分清实际跑了什么
  6. 全被掩掉时,先定义训练步骤怎么办
  7. 沿最早出现异常的位置排查
  8. 来源与验证范围

这里回答torch.where与损失mask中常见的一个误区:输出没有NaN,为什么反向仍可能异常?用两项数组拆开前向选择与导数计算,再比较先筛选输入、先保护操作数两种写法。Read in English。

前向选中的值,不等于整条路径都有效

PyTorch 2.8的Autograd说明直接给出反例:先做除零,再用索引排除无效输出,反向仍可能产生NaN;文档建议在除法前应用掩码。这是官方示例支持的框架行为,不是本文运行的模型测试。

把问题缩成x=(1,3)、d=(0,2),只让分母非零的第二项参与损失。整批除法产生(inf,1.5),随后选择有效项,损失是1.5。看损失和最终输出,会觉得一切正常;看第一项已经执行过的除法,就能找到不同证据。

若把无效输出置零,进入除法反向的上游系数可以写成(0,1)。对x的局部导数路径会涉及上游系数除以d,第一项落到0/0,不能通过“乘上零就没贡献”来当成普通实数零。我们要避免的是未定义运算进入求导路径,而不只是避免它出现在最终打印值里。

先除法:x/d → (inf,1.5)
再选值:输出(0,1.5)
损失1.5:前向标量有限
局部反向:0/0仍未定义
原创路径图:输出选择不等于删除已经发生的危险运算。

where选值,不是Python的惰性分支

torch.where定义根据条件从输入或另一组值中逐元素选择。写where(mask,x/d,0)时,普通Python调用先求出参数里的x/d,再把结果交给where;不能把函数选值理解成只为True位置执行除法。

Python表达式求值规则与条件表达式也要分开看。标量的a if condition else b可以只选择一个分支,但一批不同位置的布尔条件,不会因为传入名叫where的函数就自动成为逐元素短路求值。本文讨论普通立即执行路径,不推断编译器可能做的优化。

NumPy也有逐元素选择接口,因此下面可以实际执行数组算术,观察原始inf、选值后的有限数组和手写导数表达式里的NaN。NumPy不提供这里的PyTorch自动微分;数值实验不能冒充框架backward测试。

乘掩码,连前向都未必清干净

另一个写法是把整个商乘以0/1掩码。对普通有限值,乘零确实得到零;对inf,浮点运算中的0×inf得到NaN。因此选择无效位置输出零,和先算商再乘零,不是可以随意互换的数值程序。

这说明要检查三个不同对象:危险运算的原始输出、进入最终归约的值、进入反向计算的局部表达式。只做loss的isfinite检查,最多证明那个标量此刻有限,不能替代中间值和预期参数梯度的检查。

处理NaN时也别只检查d!=0。NaN与零不相等,会通过这个简单条件。有效谓词需要符合任务定义:例如本例同时要求分子分母有限且分母非零。真实任务若还涉及对数、开方或其他定义域,条件应放在对应的第一处危险运算之前。

先筛输入,或先保护操作数

第一种方法先取有效位置,只对x[valid]和d[valid]做除法,再按需要散回预先清零的数组。这样被排除位置没有执行除法。对固定掩码、固定d的本轮目标L=x₂/2,关于x的导数是(0,0.5),可以独立用有限差分核对。

第二种方法保留稠密形状,但在除法前构造安全操作数:无效位置的分子替换为0,分母替换为1,再做除法。本例得到相同(0,1.5)。它不是把所有真实分母都夹成一个正epsilon,也没有在有效位置改变原本的分数。

还有一种需要分开处理的情况:分母非零但非常小。取x=3、d=0.001,商为3000,对x的导数为1000,二者都有限。若把分母硬改为0.01,商变成300、导数变成100;数值变小,但目标也变了。有限不等于尺度合理,稳定化规则需要任务依据,不能与排除未定义位置混为一谈。

两种方法都需要检查实际形状、广播和数据类型。先筛选可能改变参与计算的数据规模,稠密保护则保留一部分无效位置的安全计算;哪个更快或更省内存,要测目标设备上的完整路径,不能从两行代码推断通用性能结论。

先定义有效位置
路线A:仅对有效切片做除法
路线B:先替换无效分子0、分母1
固定mask目标:∂L/∂x=(0,0.5)
原创修复对照:先保护定义域,再检查有效位置的梯度是否保留。

十二项检查,分清实际跑了什么

程序实际使用NumPy执行数组算术,显式写出masked_chain的数值公式,并验证两种前置保护。危险路径仅为展示保留,局部关闭警告;安全路径改为遇到除零或无效运算就抛错,不能把静音警告当成修复。

# NumPy arithmetic and explicit derivative formulas; no PyTorch autograd.
import numpy as np
x=np.array([1.,3.]); d=np.array([0.,2.])
valid=np.isfinite(x)&np.isfinite(d)&(d!=0)
with np.errstate(divide='ignore',invalid='ignore'):
    raw=x/d
    selected=np.where(valid,raw,0.)
    masked_chain=valid.astype(float)/d
    multiplied=raw*valid
assert np.isinf(raw[0])
assert selected.tolist()==[0.,1.5]
assert np.isfinite(selected.sum())
assert np.isnan(masked_chain[0]) and masked_chain[1]==.5
assert np.isnan(multiplied[0])

with np.errstate(divide='raise',invalid='raise'):
    safe=np.zeros_like(x)
    safe[valid]=x[valid]/d[valid]
    safe_x=np.where(valid,x,0.)
    safe_d=np.where(valid,d,1.)
    guarded=safe_x/safe_d
assert safe.tolist()==[0.,1.5]
assert np.array_equal(guarded,safe)
expected=np.zeros_like(x); expected[valid]=1/d[valid]
assert expected.tolist()==[0.,.5]

def fixed_mask_loss(z):
    return (z[valid]/d[valid]).sum()
for j in range(2):
    plus=x.copy(); minus=x.copy()
    plus[j]+=1e-5; minus[j]-=1e-5
    numerical=(fixed_mask_loss(plus)-fixed_mask_loss(minus))/(2e-5)
    assert np.isclose(numerical,expected[j],rtol=1e-8,atol=1e-8)
empty=np.zeros_like(valid,dtype=bool)
assert (x[empty]/d[empty]).sum()==0.
assert not (np.isfinite(np.array([np.nan])) & (np.array([np.nan])!=0)).any()
print('12 arithmetic and fixed-mask derivative checks passed; NumPy',np.__version__)

两个有限差分都只扰动x,保持valid和d固定。它们验证这一轮固定掩码的标量目标,不验证关于掩码条件的导数,也不验证在分母穿越0时的平滑性。当前环境未安装PyTorch,没有执行torch.where、自动微分或GPU内核。

全被掩掉时,先定义训练步骤怎么办

有效项为空时,求和可以定义为0,但对有效项求均值还需要明确空集合策略。不能让“计数为0”悄悄流入平均损失分母,再寄希望于最后一层mask。应把无有效样本作为单独测试情形,而不是只测试混合有效与无效项。

如果业务选择跳过这一步,控制流应明确跳过相关更新;不能仅凭损失为0就断言参数不会变化。优化器状态和权重衰减另有语义,站内None与零梯度的更新区别有相应反例。这里不把空批次策略伪装成普适训练规则。

若条件依赖模型输出,也要分清“按当前条件选择一条分段路径”与“整个目标在边界可微”。修复数值定义域不代表消除了选择边界,有限差分测试需要明确自己是在固定区域内比较,还是在检验跨边界行为。

沿最早出现异常的位置排查

调试时先缩成能复现的确定性输入,记录有效位置与首个非有限中间值,再沿图检查损失和相关参数梯度。比较的是同一数学目标、相同有效集合与归约方式;如果修复同时删除样本、修改分母或改变平均口径,就不能只凭NaN消失判定等价。

特别是当x本身由上游的未定义运算生成,直到这里才用where把x替换掉,仍可能太晚。前置保护应移动到最早的定义域边界。输出修补不是对整个上游计算图的自动清洗。

这与冻结特征与梯度边界的主题相邻,但问题不同:今天这里需要保留有效位置的梯度,而不是一概关闭梯度记录。为了掩盖NaN把整段放进no_grad,会同时改变训练目标。

如果这组前后顺序的反例帮你定位了“loss正常、梯度异常”,欢迎通过文末自愿支持。可以直接复用的是危险路径、前置保护与固定掩码导数的三组对照。

来源与验证范围

Autograd division-by-zero note · torch.where · Python expression evaluation · NumPy where

按PyTorch 2.8定义及2025年6月16日更新的Autograd说明核对。图形与扩展例子为原创;实际执行NumPy算术和显式导数检查,没有运行PyTorch。计划文章日期为2026年10月21日23:30北京时间,排期或实际公开状态另行核验。

支持

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

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

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

Share this Post

Leave a Comment

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

*
*