深度学习中如何通过阈值搜索平衡 Precision 和 Recall

在深度学习分类或语义分割任务中,我们经常会遇到这样的情况:模型的 Precision 很高,但 Recall 很低;或者 Recall 很高,但 Precision 明显偏低。

这种现象不一定说明模型训练失败,也可能是二值化阈值设置不合适。默认使用的 0.5 阈值并不一定适用于所有模型和数据集,因此可以在验证集上进行阈值搜索,寻找更合适的 Precision--Recall 平衡点。

本文将从指标含义、阈值影响、搜索方法、代码实现和实验注意事项几个方面,介绍如何通过阈值搜索平衡 Precision 和 Recall。


一、Precision 和 Recall 分别表示什么

在二分类任务中,模型的预测结果可以分成以下四类:

  • TP:真实为正类,并且模型也预测为正类;

  • FP:真实为负类,但模型错误地预测为正类;

  • FN:真实为正类,但模型错误地预测为负类;

  • TN:真实为负类,并且模型也预测为负类。

Precision 的计算公式为:

Precision = TP / (TP + FP)

Precision 表示:模型预测为目标的样本中,真正属于目标的比例有多高。

Recall 的计算公式为:

Recall = TP / (TP + FN)

Recall 表示:所有真实目标中,有多少被模型成功识别出来。

以红树林语义分割为例:

  • Precision 低,说明模型把较多非红树林区域误判成了红树林;

  • Recall 低,说明部分真实红树林区域没有被识别出来。

因此,Precision 主要反映误检情况,Recall 主要反映漏检情况。


二、为什么阈值会影响 Precision 和 Recall

语义分割模型通常不会直接输出 0 或 1,而是输出每个像素属于目标类别的概率。

例如,模型输出:

p = 0.72

表示该像素属于目标类别的概率为 0.72。

为了得到最终的二值分割结果,需要设置一个阈值 t:

  • 当 p ≥ t 时,预测结果为 1,即目标类别;

  • 当 p < t 时,预测结果为 0,即背景类别。

默认情况下,阈值通常设置为 0.5。但是,0.5 只是一个常用的初始值,并不一定是当前模型的最佳阈值。

1. 降低阈值

假设将阈值从 0.5 降低到 0.3,那么更多像素会被判定为目标。

通常会导致:

  • 预测目标区域变大;

  • 漏检数量 FN 减少;

  • Recall 升高;

  • 误检数量 FP 增加;

  • Precision 降低。

因此,较低的阈值会让模型更加"激进"。

2. 提高阈值

假设将阈值从 0.5 提高到 0.7,那么只有预测概率较高的像素才会被判定为目标。

通常会导致:

  • 预测目标区域变小;

  • 误检数量 FP 减少;

  • Precision 升高;

  • 漏检数量 FN 增加;

  • Recall 降低。

因此,较高的阈值会让模型更加"保守"。

可以简单概括为:

降低阈值通常有利于 Recall,提高阈值通常有利于 Precision。


三、什么是阈值搜索

阈值搜索就是在验证集上测试一系列候选阈值,并分别计算每个阈值对应的 Precision、Recall、Dice、IoU 等指标,最后根据任务需求选择最合适的阈值。

例如,可以搜索以下候选阈值:

t ∈ {0.05,0.06,0.07,......,0.95}

假设得到以下结果:

阈值 Precision Recall Dice
0.30 0.780 0.910 0.840
0.40 0.830 0.880 0.854
0.50 0.870 0.820 0.844
0.60 0.910 0.750 0.822

当阈值为 0.30 时,Recall 较高,但误检较多,因此 Precision 偏低。

当阈值为 0.60 时,Precision 较高,但模型更加保守,导致 Recall 下降。

当阈值为 0.40 时,Precision 和 Recall 相对均衡,同时 Dice 最高,因此可以将 0.40 作为候选最佳阈值。


四、如何选择最佳阈值

阈值搜索并不只有一种选择标准,需要根据任务目标确定。

方法一:选择 Dice 或 F1 最高的阈值

F1 的计算公式为:

F1 = 2 × Precision × Recall / (Precision + Recall)

在二分类语义分割任务中,针对目标类别计算时,F1 与 Dice 在数学形式上是一致的:

Dice = 2TP / (2TP + FP + FN)

