qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

两张图的 Dice 都算对了,平均分为什么不一样?

用两张大小不同的前景掩码,拆开逐图平均、合并像素与空掩码平滑,再核对损失梯度。

展开本文目录
  1. 下载完整练习包
  2. 先把分子和分母单独保存
  3. 大目标可以把小目标的失败盖过去
  4. 空掩码不是一个小数点问题
  5. 把评价和训练的约定写进报告
  6. 运行完整实验并检查报告
  7. 记住这些要点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
逐图宏平均
每张图先独立算一个分数,再给每张同样的份量。
分子平滑
在重叠量上另加一个小常数,会改变空标签时的得分与导数。
打开完整术语小抄 ↗

一张图的前景完全正确,另一张图的前景完全错开。两张分数是 1 和 0,平均显然是 0.5。把两张图的像素合起来再算,却得到 0.9。这里没有算术错误:你改变了每张图在总分里的份量。

本例只讨论一个前景通道,标签是 0 或 1,预测可用概率。先明确聚合规则,再处理一张图里完全没有前景时的特殊情况。

把例子带到自己的电脑上

下载完整练习包

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

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

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

python --version
python -B dice_aggregation.py

解压到独立练习文件夹,在含 dice_aggregation.py 的目录打开终端运行。已运行人工掩码聚合、空标签梯度与有限差分;未运行 MONAI、训练网络、处理真实图像或进行临床评测。

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

先把分子和分母单独保存

本文采用 D=(2Σpᵢyᵢ+a)/(Σpᵢ+Σyᵢ+b)。pᵢ 是第 i 个像素属于前景的预测概率,yᵢ 是 0 或 1 的真实标签;a 加在分子,b 加在分母。损失为 L=1−D,分数越高,损失越低。

p 和 y 都是二值时,Σpᵢyᵢ 数的是交集,分母是两份前景像素数相加。概率预测时,它是软版本;把概率先按阈值改成 0/1,再算评测指标,是另一条计算路线。

V-Net 的公式采用平方分母 Σpᵢ²+Σyᵢ²。二值掩码上二者相同,概率预测上并不相同。本文固定线性分母,不能把它的梯度直接套到平方分母版本。

大目标可以把小目标的失败盖过去

第一张图有 9 个前景像素,全部预测正确,分子 18、分母 18,D₁=1。第二张预测为 [1,0],标签为 [0,1],交集为零,分母为 2,D₂=0。这里暂设 a=b=0。

逐图平均给每张图同样份量:(1+0)/2=0.5。先合并再相除得到 (18+0)/(18+2)=0.9。合并结果恰好等于按各图分母加权的分数平均:18/20×1+2/20×0。

所以对同一数据,把两张图分别做两个 batch 再平均,与放入同一 batch 合并计算,结果可能不同。报告应写清楚是逐图宏平均还是合并像素;不要只写“mean Dice”。该例是跨图聚合,类别之间的平均还需要另一份约定。

空掩码不是一个小数点问题

预测和标签都为空时,原式是 0/0。若分子分母都加同一个正数 ε,结果是 1;若只在分母加 ε,结果是 0。也可以在评测中把某类空样本记为缺失并排除,但必须同时报告有效样本数。

空标签、非零预测时,如果分子完全不加平滑数,分子恒为零,Dice 损失恒为 1,对预测概率的导数也为零。这意味着该形式单独使用时,在这类样本上没有压低假前景的局部信号。加在分母的 ε 只处理除零,不能凭空产生分子的梯度。

采用 N=2Σpᵢyᵢ+a、B=Σpᵢ+Σyᵢ+b,损失对 pⱼ 的导数为 (N−2yⱼB)/B²。空标签令 yⱼ=0,得到 a/B²。对 p=[0.2,0.1]、a=b=0.00001,导数约为 0.000111104;程序用中心差分核对。这里求的是对概率的导数,若前面有 sigmoid,还要继续乘链式导数。

把评价和训练的约定写进报告

至少记录前景通道、是否含背景、概率还是阈值掩码、分母是否平方、先求和还是先相除、空标签如何处理,以及平滑数加在哪里。MONAI 的 batch 与 smooth_nr/smooth_dr 正好把其中几项暴露为参数。

本例不判断哪种汇总一定更好。若关心每位样本的效果,逐图平均更直接;若合并统计,要额外查看小目标失败。本文不提供医疗判断,也没有使用医学图像训练网络。

运行完整实验并检查报告

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

已运行人工掩码聚合、空标签梯度与有限差分;未运行 MONAI、训练网络、处理真实图像或进行临床评测。

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

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

小实验 · python
"""Dice aggregation and empty-target gradients; Python standard library."""
import json
import math
from pathlib import Path


def parts(pred, target):
    if not pred or len(pred) != len(target):
        raise ValueError('nonempty equal lengths required')
    if not all(math.isfinite(p) and 0 <= p <= 1 for p in pred):
        raise ValueError('predictions must be finite probabilities')
    if not all(y in (0, 1) for y in target):
        raise ValueError('binary labels required')
    return 2 * sum(p*y for p,y in zip(pred,target)), sum(pred)+sum(target)


def dice(pred, target, smooth_nr=0.0, smooth_dr=0.0):
    n,d=parts(pred,target)
    if not all(math.isfinite(s) and s >= 0 for s in (smooth_nr,smooth_dr)):
        raise ValueError('nonnegative finite smoothing required')
    if d+smooth_dr == 0:
        raise ValueError('undefined empty/empty Dice; choose a policy')
    return (n+smooth_nr)/(d+smooth_dr)


