ViT OOD检测实战:SVD与Typicality Maps识别分布外样本
2026/9/13 3:32:19 网站建设 项目流程

部署一个视觉 Transformer(ViT)模型到真实业务里,通常要面对的难题不一定是 Top-1 准确率差了几个点,而是模型在遇到“从来没见过的输入”时,依然会给出一个置信度很高的错误预测。比如在工业质检场景中,训练数据只包含正常产品和已标注缺陷,线上却混入了一种新型划痕;比如在自动驾驶感知里,训练数据几乎没有覆盖某种极端天气下的遮挡形态。模型不会告诉你“这超出我的认知范围”,它只会照常输出一个分布,仿佛一切都见过。

对于这类问题,业界已经有一个专门的研究领域:Out-of-Distribution Detection,也就是分布外样本检测,简称 OOD 检测。它的目标很直接:让模型学会对自己“不懂的输入”说不知道。

但如果只知道用 Softmax 置信度去筛 OOD,效果往往不理想。尤其是当骨干网络换成 Vision Transformer 之后,问题会变得更加隐蔽:CLS token 的输出概率很容易过度自信,而真正暴露输入“异样”的信息,其实散落在各个 patch token 里,散落在模型中间层的特征结构中。

这篇文章要讲的,是一种结合了 SVD(奇异值分解)和 Typicality Maps(典型性映射图)的 OOD 检测思路。它把 OOD 检测从“只看一个全局特征向量”升级为“逐 patch 分析特征是否典型”,再用 SVD 提取训练分布的主干方向,最后形成一张可以可视化的典型性热图。这个方法类别的核心判断是:ViT 的 patch token 序列天然适合做空间维度的 OOD 分析,而 SVD 是压缩和描述这类特征分布最稳定的工具之一。

读完这篇文章,你不仅会理解 SVD 与 OOD 检测之间的关系,还能拿到一套可落地的 PyTorch 参考实现,知道如何提取 ViT 中间层特征、如何用 SVD 构建典型性评分、如何聚合得到图像级得分,以及遇到训练分布漂移、patch 异常点、特征层选择等问题时该怎么排错。

1. 为什么 ViT 做 OOD 检测会比 CNN 更难

先回到一个基本问题:为什么传统 CNN 时代常用的 OOD 检测方法,搬到 ViT 上容易出现“水土不服”?

CNN 的特征提取过程是局部卷积叠加,最终通过全局池化得到一个特征向量。这个特征向量对输入的空间结构已经做了大量压缩,模型最终判断依赖的信息相对集中。很多经典 OOD 方法,比如基于 Mahalanobis 距离的方法,本质上是在这个全局特征空间里估计训练分布的均值和协方差,然后计算新样本到分布中心的距离。

ViT 不同。图片被切块成 patch,每个 patch 通过线性投影变成 token,再经过多层自注意力(Self-Attention)在 token 之间交换信息。最终分类头通常只使用 CLS token,但整个网络的中间层保留了大量 patch 级别的局部特征。

这带来一个很现实的结果:一个输入可能整体上是“正常分布”的,但其中某个局部区域,比如一块不明阴影、一个异常纹理,已经明显越出了训练分布。如果只看全局特征向量,这个异样信息很容易被忽略。相反,如果你愿意去看每个 patch token 在特征空间里是否符合训练分布,就有机会捕获这种局部异常。

所以,对 ViT 来说,OOD 检测不应该只当作“图像级分类问题”来处理,而应该拆成两个层级:

  1. Patch 层级:每个 patch token 是不是符合训练集里见过的局部特征分布?
  2. 图像层级:把 patch 层级的得分合理聚合起来,判断整张图是不是 OOD。

Typicality Maps 的出发点,恰好就是把这个问题拆成了上述两层。它把每个 patch 的“典型性”计算成一种空间映射图,再用聚合方式得到图像级判断。

2. SVD 在特征分析中的角色

SVD 的理论本身并不复杂。任意一个实数矩阵都可以分解为:

A = U * S * V^T

其中 U 和 V 是正交矩阵,S 是对角矩阵,对角线上的值就是奇异值,从大到小排列。奇异值的大小表示对应方向在数据中的重要性。

