公司动态
未知数据分类验证基线实战:基于MSP与AUROC的图像开放集评估
在真实的图像分类落地场景中困扰我们的往往不是“已知类别识别得不够准”而是模型在面对从未见过的输入时依然会给出一个十分自信的错误答案。比如安防摄像头采集到一种训练阶段完全没有出现过的物品分类器没有能力判断“我不认识它”反而会把它强行归到某个已知类别里。这个问题如果得不到控制直接上线就会带来误报、漏报和运营上的连锁风险。本文围绕 “VeriCam: A Verification Baseline for the Classification of Unknown Data” 展开。这里的关键词并不单指某一套固定代码而是代表一类任务如何在视觉分类系统中建立“未知数据分类验证基线”并用量化指标评估模型对未知输入的响应。适合正在做图像分类、开放集识别、模型上线前评估的算法工程师和学生阅读。读完你会掌握一套最小可运行的验证基线流程包括数据划分、未知样本构造、MSP 置信度基线、AUROC/FPRTPR95 指标计算以及容易踩坑的细节。1. 未知数据分类为什么需要验证基线1.1 从闭集分类到开放世界分类传统的图像分类模型通常遵循闭集假设训练集和测试集来自同一组类别模型只需要在有限类别里选择概率最大的答案。这种假设在学术 benchmark 里非常常见但现实世界并不是一个封闭集合。摄像头、传感器、用户上传内容随时可能带来未见过的类别。我们可以把这些输入统称为 Unknown Data也就是模型训练阶段没有见过的数据。此时模型面对的是开放世界既要能区分已知类别之间的差异也要能识别出“这个样本不属于任何已知类别”。“Unknown Data”并不是单一概念。它可以包括与已知类别完全不同的新类别与已知类别语义相近但分布不同的数据数据损坏、噪声、对抗扰动带来的异常输入与任务完全无关的背景内容。如果模型不具备对未知输入的判断能力那么它输出的置信度分数就没有意义。所以我们需要一种“验证基线”先把模型面对未知数据时的行为量化出来再决定是否需要改进算法。1.2 Verification Baseline 到底验证什么Verification Baseline 可以理解为一套标准化的验证流程。它回答的问题非常明确当模型遇到 Unknown Data 时它能不能保持“谨慎”具体来说验证基线需要完成三件事。第一把已知类别和未知类别放在同一个评价框架里让问题从“这张图是哪个类”变成“这张图是否来自已知类别空间”。第二为每个样本计算一个可比较的置信度分数例如最大 Softmax 概率分数高代表模型认为样本更接近已知类别。第三在固定的指标下汇报模型区分已知和未知的能力而不是只看分类准确率。用 VeriCam 这类名称作为验证基线本质上是为了强调视觉场景中的可复现评估。真实项目里算法团队经常用不同的数据集、不同的预处理方式、不同的评价口径各自验证得到的结论无法横向比较。有了统一基线后任何改进方法都可以先在同一个协议上跑一遍确认提升真实有效。1.3 与 OSR、OOD、异常检测的关系这里有必要区分几组经常混淆的概念因为不同场景里“未知数据”的定义差别很大。开放集识别也就是 Open Set Recognition简称 OSR关注的是模型能否在测试时正确拒绝“新类别”同时保留对已知类别的识别能力。分布外检测也就是 Out-of-Distribution Detection简称 OOD Detection关注的是测试样本是否来自训练分布之外比如 CIFAR-10 模型遇到 SVHN 图像。异常检测也就是 Anomaly Detection通常更关注数据中的少量异常点例如工业质检中的缺陷样本。这三个方向在方法上经常互相借鉴但评价口径并不完全一致。本文以视觉图像分类为主线重点解决 OSR 和 OOD 方向上都需要的核心问题如何构造 Known/Unknown 数据并给出一个可靠的验证基线。2. 基线实验设计与评价指标2.1 问题形式化定义为了把问题讲清楚我们可以做一个形式化定义。假设有已知类别集合K {c1, c2, ..., cM}模型使用 K 中的训练数据完成分类因此模型见过所有已知类别。测试阶段我们输入两类样本已知类样本标签确实属于 K未知类样本标签不属于 K。验证基线的目标不是判断未知样本具体属于哪个新类别而是计算一个二分类判断样本是否为已知类别可以把来自已知类别的样本视为正类把来自未知类别的样本视为负类。这样原本的“M 类分类问题”就被转换成一个“验证问题”。Verification Baseline 的名称也因此而来我们验证的其实是模型对已知类别空间的信任边界。2.2 数据集应该怎么划分建立验证基线的第一步是准备数据。为了保证实验可复现建议把数据划分成四份数据集合数据来源是否参与训练用途已知类训练集已知类别中的训练样本是训练分类器已知类验证集已知类别中的验证样本否只用于选阈值确定判定边界已知类测试集已知类别中的测试样本否评估已知类识别能力未知类测试集未知类别样本否评估对未知类的拒识能力一个常见的错误是只用训练集上的表现来评估模型或者让测试用的未知类别以任何形式出现在训练阶段。这样得到的结果会偏乐观无法反映真实线上场景。未知类数据在验证阶段应该保持完全不可见。另一个关键点是阈值不能直接在测试集上挑选否则会产生信息泄漏。更规范的做法是先用已知类验证集确定一个能保证指定已知类召回率的阈值再把这个阈值固定下来去测试集上评估最终的混淆矩阵和指标。2.3 核心指标选择分类任务里准确率并不能完整反映验证基线的质量。因为如果已知类和未知类数量不平衡模型即使把所有样本都判为“已知”也可能获得很高的准确率。这里采用更合适的指标。AUROCArea Under the ROC Curve是最常用的指标之一。它的含义是随机抽取一个已知类样本和一个未知类样本已知类样本的分数高于未知类样本的概率。AUROC 越高说明模型对已知和未知的区分能力越强。AUROC 接近 0.5 表示模型基本没有区分能力接近 1 表示区分能力很强。FPRTPR95 是另一个工程中很重要的指标。TPR 表示已知类样本被正确判定为已知类的比例FPR 表示未知类样本被错误判定为已知类的比例。FPRTPR95 的含义是当模型能识别出 95% 的已知类样本时有多少比例的未知类样本会被误判成已知类。这个指标直接对应业务中的“误报率”在很多安全敏感场景中非常关键。除了上述指标还可以额外统计已知类准确率、未知类拒识率以及混淆矩阵方便从不同角度观察结果。3. 环境准备与项目结构3.1 运行环境本文示例以 PyTorch 为基础实现环境要求如下。版本需要根据你的项目实际情况调整这里重点演示配置和代码思路。操作系统Windows / Linux / macOS 均可Python建议使用 3.8 或更高版本深度学习框架PyTorch建议使用较新的稳定版本数据集工具torchvision指标计算scikit-learn数值计算NumPy。在命令行中安装核心依赖pip install torch torchvision sklearn numpy如果你的机器有 CUDA 显卡可以安装对应版本的 GPU PyTorch如果没有CPU 版本也能完成整个演示只是训练速度会慢一些。安装时尽量统一依赖版本避免不同包之间的接口冲突。3.2 演示项目结构为了让代码模块清晰建议按下面的目录组织工程VeriCam-Demo/ ├── data_utils.py # 数据集划分与 Known/Unknown 构造 ├── models.py # 小型视觉分类网络 ├── metrics.py # 验证基线评估指标 └── run_demo.py # 主流程训练、阈值选择、评估这份结构足够做一次完整实验。你在实际项目中可以继续扩展出config.py管理超参数、inference.py对接线上推理但核心的验证协议与这里的逻辑保持一致。4. 完整代码实现4.1 数据划分与 Unknown Data 构造我们需要从 CIFAR-10 数据集中切出已知类别与未知类别。例如选择其中 5 个类作为已知类另外 5 个类作为未知类。这样能保证数据集的加载非常简单环境允许时会自动下载到本地。值得注意的是这种划分方式会让未知类与已知类来自同一个数据集语义上仍属于同一分布因此验证难度偏低。它适合用来打通实验流程。在更严格的实验中建议使用 CIFAR-100 或 Tiny ImageNet 等数据集裁剪出的图像作为真正意义上的 Unknown Data。先把流程跑通再替换为更困难的数据是比较稳妥的思路。下面是data_utils.py的完整代码# 文件路径VeriCam-Demo/data_utils.py import torch from torch.utils.data import Subset, Dataset from torchvision import datasets, transforms class VerificationDataset(Dataset): 将原始 Subset 包装成验证数据集。 is_known1 表示该样本属于已知类别空间 is_known0 表示该样本属于未知类别空间。 def __init__(self, subset, is_known): self.subset subset self.is_known int(is_known) def __len__(self): return len(self.subset) def __getitem__(self, index): x, y self.subset[index] return x, y, self.is_known def filter_by_classes(dataset, allowed_classes): 只保留 dataset 中类别在 allowed_classes 里的样本。 allowed_classes 是 list[int]。 allowed_set set(allowed_classes) indices [ i for i, (_, label) in enumerate(dataset) if int(label) in allowed_set ] return Subset(dataset, indices) def get_cifar10_datasets(data_root./data, known_classes(0, 1, 2, 3, 4)): 返回数据字典包含 - known_train: 训练分类器使用的已知类训练集 - known_val: 用来选择阈值的已知类验证集 - known_test: 已知类测试集 - unknown_test: 未知类测试集 train_transform transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomCrop(32, padding4), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) test_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) # 下载并加载完整 CIFAR-10 full_train datasets.CIFAR10( rootdata_root, trainTrue, downloadTrue, transformtrain_transform ) full_test datasets.CIFAR10( rootdata_root, trainFalse, downloadTrue, transformtest_transform ) all_classes set(range(10)) known_set set(known_classes) unknown_classes sorted(all_classes - known_set) # 过滤出已知类训练数据 known_train_full filter_by_classes(full_train, known_classes) # 再从已知类训练数据中切出验证集例如 5000 个样本 n len(known_train_full) n_val 5000 indices list(range(n)) torch.manual_seed(0) val_indices set(torch.randperm(n)[:n_val].tolist()) train_indices [i for i in indices if i not in val_indices] known_train Subset(known_train_full, train_indices) known_val Subset(known_train_full, sorted(val_indices)) # 已知类测试集和未知类测试集 known_test filter_by_classes(full_test, known_classes) unknown_test filter_by_classes(full_test, unknown_classes) # 包装成验证数据集 return { known_train: VerificationDataset(known_train, is_known1), known_val: VerificationDataset(known_val, is_known1), known_test: VerificationDataset(known_test, is_known1), unknown_test: VerificationDataset(unknown_test, is_known0), }这段代码需要注意三个地方。第一验证集的索引来自已知类训练数据内部不能和测试集有交集。第二VerificationDataset返回的第三个元素 1/0 用来告诉评估函数当前样本是已知还是未知。第三随机种子固定为 0方便复现在真实项目中可以把 seed 作为参数传入方便做多次重复实验。4.2 定义视觉分类网络本文用一个小型 CNN 而不是大型预训练网络。原因是验证基线讨论的重点在协议和指标上使用小型网络可以在普通 CPU 机器上完成训练方便读者快速复现。# 文件路径VeriCam-Demo/models.py import torch.nn as nn class SmallCNN(nn.Module): 适用于 CIFAR-10 32x32 输入的小型卷积网络。 输出维度为 num_classes不包含 Softmax。 def __init__(self, num_classes5): super(SmallCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))CIFAR-10 的图像尺寸是 32x32。经过三次MaxPool2d(2)后特征图会缩小到 4x4最后引入AdaptiveAvgPool2d((1, 1))把特征图统一池化到 1x1保证网络对输入尺寸变化有更好的鲁棒性。需要重点理解的是网络最后一层输出的是 logits不是概率。我们在后续计算最大 Softmax 概率时会先对 logits 做 Softmax再取最大值。不要把 Softmax 写进模型里因为训练时使用的交叉熵损失函数会自动结合 logits 和 Softmax单独把 Softmax 放进模型反而会增加数值不稳定性。4.3 评估指标实现在验证基线中我们要计算三个核心结果AUROC、FPRTPR95、混淆矩阵。下面把指标封装成独立模块方便在训练结束后直接调用。# 文件路径VeriCam-Demo/metrics.py import numpy as np from sklearn.metrics import roc_auc_score, roc_curve def compute_metrics(known_scores, unknown_scores, threshold): 计算验证指标。 参数 - known_scores: 已知类样本的置信度分数 - unknown_scores: 未知类样本的置信度分数 - threshold: 判定阈值分数 threshold 判定为已知类 返回 - dict 类型的指标结果 y_true np.concatenate([ np.ones(len(known_scores)), np.zeros(len(unknown_scores)), ]) y_score np.concatenate([known_scores, unknown_scores]) # 在固定阈值下计算混淆矩阵相关指标 pred (y_score threshold).astype(int) tp np.sum((pred 1) (y_true 1)) fn np.sum((pred 0) (y_true 1)) fp np.sum((pred 1) (y_true 0)) tn np.sum((pred 0) (y_true 0)) known_acc tp / max(tp fn, 1) unknown_acc tn / max(tn fp, 1) auc roc_auc_score(y_true, y_score) fprs, tprs, _ roc_curve(y_true, y_score) # 找到 TPR 0.95 时尽量低且首次满足条件的 FPR valid_indices np.where(tprs 0.95)[0] fpr_at_tpr95 fprs[valid_indices[0]] if len(valid_indices) 0 else float(nan) return { auc: auc, fpr_at_tpr95: fpr_at_tpr95, known_acc: known_acc, unknown_acc: unknown_acc, confusion_matrix: { tp: int(tp), fn: int(fn), fp: int(fp), tn: int(tn), }, } def select_threshold_by_known_scores(known_scores, tpr_target0.95): 使用已知类验证集的分数挑选阈值。 目标是让已知类样本保留 tpr_target 比例的召回率。 if len(known_scores) 0: raise ValueError(known_scores 不能为空) # 例如 tpr_target0.95则取已知样本分数分布的 5% 分位点 # 约 95% 的已知样本分数会大于等于该阈值 threshold np.percentile(known_scores, (1 - tpr_target) * 100) return float(threshold)这里的阈值选择逻辑非常重要。我们是在“已知类验证集”上选择阈值而不是在测试集上选择。测试集只负责最终评估这样可以避免阈值过拟合。如果线上业务对误报率更敏感可以把阈值定得更高如果希望尽量少漏掉已知类样本可以调低阈值。验证基线的意义恰恰就是提供一个可调节的评估框架而不是固定某一条规则。4.4 主流程训练与验证最后是主流程代码。它会依次完成训练、分数采集、阈值选择和指标计算。# 文件路径VeriCam-Demo/run_demo.py import numpy as np import torch import torch.nn as nn from torch.utils.data import DataLoader from data_utils import get_cifar10_datasets from models import SmallCNN from metrics import compute_metrics, select_threshold_by_known_scores def collect_confidence_scores(model, loader, device): 遍历数据集返回最大 Softmax 概率分数和 is_known 标签。 model.eval() scores_list [] known_list [] with torch.no_grad(): for x, _, is_known in loader: x x.to(device) logits model(x) probs torch.softmax(logits, dim1) max_probs probs.max(dim1).values scores_list.append(max_probs.cpu().numpy()) known_list.append(is_known.numpy()) scores np.concatenate(scores_list) is_known np.concatenate(known_list) return scores, is_known def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(device:, device) # 使用 5 个类别作为已知类 known_classes [0, 1, 2, 3, 4] num_classes len(known_classes) datasets get_cifar10_datasets( data_root./data, known_classesknown_classes, ) train_loader DataLoader( datasets[known_train], batch_size128, shuffleTrue, num_workers2, drop_lastTrue ) val_loader DataLoader( datasets[known_val], batch_size256, shuffleFalse, num_workers2 ) known_test_loader DataLoader( datasets[known_test], batch_size256, shuffleFalse, num_workers2 ) unknown_test_loader DataLoader( datasets[unknown_test], batch_size256, shuffleFalse, num_workers2 ) model SmallCNN(num_classesnum_classes).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() epochs 10 for epoch in range(epochs): model.train() total_loss 0.0 correct 0 total 0 for x, y, _ in train_loader: x x.to(device) y y.to(device) logits model(x) loss criterion(logits, y) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * x.size(0) pred logits.argmax(dim1) correct (pred y).sum().item() total y.size(0) train_acc correct / max(total, 1) avg_loss total_loss / max(total, 1) print(fepoch{epoch 1}, loss{avg_loss:.4f}, train_acc{train_acc:.4f}) # 1. 在已知类验证集上选择阈值 val_scores, val_known collect_confidence_scores(model, val_loader, device) # val_known 应该全是 1阈值选择只看已知类分数 threshold select_threshold_by_known_scores( val_scores, tpr_target0.95 ) print(selected threshold:, round(threshold, 4)) # 2. 在测试集上评估 known_scores, _ collect_confidence_scores(model, known_test_loader, device) unknown_scores, _ collect_confidence_scores(model, unknown_test_loader, device) results compute_metrics( known_scoresknown_scores, unknown_scoresunknown_scores, thresholdthreshold, ) print(AUROC:, round(results[auc], 4)) print(FPRTPR95:, round(results[fpr_at_tpr95], 4)) print(known_acc:, round(results[known_acc], 4)) print(unknown_acc:, round(results[unknown_acc], 4)) print(confusion_matrix:, results[confusion_matrix]) if __name__ __main__: main()这里有几个工程细节需要解释。第一模型训练使用交叉熵损失输出维度是 5。训练过程中只使用 known_classes 对应的 5 类数据未知类数据完全没有参与训练。这保证了验证基线考察的是模型面对全新类别的泛化能力而不是记忆能力。第二阈值的选择来自验证集。select_threshold_by_known_scores会找出一个阈值使得约 95% 的已知类验证样本能够被正确保留。然后我们固定这个阈值再到 known_test 和 unknown_test 上评估。第三所有测试都使用torch.no_grad()包裹并让模型进入 eval 模式。如果忘记调用model.eval()BatchNorm 层在推理时仍然会使用当前 batch 的均值和方差导致结果不稳定。5. 运行与结果分析5.1 运行命令在项目目录下直接运行python run_demo.py第一次运行会自动从互联网下载 CIFAR-10 数据集。下载完成后控制台会输出类似下面的内容device: cpu epoch1, loss1.5321, train_acc0.3892 epoch2, loss1.3147, train_acc0.5021 epoch3, loss1.1842, train_acc0.5621 ... epoch10, loss0.9313, train_acc0.6881 selected threshold: 0.4213 AUROC: 0.7721 FPRTPR95: 0.4215 known_acc: 0.6924 unknown_acc: 0.6203 confusion_matrix: {tp: 3421, fn: 1579, fp: 1898, tn: 3102}以上输出只是格式示意。不同机器、不同随机种子和不同训练轮数一定会产生差异重点不是记住某个具体数字而是理解每一行结果的含义。5.2 怎样解读这些结果AUROC 如果明显高于 0.5说明模型已经具备一定的已知/未知区分能力。AUROC 很高不代表系统可以直接上线因为线上场景的未知数据分布会更复杂。FPRTPR95 则告诉我们如果业务要求漏掉已知类的比例不超过 5%那么会有多少未知类样本被误判为已知。这个数字越低越好0 表示不存在误报1 表示全部误报。再看混淆矩阵。TP 是正确保留的已知类样本FN 是漏掉的已知类样本FP 是误判为已知的未知类样本TN 是正确拒识的未知类样本。当我们要设定业务阈值时往往是在 FN 和 FP 之间做权衡。不能说“哪个数字越小越好”因为降低 FN 通常会导致 FP 上升反之亦然。5.3 结果可视化的一种思路如果想让验证结论更直观可以增加一个简单的可视化代码片段。这里不要求必须画图但展示思路import matplotlib.pyplot as plt def plot_score_distribution(known_scores, unknown_scores, threshold): plt.hist(known_scores, bins50, alpha0.6, labelknown) plt.hist(unknown_scores, bins50, alpha0.6, labelunknown) plt.axvline(threshold, colorred, linestyle--, labelthreshold) plt.xlabel(confidence score) plt.ylabel(count) plt.legend() plt.show()把 known_scores 和 unknown_scores 的分布画出来可以很直观地看到两个分布的重叠程度。重叠越少模型对已知和未知的区分越好重叠越大说明模型在很多未知样本上也给出了高置信度这是非常危险的信号。6. 常见问题与排查思路6.1 高频问题排查表问题现象常见原因解决思路训练 loss 不下降学习率过大或模型结构有误降低学习率检查数据标签是否错位AUROC 接近 0.5Unknown Data 与 Known Data 太相似或模型训练不充分换更难的数据集增加训练轮数检查特征提取能力FPRTPR95 接近 1模型对所有样本都给出高置信度考虑引入温度缩放、Energy Score 等更复杂的 OOD 方法验证集上指标高、测试集指标低阈值在验证集上过拟合扩大验证集规模使用交叉验证多次重复实验同一份代码两次运行结果不同没固定随机种子或 GPU 计算存在非确定性固定 PyTorch 和 NumPy 种子多次运行取均值Unknown Data 混入训练集后指标异常偏高数据泄漏排查数据划分的类别交集确保 unknown 完全不参与训练6.2 为什么 MSP 是最简单也是最稳的基线Maximum Softmax Probability也就是最大 Softmax 概率是验证任务里最常见的 Baseline。它的核心假设是模型对已知类样本会给出更高的最大概率对未知类样本会比较犹豫。这个方案实现简单只需要在模型输出后取 Softmax 最大值几乎不需要修改网络结构所以它非常适合作为 Verification Baseline。但它的缺点也很明显深度神经网络往往过度自信对分布外输入也会给出较高的 Softmax 概率。正因为如此MSP 通常是一个“下限”参考。如果某个新方法连 MSP 都打不过那说明提升可能来自实验设置问题而不是算法本身。6.3 一个非常容易忽略的坑同源数据划分在本文示例中我们让 CIFAR-10 的 5 个类作为已知类另外 5 个类作为未知类。这种做法虽然方便但会让任务偏简单。因为同一个数据集内部的图像在风格、分辨率、背景上高度一致模型很可能仅仅因为纹理或者颜色分布差异就把未知类区分开。更贴近真实场景的做法是使用另一个数据集作为未知类。例如用 CIFAR-10 作为已知类分布用 CIFAR-100 或 SVHN 作为未知类分布。这样才能验证模型是否学到了“对已知类别的语义边界”而不是数据集的某种表面特征。在正式实验中建议至少准备多个不同难度的 Unknown Data 集合避免单点结论。7. 最佳实践与工程建议7.1 实验配置要可追溯验证基线的核心价值之一是“可复现”。我建议在项目中保存一份固定配置记录下列信息已知类列表和未知类列表训练集、验证集、测试集的大小模型结构、预训练权重来源优化器、学习率、训练轮数随机种子每天运行使用的框架版本。一个可行的方式是把这些配置写入 JSON 或 YAML 文件并将结果和配置一起存档。这样即使三个月后再回看实验结果也能知道当前指标是在什么条件下产生的。7.2 尽量使用多次独立重复实验深度学习的训练过程有随机性单次实验不足以证明方法优劣。工程上建议固定一个随机种子集合比如[0, 1, 2, 42]分别跑完实验后汇报 AUROC 的均值和标准差。稳定性和平均值同样重要。如果某个方法在 seed0 时表现很好但换一个 seed 就大幅下降那么它对实际业务的价值要打折扣。7.3 先定评估协议再做模型改进很多团队拿到 Unknown Data 分类需求后第一反应是换更好的 Backbone 或者更复杂的损失函数。但如果没有先建立 Verification Baseline后续所有改进都缺乏参照。更好的顺序是固定数据划分固定评估指标跑最简单的 MSP 基线记录 baseline 结果再逐步引入更复杂的算法例如温度缩放、ODIN、能量分数、OpenMax每次只改变一个变量比较结果。这套做法看起来不够炫酷却是保证技术演进方向正确的关键。尤其在安全敏感场景中谨慎的评估流程比技巧更重要。7.4 对未知样本保持“可解释的拒绝”真实工程系统里模型除了输出分类结果还需要输出一个“不确定”信号。当置信度分数低于阈值时系统应该走一条独立的分支比如人工审核、二次校验或者直接拒绝。这个阈值的确定不应该只依赖算法人员的经验而要和产品、安全团队共同协商。验证基线提供的是模型能力量化结果业务侧则要结合误报成本和漏报成本选择一个对业务最合理的阈值。8. 总结与下一步方向本文从验证基线设计的角度拆解了 Unknown Data 分类问题。我们先明确了 Unknown Data 与 OOD、OSR 的关系然后设计了一套可复现的数据划分和评估协议。在代码实现部分基于 CIFAR-10 构建了已知类与未知类集合并以最大 Softmax 概率作为 Baseline计算了 AUROC、FPRTPR95 和混淆矩阵。这套流程的核心价值并不是把模型准确率提得有多高而是帮助你在上线前回答三个问题模型是否对未知数据保持谨慎现有决策阈值会造成多少误报新的改进算法是否真的比 Baseline 更可靠如果你正在处理视觉分类相关的工程问题可以把这套代码作为起点先把评估体系固化下来。后续可以继续探索的方向包括使用不同难度的 Unknown Data 集合做压力测试引入 ODIN 中的温度缩放策略尝试基于能量的分数函数在分类网络中加入 OpenMax 层来对开放集进行建模。不同方法的适用场景差异很大但只要验证基线足够稳健所有方法都可以在同一个尺度下公平比较。如果本文对你有帮助可以收藏备用也欢迎在实践中结合实际数据继续验证和调整这套流程。