qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

Focal loss 把损失乘小了,梯度也只乘同一个数吗?

从一个正类样本出发,拆开交叉熵与难度权重的两条导数,用 40 组中心有限差分核对完整梯度。

展开本文目录
  1. 下载完整练习包
  2. 先统一正确类的概率
  3. 两项都要对 z 求导
  4. 损失缩小倍数与梯度缩小倍数不同
  5. 检查真正实现的目标,保留输入合同
  6. 运行完整实验并检查报告
  7. 记住这些要点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
调制因子
根据模型对正确类的把握程度,改变当前样本损失的系数。
冻结权重梯度
前向照常算权重,但反向假定权重不再随参数变化。
打开完整术语小抄 ↗

预测正确类的概率已经是 0.9,γ=2 的 Focal loss 会把交叉熵乘以 0.01。有人便把交叉熵梯度也直接乘 0.01。问题在于这个权重本身也由模型预测算出来,参数改变时它同样会变。

本例把完整梯度与“把权重当常数”的结果并排比较。只有前向数值一致,并不能证明训练更新一致。

把例子带到自己的电脑上

下载完整练习包

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

下载 ZIP · 5 个文件
包里有什么
  • focal_gradient.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

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

python --version
python -B focal_gradient.py

解压到独立练习文件夹,在含 focal_gradient.py 的目录打开终端运行。已运行 40 组解析梯度与中心有限差分、4 组极端 logit、对称性与非法输入检查;未调用自动微分框架、训练 RetinaNet 或验证真实检测质量。

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

先统一正确类的概率

设 z 是二分类 logit,p=1/(1+exp(−z)) 是正类概率,标签 y∈{0,1}。正确类概率 q 在 y=1 时为 p,在 y=0 时为 1−p。令 s=2y−1,则 q=sigmoid(sz),这样一个公式同时覆盖正负类。

本文先省略类别权重 α,只研究 L=−(1−q)^γ ln q,γ≥0。−ln q 是交叉熵,w=(1−q)^γ 是难度调制因子。γ=0 时 w=1,回到普通交叉熵;γ>0 时,高 q 样本的前向损失会被压低。

若再加与模型参数无关的类别权重 α_t,损失和完整梯度都整体乘 α_t。固定类别权重与随预测变化的 w 是两件事,不应因为都叫权重就按同一种求导方式处理。

两项都要对 z 求导

把 L 看成 w×CE,乘积求导为 dL/dz=w·dCE/dz+CE·dw/dz。第一项是加权的交叉熵梯度,第二项来自难度权重改变。把 w detach 或当常数时,会丢掉第二项。

利用 dq/dz=sq(1−q),可以整理为 dL/dz=s(1−q)^γ[γq ln q−(1−q)]。s 决定正负类方向,外面的幂是调制因子,方括号里的第一部分来自权重导数,第二部分来自交叉熵导数。

对 z=0、y=1,q=0.5;取 γ=2,完整梯度为 0.25×[ln(0.5)−0.5]≈−0.298287。冻结权重时只剩 0.25×(−0.5)=−0.125。二者前向损失同为约 0.173287,反向明显不同。

损失缩小倍数与梯度缩小倍数不同

对 q=0.9、γ=2,损失为交叉熵的 0.01 倍;完整梯度绝对值约为交叉熵梯度的 0.028965 倍。增加的那部分正是权重变化带来的导数。

对更难的正类样本 q=0.01,交叉熵梯度为 −0.99,完整 Focal 梯度约为 −1.060570,绝对值甚至更大。因此“调制因子不超过 1”只能直接说明前向损失不增大,不能直接说明每个 logit 梯度都更小。

这不是说困难样本越强调越好。标签错误、异常数据与分类困难可能混在一起;本例没有训练检测器,也没有比较抗噪声能力或任务精度。

检查真正实现的目标,保留输入合同

程序对两个标签、四种 γ、五个 z 共 40 个组合做中心有限差分,每个组合计算正负两个扰动,所以是 80 次扰动损失求值。报告保存解析值、差分值与误差。

实现通过稳定 softplus 得到 ln q,用 expm1 得到 1−q,避免对极端 logit 直接计算 log(0) 或 exp(1000)。额外检查 ±1000 的四个标签组合、γ=0 的交叉熵退化、正负标签对称性以及非法输入。

文档里的 from_logits 决定输入是原始分数还是概率。本文函数只收 z,不能把 p 再当 z 传入。标准库没有自动微分,梯度由上式显式计算;框架源码用于核对组成,不是一次框架运行。

运行完整实验并检查报告

保存为 focal_gradient.py,运行 python -B focal_gradient.py。预期输出来自本次实际运行;完整结果写入 report.json。

已运行 40 组解析梯度与中心有限差分、4 组极端 logit、对称性与非法输入检查;未调用自动微分框架、训练 RetinaNet 或验证真实检测质量。

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

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

小实验 · python
"""Binary focal loss analytic gradient versus central finite differences."""
import json
import math
from pathlib import Path


def terms(z,y,gamma):
    if y not in (0,1) or not math.isfinite(z) or not math.isfinite(gamma) or gamma<0:
        raise ValueError('finite logit, binary target, nonnegative gamma required')
    sign=2*y-1
    a=-sign*z
    softplus=max(a,0)+math.log1p(math.exp(-abs(a)))
    logq=-softplus
    q=math.exp(logq)
    one_minus_q=-math.expm1(logq)
    weight=one_minus_q**gamma
    loss=-weight*logq
    gradient=sign*weight*(gamma*q*logq-one_minus_q)
    frozen_weight_gradient=-sign*weight*one_minus_q
    return loss,gradient,frozen_weight_gradient,q,weight


