
要处理“SVD-Based Typicality Maps for Out-of-Distribution Detection in Vision Transformers”这个话题最关键的可以先说清楚这不是一个能直接 pip install 的现成工具而是一套基于奇异值分解的特征空间建模方法用来解决视觉 Transformer 在遇到训练分布之外的样本时“迷之自信”的问题。如果你正在做图像分类、目标识别、自动驾驶感知、医学影像辅助诊断这类对误判特别敏感的任务或者你已经在用 ViT 做特征提取但发现 softmax 置信度不可靠那这篇文章就是按“理解原理、搭环境、跑实验、看指标、查坑”的顺序来拆的。我先给一个整体判断基于 SVD 的典型性图核心思想并不复杂就是把模型中间层或输出层特征整理成矩阵通过奇异值分解找到特征空间的主要方向和能量分布再判断当前输入落在这些方向的典型程度。难的地方不在“调用 SVD 函数”而在“如何构造矩阵、如何定义典型性分数、如何稳定评估”。下面我会按实际落地顺序展开并补充很多我在测试这类方法时优先检查的细节。1. 为什么视觉 Transformer 做 OOD 检测会失效典型性图要解决什么1.1 OOD 检测解决的不是“分错类”而是“不知道是什么”先明确概念。OOD 检测全称 Out-of-Distribution Detection中文常叫分布外检测。它要做的事情是当模型收到一个和训练数据分布明显不同的输入时不要把它强行分到某个已知类别里而是标记为“我不认识”。这跟普通分类任务不一样。普通分类训练时模型只需要学习训练集里出现过的类别。你给它一张车外的陌生物体图片它也会输出一个概率分布最后选一个概率最高的类。问题是这个概率最高不一定代表它真的认识。很多研究发现softmax 置信度对 OOD 样本经常给出虚高分数。尤其是视觉 Transformer因为注意力机制会让模型更倾向全局关联某些分布外特征反而可能被“平滑”地吸收进去。所以 OOD 检测真正要解决的问题是如何让模型知道“这个输入不在我见过的东西里面”而不是“这个东西最像哪个类”。1.2 ViT 的特征空间分布和 CNN 不一样视觉 Transformer 和 CNN 最大的区别是它把图像切成 patch然后用注意力机制在 patch 之间建模关系。这意味着它提取出来的特征更像是一组带位置信息的全局描述而不是像 CNN 那样分层提炼局部纹理。这对 OOD 检测有直接影响。CNN 末层特征往往已经高度语义化分布外样本的激活模式比较容易偏离。ViT 的中间层特征则常常保留更多 patch 级信息直接拿最后一层 token 向量算距离效果不一定好。因此针对 ViT 做 OOD 检测不能简单套用“提取倒数第二层特征算马氏距离”这种老办法而是需要先想清楚到底哪一层、哪种特征表达最能反映“是否在训练分布内”。典型性图这个思路就是从这个角度切入的。它不只看一个一维向量而是把特征组织成图或矩阵然后通过 SVD 来刻画这个矩阵的典型结构。1.3 典型性图的基本假设分布内样本的特征矩阵有更稳定的低秩结构“典型性”这个词直观理解就是“像不像训练集里常见的东西”。如果把一张图片的 ViT 特征想象成一组行向量每个行向量对应一个 patch 的表征那么分布内样本的这些行向量之间通常存在强相关结构。场景、物体、纹理往往遵循训练集里出现过的某种规律因此特征矩阵的秩不会太高主要能量集中在前几个奇异值上。而分布外样本的特征矩阵因为统计规律和训练分布不一样它的行向量关系会显得“陌生”。这种陌生感会表现在奇异值衰减速率、主奇异值占比、或者特征向量方向偏移上。典型性图要做的就是把这种“陌生感”量化出来生成一张和输入空间或特征空间对应的图图中每个位置表示该区域的典型性高低。把这张图聚合成一个分数就能判断整个输入是否 OOD。2. 从 PCA 到 SVD理解奇异值分解在这里到底算什么2.1 SVD 的数学直觉不用怕SVD奇异值分解公式是 X UΣVᵀ。其中 X 是 m×n 矩阵U 是 m×m 正交矩阵V 是 n×n 正交矩阵Σ 是对角阵对角线上是奇异值。直觉上奇异值的大小表示矩阵在对应方向上的“能量”或“重要性”。前几个大奇异值对应的方向决定了矩阵的主要结构后面小奇异值对应的方向通常的噪声或细节。在特征分析里SVD 最重要的性质是它能告诉我们一个矩阵是否低秩、主要结构是什么、有没有异常方向。这也是为什么 SVD 在 PCA、推荐系统、数据压缩、气象数据分析里都有应用。2.2 SVD 和 PCA 的关系很多人会混淆PCA主成分分析是分析数据协方差矩阵的特征分解。如果你先把数据矩阵做中心化再计算协方差矩阵 C XᵀX那么 C 的特征向量就是 V 的列向量C 的特征值就是奇异值的平方除以样本数。换句话说PCA 是 SVD 在“中心化数据 协方差”语境下的一种应用。相关热词里有“svd in pca”说明很多人其实是在 PCA 场景里第一次接触 SVD。放到 OOD 检测里区别很重要PCA 通常关注数据协方差的主要方向也就是全局方差最大的方向。SVD 可以直接作用于特征矩阵本身不需要先算协方差而且可以保留矩阵的行、列结构信息。对于图像特征图我们希望保留空间结构所以直接对特征矩阵做 SVD比先展平成向量再算协方差更自然。典型性图利用的正是这种矩阵结构。2.3 从 SVD 到典型性分数核心是看奇异值分布有了特征矩阵 X对它做奇异值分解后我们会得到一组奇异值 σ₁ ≥ σ₂ ≥ ... ≥ σₖ。对应的典型性分数可以有很多种定义方式。常见思路包括前 r 个奇异值占总能量比例如果主要能量集中在前几个奇异值说明特征结构很强更可能是分布内。用前 r 个奇异值重建矩阵后的重建误差误差小表示低秩近似好更符合训练分布结构。奇异值向量与训练集平均奇异值向量的距离比如余弦相似度或 L2 距离。论文标题里的“Typicality Maps”更强调的是生成“图”。也就是说不是把整个特征矩阵一次性压成一个分数而是对特征图的不同区域分别计算典型性形成一张空间分布图。3. 搭建最小可复现实验框架3.1 你需要准备的环境和依赖这个话题并不要求超高级别硬件但因为有视觉 Transformer还是建议至少有支持 CUDA 的 GPU显存建议 6GB 以上。如果你只有 CPU也可以跑通流程只是需要把输入图片分辨率调小并且只做少量样本验证。基础环境建议这样准备# Python 3.9 或 3.10 均可 pip install torch torchvision pip install timm scikit-learn matplotlibtorch 和 torchvision 用来加载模型和处理图像。timm 里有很多现成的 ViT 预训练模型方便对比不同规模。scikit-learn 用来计算 AUROC、AUPR 等评估指标。matplotlib 用来可视化典型性图。注意这里不写死版本号因为 PyTorch 的版本迭代比较快。你安装时只要保证 torchvision 和 torch 版本匹配即可。建议先在一个虚拟环境里安装避免污染全局环境。3.2 常见的 OOD 检测实验数据集做 OOD 检测需要两类数据分布内数据 ID即训练模型时见过的数据分布通常用 CIFAR-10、CIFAR-100、ImageNet 子集。分布外数据 OOD用来测试模型能不能识别出“没见过的分布”常见有 SVHN、LSUN、Texture、iNaturalist 等。我建议先用小规模组合跑通整个流程例如IDCIFAR-10 测试集取 1000 张。OODSVHN 测试集取 1000 张。不要一上来就跑全量 ImageNet那样时间成本和磁盘空间成本都不会低。先用小样本验证 pipeline 没问题再逐步扩大。3.3 选一个合适的 ViT 预训练模型在 timm 里可以加载很多 ViT 变体例如import timm model timm.create_model(vit_base_patch16_224, pretrainedTrue) model.eval()但要注意如果你用 CIFAR-10 作为 ID 数据集直接用 ImageNet 预训练模型实际上并不完全匹配因为模型的训练分布是 ImageNet而不是 CIFAR-10。更严谨的做法是用预训练 ViT 在 CIFAR-10 上做微调让它真正适应你的 ID 分布。或者使用在 ImageNet 上训练的模型并把 ImageNet 的验证集作为 ID 分布OOD 数据选用其他数据集。考虑到复现成本先明确一个原则OOD 检测评估的是“模型见过的分布 vs 没见过的分布”所以 ID 数据必须和模型训练分布至少接近。否则你测出来的结果会混合“模型没有适配”的干扰因素。我先给一个可行的做法用 timm 加载在 ImageNet-1k 上预训练的 ViT取 ImageNet-1k 验证集中的一部分作为 ID取 CIFAR-10 作为 OOD。这不算严格的 SOTA 设置但足够用来验证典型性图方法能不能工作。4. 核心实现步骤从特征提取到典型性图4.1 提取中间层特征图ViT 模型内部通常有 Transformer Encoder每个 Encoder 会输出 197 个 token其中 1 个是 CLS token其余 196 个对应 14×14 的 patch 网格。要生成典型性图不能只取 CLS token。我们需要保留空间结构所以最好取某个 Transformer Block 输出的所有 patch token然后 reshape 成空间网格。可以用 PyTorch 的 forward hook 来提取。下面是一个通用示例import torch features {} def hook_fn(name): def forward_hook(module, input, output): # output shape: [batch_size, num_tokens, hidden_dim] features[name] output return forward_hook # 假设你用的是 vit_base_patch16_224block 数量为 12 # 可以插入到 block 11 之后 blocks model.blocks blocks[11].register_forward_hook(hook_fn(block11))这里 output 是 [B, 197, 768]其中第 0 个 token 是 CLS。提取后把 CLS 去掉得到 [B, 196, 768]再 reshape 成 [B, 14, 14, 768]。你可以把最后一个维度的特征看作每个 patch 位置的局部描述。典型性图的大小和输入分辨率有关。使用 224×224 输入patch size 16特征图就是 14×14。如果你输入是 384×384特征图就是 24×24。4.2 构造 SVD 输入矩阵对每个样本特征矩阵 X 的尺寸是 196×768。这个矩阵的行是 patch token列是特征通道。直接对这个矩阵做 SVD 是可行的但有一个问题196 行和 768 列量纲不一样SVD 结果可能不稳定。常见做法有两种对特征矩阵做标准化比如按列减去训练集的均值再除以标准差。先对特征矩阵做中心化再做 SVD类似 PCA 的预处理。中心化非常重要。如果不中心化SVD 分解出的最大奇异值会被均值主导无法反映特征分布的结构差异。这一点和 PCA 里“先中心化再算协方差”是一个道理。4.3 计算典型性图的伪代码下面是一段示例伪代码用来展示整个流程。这不是论文原作者的源码而是我按方法思路组织的验证框架import torch def compute_typicality_map(feature_map, mean_vec, std_vec, top_k16): feature_map: [H, W, C] mean_vec: [C] std_vec: [C] H, W, C feature_map.shape # 1. reshape 成矩阵 X feature_map.reshape(H * W, C) # 2. 标准化 X (X - mean_vec) / std_vec # 3. 中心化 X X - X.mean(dim0, keepdimTrue) # 4. SVD U, S, Vt torch.linalg.svd(X, full_matricesFalse) # 5. 计算典型性分数 # 这里示例使用前 top_k 奇异值占比 total_energy S.pow(2).sum() top_energy S[:top_k].pow(2).sum() score top_energy / total_energy # 6. 如果要生成图可以计算每个 patch 对主要方向的投影 # 取前 top_k 个右奇异向量作为主要方向 principal_dirs Vt[:top_k, :] # [top_k, C] # 每个 patch 在主要方向上的投影长度 proj torch.matmul(X, principal_dirs.t()) # [H*W, top_k] # 把投影长度平方求和再开方 typicality_map proj.pow(2).sum(dim-1).sqrt().reshape(H, W) return score, typicality_map这段代码里score 可以当作整张图的异常分数typicality_map 用来可视化。实际使用时你可能需要定义更复杂的分数计算方式。比如计算重建误差# 用前 top_k 个奇异值重建矩阵 X_recon U[:, :top_k] torch.diag(S[:top_k]) Vt[:top_k, :] recon_error (X - X_recon).pow(2).mean()这个重建误差越低说明矩阵越接近低秩结构通常意味着更典型。4.4 为什么先标准化再中心化以及 top_k 怎么选先标准化再中心化是为了让不同特征通道有接近的尺度。ViT 的 768 维特征中不同通道的激活值可能差异很大。如果不标准化SVD 会自动把权重放在方差大的通道上这可能会忽略掉一些对 OOD 检测更有区分度的通道。top_k 的选择直接影响效果。太小只保留最大奇异值可能丢失细节太大低秩近似会退化成原始矩阵重建误差几乎为零区分度下降。我一般先用总奇异值能量的 90% 来动态决定 k。比如从第一个奇异值开始累加直到能量占比超过 90%此时对应的 k 就是该样本的动态 rank。不过动态 rank 会导致不同样本需要截断的维度不同分数计算时要保证可比性。更稳妥的做法是在训练集上统计奇异值衰减曲线取一个固定的 top_k。比如在 CIFAR-10 或 ImageNet 子集上算出平均前 16 个奇异值占比然后把 top_k 固定为 16。这样所有样本用同一个 k分数可比性更好。4.5 训练集统计量如何计算在上一段代码中mean_vec 和 std_vec 需要提前从训练集计算。你可以这样处理从训练集中取一部分样本。经过 ViT 提取特征图得到 [N, H, W, C]。把所有样本的特征图按通道维度求均值和标准差。注意统计量应该只从 ID 训练集计算不能混入 OOD 数据。否则就相当于让模型“偷偷看到了”OOD 样本评估结果会偏乐观。统计量的计算代码也很简单all_features [] for inputs in train_loader: with torch.no_grad(): feat extract_features(inputs) # [B, H, W, C] all_features.append(feat) all_features torch.cat(all_features, dim0) # [N, H, W, C] mean_vec all_features.mean(dim(0, 1, 2)) std_vec all_features.std(dim(0, 1, 2))5. 怎么评估一个 OOD 检测方法好不好5.1 核心指标AUROC、AUPR、FPR95OOD 检测的评估指标不是分类准确率而是“能否把 ID 和 OOD 分开”。AUROCROC 曲线下面积。把所有样本按异常分数从低到高排序分数越高越可能是 OOD。AUROC 越接近 1说明 ID 和 OOD 分离越好。随机猜测是 0.5。AUPRPrecision-Recall 曲线下面积。当 OOD 样本比例很低时AUPR 比 AUROC 更敏感。FPR95当 ID 样本的召回率达到 95% 时OOD 样本被误判为 ID 的比例。这个指标在生产环境里很重要因为它对应实际使用时允许多少误报。我建议至少报告 AUROC 和 FPR95。一个方法如果 AUROC 高但 FPR95 很高说明它虽然在总体排序上不错但在高召回区间会有大量 OOD 漏网部署时要特别小心。5.2 实验设计要防止“特征泄露”评估 OOD 检测方法有个常见的坑如果你用某个数据集既做了特征统计又作为 OOD 评估数据就会产生泄露。正确的实验设计应该是用 ID 训练集计算特征均值、标准差、top_k 等参数。用 ID 测试集作为已知样本。用完全没参与过任何统计计算的 OOD 数据集作为未知样本。把所有样本的异常分数放在一起计算 AUROC。如果同时使用多个 OOD 数据集可以分别计算指标再求平均。不要把所有 OOD 数据集混在一起因为不同数据集的难度不同混合后会掩盖某个特定数据集的失败。5.3 怎样判断典型性图是否真的有效除了看分数还要看典型性图的合理性。一个合格的典型性图在 ID 样本上应该表现为大部分区域分数较高少部分背景或边缘区域分数较低。在 OOD 样本上应该出现明显的低分区域或者整体分数都很低。我通常会在跑完实验后挑几个典型样本把原始图片和典型性图并排打印出来用 colormap 可视化。这一步能快速发现实现中的 bug。比如如果所有样本的典型性图几乎一样你就要检查是不是特征提取层选错了或者标准化方向写错了。6. 常见报错和排查链路6.1 特征图 shape 不一致这个问题最常见。不同 ViT 变体patch size 不同输出 token 数量也不同。vit_base_patch16_224 输出 196 个 patch tokenvit_base_patch32_224 输出 49 个 patch token。如果不检查 shape后面 reshape 成空间网格时很容易报错。建议在提取特征后立刻打印print(feat.shape)然后根据 token 数量计算网格大小比如 196 对应 14×1449 对应 7×7576 对应 24×24。也可以直接用int(sqrt(num_tokens))动态计算。6.2 SVD 计算太慢或占用过高SVD 的计算复杂度是 O(mn²) 或 O(m²n)取决于矩阵形状。对于 196×768 的矩阵计算很快。但如果你处理的特征图更大比如 24×24576 行768 列速度会下降但也不至于太慢。如果你的批量很大建议逐个样本计算典型性图而不是同时对一个 batch 的大矩阵做 SVD。虽然可以 batch 化但调试起来更麻烦而且内存占用会随 batch size 线性增加。我一般先写单样本循环验证逻辑无误后再考虑向量化。如果确实要在低算力设备上跑可以考虑先对特征图做空间平均池化把 14×14 降采样到 7×7再用 SVD。这样会损失空间细节但可以作为快速基线。6.3 输出结果总是 0.5 附近基本等于随机猜测如果 AUROC 接近 0.5说明方法没有学到任何区分能力。这时不要急着调 top_k先按顺序排查特征层是否选对是否用了 CLS token 而没有用 patch token。是否做了正确的标准化和中心化。特征统计量是不是从训练集算的而不是从测试集算的。异常分数的方向是否反了有些分数是“越大越异常”有些是“越小越异常”计算 AUROC 时要保证方向一致。OOD 数据集是否和模型训练分布差异太小。如果 ID 和 OOD 都是自然图像且类别重叠度高区分会更难。6.4 典型性图看起来像噪声没有结构如果典型性图在 ID 和 OOD 样本上都像随机噪声说明特征矩阵结构没有被有效利用。可能的原因包括选的特征层太浅patch token 还是底层纹理空间关联弱。矩阵行数太少比如 patch size 32 时只有 49 行SVD 能捕捉的结构有限。top_k 太大或太小导致分数无法反映低秩结构。建议从较深的 Transformer Block 开始尝试。比如 vit_base 有 12 层可以分别取第 6 层和第 11 层做对比看哪种更稳定。不要默认最后一层一定最好。6.5 与 LoRA、PCA 的关系要分清相关热词里有“svd与lora”这里也顺势说明一下。LoRA 的参数微调方法也用到低秩分解但它分解的是权重矩阵增量目的是减少可训练参数量。而本文说的典型性图是把 SVD 用在特征矩阵上目的是做不确定性或分布外检测。两者都用 SVD但作用对象和目的完全不同。不要混为一谈。另外PCA 在 OOD 检测中也有应用常见的是用 PCA 对特征降维再在低维空间计算距离。典型性图的区别在于它保留了特征矩阵的空间结构对每个局部区域单独评估而不是把整张图压扁成一个向量。7. 适用边界与进阶优化方向7.1 这个方法在什么场景下更适用基于 SVD 的典型性图对“特征矩阵有较强空间结构”的任务效果更明显。比如自然图像分类、医学影像分析、卫星图像识别这些场景下 patch 之间的空间关系很有意义。如果任务是纯图级别分类而且输入本身没有明显空间布局比如某些表格数据或无序点云投影图这种方法的优势会被削弱。另外它对预训练模型的质量有依赖。如果模型在 ID 数据上本身就没有充分训练特征矩阵自然没有明显的典型结构典型性图也很难做出有效判断。7.2 低资源环境下能不能用如果你的机器只有 4GB 显存跑完整 ViT-Base 会比较吃力。可以改用 ViT-Tiny 或 ViT-Small或者直接使用输入分辨率 160×160。特征图会变小SVD 计算更快。但要注意模型变小后特征表达能力下降OOD 检测的绝对性能也会下降。这在论文里也经常出现小模型的 AUROC 整体低于大模型。所以低资源环境下可以先跑通流程再用小规模实验观察趋势别指望小模型能完全复现大模型的结果。7.3 和现有 OOD 检测方法如何组合典型性图分数可以和其他 OOD 分数组合使用。比如我们可以把 softmax 置信度、温度缩放分数、Mahalanobis 距离、典型性图分数这四个分数做一个加权融合。融合可以提升鲁棒性尤其是单个方法在某个 OOD 数据集上失效时融合往往能兜底。融合时要注意分数标准化。不同方法的分数分布差异很大建议先在 ID 测试集上做 min-max 归一化再做加权平均。权重可以用网格搜索但不要用 OOD 数据调权否则会过拟合到特定 OOD 数据集上。7.4 从离线评估到在线部署要做什么离线实验跑通后如果要部署到真实环境需要额外处理几个点推理速度SVD 在 CPU 上也能算但要注意后端优化。实际部署时可以预先缓存主奇异向量在推理阶段使用近似计算降低开销。特征统计量更新如果模型会继续微调训练集的 mean_vec 和 std_vec 要定期重算否则统计量会过期。阈值设定先用验证集确定一个异常分数阈值比如“超过多少就拒绝”。实际运行时可以记录分数分布定期监控误拒率和漏检率。8. 我自己的实践建议如果让我给初次尝试这个方向的人一个路径我建议按下面的顺序来做先把简单的 softmax 置信度 OOD 检测跑出来得到一个 AUROC 基线。提取 ViT 特征不加任何处理直接算均值距离或马氏距离作为第二个基线。实现 SVD 典型性分数用固定 top_k16 的配置先试一次。对比三个结果看典型性图是否真的带来了提升。如果提升不明显再尝试不同层、不同 score 定义、不同 top_k。这个方法真正落地时最该盯住的不是功能列表而是输入格式、资源占用和失败重试。只要特征统计量、标准化方向、分数方向三个点没有搞错输出通常不会差到哪去。最后留下一句话不要一上来就跑全量数据集先用 1000 张 ID 和 1000 张 OOD 把整个流程走通确认输出稳定、指标可解释再放大实验规模。踩过几次之后你会发现很多问题不是方法本身能力不够而是特征提取层选错、统计量计算方式不对、或者分数方向反了。