因此,可以选择使 Dice 或 F1 最高的阈值:

最佳阈值 t = 使 Dice(t) 最大的阈值*

这种方法同时考虑了误检和漏检,一般是最常用、最稳妥的选择方法。

方法二:选择 Precision 和 Recall 差距最小的阈值

如果特别强调二者平衡,也可以选择使下面这个差值最小的阈值:

|Precision(t) - Recall(t)|

但不建议只使用这一标准。

例如存在两个候选阈值:

阈值 Precision Recall 差值
A 0.700 0.700 0.000
B 0.850 0.820 0.030

虽然阈值 A 对应的 Precision 和 Recall 完全相等,但二者都比较低。阈值 B 虽然存在一定差距,但整体性能明显更好。

因此,更推荐采用以下规则:

首先选择 Dice 最高的阈值;如果多个阈值的 Dice 非常接近,再选择 Precision 和 Recall 差距更小的阈值。

方法三:根据实际任务设置约束

不同任务对误检和漏检的容忍程度不同。

如果任务要求尽量减少漏检,可以在 Precision 不低于某个最低值的前提下,选择 Recall 最高的阈值。

例如:

在 Precision ≥ 0.85 的候选阈值中,选择 Recall 最高的阈值。

如果任务要求尽量减少误检,可以在 Recall 不低于某个最低值的前提下,选择 Precision 最高的阈值。

例如:

在 Recall ≥ 0.85 的候选阈值中,选择 Precision 最高的阈值。

在红树林资源调查中,如果更关注完整提取红树林区域,可以适当偏向 Recall;如果分割结果将直接用于面积统计,过多误检会导致面积被高估,则需要更加重视 Precision。


五、阈值搜索的完整代码

下面的代码适用于二分类语义分割任务。它会在验证集上搜索 0.05~0.95 之间的阈值,并计算每个阈值对应的 Precision、Recall、Dice 和 IoU。

复制代码
import torch


def search_best_threshold(
    model,
    validation_loader,
    device,
    thresholds=None
):
    """
    在验证集上搜索二分类语义分割的最佳阈值。

    参数:
        model:
            已经训练完成的模型。

        validation_loader:
            验证集 DataLoader。

        device:
            模型运行设备,例如 "cuda" 或 "cpu"。

        thresholds:
            需要搜索的候选阈值。

    返回:
        best_result:
            最佳阈值及其对应指标。

        all_results:
            所有候选阈值的评价结果。
    """

    if thresholds is None:
        thresholds = [i / 100 for i in range(5, 96)]

    statistics = {
        threshold: {
            "tp": 0,
            "fp": 0,
            "fn": 0
        }
        for threshold in thresholds
    }

    model.eval()

    with torch.inference_mode():
        for images, labels in validation_loader:
            images = images.to(device)
            labels = labels.to(device)

            # 模型输出原始 logits
            logits = model(images)

            # 将 logits 转换为 0~1 范围的概率
            probabilities = torch.sigmoid(logits)

            # 将真实标签转换为布尔类型
            labels = labels > 0.5

            for threshold in thresholds:
                predictions = probabilities >= threshold

                tp = torch.logical_and(
                    predictions,
                    labels
                ).sum().item()

                fp = torch.logical_and(
                    predictions,
                    torch.logical_not(labels)
                ).sum().item()

                fn = torch.logical_and(
                    torch.logical_not(predictions),
                    labels
                ).sum().item()

                statistics[threshold]["tp"] += tp
                statistics[threshold]["fp"] += fp
                statistics[threshold]["fn"] += fn

    all_results = []
    epsilon = 1e-8

    for threshold in thresholds:
        tp = statistics[threshold]["tp"]
        fp = statistics[threshold]["fp"]
        fn = statistics[threshold]["fn"]

        precision = tp / (tp + fp + epsilon)
        recall = tp / (tp + fn + epsilon)

        dice = (
            2 * tp /
            (2 * tp + fp + fn + epsilon)
        )

        iou = (
            tp /
            (tp + fp + fn + epsilon)
        )

        result = {
            "threshold": threshold,
            "precision": precision,
            "recall": recall,
            "dice": dice,
            "iou": iou,
            "precision_recall_gap": abs(
                precision - recall
            )
        }

        all_results.append(result)

    # 首先按照 Dice 从高到低排序;
    # Dice 相同时,优先选择 Precision 和 Recall 差距更小的阈值。
    all_results.sort(
        key=lambda result: (
            -result["dice"],
            result["precision_recall_gap"]
        )
    )

    best_result = all_results[0]

    return best_result, all_results

