qinghuan
← 回到知识探索

AI 直觉 / 动手实验

已经猜对了,标签平滑为什么还会把正确类分数往下拉?

用两个类别算清目标概率、最优分数差与梯度方向,分清两种同名的平滑约定。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先把“平滑 0.2”写成具体数字
  4. 两个分数,只需要看它们相差多少
  5. 为什么最优分数差会停在有限位置
  6. 从 90% 推到 99.3%,会发生什么
  7. 改平滑方式,也就改了最优点
  8. 运行完整实验,核对数值和方向
  9. 目标更温和,不等于结果一定更可靠
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
  • 知道概率总和为 1
  • 能区分原始分数和概率
  • 先读 Softmax 分类与损失:先了解原始分数如何变成概率,以及交叉熵为什么要读取目标分布。
遇到不熟悉的词,先看这 2 个解释
标签平滑
把全给正确类的训练目标,改为给多个类别分配少量概率的目标。
分数差
正确类原始分数减去另一个类原始分数。
打开完整术语小抄 ↗

一张图片的正确类别是猫。模型已经给猫 99.3% 的概率,为什么训练还可能要求它收回一些把握?关键不在“猜没猜对”,而在损失现在要求它接近哪一组概率。

本篇固定两个类别,直接计算一个样本的损失。我们会看到:采用本文的标签平滑后,90% 是这个独立小问题的最优目标;继续推到 99.3%,损失反而上升。它不是对整张神经网络每个输出设置了 90% 的硬上限。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:完整可运行源码
  • README.md:运行步骤与练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和运行验证记录
  • SHA256SUMS.txt:文件摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B demo.py

解压后,在含 demo.py 的目录打开终端。命令分别回车执行。双类别自由分数的标签平滑标量目标;未训练神经网络,也未验证校准或准确率。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

给另一类留一点概率,最优分数差会停在哪里?

正在准备实验控件…

真类目标概率 q₀
0.9000
当前预测概率 p₀
0.5000
梯度 p₀−q₀
-0.400000
当前交叉熵
0.693147

理论最优 d:2.197225

梯度为负:梯度下降会把 d 往大推。 先比较 ε=0 和 ε=0.2,再切换平滑约定。

固定两类、真类为 0,直接优化一条样本的交叉熵,d=z₀−z₁。只研究这个数学目标;真实模型可能无法独立拟合每条样本,不包含正则项,也不证明模型校准或准确率会提高。

先把“平滑 0.2”写成具体数字

原始标签是 [1, 0]。本文采用原论文与 PyTorch 文档的均匀混合定义:保留 1−epsilon 份原标签,再把 epsilon 平均分给全部 K 个类别,正确类也分到一份。

公式是 q_k=(1−epsilon)×1[k=y]+epsilon/K。K=2、epsilon=0.2、正确类 y=0 时,目标 q=[0.9, 0.1]。q 是训练时希望接近的分布,不是声称这张图有 10% 概率真的是另一类。

还有一种实现把 epsilon 只分给其他类别,得到 [0.8, 0.2]。这也是可定义的目标,但同样填写 0.2 时与本文不是同一个损失;读代码时要看公式,不能只看参数名。

两个分数,只需要看它们相差多少

设正确类分数为 z0,另一类为 z1,分数差 d=z0−z1。两个分数同时加一百,不会改变 softmax 概率,因此我们把 z1 固定为零,只研究 d。

此时正确类概率 p0=1/(1+exp(−d)),另一类是 1−p0。d=0 时各占一半;d 越大,正确类概率越接近一。这里的分数差不表示百分比。

目标 [0.9,0.1] 对应损失 L=−0.9×ln(p0)−0.1×ln(1−p0)。第二项会处罚“把另一类概率压得几乎为零”,所以损失不能只保留正确类那一项。

为什么最优分数差会停在有限位置

对 d 求导得到 dL/dd=p0−0.9。p0 低于 0.9 时,梯度为负;按梯度下降减去梯度,d 会变大。p0 高于 0.9 时,梯度为正,d 会被拉回来。