在特征分析中,SVD 之所以比直接求协方差矩阵的特征分解更稳定,是因为它在数值计算上通常更稳健,尤其当特征维度较高时,特征值分解容易出现累积误差,而 SVD 的数值表现更可靠。这也是很多 PCA 实现底层并不直接算协方差矩阵,而是对数据矩阵直接做 SVD 的原因。你在热词里看到“svd in pca”,说的正是这件事。

在 OOD 检测的语境下,SVD 的作用可以概括为三句话:

  1. 对训练集特征做 SVD,得到一组彼此正交的数据主方向。
  2. 奇异值大小刻画了训练分布在不同方向上的“典型变化幅度”。
  3. 对新样本的特征,可以通过投影残差判断它是否偏离训练分布。

如果直接把输入图像通过 ViT 得到的 patch 特征收集起来,平铺成一个矩阵,对这个矩阵做 SVD,我们就能得到训练数据的“特征主干”。一个典型的 patch token,应当在该主干方向附近;一个非典型的 patch token,会在主干方向之外产生较大的残差。

这就是 Typicality Maps 方法类别的共同数学基础。至于分数如何定义,是直接用残差,还是用 SVD 投影后在低维子空间里的密度估计,不同论文有不同设计。但核心都是同一个思想:用训练分布的主干方向作为尺子,量一量每个局部特征“歪”了多少。

3. Typicality Maps 的核心工作流程

为了讲清楚整个方法,这里按步骤拆解。你也可以把它当作算法设计文档来参考。

3.1 总体流程概览

整体流程分为两个阶段:

阶段一:训练阶段(构建分布基线)

  • 把训练集(或一个有代表性的子集)传入 ViT,提取指定中间层的 patch token 特征。
  • 将所有 patch 特征收集起来,组成一个大矩阵。
  • 对该矩阵做 SVD,保存主方向矩阵 U、奇异值 S,以及可选的截断维度 d。
  • 根据需要计算训练集主特征的均值向量。

阶段二:推理阶段(计算典型性得分)

  • 对测试图像提取 ViT 同层的 patch token 特征,形状依然是[num_patches, feature_dim]
  • 将每个 patch 特征投影到训练子空间,计算投影残差或马氏距离。
  • 对每个 patch 得分做归一化,得到一张与 patch 网格对应的 2D Typicality Map。
  • 对整张图的 patch 得分进行聚合,得到图像级 OOD 分数。

3.2 为什么选择中间层而不是最后一层

选择哪一层提取特征,直接决定方法效果。最后一层特征经过多层自注意力后,patch 之间的信息已经高度融合,每个 token 都包含了大量全局上下文,单独看一个 patch 的“局部性”已经不强了。而过于靠前的层,特征语义不够抽象,对内容变化太敏感,正常样本之间的差异也可能很大。

从方法类别的普遍经验来看,中间偏后但未到最后一层的 Transformer Block 更有价值。比如 ViT-B/16 通常有 12 个 block,选择 7 到 10 层的 patch token 特征,往往能同时保留局部空间信息和语义抽象能力。具体选择可以参考训练集上的验证效果,而不是拍脑袋固定选一层。

3.3 SVD 子空间与典型性得分

假设训练阶段收集到的矩阵为A,形状是N x D,N 是所有 patch token 数量,D 是特征维度。对 A 做 SVD:

U, S, Vt = torch.linalg.svd(A, full_matrices=False)

保留前 d 个奇异向量,得到子空间投影矩阵U_r = Vt[:d].T(注意 PyTorch 的torch.linalg.svd返回的 V 是右奇异向量的转置,需要正确取用)。

对于新样本中的某个 patch 特征向量f,其典型性得分可以定义为:

  • 投影残差 L2 范数score = ||f - U_r @ U_r.T @ f||_2
  • 马氏距离范数:在 SVD 子空间内,结合奇异值做缩放,得到分布距离。
  • 重建误差:用低秩重建特征与原始特征的余弦或欧氏距离。

其中投影残差最为直观,它的含义是:假设训练特征只在这 d 个主方向上变化,那么这个 patch 特征在主干方向之外还有多少“多余能量”。多余能量越大,越可能是 OOD。

3.4 从 patch 分数到图像级得分