使用方式如下:

复制代码
best_result, all_results = search_best_threshold(
    model=model,
    validation_loader=validation_loader,
    device=device
)

print("最佳阈值:", best_result["threshold"])
print("Precision:", best_result["precision"])
print("Recall:", best_result["recall"])
print("Dice:", best_result["dice"])
print("IoU:", best_result["iou"])

假设输出为:

复制代码
最佳阈值:0.43
Precision:0.856
Recall:0.849
Dice:0.852
IoU:0.742

这表示模型在验证集上的最佳阈值为 0.43。后续在测试集上评价时,应固定使用 0.43,不能再次搜索。


六、不同模型输出的处理方式

阈值搜索前,需要先将模型输出转换为目标类别的概率。

1. 单通道二分类输出

如果模型输出形状为:

复制代码
[B, 1, H, W]

并且训练时使用的是:

复制代码
torch.nn.BCEWithLogitsLoss()

那么模型输出的是 logits,需要使用 Sigmoid:

复制代码
probabilities = torch.sigmoid(logits)

之后再进行阈值处理:

复制代码
predictions = probabilities >= threshold

2. 双通道分类输出

如果模型输出形状为:

复制代码
[B, 2, H, W]

第 0 通道表示背景,第 1 通道表示目标,那么需要使用 Softmax:

复制代码
probabilities = torch.softmax(
    logits,
    dim=1
)[:, 1:2, :, :]

然后对目标类别概率进行阈值处理:

复制代码
predictions = probabilities >= threshold

不能直接对未经 Sigmoid 或 Softmax 处理的 logits 使用 0.5 阈值,否则阈值的实际含义会发生变化。


七、正确的实验流程

阈值搜索必须在验证集上完成,正确流程如下:

  1. 使用训练集训练模型;

  2. 保存验证集表现最好的模型参数;

  3. 使用该模型对验证集进行预测;

  4. 在验证集上搜索最佳阈值;

  5. 固定模型参数和最佳阈值;

  6. 使用固定阈值在测试集上进行一次最终评价。

最重要的原则是:

测试集只能用于最终评价,不能用于搜索阈值。

如果在测试集上尝试多个阈值,然后选择测试指标最高的结果,相当于利用测试集进行调参,会导致测试结果偏高,无法客观反映模型的泛化能力。


八、多区域语义分割应该怎样搜索阈值

对于包含多个研究区域或多个 Pair 的数据集,常见做法有两种。

1. 搜索统一的全局阈值

将所有验证样本的 TP、FP 和 FN 累计起来,计算整体 Precision、Recall 和 Dice,然后选择一个统一阈值。

这种方法的优点是:

  • 实验流程简单;

  • 模型部署时不需要提前知道样本来自哪个区域;

  • 不容易因单个区域样本较少而造成阈值过拟合;

  • 不同方法之间更容易进行公平对比。

对于多区域红树林分割任务,通常更推荐使用统一的全局阈值。

2. 为每个区域单独搜索阈值

这种方法可能让每个区域获得更高的指标,但存在明显问题:

  • 需要提前知道影像属于哪个区域;

  • 每个区域都需要单独的验证数据;

  • 容易对区域数据产生过拟合;

  • 实际应用到新区域时无法确定应使用哪个阈值;

  • 容易造成测试指标不公平地升高。

因此,除非研究目标就是区域自适应校准,否则不建议为每个测试区域单独搜索阈值。

如果最终评价采用按样本数量加权的多区域汇总方式,那么阈值搜索阶段也应尽量采用一致的汇总方式,避免阈值选择标准与最终评价标准不一致。


九、粗搜索与精细搜索

如果验证集规模较大,可以采用两阶段搜索。

第一阶段:粗搜索

先以 0.05 为步长搜索:

复制代码
0.05、0.10、0.15、......、0.90、0.95

假设发现 0.40 附近的 Dice 最高。

第二阶段:精细搜索

再在 0.35~0.45 之间,以 0.01 为步长搜索:

复制代码
0.35、0.36、0.37、......、0.44、0.45