把 p0=0.9 代回概率公式,得到 d*=ln(0.9/0.1)=ln(9),约 2.197225。这里求的是一个样本、两个自由分数、无额外惩罚的最优点。所有类别概率都为正时,更一般地有 z_y−z_j=ln(q_y/q_j)。

这也解释了硬标签的区别:目标 [1,0] 要求 p0 趋近一,有限的 d 达不到精确的一;这个独立问题的损失下确界要在 d 趋向正无穷时逼近。真实模型还受其他样本、参数共享和正则项影响。

从 90% 推到 99.3%,会发生什么

实验在 d=0、ln(9)、5 三处计算。平滑损失依次约为 0.693147、0.325083、0.506715。第三处仍然预测正确,但因为过度偏离软目标,损失高于中间那一点。

d=5 时,p0≈0.993307。平滑目标下梯度约 +0.093307,会把分数差往回拉;硬标签下梯度约 −0.006693,仍推动分数差变大。两者方向不同,是目标改变后的正常结果。

不能把这两种训练损失的绝对数值直接当作模型优劣排名。它们评价的目标已经不同,至少要在相同测试目标下另算指标。

改平滑方式,也就改了最优点

如果把 0.2 只分给另一类,q=[0.8,0.2],最优分数差变为 ln(4)≈1.386294。即使两个实现都叫 label smoothing,同一个数值参数也可能代表不同强度。

epsilon=0 时回到硬标签;epsilon=1 时,本文均匀混合目标是 [0.5,0.5],最优分数差为零。多类别下也要把 K 写清楚,不能直接照搬二分类的 0.9。

如果数据流程已经生成了平滑后的概率目标,再让损失函数做一次平滑,目标会再次改变。检查一条具体样本最终进入损失的目标分布,比仅确认开关已经打开更可靠。

运行完整实验,核对数值和方向

保存为 demo.py,用 Python 3.14 标准库运行 python demo.py,无需第三方包。代码同时用小扰动核对导数,并对分数差 1000 检查稳定的对数计算。

这里没有训练神经网络、读取图片或评估准确率。代码验证的是明确写出的标量目标与十个数值条件;输出保留六位小数,断言使用更高精度。

