要处理“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 matplotlib- torch 和 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 等。
我建议先用小规模组合跑通整个流程,例如:
- ID:CIFAR-10 测试集,取 1000 张。
- OOD:SVHN 测试集,取 1000 张。
不要一上来就跑全量 ImageNet,那样时间成本和磁盘空间成本都不会低。先用小样本验证 pipeline 没问题,再逐步扩大。
3.3 选一个合适的 ViT 预训练模型
在 timm 里可以加载很多 ViT 变体,例如:
import timm model = timm.create_model('vit_base_patch16_224', pretrained=True) 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_224,block 数量为 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_k=16): """ 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(dim=0, keepdim=True) # 4. SVD U, S, Vt = torch.linalg.svd(X, full_matrices=False) # 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, dim=0) # [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、FPR@95
OOD 检测的评估指标,不是分类准确率,而是“能否把 ID 和 OOD 分开”。
- AUROC:ROC 曲线下面积。把所有样本按异常分数从低到高排序,分数越高越可能是 OOD。AUROC 越接近 1,说明 ID 和 OOD 分离越好。随机猜测是 0.5。
- AUPR:Precision-Recall 曲线下面积。当 OOD 样本比例很低时,AUPR 比 AUROC 更敏感。
- FPR@95:当 ID 样本的召回率达到 95% 时,OOD 样本被误判为 ID 的比例。这个指标在生产环境里很重要,因为它对应实际使用时允许多少误报。
我建议至少报告 AUROC 和 FPR@95。一个方法如果 AUROC 高但 FPR@95 很高,说明它虽然在总体排序上不错,但在高召回区间会有大量 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 token,vit_base_patch32_224 输出 49 个 patch token。如果不检查 shape,后面 reshape 成空间网格时很容易报错。
建议在提取特征后立刻打印:
print(feat.shape)然后根据 token 数量计算网格大小,比如 196 对应 14×14,49 对应 7×7,576 对应 24×24。也可以直接用int(sqrt(num_tokens))动态计算。
6.2 SVD 计算太慢或占用过高
SVD 的计算复杂度是 O(mn²) 或 O(m²n),取决于矩阵形状。对于 196×768 的矩阵,计算很快。但如果你处理的特征图更大,比如 24×24=576 行,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_k=16 的配置先试一次。
- 对比三个结果,看典型性图是否真的带来了提升。
- 如果提升不明显,再尝试不同层、不同 score 定义、不同 top_k。
这个方法真正落地时,最该盯住的不是功能列表,而是输入格式、资源占用和失败重试。只要特征统计量、标准化方向、分数方向三个点没有搞错,输出通常不会差到哪去。
最后留下一句话:不要一上来就跑全量数据集,先用 1000 张 ID 和 1000 张 OOD 把整个流程走通,确认输出稳定、指标可解释,再放大实验规模。踩过几次之后你会发现,很多问题不是方法本身能力不够,而是特征提取层选错、统计量计算方式不对、或者分数方向反了。