KLDivLoss:负损失是怎么来的
KLDivLoss出现明显负值时,先检查input是不是对数概率,而不是先把负值截成零。默认接口计算target×(log(target)−input):把softmax概率误放进input,会得到另一个表达式,它没有KL非负性的保证。
文章目录
例如教师q=(0.75,0.25)、学生p=(0.5,0.5),正确KL约为0.130812;误把p直接传入对数输入位置,结果约为−1.062335。下面从这个反例推到方向、log_target和归约分母,避免只修正一个符号。Read in English。
先写清究竟在比较哪两个分布
令q是教师或目标分布,p是学生分布。本文比较D(q‖p)=Σᵢqᵢ(log qᵢ−log pᵢ),对类别求和。它对q加权,通常不等于D(p‖q)。写出这个式子,比只在变量上写teacher和student更可靠。
这里先限定每项概率严格为正、各自和为1,并使用自然对数。这个前提使后面的标准库示例可以直接调用log;数学上qᵢ=0的项按极限约定为0;若qᵢ>0而pᵢ=0,则该项为正无穷。实现不能直接依赖log(0)之后的普通乘法来自动获得这些约定,教学函数也不是完整损失库。
数学上的非负性属于完整、归一化分布的KL总和,不属于每个类别的单项。某一项qᵢlog(qᵢ/pᵢ)可以是负的,其他项会补偿。看到reduction=none的局部负值,与看到有效分布总KL显著为负,是两种问题。
API的第一个参数,不是公式的第一个分布
PyTorch 2.14参考规定input使用log空间;默认target使用概率空间,log_target=True时target也使用log空间。由此可知,要算D(q‖p),应把log p放在input、q放在默认target,而不是照着KL符号从左往右排列参数。
当target也是log q时,对应点式是exp(log q)×(log q−log p)。这只是表示形式改变,没有把方向反过来;log_target开关也不会把传入的任意数值自动变成合法分布。
学生logits通常沿类别轴做log_softmax。官方文档说明它用组合形式改善数值稳定性;不要为满足“先softmax再取log”的字面顺序,额外引入概率下溢的中间值。
一个负值反例,定位输入单位
在q=(0.75,0.25)、p=(0.5,0.5)中,正确表达式为0.75log(1.5)+0.25log(0.5),约0.130812。对调两个分布得到另一数值,不能把这个方向差异归咎于平均方式。
若input错误地直接使用p,点式求和变成Σqᵢlog qᵢ−Σqᵢpᵢ。前一项是−H(q),后一项在本例为0.5,所以结果是−H(q)−0.5≈−1.062335。这不是“KL允许为负”的证据,而是计算对象已经变了。
这类明显负值不该通过abs或clamp修补。它们会遮住输入契约错误,并可能改变梯度。先检查分布、表示空间和方向,再看浮点误差;接近零的微小负数与本例量级不同,需要结合精度和容差判断。
十二项检查复现方向与负值
代码只使用Python标准库,对上面两个严格正概率分布做数值检查。没有运行PyTorch、蒸馏训练或GPU;zip在这里只用于已知同长元组,生产接口应单独验证形状,不能依赖它静默截短。
# Positive, normalized teaching distributions; no PyTorch/model run.
from math import log, exp, isclose
def point_sum(log_input, target):
return sum(q*(log(q)-x) for x,q in zip(log_input,target))
q, p = (0.75,0.25), (0.5,0.5)
lq, lp = tuple(map(log,q)), tuple(map(log,p))
k = point_sum(lp,q)
wrong = point_sum(p,q) # Probabilities in the log-input position.
h = -sum(x*log(x) for x in q)
assert isclose(sum(q),1.0)
assert isclose(sum(p),1.0)
assert isclose(k,0.13081203594113697)
assert isclose(point_sum(lq,q),0.0,abs_tol=1e-12)
assert not isclose(point_sum(lq,p),k)
assert wrong < 0
assert isclose(wrong,-h-0.5)
assert isclose(sum(exp(y)*(y-x) for x,y in zip(lp,lq)),k)
total, batch, classes = 2*k, 2, 2
assert isclose(total/batch,k)
assert isclose(total/(batch*classes),k/2)
# Same two distributions stored as [B=1,L=2,C=2].
assert isclose(total/1,2*k) # batchmean over sequences
assert isclose(total/2,k) # mean over two valid tokens
print("12 KL checks passed", k, wrong)
十二项检查覆盖归一化、正确值、自身KL为零、方向不对称、错误输入的负值、log_target等价表示,以及两种张量布局下的分母差异。代码中的最后两项不是调用框架,而是明确计算分母的教学对照。
mean与batchmean,差了类别维
对形状[B,C]、每行一个分布的损失,先按类别求和得到每样本KL。参考文档的归约定义是sum对全部元素求和,batchmean除以第一维大小,而mean除以全部元素数量。
因此两个相同样本、两类别、每样本KL为K时,sum=2K,batchmean=K,mean=K/2。mean没有改变方向或修复输入,反而使数值多除一个类别数。与其他损失混合时,这会改变相对权重。
但不要把batchmean理解成所有布局下的“平均token KL”。对[B,L,C],它只除B,仍然把长度L求和。相同两个分布从[2,2]改放在[1,2,2],总和没变,第一维分母却从2变成1。布局改变,统计单位也可能跟着改变。
有padding时,先决定平均单位
语言模型蒸馏通常希望先沿词表维求和,再依据有效token掩码汇总,最后除以有效token数量。若希望每条序列同权,则应先得到每序列自己的平均值,再平均序列;长短不同时,这与全体token同权不是同一个目标。
这与上午的梯度累积归一化相连:每个微批次的KL均值不能无条件等权相加,必须看它代表多少有效项。名字相同的loss字段,并不保证分母相同。
若一个批次没有有效token,应预先定义跳过或报错策略,不把0除以0后产生的NaN留给后续梯度处理。掩码也不应被当作通用NaN清洗器;若先算出了非法对数,再简单乘零,未必能得到安全结果。
调试时保留能解释数值的信息
先在一个小批次上查看类别轴、形状、q的行和与exp(log p)的行和,再核对target表示与log_target是否匹配。还须确认两个分布的类别含义逐项一致:不同词表或类别顺序不能仅靠相同形状就直接计算KL。类别轴选错时,沿某个轴归一化成功,不代表沿任务所需的类别轴也是合法分布。
随后保留逐分布的类别和、有效项数量、最终分母及混合损失权重。改变词表、padding比例或布局后,原始标量loss未必可直接横向比较。教师是否固定、温度如何设置,也应作为独立训练设计记录,不从这段KL数值示例推断。
读者可以先用本文的两类别分布验证输入契约,再恢复真实logits和掩码。若这个反例帮你定位了负损失或异常缩小的KL,欢迎通过文末支持区自愿支持。
来源与验证范围
于2026年10月17日核对KLDivLoss与log_softmax官方参考,这是一篇接口与数学原理解读。两幅图及十二项标准库检查为原创教学内容;没有测量模型效果,也没有验证特定设备上的数值误差界。
支持
如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。
二维码可点击放大。更多支持方式见支持页面。


