开始前,会这些就够了
- 知道 sigmoid 把分数变成概率
- 理解乘积求导与中心有限差分
- 可先读站内交叉熵或标签平滑文章
- 先读 蒸馏把温度升高,为什么还要把损失乘回 T²?:按该文准备相关概念。
- 先读 前向只选一个,反向为什么还有梯度?:按该文准备相关概念。
预测正确类的概率已经是 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 或验证真实检测质量。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""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 的类别权重后,正类和负类的梯度分别怎样变化?
- 固定 z=0、γ=2,手算两个标签对应的 α
_t。 - 分别缩放完整梯度与冻结权重的梯度,再做有限差分。
- 将 γ 改为 0,检查它是否退化到带类别权重的交叉熵。
做到这些,就算完成
- 明确标签采用 0
/1。 - 区分类别常数权重与随预测变化的权重。
- 每项有限差分核对相同的前向目标。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Focal Loss for Dense Object Detection (新标签页)↗
Focal Loss 公式、γ=0 与类别权重
- torchvision focal_loss 源码 (新标签页)↗
p_t、BCE with logits 与可微调制因子 - BinaryFocalCrossentropy (新标签页)↗
正负类 focal factor、gamma 与
from_logits输入合同