文章目录
-
- [01 本篇要解决什么问题](#01 本篇要解决什么问题)
- [02 背景:损失负责优化,指标负责解释表现](#02 背景:损失负责优化,指标负责解释表现)
- [03 核心知识:从输入约定到评价规则](#03 核心知识:从输入约定到评价规则)
- [04 一个具体例子:用可手算的数据核对指标](#04 一个具体例子:用可手算的数据核对指标)
- [05 常见问题与自测](#05 常见问题与自测)
- [06 本篇回顾与下一步](#06 本篇回顾与下一步)
01 本篇要解决什么问题
模型已经能前向传播,训练也在输出loss,但还不能马上判断流程正确:分类前是否要Softmax?分割该用Dice还是IoU?准确率95%,为什么正类一个也找不出来?
本篇先对齐任务、输出和标签,再解释分类、分割与回归中的常用指标。最后用一组能手算的数据检查实现,避免只看到一个数字就下结论。
- 本篇关键词:交叉熵、BCE、MSE、Precision、Recall、F1、Dice、IoU、PSNR。
- 完成标志:能为基础任务选定相容的损失输入,并明确说明指标的计算与汇总规则。
- 练习约定:评价脚本只依赖NumPy;另附PyTorch损失接口检查脚本,不下载数据。
02 背景:损失负责优化,指标负责解释表现
损失函数需要给训练提供合适的优化信号;评价指标回答某个具体问题,例如错分多少、漏检多少、区域重叠多大、重建误差多大。
两者有时采用相同公式,有时不同。准确率涉及离散类别判断,通常不直接作为普通梯度训练的目标;交叉熵可以用于优化,但它的数值也不能直接翻译成"准确率多少"。
选择之前先写清任务:每个样本只有一个类别,还是可同时属于多个类别?标签代表整张图,还是每个像素?目标是离散类别,还是连续数值?
03 核心知识:从输入约定到评价规则
(1)先把输出、标签和损失对齐
下表展示最常见的一组用法,采用默认普通监督设置; N N N为批次, C C C为类别数。
| 任务 | 模型输出 | 标签 | 一种常用损失 |
|---|---|---|---|
| 单标签多分类 | 浮点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 c c的交叉熵可以写为:
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-softDice 1−softDice,可以与CE/BCE组合;但类别稀少时也不能只靠一个损失名称解决问题,还要检查采样、标签质量与训练稳定性。
(5)回归与重建:误差、量纲和动态范围
对相同形状的预测与目标,先转浮点再计算误差 e = y ^ − y e=\hat y-y e=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 M M指参与统计的元素数,要说明是否包含所有通道、像素或有效区域。
常见图像指标PSNR为:
P S N R = 10 log 10 R 2 M S E . PSNR=10\log_{10}\frac{R^2}{MSE}. PSNR=10log10MSER2.
R R R是约定的信号动态范围,例如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。
python
"""Binary metrics and regression metrics with explicit empty-denominator handling."""
import json
import math
import numpy as np
def divide_or_nan(numerator, denominator):
return float(numerator / denominator) if denominator else float("nan")
def binary_metrics(target, prediction):
target, prediction = np.asarray(target), np.asarray(prediction)
if target.shape != prediction.shape or target.size == 0:
raise ValueError("Inputs must have identical, non-empty shapes")
if not (np.isin(target, [0, 1]).all() and np.isin(prediction, [0, 1]).all()):
raise ValueError("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)}
def regression_metrics(target, prediction, data_range):
target = np.asarray(target, dtype=np.float64)
prediction = np.asarray(prediction, dtype=np.float64)
if target.shape != prediction.shape or target.size == 0:
raise ValueError("Inputs must have identical, non-empty shapes")
if data_range <= 0 or not np.isfinite(data_range):
raise ValueError("data_range must be positive and finite")
if not (np.isfinite(target).all() and np.isfinite(prediction).all()):
raise ValueError("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) if mse else float("inf")}
def json_safe(value):
if isinstance(value, dict):
return {k: json_safe(v) for k, v in value.items()}
if isinstance(value, float) and not math.isfinite(value):
return None
return value
def main():
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] for k in ["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))
assert math.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均适用:
powershell
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篇场景项目。具体场景按专栏后续安排展开,并把本部分的检查方法带进每个任务。
参考与延伸阅读