得到每个 patch 的分数后,需要聚合成图像级 OOD 分数。常见策略:

聚合策略说明适用场景
Mean对所有 patch 分数求平均全局性漂移,整体纹理差异
Max取最大 patch 分数局部异物,一个 patch 异常明显
Top-k Mean取前 k 个最大分数的平均兼顾局部异常和波动
对比类与相邻 patch 做差值只关心局部突变而忽略整体漂移

选择聚合策略时,关键是要结合业务场景。如果线上异常主要表现为“某个小区域出现异物”,Max 或 Top-k Mean 更合适;如果异常主要表现为“整张图风格漂移”,Mean 更合适。实际项目里可以同时输出 map 和图像级分数,让人工抽查时既能知道判断结果,也能看到模型到底因为什么区域产生了高 OOD 分数。

4. 环境准备与前置条件

在开始代码实现前,先把环境准备好。本文的示例基于 PyTorch 和 torchvision,不依赖额外的自定义算子。下面的版本信息是参考值,请以你本地的实际环境为准。

  • Python 3.9 及以上
  • PyTorch 2.0 及以上
  • torchvision 0.15 及以上(用于加载 ViT 模型)
  • numpy
  • scikit-learn(可选,用于计算 AUROC 等指标)
  • CUDA 可选,CPU 也能跑通演示流程

建议在 Jupyter Notebook 或脚本中逐段执行。主要涉及特征提取的部分比较耗时,如果机器没有 GPU,可以将训练集规模缩小到几千张,或者直接使用预提取的缓存特征。

5. 完整示例:基于 PyTorch 的 SVD Typicality Map 实现

下面给出一个完整的参考实现。代码目的是让你跑通整个流程:用 ViT 提取 patch token 特征,用 SVD 构建基线,计算 Typicality Map,最后评估 OOD 检测效果。

5.1 提取 ViT 中间层 patch 特征

这里最大的技术点是:torchvision 自带vit_b_16模型,默认只返回分类层之前的编码特征。如果想拿到中间层某个 Block 的 patch token 特征,最简单的方式是用 hook,或者修改模型的 forward,在指定 Block 后把 patch token 保存出来。

为了保证代码可复用,下面写一个通用的特征提取器,它接收一个 ViT 模型和层索引,返回指定层的 patch token 特征。

# 文件路径:feature_extractor.py import torch import torch.nn as nn class ViTFeatureExtractor(nn.Module): """从 ViT 中提取指定层 block 的 patch token 特征。 假设模型结构符合 torchvision 的 Vit 实现风格: model.blocks 是一个 ModuleList,每个 block 的输入输出都是 [batch, num_tokens, dim]。 """ def __init__(self, vit_model: nn.Module, layer_index: int): super().__init__() self.vit_model = vit_model self.layer_index = layer_index # 只保留到目标层,减少内存和计算量 self.patch_embed = vit_model.conv_proj self.positional_embedding = vit_model.encoder.pos_embedding self.blocks = vit_model.encoder.layers[:layer_index + 1] self.ln_pre = vit_model.encoder.ln_encoder def forward(self, x: torch.Tensor): # 对应 torchvision 的 ViT forward 设计 x = self.patch_embed(x) # [B, D, H, W] x = x.flatten(2).transpose(1, 2) # [B, N, D] x = x + self.positional_embedding x = self.blocks(x) x = self.ln_pre(x) # 去掉 CLS token,只保留 patch token,形状 [B, N-1, D] return x[:, 1:, :]

5.2 训练集 SVD 基线的构建

这个阶段的目标:从训练集中取一批图像,提取 patch token 特征,组成一个二维大矩阵,做 SVD,保存主方向。

