文章目录
- 01 本篇要解决什么问题
- 02 背景:损失负责优化,指标负责解释表现
- 03 核心知识:从输入约定到评价规则
- (1)先把输出、标签和损失对齐
- (2)Softmax、Sigmoid应该放在哪里
- (3)分类指标先回到混淆矩阵
- (4)分割指标:重叠程度与汇总方式都要写清
- (5)回归与重建:误差、量纲和动态范围
- (6)评价之前,先固定“尺子”
- 04 一个具体例子:用可手算的数据核对指标
- 05 常见问题与自测
- 06 本篇回顾与下一步
01 本篇要解决什么问题
模型已经能前向传播,训练也在输出loss,但还不能马上判断流程正确:分类前是否要Softmax?分割该用Dice还是IoU?准确率95%,为什么正类一个也找不出来?
本篇先对齐任务、输出和标签,再解释分类、分割与回归中的常用指标。最后用一组能手算的数据检查实现,避免只看到一个数字就下结论。
- 本篇关键词:交叉熵、BCE、MSE、Precision、Recall、F1、Dice、IoU、PSNR。
- 完成标志:能为基础任务选定相容的损失输入,并明确说明指标的计算与汇总规则。
- 练习约定:评价脚本只依赖NumPy;另附PyTorch损失接口检查脚本,不下载数据。
02 背景:损失负责优化,指标负责解释表现
损失函数需要给训练提供合适的优化信号;评价指标回答某个具体问题,例如错分多少、漏检多少、区域重叠多大、重建误差多大。
两者有时采用相同公式,有时不同。准确率涉及离散类别判断,通常不直接作为普通梯度训练的目标;交叉熵可以用于优化,但它的数值也不能直接翻译成“准确率多少”。
选择之前先写清任务:每个样本只有一个类别,还是可同时属于多个类别?标签代表整张图,还是每个像素?目标是离散类别,还是连续数值?
03 核心知识:从输入约定到评价规则
(1)先把输出、标签和损失对齐
下表展示最常见的一组用法,采用默认普通监督设置;N NN为批次,C CC为类别数。
| 任务 | 模型输出 | 标签 | 一种常用损失 |
|---|---|---|---|
| 单标签多分类 | 浮点logits[N,C] | long类别ID[N] | CrossEntropyLoss |
| 二分类,单输出 | 浮点logits[N,1] | 浮点0/1[N,1] | BCEWithLogitsLoss |
| 多标签分类 | 浮点logits[N,C] | 浮点多热标签[N,C] | BCEWithLogitsLoss |
| 多类语义分割 | 浮点logits[N,C,H,W] | long类别ID[N,H,W] | CrossEntropyLoss |
| 二值分割 | 浮点logits[N,1,H,W] | 浮点0/1,形状相同 | BCEWithLogitsLoss |
| 回归与强度重建 | 浮点预测 | 浮点目标,形状相同 | MSELoss或L1Loss |
这是整数类别标签下的常用交叉熵接口,标签值通常应在0~C-1;配置ignore_index时另有约定。交叉熵也支持特定形式的概率目标,但不要与整数标签用法混用,详见CrossEntropyLoss文档。
(2)Softmax、Sigmoid应该放在哪里
对单个样本,类别c cc的交叉熵可以写为:
L C E = − z c + log ∑ j e z j . L_{CE}=-z_c+\log\sum_j e^{z_j}.LCE=−zc+logj∑ezj.
PyTorch的CrossEntropyLoss接收原始logits,内部完成相应计算;不要提前Softmax后再当成logits传入。BCEWithLogitsLoss同样直接接收logits,内部组合Sigmoid与二元交叉熵,以改善数值稳定性,见BCEWithLogitsLoss文档。
推理时,单标签分类可对类别维argmax;二值任务需要概率时再Sigmoid,然后按预先确定的阈值判断。默认0.5只是一个起点,若需要调整阈值,应在验证数据上确定,再固定用于测试。
(3)分类指标先回到混淆矩阵
把关注类别定义为正类:TP是正确找出的正类,FP是误报的正类,FN是漏掉的正类,TN是正确判断的负类。
P r e c i s i o n = T P T P + F P , R e c a l l = T P T P + F N , F 1 = 2 T P 2 T P + F P + F N . Precision=\frac{TP}{TP+FP},\quad Recall=\frac{TP}{TP+FN}, \quad F1=\frac{2TP}{2TP+FP+FN}.Precision=TP+FPTP,Recall=TP+FNTP,F1=2TP+FP+FN2TP.
Precision关心预测为正的结果有多少可靠;Recall关心实际正类找回多少。100个样本只有5个正类,全部预测为负类,准确率仍有95%,但正类召回率为0,F1也为0。
多分类汇总时,macro先逐类计算再等权平均;micro先累计相应计数;weighted按支持样本数加权。报告时应注明类别、平均方式和零分母策略,基本定义见scikit-learn分类评价说明。
AUROC衡量分数的排序区分能力,通常使用连续分数而不是阈值后的0/1结果,见roc_auc_score文档。AP关注Precision-Recall关系,其加权定义不等同于用梯形法积分的PR曲线面积,见average_precision_score文档。
(4)分割指标:重叠程度与汇总方式都要写清
对同一个二值前景掩膜,硬预测下的Dice与IoU为:
D i c e = 2 T P 2 T P + F P + F N , I o U = T P T P + F P + F N . Dice=\frac{2TP}{2TP+FP+FN},\qquad IoU=\frac{TP}{TP+FP+FN}.Dice=2TP+FP+FN2TP,IoU=TP+FP+FNTP.
在相应分母有定义时,两者满足D i c e = 2 I o U / ( 1 + I o U ) Dice=2IoU/(1+IoU)Dice=2IoU/(1+IoU)。这里的硬Dice与二分类F1形式相同;训练中使用连续概率构造的soft Dice是另一种实现,需要说明平滑项、归约方式和背景处理。
预测与真值都为空时,上式分母为0。本篇返回NaN表示未定义,保存JSON时转成null;这是一种明确约定。项目也可以约定计为1或跳过,但必须解释,并报告有效样本数,不能无说明地混入平均。
逐图计算后平均,与先累计全数据集TP/FP/FN再计算,并不是相同统计量。多类分割还要说明是否包括背景、哪些类别被忽略。跨论文比较前,应先核对这些条件。
Dice loss常写成1 − s o f t D i c e 1-softDice1−softDice,可以与CE/BCE组合;但类别稀少时也不能只靠一个损失名称解决问题,还要检查采样、标签质量与训练稳定性。
(5)回归与重建:误差、量纲和动态范围
对相同形状的预测与目标,先转浮点再计算误差e = y ^ − y e=\hat y-ye=y^−y:
M A E = 1 M ∑ i ∣ e i ∣ , M S E = 1 M ∑ i e i 2 , R M S E = M S E . MAE=\frac1M\sum_i|e_i|,\quad MSE=\frac1M\sum_i e_i^2, \quad RMSE=\sqrt{MSE}.MAE=M1i∑∣ei∣,MSE=M1i∑ei2,RMSE=MSE.
MAE与目标量纲相同;MSE更强调较大误差;RMSE恢复目标量纲。这里M MM指参与统计的元素数,要说明是否包含所有通道、像素或有效区域。
常见图像指标PSNR为:
P S N R = 10 log 10 R 2 M S E . PSNR=10\log_{10}\frac{R^2}{MSE}.PSNR=10log10MSER2.
R RR是约定的信号动态范围,例如0~1图像通常取1,0~255图像取255,而不是每张预测图自己的最大值。若图像已标准化,应先回到统一评价域。MSE为0时PSNR为正无穷,写文件时也需明确表示方法。
SSIM还涉及窗口、常数、通道和动态范围等设置,报告时需要说明实现。高PSNR并不保证所有结构细节或视觉感受都更好,应结合任务需求和误差图分析。
(6)评价之前,先固定“尺子”
明确预处理、裁剪区域、阈值、是否按图平均、是否忽略背景,以及空样本策略。图像和标签的空间变换需一致;不同图像不能各自按结果重新归一化后直接比较误差。
第09篇按验证损失选择模型,是一种示例规则。真实项目可以按主要验证指标选择,但应提前确定;测试集不能反复用于挑阈值、损失组合和最佳轮次。
图:本篇原创接口与指标示意,右侧计数来自下方固定练习数据。
04 一个具体例子:用可手算的数据核对指标
保存为examples/metrics_demo.py。第一组数据有TP=3、FP=1、FN=2、TN=4,因此Accuracy=0.7、Precision=0.75、Recall=0.6、F1=2/3、IoU=0.5。
"""Binary metrics and regression metrics with explicit empty-denominator handling."""importjsonimportmathimportnumpyasnpdefdivide_or_nan(numerator,denominator):returnfloat(numerator/denominator)ifdenominatorelsefloat("nan")defbinary_metrics(target,prediction):target,prediction=np.asarray(target),np.asarray(prediction)iftarget.shape!=prediction.shapeortarget.size==0:raiseValueError("Inputs must have identical, non-empty shapes")ifnot(np.isin(target,[0,1]).all()andnp.isin(prediction,[0,1]).all()):raiseValueError("Expected binary IDs, not scores")target,prediction=target.astype(bool),prediction.astype(bool)tp=int(np.sum(target&prediction))fp=int(np.sum(~target&prediction))fn=int(np.sum(target&~prediction))tn=int(np.sum(~target&~prediction))return{"tp":tp,"fp":fp,"fn":fn,"tn":tn,"accuracy":(tp+tn)/target.size,"precision":divide_or_nan(tp,tp+fp),"recall":divide_or_nan(tp,tp+fn),"f1_dice":divide_or_nan(2*tp,2*tp+fp+fn),"iou":divide_or_nan(tp,tp+fp+fn)}defregression_metrics(target,prediction,data_range):target=np.asarray(target,dtype=np.float64)prediction=np.asarray(prediction,dtype=np.float64)iftarget.shape!=prediction.shapeortarget.size==0:raiseValueError("Inputs must have identical, non-empty shapes")ifdata_range<=0ornotnp.isfinite(data_range):raiseValueError("data_range must be positive and finite")ifnot(np.isfinite(target).all()andnp.isfinite(prediction).all()):raiseValueError("Non-finite values in inputs")error=prediction-target mse=float(np.mean(error**2))return{"mae":float(np.mean(np.abs(error))),"mse":mse,"rmse":math.sqrt(mse),"psnr":10*math.log10(data_range**2/mse)ifmseelsefloat("inf")}defjson_safe(value):ifisinstance(value,dict):return{k:json_safe(v)fork,vinvalue.items()}ifisinstance(value,float)andnotmath.isfinite(value):returnNonereturnvaluedefmain():target=np.array([1,1,1,1,1,0,0,0,0,0])prediction=np.array([1,1,1,0,0,1,0,0,0,0])metrics=binary_metrics(target,prediction)assert(metrics["tp"],metrics["fp"],metrics["fn"],metrics["tn"])==(3,1,2,4)np.testing.assert_allclose([metrics[k]forkin["accuracy","precision","recall","f1_dice","iou"]],[0.7,0.75,0.6,2/3,0.5])empty=binary_metrics(np.zeros(4),np.zeros(4))assertmath.isnan(empty["f1_dice"])regression=regression_metrics([0.0,1.0],[0.1,0.9],data_range=1.0)np.testing.assert_allclose([regression["mae"],regression["mse"],regression["psnr"]],[0.1,0.01,20.0])print(json.dumps(json_safe({"binary":metrics,"regression":regression,"both_empty":empty}),indent=2,allow_nan=False))if__name__=="__main__":main()从项目根目录执行,PowerShell与Bash均适用:
python examples/metrics_demo.py python examples/loss_contracts.py第一条只需NumPy,同时检查回归例子的MAE=0.1、MSE=0.01、PSNR=20 dB,以及双空掩膜的处理。第二条需要PyTorch,检查CE、BCE与分割CE的形状及可核对的数值:约0.1269、0.6931和1.0986。完整脚本随本文放在examples目录。
这里的数值是固定输入的数学结果,不是模型在真实数据集上的成绩。先用这种小例子对齐定义,再接入大量数据,可以更早发现实现错误。
05 常见问题与自测
问题1:CrossEntropyLoss前面要不要Softmax?
本篇的标准PyTorch用法不需要,直接传logits。只有展示概率等场景,再单独做相应变换。
问题2:二分类只有两个类别,必须输出两个通道吗?
不必须。可以输出两个logits配CE,也可以一个logit配BCEWithLogits;标签形状和类型要跟着相应方案保持一致。
问题3:Dice与IoU可以混着比较吗?
不能只看数值直接比较。即使同一掩膜下可建立关系,数据汇总、背景与空类处理不同也会改变结果。
问题4:为什么准确率很高,正类效果仍很差?
类别不平衡时,大量负类可能主导准确率。应结合混淆矩阵、Precision、Recall、F1等观察关注类别。
问题5:两段代码算出的PSNR不同,先查什么?
先查输入范围、data_range、裁剪区域、通道与平均方式,再查实现细节,不能只比较函数名。
06 本篇回顾与下一步
到这里,第二部分已经把项目入口、图像数组、PyTorch计算、训练流程、模型结构和评价规则接起来。进入具体场景前,至少应能清楚描述:输入怎样处理、模型输出什么、损失如何计算、指标按什么规则汇总。
| 中文 | 常见英文或写法 |
|---|---|
| 交叉熵 | Cross Entropy / CE |
| 精确率、召回率 | Precision、Recall |
| 交并比 | Intersection over Union / IoU |
| 平均绝对误差 | Mean Absolute Error / MAE |
| 峰值信噪比 | Peak Signal-to-Noise Ratio / PSNR |
下一部分进入第12—32篇场景项目。具体场景按专栏后续安排展开,并把本部分的检查方法带进每个任务。
参考与延伸阅读
- PyTorch常用损失函数
- scikit-image:PSNR与SSIM接口及data_range说明