行业资讯
📅 2026/8/29 4:59:52
ViT OOD检测实战:SVD与Typicality Maps识别分布外样本
部署一个视觉 TransformerViT模型到真实业务里通常要面对的难题不一定是 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 检测不应该只当作“图像级分类问题”来处理而应该拆成两个层级Patch 层级每个 patch token 是不是符合训练集里见过的局部特征分布图像层级把 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 的作用可以概括为三句话对训练集特征做 SVD得到一组彼此正交的数据主方向。奇异值大小刻画了训练分布在不同方向上的“典型变化幅度”。对新样本的特征可以通过投影残差判断它是否偏离训练分布。如果直接把输入图像通过 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 DN 是所有 patch token 数量D 是特征维度。对 A 做 SVDU, S, Vt torch.linalg.svd(A, full_matricesFalse)保留前 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 模型numpyscikit-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, desccollecting 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, axis0) 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(axis0, keepdimsTrue) x_center feature_matrix - mean_vector U, S, Vt np.linalg.svd(x_center, full_matricesFalse) 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, dtypetorch.float32)) self.register_buffer(singular_values, torch.tensor(singular_values, dtypetorch.float32)) self.register_buffer(mean_vector, torch.tensor(mean_vector, dtypetorch.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, aggtopk, k10): 将 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, kk, 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, fpatch 数量 {P} 与网格 {H}x{W} 不对应 return patch_scores.reshape(B, H, W)5.4 评估指标AUROC 与 FPR95TPROOD 检测效果最常用的两个指标一个是 AUROCArea Under the ROC Curve另一个是 TPR 为 95% 时对应的 FPR通常写作 FPR95TPR。AUROC 的含义可以通俗理解为随机取一个正常样本和一个 OOD 样本模型给 OOD 样本打出更高异常分数的概率。这个指标对阈值不敏感适合评估算法本身的排序能力所以在论文和工业评估中最常用。FPR95TPR 代表在确保能正常识别出 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 和 FPR95TPR。 # 正样本是 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-10OOD 使用 SVHN街景门牌号或者 CIFAR-100 的某个超类。这个设置在 OOD 检测论文中非常常见因为 CIFAR-10 与 SVHN 在颜色分布、字体结构、纹理上都差异明显SVD 残差方法很容易体现效果差异。具体操作步骤加载 CIFAR-10 训练集作为信息集。加载 torchvision 预训练 ViT-B/16在 ImageNet 上训练过。用第 8 层0-based 索引的 patch token 特征构建 SVD 基线。从 CIFAR-10 测试集和 SVHN 测试集中各取一部分图像计算 OOD 分数。计算 AUROC 和 FPR95TPR。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 残差的均值和方差变化。如果训练集特征本身残差也很大说明选择的层或特征基线没有很好地描述训练分布需要调整。更直观的确认方法是可视化典型性图。用matplotlib把typicality_map的输出叠加到原图上。如果正常样本的热图基本平坦而异常样本在某个局部区域出现明显亮点说明方法的行为符合预期。import matplotlib.pyplot as plt # 假设 img 是 [3, H, W] tensormap_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, cmapjet, alpha0.5) plt.imshow(img.permute(1, 2, 0).cpu().numpy(), alpha0.5) plt.axis(off) plt.show()7. 常见问题与排查思路实现这种 SVD-Based Typicality Map 方法代码本身并不复杂真正容易出问题的地方往往在细节上。下面列出几个高频问题。问题现象可能原因排查方式解决方案AUROC 接近 0.5没有区分度选择的特征层太深patch token 已经被全局信息污染绘制训练集和 OOD 集的残差分布直方图换用更中间的层比如 7 到 9 层Normalize 后分数方向反了把训练集残差和 OOD 残差的数值方向搞反打印两类分数均值对比统一约定残差大 不典型内存爆炸全量训练集所有 patch 特征被一次性拼接检查收集函数里是否设置了 sample_limit降低采样数量或使用在线 SVDpatch 数量对不上不同图像分辨率导致 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, ratio0.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 检测时第一步就期望端到端的完美方案。更稳妥的做法是先用现成的预训练 ViT 本文的 SVD Typicality Map 跑通全流程。对比几个简单的 Baseline最大 Softmax 概率、Temperature Scaling、Mahalanobis 距离。只有当 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 检测的实际经验。