KLDivLoss:负值、方向与平均分母
KLDivLoss算出负数,先检查传入的是否是log概率,以及target是否为归一化分布;数值小了一个类别数,则检查reduction。它的参数顺序与数学记号不直观,误把概率当作log概率后,代码仍可能返回一个有限数,却已经不是你想计算的KL散度。
文章目录
这里回答“KLDivLoss的input和target该放什么,mean与batchmean为什么不同”,用一个二分类分布把方向、数值空间和分母逐项拆开。Read in English。
先把数学方向写在调用旁边
设P是目标分布,Q是模型分布,所需量为D(P∥Q)=ΣᵢPᵢ(log Pᵢ−log Qᵢ)。这不是对称距离:通常交换P与Q会改变结果。本文算例限定两个分布均严格为正、每行和为1,使用自然对数。
PyTorch 2.8定义要求input为log空间;默认target为概率,log_target=True时target也为log空间。因此要算D(P∥Q),对应input=log Q、target=P,而不是把数学记号从左到右直接塞进两个参数。
拿P=(0.8,0.2)、Q=(0.5,0.5),计算得到0.8log1.6+0.2log0.4,约0.192745。反方向D(Q∥P)约0.223144。这两个数都可以合法,但服务于不同目标;不能只看哪一个更小来决定参数顺序。
把概率误当log概率,为什么会变负
如果把Q的概率值0.5直接放进input,点式表达式就变成ΣPᵢ(log Pᵢ−Qᵢ)。本例约为−1.000402,已经没有log Q项,自然不再受正规KL非负性的保证。不是模型“比完美更好”,而是函数输入的表示错了。
正规分布下非负性针对沿整个概率支持求和后的KL,不针对每个类别项。上述正确算例中,第二项0.2log0.4本来就是负的,只是总和仍为正。因此看到reduction=’none’里的负元素,也不能直接判定接口出错。
还要区分量级很小的浮点负误差与明显的表示错误。只有先确认方向、归一化、支持与输入空间,讨论误差容限才有意义。这里使用的负一左右反例足以暴露概念问题,不是在建议给所有训练任务设置同一个数值阈值。
两行两类,分母差在哪里
构造一个2×2批次:第一行使用上面的P与Q,第二行两者都为(0.5,0.5)。两行逐类别项求和为S≈0.192745。按样本平均是S/2≈0.0963724;按四个元素平均是S/4≈0.0481862,刚好相差类别数2。
接口的batchmean将元素总和除以第一维大小,mean则按所有元素取平均。对明确形状[B,C],每一行就是一份分布时,前者对应每样本KL的平均。若张量为[B,T,C]且每个时间步是一份分布,batchmean仍只除B,得到的是每样本跨时间总和的平均,不会自动变成每token平均。
因此序列任务需要先决定计量单位。若存在padding,可先得到逐元素项、沿类别求和,再按有效token掩码累加,除以有效token数。没有有效token时应跳过该目标或采用已定义的空批策略;分母为0时应明确处理,而不是拿一个任意常数掩盖无有效数据的批次。这是目标函数设计,不能靠更换reduction名字自动完成。
log_target只改变表示,不改变方向
默认表示是Pᵢ(log Pᵢ−log Qᵢ)。若target保存的是aᵢ=log Pᵢ,同一项可写成exp(aᵢ)(aᵢ−log Qᵢ)。两式相等,前提是a真的是对应概率的自然对数。设置log_target=True不会帮你交换KL方向,也不是对原始logits自动归一化。
模型输出若是logits,可使用沿正确类别轴的log_softmax得到log概率。类别轴写错,形状可能仍合法,却改变了归一化对象。目标来自另一个模型时,也要明确其分布构造方式,而不是把任意浮点数组叫作软标签。
本文严格正分布避免了零概率边界。实际应用中P=0、Q=0涉及零贡献与无穷散度等情况;不能把教学代码中的log直接用于零值,也不能默认加ε不会改变目标。需要依据实际支持和数值策略另行验证。
九项检查把三个错误分开
下面代码验证正确数值、相同分布、方向差异、log表示等价、错误空间的负结果以及两种分母,并拒绝一个未归一化输入。它使用标准库math,既没有运行PyTorch,也没有测试反向梯度。
# Positive normalized distributions; standard-library math only.
from math import log, exp, isclose
def kl(p, q):
if len(p) != len(q) or not p:
raise ValueError('shape')
if min(p+q) <= 0 or not isclose(sum(p),1) or not isclose(sum(q),1):
raise ValueError('positive normalized probabilities required')
return sum(a*(log(a)-log(b)) for a,b in zip(p,q))
p=(0.8,0.2); q=(0.5,0.5)
d=kl(p,q)
assert isclose(d,0.19274475702175753)
assert d > 0 and isclose(kl(p,p),0)
assert not isclose(d,kl(q,p))
log_input=tuple(map(log,q))
log_target=tuple(map(log,p))
by_log=sum(exp(a)*(a-b) for a,b in zip(log_target,log_input))
assert isclose(by_log,d)
wrong=sum(a*(log(a)-b) for a,b in zip(p,q))
assert wrong < 0
B,C=2,2
summed=d+kl((0.5,0.5),(0.5,0.5))
assert isclose(summed/B, d/2)
assert isclose(summed/(B*C), d/4)
assert isclose((summed/B)/(summed/(B*C)), C)
try:
kl((0.8,0.8),q)
except ValueError:
pass
else:
raise AssertionError('unnormalized target accepted')
print('9 KL arithmetic checks passed')
这个校验函数刻意只接受严格正、归一化的向量。拒绝零值是示例范围限制,不是声称KL在所有含零分布上都没有定义。把教学前提写进输入校验,能防止示例被无提示地推广到不适用的场景。
对照损失时保留分子与分母
调试记录至少应含方向D(P∥Q)、两侧表示、类别轴、逐分布求和方式与最终分母。两个实验loss相差常数时,先对齐这些定义,再讨论模型质量或学习率。改变类别数、序列长度和padding比例,都会暴露隐藏的分母差异。
此前的ignore_index分母分析也强调先确认哪些位置参与目标。KL额外需要处理分布方向与log空间,不能只沿用另一个损失函数的“mean”直觉。
如果这组数值对照帮你定位了负KL或损失缩放问题,欢迎通过文末支持区自愿支持。把方向、表示和分母分开记录,是下次迁移蒸馏目标时仍能复用的方法。
来源与补发说明
按原计划时段前已有的PyTorch 2.8版本定义核对,推导和图示为原创。本次补发保留10月20日19:00北京时间文章日期,实际公开时间另行核验。执行了标准库示例,没有运行PyTorch集成测试或设备性能测量。
支持
如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。
二维码可点击放大。更多支持方式见支持页面。