def main():
    rows=[]; errors=[]
    for y in [0,1]:
        for gamma in [0.0,0.5,2.0,5.0]:
            for z in [-4.0,-1.0,0.0,1.0,4.0]:
                loss,grad,frozen,q,weight=terms(z,y,gamma)
                h=1e-5
                fd=(terms(z+h,y,gamma)[0]-terms(z-h,y,gamma)[0])/(2*h)
                errors.append(abs(grad-fd))
                rows.append(dict(y=y,gamma=gamma,z=z,loss=loss,gradient=grad,frozen=frozen,fd=fd))
                if gamma==0:
                    p=1/(1+math.exp(-z))
                    assert abs(grad-(p-y))<1e-12
                assert abs(loss-terms(-z,1-y,gamma)[0])<1e-12
    assert max(errors)<1e-8
    midpoint=terms(0,1,2)
    easy=terms(math.log(9),1,2)
    hard=terms(math.log(1/99),1,2)
    assert abs(midpoint[1]-midpoint[2])>0.17
    assert abs(easy[4]-0.01)<1e-12
    assert abs(hard[1])>0.99
    extremes=[]
    for z in [-1000.0,1000.0]:
        for y in [0,1]:
            result=terms(z,y,2)
            assert all(math.isfinite(x) for x in result)
            extremes.append(dict(z=z,y=y,loss=result[0],gradient=result[1]))
    rejected=0
    for args in [(0,2,2),(0,1,-1),(float('nan'),1,2),(0,1,float('nan'))]:
        try: terms(*args)
        except ValueError: rejected+=1
    assert rejected==4
    report=dict(midpoint=midpoint,easy=easy,hard=hard,max_gradient_error=max(errors),
        finite_difference_cases=rows,extreme_logits=extremes,invalid_rejections=rejected,checks_passed=True)
    Path('report.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
    print(f'p_t=0.5: loss={midpoint[0]:.6f}; full gradient={midpoint[1]:.6f}; frozen weight={midpoint[2]:.6f}')
    print(f'p_t=0.9: loss ratio to CE={easy[4]:.6f}; gradient ratio to CE={abs(easy[1])/0.1:.6f}')
    print(f'p_t=0.01: focal gradient={hard[1]:.6f}; CE gradient=-0.990000')
    print('finite differences=40 cases / 80 perturbed evaluations; extreme logits=4 cases; checks=PASS')
    print('report.json written')


if __name__=='__main__': main()

怎样运行:在独立练习文件夹中执行 python -B focal_gradient.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。已运行 40 组解析梯度与中心有限差分、4 组极端 logit、对称性与非法输入检查;未调用自动微分框架、训练 RetinaNet 或验证真实检测质量。

运行后对照结果
p_t=0.5: loss=0.173287; full gradient=-0.298287; frozen weight=-0.125000
p_t=0.9: loss ratio to CE=0.010000; gradient ratio to CE=0.028965
p_t=0.01: focal gradient=-1.060570; CE gradient=-0.990000
finite differences=40 cases / 80 perturbed evaluations; extreme logits=4 cases; checks=PASS
report.json written

记住这些要点

  • Focal 权重随预测改变,求导必须包含它自己的导数。
  • 冻结权重保留前向数值,却改变反向更新。
  • 损失缩小比例不等于梯度缩小比例。
  • 数值稳定与 logits/probability 输入合同都需要单独检查。

别把例子用过头

  • 把 (1−q)^γ 当成常数,只缩放交叉熵梯度。
  • 把概率再次传入只接收 logits 的函数。
  • 凭一条损失曲线推断真实任务提升。

自己试一小步

加入 α=0.25 的类别权重后,正类和负类的梯度分别怎样变化?

  1. 固定 z=0、γ=2,手算两个标签对应的 α_t。
  2. 分别缩放完整梯度与冻结权重的梯度,再做有限差分。
  3. 将 γ 改为 0,检查它是否退化到带类别权重的交叉熵。

做到这些,就算完成

  • 明确标签采用 0/1。
  • 区分类别常数权重与随预测变化的权重。
  • 每项有限差分核对相同的前向目标。

查原始资料

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

  • Focal Loss for Dense Object Detection (新标签页)↗Lin 等原作者 · 英文

    Focal Loss 公式、γ=0 与类别权重

    2026-10-18 已打开并阅读第 3.1、3.2 节式 (3)–(5);本文只转述相关定义,实验数字由随文程序产生。

  • torchvision focal_loss 源码 (新标签页)↗PyTorch / torchvision · 英文

    p_t、BCE with logits 与可微调制因子

    2026-10-18 已打开并阅读sigmoid_focal_loss 函数主体;本文只转述相关定义,实验数字由随文程序产生。

  • BinaryFocalCrossentropy (新标签页)↗TensorFlow · 英文

    正负类 focal factor、gamma 与 from_logits 输入合同

    2026-10-18 已打开并阅读focal factor 定义及 Args 中 gamma/from_logits;本文只转述相关定义,实验数字由随文程序产生。