
我在处理一个图片分类需求时发现模型在训练集上表现不错但遇到一张训练集里没有的新类型图片时它还是会给出一个非常肯定的错误标签。后来我尝试用类似 VeriCam 这种“先验证再分类”的方式去搭基线问题才真正变得可控。VeriCam 这个项目名里关键信息其实是两个一是 Verification二是 Baseline。“验证”和“基线”放在一起说明它要解决的不是“把已知类别分得更准”的问题而是“分类任务遇上未知数据时模型应该如何表态”的问题。这篇文章我不会复述某个仓库的文档因为项目正文几乎没有给细节。我更想把它当成一套处理未知数据的通用方法论来讲先想清楚为什么需要验证基线再讲搭建最小工作流的步骤最后落到工程化的时候会遇到哪些坑。1. 我为什么不再迷信模型置信度1.1 高置信度不等于模型真的认识这个样本很多团队在训练图像分类模型时已经习惯了“最后一层 softmax 概率最高的是预测结果”这套逻辑。模型给出 0.99 的概率我们就认为它非常有把握。但在开放环境下这个 0.99 经常是假的。原因不复杂。Softmax 的设计本身就把所有类别的输出做了归一化最后所有概率加起来等于 1。哪怕一个样本不属于任何一个已知类别模型也必须在所有已知类别里“选一个”。常见的结果是它选一个在特征空间里离得最近的类别然后把这个类别的概率拉得很高。这个行为看起来像“自信”实际是结构造成的强制输出。你可能会说那我把置信度阈值设高一点低于 0.95 的就拒绝。真这么做会发现两个问题第一某些真正的已知样本它也只有 0.7你会把已知样本误杀。第二某些完全不属于任何已知类别的未知样本它照样给出 0.99。所以单看置信度不能真正区分“知道”和“不知道”。这也是我把“验证”单独立出来而不是继续叠分类层的原因。1.2 验证基线的核心目标让“不知道”变得显式VeriCam 名字里的 Verification更像是在分类器前面加一道“准入检查”先判断这个输入和模型已知的数据分布是否一致一致才允许分类器给出具体类别不一致就明确返回“未知/拒绝”而不是硬控一个类别。这里容易误解的是很多人以为把“未知样本”也当成一个类别来训练就行。实践一段时间后你会发现未知数据是无限的。今天你能收进来的“第 101 类”明天可能又会冒出一个“第 102 类”。你不可能提前把所有未知类别全部收集起来。这恰恰是“开放类别”和普通分类最大的不同。所以验证基线要解决的真正问题不是“这个样本属于哪一类”而是“这个样本到底属不属于我当前能够可靠分类的空间”。项目里如果没有这个验证层分类器再准也只是在一个闭合世界里准。生产环境一旦开放这个前提就失效了。2. VeriCam 这类方案想建立的最小工作流如果只从标题层面理解VeriCam 给定的是一个基线而不是一个完整业务系统。要复现一个类似思路我习惯先把最小流程拆成四步。2.1 先明确你说的“未知数据”是哪一种未知数据不是一个单一的输入类型。常见有三种类别外数据训练集里本来就不存在这个类别比如模型只见过猫和狗现在输入了一张卡车照片。领域漂移数据语义上还是同一个类别但图像风格、拍摄环境、分辨率全变了比如白天拍的猫变成夜里红外相机拍的猫。噪声和异常输入损坏图片、遮挡图片、空白背景、被抓拍到的半张脸。这几种数据对系统的要求不一样。如果只是类别外数据特征距离通常有效但如果是领域漂移有可能模型把夜里的猫也当成未知因为特征偏移整体都变了。所以第一步不是先写代码而是定义清楚你未来会遇到哪一种“未知”。否则后面选阈值、选指标都会像无头苍蝇。2.2 用特征表达和距离度量取代“只看概率”在分类模型里最后一层之前的嵌入向量往往是比 softmax 概率更值得看的东西。可以把嵌入向量理解成模型对输入的一种“压缩描述”。同一个已知类别的样本在特征空间里通常比较靠近未知类别的样本虽然可能也会挤到某个区域但整体距离已知类别的原型中心会更远。一个很自然的方案是拿训练好的分类模型去掉最后的全连接分类层对每个已知类别的样本提取嵌入特征计算每个类别的中心向量或统计分布判断新样本时看它到最近类别中心的距离距离小于阈值才进入分类阶段距离大于阈值直接标记为“未知”。看起来很像最近邻分类。是的本质上就是把分类问题换成“验证该样本是否在已知半径以内”。2.3 阈值要用小样本验证不能拍脑袋如果输入材料里没有给你现成的阈值不要盲调。我第一次做的时候直接把 95 分位的距离当阈值结果误杀率特别高。更稳的做法是先准备一个平衡验证集包含两部分已知类别的样本用来算“已知数据的保留率”与训练类别分布明显不同的样本用来算“未知数据的拒识率”。然后在验证集上画曲线找到一个可接受的平衡点。这个阶段不需要太多数据但样本要有代表性。重点是先跑通“提取特征—计算距离—判定阈值”这条链路验证它是不是真的能把已知和未知粗略分开。3. 用代码搭一个轻量验证层接下来给一个通用示例。这个示例更接近于工程骨架不是某个特定模型的最优实现。如果你想复现需要替换成自己的数据和模型。3.1 从已有分类模型里抽取特征我们用最常见的 PyTorch 风格写一个抽取函数。import torch from torchvision import models model models.resnet18(pretrainedTrue) # 去掉最后的全连接层让模型输出特征向量 model torch.nn.Sequential(*(list(model.children())[:-1])) model.eval() def extract_features(image_tensor): with torch.no_grad(): feat model(image_tensor.unsqueeze(0)) return feat.flatten().cpu().numpy()这里不一定要用预训练模型关键是产出和业务数据匹配的特征。如果你的数据是工业缺陷图ImageNet 预训练模型不一定好用如果数据量大最好在业务数据上 fine-tune 之后再抽特征。3.2 给每个已知类别建立原型假设我们已经有一批已知类别的训练图片可以先给每个类别抽取特征并求平均向量。import numpy as np # 伪代码事先准备好 {label: [image_paths]} class_prototypes {} for label, paths in known_samples.items(): features [] for image_path in paths: img_tensor load_and_preprocess(image_path) # 各项目不同 feat extract_features(img_tensor) features.append(feat) class_prototypes[label] np.mean(features, axis0)这里有一个细节平均值只能代表一类样本的中心如果某个类别内部方差很大比如“动物”类里面既有猫又有狗把这类样本压缩成一个中心点会损失很多形状信息。更精细一点的方案是用高斯分布表示每个类别保留均值和协方差再计算马氏距离而不是欧氏距离。对于 baseline 阶段用欧氏距离先跑通没问题如果验证结果不理想优先检查是不是类别内部方差问题再决定要不要换分布模型。3.3 在进入分类头之前加一道“开关”def predict_veri_class(image_tensor, threshold): feat extract_features(image_tensor) distances {} for label, prototype in class_prototypes.items(): distances[label] np.linalg.norm(feat - prototype) min_label min(distances, keydistances.get) min_dist distances[min_label] if min_dist threshold: return min_label, min_dist, known else: return None, min_dist, unknown只看代码逻辑它不复杂。但实际运行时要注意threshold 不是全局不变的。随着业务数据分布变化原来合适的阈值可能六个月后就失效了。所以这个“开关”后面一定要跟监控而不是一劳永逸。最小工作流跑通后不要急着把它包装成一个高大上的系统。先拿一条真实未知样本和一条已知样本确认边界符合预期再往下做。4. 从离线验证基线到在线使用还差什么很多项目在离线测试时好端端一上线就被打回原形。问题多半不是模型本身而是工程化程度不够。4.1 单次跑通只代表没有断点不代表稳定可用一个 baseline 在 notebook 里跑通是很容易的。你手工调用了几个函数路径是写死的前处理是手工做的输出直接打在屏幕上。但如果你要把它接到 API 服务里就会遇到这几个问题请求图片的分辨率不统一前处理怎么写如果图片损坏读取失败是返回“未知”还是报 500特征提取需要多少 GPU 显存并发 10 个请求会不会 OOM模型服务重启后预计算的原型向量还存在吗新类别加入后什么时候重新训练原型这些问题和分类算法没有关系但都决定系统能不能长期稳定跑下去。4.2 原型向量和阈值也要纳入版本管理大多数人习惯管理模型文件但容易忽略原型向量和阈值也是模型的一部分。你更新了特征提取模型原来的原型向量就不能直接沿用必须连同验证集一起重新计算。阈值也是一样模型更新后特征空间发生变化原来的距离分布不一定还成立。所以建议把下面这些东西放到同一个模型包里特征提取器权重每个类别的原型向量验证集样本路径或哈希阈值及其对应指标特征提取器版本、依赖库版本。这样未来如果出现线上误判你还能追溯当时用的到底是什么版本。4.3 线上监控不能只盯准确率在线上的真实请求里你并不知道每一个样本的真实类别。你只能看到系统输出的是“已知类别 A”还是“未知”。这时候最需要监控的是分布的漂移被判定为“未知”的比例是不是突然升高判为“已知”的样本特征距离是不是逐渐变大某个类别被召回的分布是不是开始集中到某几个 request 来源。如果未知比例持续升高往往不是模型坏了而是业务输入分布变了。这时候重新调阈值才有意义如果直接硬调阈值反而可能把所有输入都误杀。5. 落地过程中容易踩的几个坑5.1 验证集里混入了已知类别的脏样本有一个很容易忽略的数据问题在准备“未知样本”验证集时你以为它是未知实际上它和某个已知类别高度相似甚至是从同一批源数据分出来的。如果这种脏样本太多验证出来的阈值会过于乐观。我在早期做这类实验时喜欢从网上随机抓一批图片当未知样本。后来发现其中一部分是之前训练集的模糊缩略图模型在里面“见过”特征距离自然比较近。于是实验里看起来模型的拒识能力很强实际换到真实未见过数据表现就崩了。更可靠的做法是把未知样本的来源尽量独立并且做一个简单去重计算一下未知样本与已知训练集样本的相似度把相似度过高的先删掉再验证。5.2 用指标判断不要用感觉判断判断验证基线好还是差最好建立两个指标已知数据保留率应该被接受的已知样本里有多少被正确接受未知数据拒识率应该拒绝的未知样本里有多少被正确拒绝。调阈值时看这两个指标的权衡曲线而不是只盯着某个想象出来的准确率。这里要说清楚不存在一个“最优阈值”永远适用。它取决于业务你更怕哪种错误如果是医疗影像筛选误把新病灶当作未知拒绝比把已知病灶误分更严重如果是垃圾图片过滤放过未知类型可能比误杀正常图片更容易造成投诉。所以在选择阈值之前一定要先把业务损失函数想明白。5.3 环境提示看起来和模型无关也会浪费一下午实践里很多时间不是花在算法上而是花在环境不一致上。比如换了一台 CPU 机器跑推理某些底层库会提示编译优化级别不匹配导致程序直接报错又或者是不同机器上模型权重加载路径不同特征提取结果不收敛。我不建议遇到这类问题先去调模型参数。先按这个顺序排查输入图片能否成功读取尺寸、通道数、像素范围是否一致模型权重的版本和特征提取器预期是否一致原型向量文件和当前模型是否配套依赖库版本和模型保存时的环境是否一致阈值文件是不是旧版模型留下的。如果程序能跑但结果不对也先从输入和模型版本入手不要一开始就认为是阈值选得不好。5.4 系统里不能只有一个“拒绝”动作最后这个坑比较隐蔽。很多验证基线会把“未知”当作终点拒绝掉就结束了。但在真实业务里“未知”只是一个中间态它后面还需要有一个处理流程。比如你是做客服工单分类模型遇到一个以前没见过的业务问题你不能只返回“未知”就算完事。更合理的方式可能是先进入“待人工确认”队列记录这个样本的特征每过一段时间把人工确认后的新样本拿出来聚类看是否值得新增类别确认后的样本进入特征池更新原型或训练数据。这也是为什么 VeriCam 里“Baseline”这个词重要它不是最终系统只是验证和落地的参照。真正的系统要在基线之上继续长出一个“未知样本流转闭环”。6. 沉淀成一套可复用的判断框架我把这些经验整理成一个五步判断框架也适合用在你自己的项目里。第一步先看待分类问题是不是开放集问题。如果业务环境里不可能出现训练集之外的输入那么传统分类可以继续用一旦可能遇到新类别或风格漂移就必须引入验证层。第二步看验证用什么表示。类别多但每个类别样本少直接用样本库做最近邻更简单每个类别样本充分可以用原型中心加距离阈值类别内部形态复杂可能需要更细的划分子簇或者用概率分布模型。第三步看阈值是否存在稳定区间。如果无论怎么调阈值已知保留率和未知拒识率都无法同时达到业务要求那问题不在阈值而在特征本身。可能要换更大的特征提取模型、做更强数据增强或者重新设计训练目标。第四步看未知样本被拒之后去哪里。如果系统只有拒绝没有后续那验证做得再好也只是把问题转移给用户或人工。真正负责的做法是把“未知”变成下一次迭代的输入。第五步看这套基线能不能持续维护。原型向量能不能自动更新阈值变化有没有监控模型更新时验证集会不会跟着更新。能持续维护的方案才谈得上长期价值否则它只是一次离线实验。最后想说的一个经验处理未知数据最容易被低估的不是模型能力而是“承认自己不知道”的能力。我们做了那么多分类模型不断追求准确率提升却很少让模型学会在输入不在可信范围内时拒绝回答。VeriCam 这类项目提醒我的恰恰是这一件事分类不是终点验证才是兜底。如果现在让我给刚接触这类问题的人一个建议我不会先让他去调更复杂的模型而是让他准备一个含已知和未知样本的小验证集用最朴素的“特征距离 阈值”先跑一遍。先看看模型的特征空间到底长什么样哪些未知样本会被误判成已知哪些已知样本又会被误拒。这个过程跑通之后你对 baseline 的理解会比任何复杂模型都更扎实。然后再去考虑分布建模、异常检测、聚类这批工具你也能搞清楚它们每一层到底在解决什么问题。技术方案会变模型结构会换但“先验证再分类先拒绝后纠正”这条思路在真实业务里会一直有用。