1. 当你的模型“睁眼瞎”闭集分类在现实世界中的尴尬咱们先来聊一个我亲身经历过的场景。几年前我参与了一个智能安防摄像头的项目核心任务是人脸识别门禁。我们花了大力气收集了公司所有员工的人脸照片大概几百号人训练了一个在当时看来准确率高达99.8%的深度卷积神经网络CNN。上线第一天测试一切顺利同事们刷脸进门模型表现得像个优等生。但第二天问题来了一位快递小哥来送件他走到摄像头前系统屏幕上的识别框稳稳地锁定了他然后……在几百个已知员工名单里它“自信地”给出了一个概率最高的匹配结果把快递小哥认成了某位发型相似的同事。这就是典型的“闭集分类”模型在开放世界中的翻车现场。所谓闭集分类就是我们训练模型时预先告诉它“世界就这K个类别你只管在这K个里选一个最像的。” 模型学到的是如何在这K个类别构成的封闭决策边界里做最优划分。它就像一个只学过课本上固定题库的学生考试时哪怕遇到完全没见过的超纲题它也硬着头皮要从学过的答案里选一个最“像”的填上去——因为它的大脑里根本没有“这道题我不会”这个选项。在实验室里在MNIST、CIFAR这些经典数据集上闭集分类模型可以刷出惊人的高分。但现实世界是“开放”的充满了未知。你的安防系统总会遇到访客、快递员、推销员你的自动驾驶视觉系统总会遇到训练集里没有的奇特车型、罕见的交通标志、甚至是路上突然滚过来的一个彩色皮球。传统的SoftMax函数在输出层会强制将所有输入归类到已知类别之一并且所有类别的概率之和为1。这导致模型对于任何输入哪怕是完全没见过的“外星人”图片也必须给出一个“概率分布”而且往往因为神经网络强大的拟合能力这个分布里某个已知类的概率值可能还不低。这就造成了模型对于未知事物“过度自信”的误判是许多AI系统在实际部署中可靠性危机的根源。所以我们需要给模型装上一种能力在识别已知事物的同时能坦然地说出“我不认识这个玩意儿”。这种能力就是“开集识别”Open Set Recognition, OSR。它不满足于做一个封闭世界的“选择题高手”而是要成为一个开放世界的“侦察兵”既能确认友军已知类也能警觉地发现并报告潜在的敌军或未知目标未知类。接下来我们要深入探讨的OpenMax算法就是为深度网络装上这样一个“未知探测器”的经典且巧妙的方法。2. OpenMax的核心用“尾部风险”量化未知OpenMax算法的论文标题直白地表达了它的野心——《Towards Open Set Deep Networks》。它的核心思想非常直观既然模型在已知类“边界”附近的样本上最容易失效比如长得有点像员工A的快递员那我们能不能量化这种“失效的可能性”并用它来调整模型的输出信心这个想法类比一下金融领域的“风险控制”就很好懂了。银行给客户做信用评估不仅看他的平均收入类似模型的分类得分更关注他的“尾部风险”——比如负债极高、收入极不稳定的极端情况。即使这个人平均收入不错但如果存在巨大的尾部风险银行也会非常谨慎。OpenMax做的就是类似的事情它为每个已知类别都建立一个“风险模型”用来评估一个新样本属于该类别的“极端异常”程度也就是它落在该类别的数据分布“尾部”的可能性。那么如何建模这个“尾部”呢这里就用到了一个听起来很理论但实际非常有力的工具极值理论。2.1 极值理论我们为什么关心“最不像”的样本极值理论是统计学中专门研究极端偏离事件极大值或极小值概率分布的一个分支。为什么普通分布比如高斯分布不行呢想象一下正态分布的钟形曲线它的两端尾部是无限趋近于零但无限延伸的。实际中我们对于一类物体的认知边界往往是有限的、突然的。比如“猫”这个类别有一些长得非常奇怪的猫极端样本但再怎么奇怪它和“狗”之间还是有鸿沟的。用高斯分布去拟合“猫”类所有样本的特征其尾部概率可能会错误地给一些狗图片赋予一个非零但很小的概率导致模型无法坚决地拒绝它。极值理论不关心数据的整体分布形状它只聚焦于极端值的分布规律。它告诉我们对于许多类型的独立同分布数据其极大值或极小值的分布会收敛到三种形式之一Gumbel型、Fréchet型和Weibull型。在开集识别的场景下我们关心的是“距离某个类别中心最远的、最不像该类别的那些样本”的分布。实践证明用Weibull分布来拟合这个“距离的尾部”是非常有效的。你可以把Weibull分布想象成一个更灵活、更能刻画“边界”的分布模型它擅长描述像材料疲劳强度、设备寿命这类有“阈值”概念的极端事件——正好对应我们“类别边界”的概念。在OpenMax中对于每一个已知类别我们都会用该类别的训练样本拟合一个Weibull分布这个分布专门用来描述“这个类别的样本能‘奇怪’到什么程度”的概率。拟合的数据就是每个训练样本到其类别中心的距离中的那些极大距离。2.2 OpenMax算法两步走建模与校准理解了“用Weibull拟合尾部”这个核心思想OpenMax的具体步骤就清晰了。它主要分为两个阶段离线建模阶段和在线校准阶段。离线建模阶段是为每个已知类别打造一把专属的“标尺”。训练一个标准的闭集分类网络比如一个在ImageNet上预训练的ResNet然后在你的已知类别数据上做微调。这个网络作为我们的基础特征提取器和分类器。为每个类别收集“健康样本”把第i类的所有训练图片喂给网络取出网络倒数第二层通常是全连接层之前的激活向量Activation Vector, AV。这里有个关键只保留那些被网络正确分类为第i类的样本的AV。那些被网络自己就分错了的样本说明它们可能本身就模棱两可不适合用来定义类别的“健康”边界。这些正确的AV集合记为AV_i。计算类别质心把AV_i里所有向量求个平均得到这个类别的“平均激活向量”MAV_i你可以把它理解为这个类别在特征空间里的“中心点”或“典型代表”。计算距离并拟合Weibull计算AV_i中每一个向量到其质心MAV_i的距离通常用余弦距离或欧氏距离得到一组距离集合D_i。然后我们不是用整个D_i去拟合分布而是专门抽取其中最大的若干个距离比如最大的20%用这些极大距离去拟合一个Weibull分布。这个拟合好的Weibull模型就是这个类别的“异常度量尺”——它告诉我们一个样本距离本类中心多远时其“离谱”程度已经进入了小概率的极端区间。在线校准阶段就是用这些“标尺”去衡量每一个新来的样本。获取新样本的激活向量来了张新图片通过网络得到它的激活向量AV_x以及原始的SoftMax得分向量[s1, s2, ..., sK]。测量到各类中心的距离计算AV_x到每一个已知类别质心MAV_j的距离d_xj。查询“异常概率”将距离d_xj输入到第j类对应的Weibull模型中。这个模型会返回一个概率值w_score(d_xj)。这个值的含义是对于一个属于j类的样本其到类中心的距离超过d_xj的概率有多大换句话说它量化了当前样本相对于j类来说“有多极端”。校准原始得分显然w_score(d_xj)越大说明这个样本越不像j类。因此我们定义样本属于j类的可信度权重为1 - w_score(d_xj)。然后用这个权重去衰减该类的原始SoftMax得分s_j s_j * (1 - w_score(d_xj))。如果一个样本距离某个类中心非常远w_score接近1那么该类得分就会被严重衰减甚至接近0。生成未知类得分最关键的一步来了OpenMax会计算一个“未知类”的得分。它的逻辑是这个样本被误判为任何一个已知类的风险总和。具体计算是s_unknown Σ [s_j * w_score(d_xj)]即用每个已知类的原始得分乘以它“不属于该类”的概率异常概率然后求和。如果一个样本对所有已知类都很异常所有w_score都很大那么s_unknown就会很高。重新归一化与决策现在我们有了一个校准后的得分向量[s1, s2, ..., sK, s_unknown]。对这个向量做一次SoftMax就得到了包括“未知类”在内的新概率分布。最后设定一个阈值比如0.5如果“未知类”的概率最高或者所有已知类的概率都低于某个阈值我们就判定该样本为未知类别。这个过程就像是一个严谨的质检员。传统模型只看产品样本最像哪个标准品已知类。而OpenMax质检员会额外做一件事拿起每个标准品的“瑕疵容忍度量尺”Weibull模型量一下这个产品离标准品的偏差有多大。如果偏差超过了所有量尺的容忍限度就贴上一个“未知型号”的标签。3. 手把手实战用代码理解OpenMax光说不练假把式咱们直接上代码看看OpenMax的关键步骤如何实现。这里我用PyTorch框架和一个简单的示例来演示核心逻辑。假设我们已经有了一个训练好的闭集分类模型model以及已知类别的训练数据。首先是离线建模阶段为每个类别拟合Weibull分布。我们需要用到libMR这个库它是OpenMax作者团队发布的专门用于极值分布拟合。import numpy as np import torch from libmr import Weibull def fit_weibull_models(model, train_loader, known_classes, tail_size20): 为每个已知类别拟合Weibull模型。 Args: model: 训练好的闭集分类模型。 train_loader: 训练数据加载器只包含已知类别。 known_classes: 已知类别的列表。 tail_size: 用于拟合的尾部样本数量或比例。 Returns: weibull_models: 字典key为类别索引value为拟合好的Weibull对象。 class_centroids: 字典key为类别索引value为该类别的质心(MAV)。 model.eval() device next(model.parameters()).device # 存储每个类别的所有激活向量和距离 class_activations {cls: [] for cls in known_classes} class_centroids {} # 第一步收集每个类别被正确分类的激活向量 with torch.no_grad(): for images, labels in train_loader: images, labels images.to(device), labels.to(device) # 获取倒数第二层的激活向量。这里假设model有一个方法get_activations activations model.get_activations(images) # shape: [batch_size, feat_dim] outputs model.classifier(activations) # 分类头 _, preds torch.max(outputs, 1) for idx in range(len(labels)): if labels[idx] in known_classes and preds[idx] labels[idx]: # 只保留正确分类的样本 cls labels[idx].item() class_activations[cls].append(activations[idx].cpu().numpy()) # 第二步计算每个类别的质心MAV和距离 weibull_models {} for cls in known_classes: if not class_activations[cls]: continue activations np.array(class_activations[cls]) # [num_samples, feat_dim] centroid np.mean(activations, axis0) # 计算质心 class_centroids[cls] centroid # 计算每个样本到质心的距离这里使用余弦距离更常用在特征空间 # 余弦距离 1 - 余弦相似度 norms np.linalg.norm(activations, axis1, keepdimsTrue) norm_activations activations / norms centroid_norm centroid / np.linalg.norm(centroid) # 余弦相似度 cos_sim np.dot(norm_activations, centroid_norm) distances 1 - cos_sim # 得到余弦距离集合 D_i # 第三步选取尾部最大距离来拟合Weibull sorted_distances np.sort(distances) tail_distances sorted_distances[-tail_size:] # 取最大的tail_size个距离 # 使用libMR进行Weibull拟合拟合极大值 weibull Weibull() weibull.FitHigh(tail_distances, len(distances)) # 传入尾部距离和总样本数 weibull_models[cls] weibull return weibull_models, class_centroids接下来是在线校准阶段对新样本进行预测。def openmax_predict(model, weibull_models, class_centroids, image, known_classes): 使用OpenMax对新图像进行预测。 Args: ... (同上) image: 单张输入图像 [C, H, W] Returns: openmax_probs: 包含未知类的概率向量。 predicted_class: 预测的类别可能是已知类索引或-1代表未知。 model.eval() device next(model.parameters()).device image image.unsqueeze(0).to(device) with torch.no_grad(): # 获取新样本的激活向量和原始得分 activation model.get_activations(image) # [1, feat_dim] activation_np activation.squeeze().cpu().numpy() original_scores model.classifier(activation) # [1, num_known_classes] original_scores torch.softmax(original_scores, dim1).squeeze().cpu().numpy() # 计算到每个已知类质心的距离 dist_to_centroids {} for cls, centroid in class_centroids.items(): # 同样使用余弦距离 a_norm activation_np / np.linalg.norm(activation_np) c_norm centroid / np.linalg.norm(centroid) cos_sim np.dot(a_norm, c_norm) dist_to_centroids[cls] 1 - cos_sim # 校准已知类得分并计算未知类得分 calibrated_scores np.zeros(len(known_classes)) unknown_score 0.0 for i, cls in enumerate(known_classes): if cls not in weibull_models: calibrated_scores[i] original_scores[i] continue weibull weibull_models[cls] dist dist_to_centroids[cls] # w_score 是样本不属于该类距离极端的概率 w weibull.w_score(dist) # 调用libMR的w_score函数 # 校准得分用属于该类的概率 (1-w) 作为权重 calibrated_scores[i] original_scores[i] * (1 - w) # 累加未知类得分用不属于该类的概率 (w) 加权 unknown_score original_scores[i] * w # 组合得分向量已知类校准得分 未知类得分 combined_scores np.append(calibrated_scores, unknown_score) # 使用SoftMax得到最终概率 openmax_probs np.exp(combined_scores) / np.sum(np.exp(combined_scores)) # 决策找出最大概率的类别 predicted_idx np.argmax(openmax_probs) if predicted_idx len(known_classes): # 如果最大概率是最后一个未知类 predicted_class -1 # 用-1表示未知类 else: predicted_class known_classes[predicted_idx] return openmax_probs, predicted_class通过这段代码你可以清晰地看到OpenMax并不是一个全新的网络结构而是一个后处理校准框架。它建立在任何已有的闭集分类模型之上通过额外的距离计算和极值分布建模为模型输出增加了“不确定性”的维度。这种非侵入式的设计使得它很容易集成到现有的AI系统中。4. OpenMax的优劣与演进不止于WeibullOpenMax为开集识别提供了一个优雅而有效的基线方案但它并非完美无缺。在实际项目中应用它有几个坑需要注意。优势方面首先是概念清晰实现相对简单。它不需要修改网络结构或损失函数只需要在模型推理前增加一个离线建模步骤对线上预测的计算开销增加也很小。其次它的可解释性强。我们可以明确地看到一个样本因为距离某个类中心太远w_score值高而导致该类得分被衰减最终被判为未知。这比一些端到端的黑箱方法更让人安心。局限性也很明显。第一对特征空间的质量依赖很高。OpenMax的有效性建立在“同一类样本在特征空间内聚集不同类样本相互分离”的假设上。如果基础闭集模型提取的特征本身区分度不够或者类别边界模糊那么距离度量和Weibull拟合都会失效。第二“尾部”样本的选取敏感。tail_size这个参数需要调优选多了可能把正常样本当极端选少了可能对尾部刻画不准。第三仅使用到类中心的距离可能忽略了类内分布的更复杂结构比如多模态分布。正因为这些限制研究者们在OpenMax的基础上提出了许多改进。例如PROSER算法通过合成“虚拟”的未知类样本在训练时就让模型主动学习拒绝它们。ARPL及其变体则通过设计新的损失函数在特征空间里同时拉近同类样本、推开不同类样本并扩大整个已知类别集合与外部未知空间的边界。这些方法尝试将开集识别的能力直接“训练”到网络里而不是事后校准。另一个重要的方向是结合其他不确定性估计方法。OpenMax本质是一种基于距离的不确定性估计。我们可以把它与基于贝叶斯神经网络的认知不确定性估计或者基于模型集成的方法结合起来构建更鲁棒的开集识别系统。例如一个样本如果同时被OpenMax判定为“距离远”又被Dropout多次推理的方差判定为“认知不确定性高”那么它属于未知类的置信度就更高了。在我经手的一个工业缺陷检测项目中我们就融合了OpenMax和蒙特卡洛Dropout。产品表面缺陷种类繁多且不断有新的缺陷类型出现。我们先用正常品和一批已知缺陷品训练模型用OpenMax处理明显的未知缺陷。同时对于OpenMax置信度不高的“灰色地带”样本再用Dropout方差进行二次校验有效降低了将新缺陷误判为已知缺陷的风险。5. 给你的模型装上探测器实践建议与避坑指南如果你正准备在自己的项目中尝试开集识别特别是OpenMax方法这里有一些从实战中总结出来的建议。第一步也是最重要的一步夯实你的闭集模型。OpenMax的性能天花板很大程度上取决于你基础特征提取网络的好坏。务必确保你的模型在已知类别上的分类准确率足够高并且特征具有判别性。可以先用t-SNE或UMAP可视化一下特征空间看看各类是否聚拢、类间是否分离。如果特征空间一团糟后续的Weibull拟合就是无源之水。第二步精心设计距离度量。原始OpenMax论文中使用了余弦距离这在许多基于深度特征的任务中是有效的因为它对特征的幅度不敏感只关注方向。但在某些场景下欧氏距离或马氏距离可能更合适。你可以尝试不同的距离度量并在一个小的验证集包含一些模拟的未知类样本上对比效果。第三步调优Weibull拟合参数。libMR的FitHigh函数除了尾部样本数量还可能涉及其他参数。关键是理解你拟合的是什么——是每个类“最不像自己”的那批样本的距离。你可以根据每个类别的样本数量动态设置尾部比例例如对样本多的类别取比例小点样本少的类别比例大点而不是固定值。第四步设置合理的决策阈值。OpenMax输出未知类的概率后直接取最大概率作为判断是一种方式。但在实际应用中你可能需要为“已知类”设置一个接受阈值。比如只有当某个已知类的概率大于0.7且大于未知类概率时才判定为该已知类否则都判为未知。这个阈值需要根据你对误拒把已知类判为未知和误纳把未知类判为已知的容忍度来权衡。一个常见的“坑”是训练集中已知类的数据分布不平衡。如果某个类别样本很少那么计算出的质心可能不具代表性拟合的Weibull分布也会非常不稳定。对于小样本类别可能需要采用特征增强、或使用更鲁棒的质心估计方法如基于支持向量的中心甚至考虑在特征层面进行归一化处理。最后记住开集识别是一个系统级问题而不仅仅是算法问题。除了改进模型在数据流设计上也可以下功夫。比如建立一个“未知样本池”将所有被判为未知的样本暂时存储起来由人工或更高级的系统进行复核。定期将这些复核后确认为新类别的样本加入训练集重新训练和更新OpenMax模型让你的系统能够持续学习、进化。这套流程才是应对真正开放世界的长久之道。从OpenMax这个精巧的“未知探测器”开始你可以逐步构建起一个能坦然面对未知、并从中学习的健壮AI系统。