# 文件路径:build_baseline.py import torch import numpy as np from tqdm import tqdm def collect_patch_features( data_loader, feature_extractor, device, sample_limit: int, ): """从 dataloader 中收集 patch token 特征,返回形状为 [N, D] 的 numpy 矩阵。""" all_features = [] total = 0 feature_extractor.eval() with torch.no_grad(): for images, _ in tqdm(data_loader, desc="collecting features"): if total >= sample_limit: break images = images.to(device) feats = feature_extractor(images) # [B, num_patches, D] B, P, D = feats.shape all_features.append(feats.reshape(B * P, D).cpu().numpy()) total += B if total >= sample_limit: break if len(all_features) == 0: raise RuntimeError("没有收集到任何特征,请检查 dataloader。") return np.concatenate(all_features, axis=0) def build_svd_baseline(feature_matrix: np.ndarray, dim: int): """对特征矩阵做截断 SVD,返回主方向矩阵。 参数: feature_matrix: 形状 [N, D] dim: 保留的主方向数量 返回: V_r: 形状 [D, dim],每一列是一个主方向 singular_values: 形状 [dim] mean_vector: 训练特征的均值向量,形状 [D] """ mean_vector = feature_matrix.mean(axis=0, keepdims=True) x_center = feature_matrix - mean_vector U, S, Vt = np.linalg.svd(x_center, full_matrices=False) V_r = Vt[:dim].T # 形状 [D, dim] return V_r, S[:dim], mean_vector.ravel()

这里要注意:由于 patch token 数量通常很多,例如 CIFAR-10 训练集 5 万张图,每张图 ViT-B/16 输出 196 个 patch token,全量收集的矩阵会非常大。实际项目中会做降采样,比如每类随机取几百张,或者限制总样本数在 1 万张以内,既能代表分布,又不至于内存爆炸。

5.3 计算 Typicality Map 和图像级得分

接下来是核心部分:给定一张测试图,计算每个 patch 的典型性分数,并生成映射图。

# 文件路径:typicality_map.py import torch import numpy as np class SVDOODScorer: """基于 SVD 子空间残差的 OOD 评分器。""" def __init__(self, V_r, singular_values, mean_vector): # 转为 torch 张量,方便在 GPU 上计算 self.register_buffer("V_r", torch.tensor(V_r, dtype=torch.float32)) self.register_buffer("singular_values", torch.tensor(singular_values, dtype=torch.float32)) self.register_buffer("mean_vector", torch.tensor(mean_vector, dtype=torch.float32)) def register_buffer(self, name, value): setattr(self, name, value) value.requires_grad_(False) @torch.no_grad() def score_patches(self, patch_features): """输入 patch_features 形状 [B, P, D] 或 [P, D],输出每个 patch 的残差分数。""" if patch_features.dim() == 2: patch_features = patch_features.unsqueeze(0) B, P, D = patch_features.shape device = patch_features.device V_r = self.V_r.to(device) mean = self.mean_vector.to(device).unsqueeze(0).unsqueeze(0) # [1, 1, D] centered = patch_features - mean # 投影到子空间 proj = torch.matmul(centered, V_r) # [B, P, dim] reconstructed = torch.matmul(proj, V_r.T) # [B, P, D] residual = centered - reconstructed residual_norm = torch.norm(residual, dim=-1) # [B, P] return residual_norm @torch.no_grad() def image_score(self, patch_features, agg="topk", k=10): """将 patch 分数聚合为图像级分数。""" patch_scores = self.score_patches(patch_features) # [B, P] if agg == "mean": return patch_scores.mean(dim=-1) elif agg == "max": return patch_scores.max(dim=-1).values elif agg == "topk": k = min(k, patch_scores.size(-1)) return torch.topk(patch_scores, k=k, dim=-1).values.mean(dim=-1) else: raise ValueError(f"不支持聚合方式: {agg}") @torch.no_grad() def typicality_map(self, patch_features, grid_size=(14, 14)): """把 [B, P] 的分数映射回 2D 热图。""" patch_scores = self.score_patches(patch_features) # [B, P] H, W = grid_size B, P = patch_scores.shape assert P == H * W, f"patch 数量 {P} 与网格 {H}x{W} 不对应" return patch_scores.reshape(B, H, W)

5.4 评估指标:AUROC 与 FPR@95TPR

OOD 检测效果最常用的两个指标,一个是 AUROC(Area Under the ROC Curve),另一个是 TPR 为 95% 时对应的 FPR,通常写作 FPR@95TPR。

AUROC 的含义可以通俗理解为:随机取一个正常样本和一个 OOD 样本,模型给 OOD 样本打出更高异常分数的概率。这个指标对阈值不敏感,适合评估算法本身的排序能力,所以在论文和工业评估中最常用。