def loss_gradient(pred,target,a,b):
    n,d=parts(pred,target)
    n += a; d += b
    return [(n-2*y*d)/(d*d) for y in target]


def main():
    batch=[([1.0]*9,[1]*9),([1.0,0.0],[0,1])]
    individual=[dice(p,y) for p,y in batch]
    macro=sum(individual)/len(individual)
    sums=[parts(p,y) for p,y in batch]
    pooled=sum(n for n,d in sums)/sum(d for n,d in sums)
    weighted=sum(d*s for (n,d),s in zip(sums,individual))/sum(d for n,d in sums)
    assert macro == 0.5 and pooled == weighted == 0.9
    # A different batching policy changes the mean of batch Dice scores.
    microbatch_mean=macro
    whole_batch=pooled
    empty=[0.0,0.0]; labels=[0,0]; epsilon=1e-5
    empty_both=dice(empty,labels,epsilon,epsilon)
    empty_denominator=dice(empty,labels,0,epsilon)
    assert empty_both == 1 and empty_denominator == 0
    pred=[0.2,0.1]
    grad=loss_gradient(pred,labels,epsilon,epsilon)
    no_numerator=loss_gradient(pred,labels,0,epsilon)
    h=1e-6
    fd=[]
    for i in range(len(pred)):
        up=pred[:]; down=pred[:]; up[i]+=h; down[i]-=h
        fd.append(((1-dice(up,labels,epsilon,epsilon))-(1-dice(down,labels,epsilon,epsilon)))/(2*h))
    assert max(abs(a-b) for a,b in zip(grad,fd)) < 1e-9
    assert no_numerator == [0.0,0.0]
    rejected=[]
    for p,y in [([],[]),([1,0],[1]),([float('nan')],[0]),([0],[2])]:
        try: parts(p,y)
        except ValueError: rejected.append(True)
    try: dice(empty,labels)
    except ValueError: rejected.append(True)
    assert len(rejected)==5
    report=dict(individual=individual,macro=macro,pooled=pooled,denominator_weighted=weighted,
        microbatch_mean=microbatch_mean,whole_batch=whole_batch,empty_both_smoothed=empty_both,
        empty_denominator_only=empty_denominator,empty_target_loss_gradient=grad,
        finite_difference=fd,denominator_only_gradient=no_numerator,invalid_rejections=len(rejected),checks_passed=True)
    Path('report.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
    print('per-image Dice =',individual)
    print(f'mean of image scores={macro:.6f}; pooled score={pooled:.6f}')
    print(f'empty/empty: numerator+denominator={empty_both:.1f}; denominator-only={empty_denominator:.1f}')
    print('empty-target gradient with numerator smoothing =',[round(x,9) for x in grad])
    print('empty-target gradient with denominator-only smoothing =',no_numerator)
    print('checks=PASS; report.json written')


if __name__ == '__main__': main()

怎样运行:在独立练习文件夹中执行 python -B dice_aggregation.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。已运行人工掩码聚合、空标签梯度与有限差分;未运行 MONAI、训练网络、处理真实图像或进行临床评测。

运行后对照结果
per-image Dice = [1.0, 0.0]
mean of image scores=0.500000; pooled score=0.900000
empty/empty: numerator+denominator=1.0; denominator-only=0.0
empty-target gradient with numerator smoothing = [0.000111104, 0.000111104]
empty-target gradient with denominator-only smoothing = [0.0, 0.0]
checks=PASS; report.json written

记住这些要点

  • 逐图平均与合并求比值回答不同问题。
  • 合并 Dice 会按每张图的分母改变份量。
  • 空掩码得分与梯度都依赖明确的平滑约定。
  • 概率版、阈值版与平方分母版不能混用导数。

别把例子用过头

  • 只写平均 Dice,却不写先求和还是先相除。
  • 把分母加 ε 误认为空标签一定有训练信号。
  • 把本例的线性分母导数套到平方分母实现。

自己试一小步

加入 20 张预测和标签都为空的图,总分会怎样改变?

  1. 分别用空图记 1、记 0、忽略三种规则计算逐图平均。
  2. 保留分子分母,比较合并结果并注明平滑是在每图加入还是合并后加入。
  3. 为含一个假前景的空标签图计算损失对概率的导数。

做到这些,就算完成

  • 每种规则都报告有效样本数。
  • 能解释至少两种规则为何不一致。
  • 保留独立手算与程序输出,并解释它们是否一致。

查原始资料

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

  • MONAI DiceLoss 源码与参数 (新标签页)↗MONAI · 英文

    batch 参数、分子分母平滑与求和轴

    2026-10-18 已打开并阅读DiceLoss 文档字符串、forward 中 reduce_axis 及 numerator/denominator;本文只转述相关定义,实验数字由随文程序产生。

  • V-Net 第 3 节 Dice loss layer (新标签页)↗Milletari 等原作者 · 英文

    Dice 损失背景及平方分母变体

    2026-10-18 已打开并阅读第 3 节公式与对应导数;本文只转述相关定义,实验数字由随文程序产生。

  • Generalised Dice overlap (新标签页)↗Sudre 等原作者 · 英文

    空集合稳定项及类别权重是不同设计

    2026-10-18 已打开并阅读第 2.1 节 Dice loss 与 Generalized Dice Loss;本文只转述相关定义,实验数字由随文程序产生。