展开收起代码 · 小实验python · 59 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Two-class label smoothing: an exact objective, not a trained network."""
import math


def smooth_target(classes, correct, epsilon):
    if classes < 2 or not 0 <= correct < classes or not 0 <= epsilon <= 1:
        raise ValueError("invalid class count, label, or epsilon")
    return [epsilon / classes + (1 - epsilon if i == correct else 0)
            for i in range(classes)]


def loss_and_gradient(margin, target):
    # The second logit is fixed at zero. Compute log probabilities stably.
    normalizer = max(margin, 0) + math.log1p(math.exp(-abs(margin)))
    log_p = (margin - normalizer, -normalizer)
    loss = -sum(q * lp for q, lp in zip(target, log_p))
    probability = math.exp(log_p[0])
    return loss, probability - target[0], probability


def main():
    target = smooth_target(2, 0, 0.2)
    optimum = math.log(target[0] / target[1])
    optimal_loss, optimal_grad, optimal_p = loss_and_gradient(optimum, target)
    hard = smooth_target(2, 0, 0)
    loss_zero, grad_zero, _ = loss_and_gradient(0, target)
    loss_large, grad_large, _ = loss_and_gradient(5, target)
    hard_large, hard_grad, _ = loss_and_gradient(5, hard)
    alternative = (0.8, 0.2)  # A DIFFERENT convention: smooth only other classes.
    alternative_margin = math.log(alternative[0] / alternative[1])
    h = 1e-5
    numerical = (loss_and_gradient(5 + h, target)[0]
                 - loss_and_gradient(5 - h, target)[0]) / (2 * h)
    checks = {
        "uniform_mixture_includes_true_class": target == [0.9, 0.1],
        "target_sums_to_one": math.isclose(sum(target), 1),
        "finite_stationary_point": abs(optimal_grad) < 1e-12,
        "stationary_point_is_better_than_both_sides": optimal_loss < min(loss_zero, loss_large),
        "gradient_changes_sign": grad_zero < 0 < grad_large,
        "hard_target_still_pushes_up": hard_grad < 0 and hard_large < optimal_loss,
        "finite_difference_agrees": math.isclose(numerical, grad_large, abs_tol=1e-9),
        "different_convention_different_margin": alternative_margin < optimum,
        "epsilon_one_is_uniform": smooth_target(2, 0, 1) == [0.5, 0.5],
        "finite_large_margin_loss": math.isfinite(loss_and_gradient(1000, target)[0]),
    }
    assert all(checks.values()), checks
    print("convention=uniform mixture over ALL classes")
    print("target=[0.900000, 0.100000]")
    print(f"optimal_margin={optimum:.6f} probability={optimal_p:.6f} loss={optimal_loss:.6f}")
    for margin in (0, optimum, 5):
        loss, gradient, probability = loss_and_gradient(margin, target)
        print(f"margin={margin:.6f} p_true={probability:.6f} loss={loss:.6f} gradient={gradient:.6f}")
    print(f"hard_target_at_5: loss={hard_large:.6f} gradient={hard_grad:.6f}")
    print(f"other_classes_only: target=[0.800000, 0.200000] optimal_margin={alternative_margin:.6f}")
    print(f"PASS {len(checks)} checks; scalar objective only, no model training")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。双类别自由分数的标签平滑标量目标;未训练神经网络,也未验证校准或准确率。

运行后对照结果
convention=uniform mixture over ALL classes
target=[0.900000, 0.100000]
optimal_margin=2.197225 probability=0.900000 loss=0.325083
margin=0.000000 p_true=0.500000 loss=0.693147 gradient=-0.400000
margin=2.197225 p_true=0.900000 loss=0.325083 gradient=0.000000
margin=5.000000 p_true=0.993307 loss=0.506715 gradient=0.093307
hard_target_at_5: loss=0.006715 gradient=-0.006693
other_classes_only: target=[0.800000, 0.200000] optimal_margin=1.386294
PASS 10 checks; scalar objective only, no model training

目标更温和,不等于结果一定更可靠

标签平滑改变的是训练目标。有限最优分数差不是预测概率的硬限幅;共享参数的模型不一定能让每个样本同时达到自己的最优分布。

论文研究过校准与蒸馏的变化,但一个两类算式不能证明你的真实任务准确率更高、概率已校准,或教师模型更适合蒸馏。应分别测任务指标、可靠性和下游用途。

学习时先问三个具体问题:质量要求是什么;平滑后的每类目标是多少;训练和测试是否用了相同的评价约定。这样就能解释“预测正确却仍被往回拉”,而无需把它当作优化器故障。

记住这三点

  • 标签平滑的公式决定真实目标,参数名不足以说明约定。
  • 本文二分类 epsilon=0.2 得到 [0.9,0.1],独立样本的最优分数差为 ln(9)。
  • 梯度方向由预测概率减去目标概率决定;已经分类正确仍可继续被调整。

别把例子用过头

  • 把 0.2 均摊全部类别误写成正确类必定 0.8。
  • 把独立样本的最优概率当成模型输出硬上限。
  • 只取正确类负对数,漏掉软标签的其他类别。
  • 跨不同训练目标比较 loss 数值来断定模型好坏。

自己试一小步

把类别数改为 4、epsilon 改为 0.1,怎样核对正确类与任一其他类的最优分数差?

  1. 先分别写出均摊全部类与只分其他类的目标分布。
  2. 检查每份目标是否和为一,再推导两种分数差。
  3. 示例的 loss_and_gradient 只计算两类。先把它扩成四类 softmax,并把四类的交叉熵都加起来;不能只修改 classes。
  4. 为两个候选分数差各选一个更大和更小的邻点,比较损失与梯度。

做到这些,就算完成

  • 记录你使用的平滑公式和 K。
  • 不要把目标概率直接填入 logits。
  • 说明实验验证了什么,以及没有验证什么。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。