FPR@95TPR 代表:在确保能正常识别出 95% 的正常样本(不误报为 OOD)的前提下,OOD 样本中被漏掉的概率。在安全敏感场景,比如自动驾驶或医疗,这个指标比 AUROC 更严格。

下面是一个简单的评估脚本框架:

# 文件路径:evaluate.py import numpy as np from sklearn.metrics import roc_auc_score def compute_ood_metrics(id_scores, ood_scores): """输入正常样本分数和 OOD 样本分数,返回 AUROC 和 FPR@95TPR。""" # 正样本是 OOD,我们希望 OOD 分数更高 y_true = np.concatenate([np.zeros(len(id_scores)), np.ones(len(ood_scores))]) y_score = np.concatenate([id_scores, ood_scores]) auroc = roc_auc_score(y_true, y_score) # 以 95% TPR 为阈值,计算 FPR id_sorted = np.sort(id_scores) # 找到能覆盖 95% ID 样本的分数阈值 threshold = id_sorted[int(len(id_sorted) * 0.95)] fpr = np.sum(ood_scores <= threshold) / len(ood_scores) return auroc, fpr

注意:这里假设“分数越高越像 OOD”。如果你的设计中分数越低越像 OOD,需要在计算前做反转,或者调整正负标签。这种方向性问题非常容易踩坑,后面会专门讲。

6. 运行验证与效果分析

6.1 准备数据集

为了验证这个方案,我建议用一个对照设置:ID 使用 CIFAR-10,OOD 使用 SVHN(街景门牌号)或者 CIFAR-100 的某个超类。这个设置在 OOD 检测论文中非常常见,因为 CIFAR-10 与 SVHN 在颜色分布、字体结构、纹理上都差异明显,SVD 残差方法很容易体现效果差异。

具体操作步骤:

  1. 加载 CIFAR-10 训练集作为信息集。
  2. 加载 torchvision 预训练 ViT-B/16(在 ImageNet 上训练过)。
  3. 用第 8 层(0-based 索引)的 patch token 特征构建 SVD 基线。
  4. 从 CIFAR-10 测试集和 SVHN 测试集中各取一部分图像计算 OOD 分数。
  5. 计算 AUROC 和 FPR@95TPR。

6.2 预期结果与判断方法

不写具体跑分,因为不同的预训练权重、层选择、patch 数量会带来差异,但可以说明预期的行为模式:

  • 如果方法有效,SVHN 图像的 Typicality Map 会明显比 CIFAR-10 图像更“红”,也就是高残差区域更多。
  • AUROC 应该明显高于随机猜测 0.5。以这个思路实现,在中低难度 OOD 对(比如 CIFAR-10 vs SVHN)上,通常能达到相当不错的效果;而当 ID 和 OOD 视觉上非常接近时(比如 CIFAR-10 vs CIFAR-100 部分类),分数差异会被压缩,AUROC 会下降。这很正常,也符合 OOD 检测的基本规律。
  • 多层聚合、特征层选择、SVD 截断维度都会对结果产生影响。如果 AUROC 没有超过 0.8,优先检查特征层和截断维度,而不是怀疑框架。

6.3 如何确认建模正确

一个简单但有效的自检方式:把 SVD 截断维度从 1 逐步调到 50 或 100,观察训练集自身 patch 残差的均值和方差变化。如果训练集特征本身残差也很大,说明选择的层或特征基线没有很好地描述训练分布,需要调整。

更直观的确认方法是可视化典型性图。用matplotlibtypicality_map的输出叠加到原图上。如果正常样本的热图基本平坦,而异常样本在某个局部区域出现明显亮点,说明方法的行为符合预期。

import matplotlib.pyplot as plt # 假设 img 是 [3, H, W] tensor,map_2d 是 [14, 14] 的分数图 # 分数图需要上采样到原图尺寸 heatmap = np.uint8(255 * (map_2d - map_2d.min()) / (map_2d.max() - map_2d.min() + 1e-8)) heatmap = np.array(Image.fromarray(heatmap).resize((img.shape[2], img.shape[1]))) plt.imshow(heatmap, cmap="jet", alpha=0.5) plt.imshow(img.permute(1, 2, 0).cpu().numpy(), alpha=0.5) plt.axis("off") plt.show()