这种方法能够减少计算量,同时获得较为精确的阈值结果。

一般没有必要把阈值搜索得过于精细。例如,从 0.4312 调整到 0.4313 所带来的差异通常没有实际意义,还可能造成验证集过拟合。


十、论文中应该如何描述

可以在论文的实验设置部分写成:

为减小固定二值化阈值对分割结果的影响,本文在验证集上对预测概率阈值进行搜索。候选阈值范围设置为 0.05~0.95,搜索步长为 0.01,并以 Dice 系数最大作为最佳阈值的选择标准。当多个候选阈值的 Dice 系数相近时,优先选择 Precision 与 Recall 差距较小的阈值。阈值确定后,在测试阶段保持模型参数和二值化阈值不变,并使用该固定阈值完成所有测试样本的评价。

如果最终仍然使用 0.5,也可以写成:

验证集阈值搜索结果表明,0.5 附近取得了最佳 Dice 系数。为保持评价流程的简洁性和不同模型之间的一致性,本文最终采用 0.5 作为统一二值化阈值。

需要注意,如果对多个对比模型进行实验,应该提前统一阈值策略。例如:

  • 所有模型都固定使用 0.5;

  • 或者每个模型都在各自验证集预测结果上独立搜索最佳阈值。

不能只为自己的模型搜索最佳阈值,而让其他对比模型固定使用 0.5,否则对比结果可能不公平。


十一、阈值搜索的局限性

阈值搜索只能改变模型在 Precision 和 Recall 之间的工作点,不能真正提高模型的特征提取能力。

例如:

  • 降低阈值可以减少漏检,但可能增加误检;

  • 提高阈值可以减少误检,但可能增加漏检;

  • 阈值调整无法让原本混淆的正负样本真正分离。

如果模型在所有阈值下的 Dice 和 IoU 都比较低,说明问题可能来自:

  • 模型特征提取能力不足;

  • 正负类别严重不平衡;

  • 训练数据存在噪声;

  • 标签质量较差;

  • 训练集与测试集分布不同;

  • 云层、阴影或复杂背景造成特征混淆;

  • 损失函数不适合当前任务。

此时仅调整阈值并不能从根本上解决问题,还需要从数据、模型结构、损失函数和训练策略等方面进行改进。


十二、总结

阈值是深度学习二分类和语义分割中非常重要、但容易被忽略的参数。

一般来说:

  • 阈值降低,Recall 通常升高,Precision 通常降低;

  • 阈值提高,Precision 通常升高,Recall 通常降低;

  • 默认阈值 0.5 不一定是最佳阈值;

  • 建议在验证集上搜索阈值;

  • 通常选择 Dice 或 F1 最高的阈值;

  • 如果多个阈值的 Dice 接近,再考虑 Precision 和 Recall 的差距;

  • 最佳阈值确定后,必须在测试集上固定使用;

  • 不能在测试集上搜索最佳阈值;

  • 多区域任务通常应使用统一的全局阈值;

  • 阈值搜索只能调整误检与漏检的平衡,不能代替模型本身的改进。

对于语义分割任务而言,除了比较 Precision、Recall、Dice 和 IoU,还应结合预测图观察误检和漏检的空间分布。只有将定量指标与可视化结果结合起来,才能准确判断阈值调整是否真正改善了模型的实际分割效果。

相关推荐
哈哈哈也不行吗38 分钟前
用大角几何整理几何教研案例:从作图到复用的一个思路
人工智能·在线工具·几何绘图·大角几何
新知图书40 分钟前
16.1 基于MCP的多Agent旅行规划助手系统概述
人工智能·agent·ai agent·智能体
u1301301 小时前
AI 日报(2026年8月29日)
人工智能
chunmiao30322 小时前
OpenAI 等 116 家机构联名发公开信:AI 网络攻击将进入高发期
人工智能
VALENIAN瓦伦尼安教学设备2 小时前
设备状态检测振动分析实训台案例分析
大数据·数据库·人工智能·嵌入式硬件·算法
Warren2Lynch2 小时前
面向对象设计中的 UML 类图指南
人工智能·uml
2601_966871402 小时前
周大都督2026年零基础手写大模型系统课
人工智能
hyuk的AI工坊2 小时前
向量数据库实战:LangChain4j 双后端向量检索(InMemory 持久化 + ChromaDB)
人工智能