【深度学习项目实战·11】损失函数与评价指标:让模型输出、标签与结果真正对应

文章目录

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篇场景项目。具体场景按专栏后续安排展开,并把本部分的检查方法带进每个任务。

参考与延伸阅读

相关推荐
RanceGru32 分钟前
AI嵌入式7——瑞芯微RK3588s+RK1828 NPU 部署VLM模型(Qwen3-VL-2B)
人工智能
阿里云大数据AI技术1 小时前
云栖实录 | 具身智能前沿对话
人工智能
imythu1 小时前
让 Claude Code 在家跑,我在外面用手机盯着:做一个开源工具踩过的几个坑
人工智能·github
用户6602638008361 小时前
8GB 显存跑 7B 大模型:vLLM 消费级显卡极限实战白皮书
人工智能
躺柒1 小时前
读数据安全架构设计与实践20安全政策文件体系(上)
大数据·人工智能·安全·数据安全·安全架构·安全体系
用户302514728061 小时前
从对话到行动:用 AIUI 构建「意图驱动」的隐形界面——「言值」AI 表达教练开发实录
人工智能
武子康1 小时前
工作流 JSON 没变,为什么 ComfyUI 更新后仍可能跑出不同结果?
人工智能·agent
海螺_AIHCC2 小时前
Agent 能自己调工具之后,安全就不该写在提示词里:一个 200 行拦截器的工程实践
人工智能
加宝的起点雷达2 小时前
三个定时任务被删那天,系统一声不吭
人工智能·后端