7. 常见问题与排查思路

实现这种 SVD-Based Typicality Map 方法,代码本身并不复杂,真正容易出问题的地方往往在细节上。下面列出几个高频问题。

问题现象可能原因排查方式解决方案
AUROC 接近 0.5,没有区分度选择的特征层太深,patch token 已经被全局信息污染绘制训练集和 OOD 集的残差分布直方图换用更中间的层,比如 7 到 9 层
Normalize 后分数方向反了把训练集残差和 OOD 残差的数值方向搞反打印两类分数均值对比统一约定:残差大 = 不典型
内存爆炸全量训练集所有 patch 特征被一次性拼接检查收集函数里是否设置了 sample_limit降低采样数量,或使用在线 SVD
patch 数量对不上不同图像分辨率导致 patch 数不一致打印特征提取器输出的形状固定图像分辨率,ViT 对 patch 数量敏感
Typicality Map 全图都很红SVD 截断维度太小,训练分布本身没被刻画充分可视化训练集 patch 残差分布增大截断维度,或检查均值中心化是否有问题
向量方向投影用了错误的转置np.linalg.svd返回的 Vt 和预期矩阵形状不一致打印 V_r 的 shape,确认 D 和 dim 顺序使用Vt[:dim].T,得到 [D, dim]
指标受阈值影响大测试集样本太少,统计噪声大增加测试集数量,检查分数分布是否稳定至少使用 1000 张以上 ID 和 OOD 测试图

7.1 特征层选择带来的效果波动

这个方向我单独强调一下。很多第一次实现的人会习惯性选择最后一层特征,理由是“最后一层特征最有判别力”。但对于 OOD 检测,最后一层特征往往是最危险的。

原因是:经过全部自注意力层之后,CLS token 和 patch token 都已经深度全局化,patch 间的边界被模糊化,局部异常信息在特征层面被“平均”掉了。多个在 OOD 检测论文中被反复验证的结论都表明,中间层的 patch token 特征比最后一层更适合作为 OOD 分析的基础。

实际操作时,不一定要手动遍历全部层。可以先选择一个凭经验值比较稳的位置,比如总层数的 60% 到 70%,然后对照训练集上的验证效果微调。例如 ViT-B/16 有 12 层,从第 7 到 10 层之间做选择成本不高,可以写成循环自动比较。

7.2 SVD 截断维度的选择

截断维度 d 是一个超参数。d 太小,SVD 子空间只能刻画训练数据最粗浅的方向,所有样本的残差都会偏大;d 太大,子空间接近原始特征空间,残差趋近于零,区分能力随之消失。

一个比较稳妥的经验法是:选取能累积解释约 90% 奇异值能量的最小维数。下面的代码可以辅助计算:

def select_dim_by_energy(singular_values, ratio=0.9): energy_cumsum = np.cumsum(singular_values ** 2) total_energy = energy_cumsum[-1] dim = int(np.searchsorted(energy_cumsum, total_energy * ratio) + 1) return dim

需要注意的是,不同层的 patch 特征,其奇异值衰减速度不同。浅层特征通常有效秩更高,需要更大 d;深层特征信息集中,可能用很小的 d 就能覆盖主要结构。所以工程上建议每一层或每个数据集单独计算,而不是全局用同一个 d。

8. 最佳实践与工程建议

做完实验之后,真正落到项目里,还有几个工程细节值得注意。这些经验可以帮助你减少在真实环境里遇到的坑。

8.1 先做好 Baseline 再谈优化

很多团队引入 OOD 检测时,第一步就期望端到端的完美方案。更稳妥的做法是:

  1. 先用现成的预训练 ViT + 本文的 SVD Typicality Map 跑通全流程。
  2. 对比几个简单的 Baseline:最大 Softmax 概率、Temperature Scaling、Mahalanobis 距离。
  3. 只有当 SVD 方案的指标明显优于 Softmax 方法时,才继续做特征层选择、聚合函数调优。

这样做的原因很实际:如果连 baseline 对比都没有,你很难判断提升究竟来自“SVD 子空间”,还是仅仅因为“换了一个更合理的特征表示”。

8.2 训练集基线要定期更新

在真实业务中,模型会不断接收新的数据,甚至会在某些阶段做微调。SVD 基线是基于部署初期的训练数据构建的,这里的“训练”不是指模型微调,而是指用于构建分布基线的特征来源。如果业务数据分布逐渐发生偏移,旧基线会越来越多地把正常新样本误判为 OOD。

建议在持续迭代的项目中,至少每月或每个模型版本更新一次 SVD 基线。更新流程应当完全自动化:采集一个代表性数据子集,重新提取特征,计算 SVD,保存新的基线和评分器版本。线上评分器必须能做版本回滚。

8.3 分数要记录,便于后续分析

OOD 检测不只是输出一个 bool,它应该输出一个分数。把每个测试样本的 OOD 分数记录到日志和数据库,是非常有价值的事情。当业务方反馈“系统把某些正常图片拦截了”,你能回溯这个批次的分数分布,分析是基线过期,还是真的出现了新的数据模式。

一个好的日志字段至少包含:

  • 样本 ID
  • 图像级 OOD 分数
  • Typicality Map 的均值、最大、Top-k 聚合值
  • 使用的基线版本号
  • 特征层索引和 SVD 截断维度

有了这些字段,后续做阈值调整时就不需要重新跑全部推理,直接用历史分数做离线分析即可。

8.4 安全边界与最小权限

需要特别提醒:OOD 检测本身不应该替代业务规则。在生产环境中,建议把 OOD 分数作为“风险提示”而非“最终决策”。特别是涉及审批、删除、封禁等敏感操作时,OOD 高分样本应该进入人工复核流程,而不是直接触发自动操作。任何阈值调整和数据变更,都应当在测试环境中验证,并保留回滚能力。

8.5 可视化是排查利器

Typicality Map 最大的工程价值之一,在于它能可视化。大多数集成模型的黑盒问题,在 OOD 检测里会因为看不到原因而难以推进。有了热图,你可以直接告诉业务同事:这张图被判为异常,是因为右下角那个 patch 的特征偏离了训练分布。这种解释能力在很多场景中是刚需。

所以在实现时,建议把typicality_map输出接口从开始就保留好,即使线上服务不展示,也要有离线生成热图的脚本。

9. 进一步延伸的方向

如果读完本文你已经跑通了代码,下一步可以从这几个方向继续深入。

第一,尝试用更丰富的距离度量替代朴素残差。当前实现用的是 L2 残差范数,实际项目中可以替换为马氏距离,把每个奇异值作为对应方向的标准差进行缩放。这种缩放能让不同方向对最终分数的贡献更均衡,减少大奇异值方向对整体分数的支配。

第二,把 Typicality Map 与图像分割或目标检测结合。如果输入是遥感影像或医疗切片,一张图里可能同时存在多个区域,每个区域的典型性差别很大。此时图像级 OOD 分数不够用,区域级聚合会更有价值。

第三,思考 SVD 与 LoRA 这类参数高效微调方法的关系。“svd与lora”是现代大模型微调中被反复讨论的组合:LoRA 本质上是对权重增量做低秩分解,而 SVD 是寻找和压缩低秩结构的数学工具。如果你在做 ViT 微调,微调之后训练分布基线也要同步重建,因为特征分布已经发生了变化。这里的数学工具和本文是同一套体系,值得串联学习。

第四,关注 ViT 注意力头对典型性图的影响。patch 特征是由多个注意力头融合而来的,有些头更关注局部纹理,有些头更关注全局结构。如果能把 SVD 分析作用在每个注意力头的输出上,再融合成最终的典型性图,可能会得到更细腻的异常解释。不过这个方向的计算成本会明显上升,需要结合项目实际评估。

最后,建议你把这个方法理解成一种“特征空间的可解释异常检测框架”,而不只是某个固定模型。SVD 提供的是数学骨架,Typicality Map 提供的是空间可视化表达,真正决定效果的,是你怎么选特征层、怎么聚合、怎么结合业务先验。这套方法可以换成 ResNet 特征,也可以换成 Swin Transformer 的窗口特征,原理不变。

如果这篇文章对你有帮助,建议收藏备用,也欢迎在评论区分享你用 ViT 做 OOD 检测的实际经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询