2026/8/30 11:59:43

基于SVD与ViT的OOD检测:用Typicality Map定位分布外样本

基于SVD与ViT的OOD检测:用Typicality Map定位分布外样本 在视觉模型真正进入业务系统之前我们对模型能力的要求只是“准确”一旦模型被部署到开放、复杂、不可控的真实环境中问题就变成了“当它遇到没见过的东西时它知不知道它没见过”。这正是 Out-of-DistributionOOD检测要解决的底层问题。本文从一个论文标题出发结合 Vision Transformer 的特征结构完整拆解一种基于 SVD 构建 Typicality Maps 的 OOD 检测思路并给出可复用的工程代码和避坑经验。1. 为什么 ViT 也需要 OOD 检测1.1 OOD 检测要解决什么问题OOD 检测全称 Out-of-Distribution Detection中文通常翻译为“分布外检测”。它要解决的核心问题非常具体训练阶段模型只会看到某一类数据分布In-Distribution简称 ID比如工业质检场景中的“正常工件”和“已知缺陷类型”但在部署阶段模型可能遇到完全没有见过的数据比如新的缺陷形态、异物、光照异常、传感器噪声甚至是被攻击者构造的对抗样本。传统的分类模型有一个很危险的特性即使输入完全不来自训练分布Softmax 层依然会给出一个看似“自信”的概率分布。换句话说模型不仅会犯错而且会“充满信心地犯错”。OOD 检测的目标就是在模型给出分类结果之前先用一个额外的判断机制回答“这个输入是否来自训练分布”。如果判定为 OOD就不应该信任模型的分类输出而是进入人工处理、拒识、告警等兜底流程。OOD 检测与异常检测Anomaly Detection有重叠但不完全相同。异常检测通常面对的是无监督或半监督场景侧重发现“少见的”样本而 OOD 检测更强调分布层面的区分需要同时利用 ID 数据的统计结构和模型学到的特征表示。1.2 ViT 时代的新问题Vision TransformerViT把图像切成固定大小的 patch比如 16×16然后将每个 patch 通过线性映射变成 token再加上位置编码输入 Transformer 编码器。与 CNN 不同ViT 通过全局自注意力建模任意两个 patch 之间的关系因此对局部纹理的依赖方式与卷积核完全不同。这种结构差异带来了 OOD 检测的新挑战。一方面ViT 的 CLS token 经过多层全局注意力之后已经高度抽象适合分类但丢失了大量空间细节另一方面每个 patch token 保留了对应图像区域的局部信息这些信息天然具备空间语义非常适合用来判断“这个局部区域是否偏离训练分布”。如果能充分利用 patch token就可以把 OOD 检测从“整图判断”升级为“逐区域判断”输出一张体现异常位置的典型性热图Typicality Map。本文讨论的方法正是围绕 ViT 的 patch token 展开使用 SVD 对 patch 特征进行降维建模在低维主成分空间中计算每个 patch 的典型性分数最终形成用于检测和定位的典型性图。1.3 为什么选择 SVD 加 Typicality Map 的组合先看一个很现实的工程问题ViT-Base 的 patch token 维度通常是 768一张 224×224 的图会产生 196 个 patch token如果直接用高维特征去计算马氏距离会面临协方差矩阵太大、估计不稳定、计算开销高等问题如果只取少数几个维度特征又会丢失关键信息。SVD奇异值分解在这里的价值是通过正交变换找到数据方差最大的方向用少数主成分表达数据的主要结构。这样既完成了降维去噪又保留了数据分布的核心信息。在此基础上每个 patch 在低维主成分空间里的相对位置可以通过统计距离转换成“典型性”分数距离分布中心越近说明这个 patch 越符合训练数据距离越远说明越可能来自分布外。将每个 patch 的典型性分数按原始空间位置重新排列就得到了 Typicality Map。这张图可以从两个层面使用整图聚合后作为 OOD 检测分数逐区域观察则可以定位异常发生的位置。2. Typicality 与 SVD 的核心思想2.1 什么是典型性“典型性”是一个统计学味道很浓的概念。直观理解给定一组已知样本如果一个新的样本落在已知分布的高概率区域它就是“典型的”如果落在分布稀疏的尾部它就是“不典型的”。在特征空间中典型性可以用概率密度来刻画但在高维空间直接估计概率密度非常困难。一个更实用的做法是使用距离度量假设 ID 数据的 patch 特征近似服从多维高斯分布那么某个 patch 距离分布中心均值越远它属于分布外区域的可能性越高。这里的“距离”不能是普通欧氏距离因为不同特征维度的方差不同相关性也不同。马氏距离Mahalanobis Distance通过协方差矩阵对各个维度做归一化是更合理的度量方式。本文中的 Typicality 可以理解为马氏距离的“概率化版本”将距离映射到 [0, 1] 区间值越接近 1代表该 patch 在 ID 数据中越典型。这样得到的分数天然适合做可视化不同 patch 之间的关系也更直观。2.2 SVD 与 PCA 的数学关系许多读者看到 SVD 的第一反应是“矩阵分解”看到 PCA 的第一反应是“降维”。实际上PCA 的数值实现通常就是依赖 SVD 完成的。对于一个中心化后的特征矩阵 XPCA 首先计算协方差矩阵然后求特征值与特征向量。而 SVD 直接对 X 做分解X UΣVᵀ其中 U 是左奇异向量Σ 是对角奇异值矩阵V 的列是右奇异向量。数学上可以证明协方差矩阵 C XᵀX / (n-1) 的特征向量就是 V 的列特征值等于奇异值的平方除以 (n-1)。因此通过 SVD 得到右奇异向量实际上就得到了 PCA 的主方向。在 Python 的 scikit-learn 中PCA 默认使用 LAPACK 的完整 SVD 实现svd_solverfull而不是直接求协方差矩阵。这样做的优势是数值稳定性更好对病态矩阵更友好。本文代码也直接使用 PCA 作为 SVD 的高层封装既保留 SVD 的数学本质又减少手写分解带来的出错概率。2.3 SVD 在深度学习中的通用价值SVD 并不是只在 PCA 里出现它在深度学习生态中几乎无处不在理解这一点能帮助你建立对本文方法更宏观的认知。近期经常被讨论的 LoRALow-Rank Adaptation就是一个典型例子。LoRA 在微调大模型时冻结原始权重 W只学习一个低秩增量 ΔW B·A。为什么要用低秩矩阵因为通过 SVD 可以证明在 Frobenius 范数意义下矩阵的最优低秩近似就是截断 SVD。也就是说如果权重的更新量本质上集中在一个低秩子空间里用低秩矩阵去近似它就是理论上最优的选择。SVD 在传统科学计算中同样应用广泛。比如气象数据分析中SVD 被用于研究两个变量场例如海温场与降水场之间的耦合关系通过分解交叉协方差矩阵找到两个场之间相关性最强的模态用来揭示大尺度气候系统内部的统计关联。这些看似领域迥异的场景本质上都是把 SVD 当作“从数据中发现主要结构”的工具。本文的做法遵循同样的思想把 viT patch 特征当作待分析的数据点用 SVD 提取主要分布方向再在新的低维坐标系中度量异常程度。3. SVD-Based Typicality 检测方法拆解3.1 整体流程整个方法可以拆为两个阶段离线参考分布拟合阶段和在线检测阶段。离线阶段需要使用一批来自 ID 分布的图像提取它们的所有 patch token 特征将大量 patch 特征拼接成一个二维矩阵然后使用 PCA/SVD 拟合主成分空间保存均值向量、主成分方向、各主成分方差等信息。在线阶段对每一个待检测图像提取它的 patch token 特征将每个 patch 投影到离线拟合的主成分空间中计算马氏距离再通过卡方分布将距离转换为典型性分数重排成 Typicality Map。最后对整张图聚合出一个 OOD 分数与阈值比较完成判定。这样一个流程同时解决了两个问题判断这张图是否属于分布外以及定位图中哪些区域最异常。3.2 特征收集时为什么使用 Patch Token在 ViT 中模型每一层输出的序列都包含一个 CLS token 和 N 个 patch token。CLS token 经过注意力机制汇总了全局信息适合做分类patch token 则对应原始图像的不同区域。做 OOD 检测时patch token 比 CLS token 更有价值原因有两个第一patch token 保留了空间结构。把典型性分数重排为二维热图时必须依赖 patch token 的空间位置信息CLS token 只有一个向量无法完成空间定位。第二patch token 的统计分布更稳定。CLS token 的表示受分类任务影响非常大某些类别的 CLS 特征可能聚集在特定区域patch token 包含大量底层纹理和形状信息这些信息在不同类别之间具有共性因此更适合用来刻画“整个数据集长什么样”的分布特征。实际操作中通常取模型倒数第二层或倒数第一层之前的中间层特征。深层特征语义更强但空间细节偏少浅层特征保留了更多纹理信息。本文示例使用 timm 库中 ViT 模型的 forward_features 输出它返回的是编码器最后一层的完整 token 序列。3.3 主成分空间中的马氏距离在原始高维特征空间马氏距离的定义是D_M(x) sqrt((x - μ)ᵀ Σ⁻¹ (x - μ))其中 μ 是均值向量Σ 是特征协方差矩阵。当特征维度很高时Σ 的估计非常困难求逆的数值稳定性也差。利用 SVD 降维后计算可以大幅简化。假设我们保留 k 个主成分将 x 中心化后投影到主成分空间得到 z那么每个主成分方向上的方差就是对应的特征值 λⱼ。由于主成分方向之间相互正交马氏距离就可以分解成各主成分上标准化距离的平方和D_M²(x) Σⱼ (zⱼ² / λⱼ)也就是说把每个主成分上投影的坐标除以该方向的标准差再求平方和就得到了马氏距离的平方。在 scikit-learn 中PCA 对象提供了 transform 方法用于投影用 explained_variance_ 可以拿到每个主成分的方差。这个计算可以直接用向量化方式实现非常高效。3.4 从距离到 Typicality Map马氏距离本身可以作为 OOD 分数使用但为了让结果更直观、更容易做跨样本比较可以进一步把距离转换为典型性分数。这里用到概率论中的结论如果 ID 数据的 patch 特征近似服从 k 维高斯分布那么其马氏距离的平方服从自由度为 k 的卡方分布。所以可以用卡方分布的生存函数survival function来计算typicality 1 - F( D_M² )其中 F 是卡方分布的累积分布函数CDF。这个值的含义是在该分布假设下一个 ID patch 的马氏距离比当前 patch 更大的概率。如果当前 patch 非常典型距离接近 0typicality 接近 1如果当前 patch 严重偏离分布距离非常大typicality 就趋近于 0。这样得到的典型性分数天然落在 [0, 1] 区间。将每个 patch 的分数按空间位置排列为二维矩阵就得到了 Typicality Map。一张完全来自 ID 分布的图像其典型性图整体偏亮值接近 1含异常的图像会在异常区域出现暗色区域值接近 0。3.5 图像级分数聚合有了逐 patch 的典型性图还需要聚合出一个图像级的判定分数。最常见的做法是取典型性图的最小值因为异常可能只出现在图中很小的一块区域。如果只取均值局部小面积异常会被大面积正常区域稀释。更稳健的做法是取某个低分位数例如 5% 分位数这样既对极端单点噪声不太敏感又能捕捉到小面积异常。实际工程中可以通过验证集对比不同聚合方式的 AUROC 来决定。这里需要注意判定方向typicality 越低越可能是 OOD。如果使用马氏距离作为分数则距离越大越可能是 OOD。为了保证评估指标的意义清晰在代码中我们会明确区分这两个方向。4. 环境准备与项目结构4.1 环境依赖本文代码基于 Python 与 PyTorch 生态核心依赖如下Python 3.9 或更高版本PyTorch 2.xtimm用于加载预训练 ViT 模型torchvision数据加载与预处理scikit-learnPCA/SVD 实现scipy卡方分布计算matplotlib可视化NumPy版本不需要完全一致但建议保持 Python 3.9 以上。timm 库的 API 在不同版本中可能有细微变化如果使用非常老的版本forward_features 的返回结构可能不同建议安装较新的 timm。安装命令pip install torch torchvision timm scikit-learn scipy matplotlib numpy如果你的环境是 CUDA 环境建议先根据 PyTorch 官网选择合适的 torch 安装命令再安装其他依赖。4.2 项目结构为了代码更清晰我们将整个流程拆分为四个脚本svd_typicality_ood/ ├── extract_features.py # 提取 ViT patch 特征 ├── fit_basis.py # SVD/PCA 拟合参考分布 ├── compute_typicality.py # 构建 Typicality Map ├── evaluate.py # 图像级 OOD 检测评估 └── visualize.py # 可视化 Typicality Map实际项目中这些脚本可以根据需要合并成一个类或者一个 pipeline但每一步的功能边界建议保持清晰。下面我们逐个实现。5. 完整代码实战5.1 加载预训练 ViT 并提取 Patch 特征首先定义一个函数加载预训练 ViT 模型并提取每个样本的 patch token 特征。# 文件路径svd_typicality_ood/extract_features.py import argparse import numpy as np import timm import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms def build_model(backbone: str vit_base_patch16_224, device: str cuda): 加载预训练 ViT 模型返回 feature 输出模式。 model timm.create_model(backbone, pretrainedTrue) model.eval() model.to(device) return model def extract_features(model, loader, device: str cuda): 提取每个样本的全部 patch token 特征不包含 CLS token。 model.eval() all_patches [] all_labels [] with torch.no_grad(): for images, labels in loader: images images.to(device) # forward_features 返回 [B, N, D]N 1 num_patches features model.forward_features(images) # 去掉 CLS token保留 patch token[B, num_patches, D] patch_tokens features[:, 1:, :] all_patches.append(patch_tokens.cpu().numpy()) all_labels.append(labels.numpy()) X np.concatenate(all_patches, axis0) y np.concatenate(all_labels, axis0) return X, y def main(): parser argparse.ArgumentParser() parser.add_argument(--data_dir, typestr, requiredTrue) parser.add_argument(--save_path, typestr, defaultid_features.npz) parser.add_argument(--batch_size, typeint, default64) parser.add_argument(--device, typestr, defaultcuda) args parser.parse_args() transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) dataset datasets.ImageFolder(args.data_dir, transformtransform) loader DataLoader(dataset, batch_sizeargs.batch_size, shuffleFalse, num_workers4) model build_model(deviceargs.device) X, y extract_features(model, loader, args.device) np.savez_compressed(args.save_path, featuresX, labelsy) print(f特征已保存到 {args.save_path}特征形状{X.shape}) if __name__ __main__: main()这里的forward_features返回的是 ViT 编码器最后一层的 token 序列。第一个 token 是 CLS token对应数据集类别标记后续 token 对应原始图像按顺序切分的 patch。输入是 224×224 时patch 数量为 14×14196因此最终特征形状为 [B, 196, 768]。运行命令python extract_features.py --data_dir /path/to/id_data --save_path id_features.npz建议使用一个较小但类别结构清晰的 ID 数据集来跑通流程。后面做评估时还需要准备一份包含 ID 和 OOD 混合图像的测试数据。5.2 用 SVD 拟合典型性参考分布拿到 ID 数据的 patch 特征后就可以拟合 PCA 模型。这里把所有图像的所有 patch 特征拼成一个二维矩阵相当于把每个 patch 当作一个独立样本。# 文件路径svd_typicality_ood/fit_basis.py import argparse import numpy as np from sklearn.decomposition import PCA def fit_pca_from_features(feature_path: str, variance_ratio: float 0.95, save_path: str pca_basis.npz): 将特征转为二维矩阵拟合 PCA保存主成分信息。 data np.load(feature_path) features data[features] # [num_images, num_patches, D] num_images, num_patches, dim features.shape # 将 [num_images, num_patches, D] 转为 [num_images * num_patches, D] X features.reshape(-1, dim).astype(np.float64) # 自动保留 95% 方差 pca PCA(n_componentsvariance_ratio, svd_solverfull) pca.fit(X) np.savez_compressed( save_path, meanpca.mean_, componentspca.components_, explained_variancepca.explained_variance_, n_componentsnp.array(pca.n_components_), ) print(f原始维度{dim}保留主成分数{pca.n_components_} f解释方差比例{pca.explained_variance_ratio_.sum():.4f}) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--feature_path, typestr, defaultid_features.npz) parser.add_argument(--save_path, typestr, defaultpca_basis.npz) parser.add_argument(--variance_ratio, typefloat, default0.95) args parser.parse_args() fit_pca_from_features(args.feature_path, args.variance_ratio, args.save_path)PCA(n_components0.95)表示自动保留能够解释 95% 方差的成分数。对于 ViT-Base 的 patch 特征通常需要保留的维度在 100 到 200 之间相比原始 768 维已经有明显降维。如果希望固定维度也可以传整数例如n_components128。拟合过程本质上是 SVD 分解。得到的components_是主成分方向explained_variance_是每个主成分对应的方差也就是特征值。运行命令python fit_basis.py --feature_path id_features.npz --save_path pca_basis.npz5.3 计算 Typicality Map接下来编写核心函数对一张测试图像的所有 patch token计算马氏距离并转换成 Typicality Map。# 文件路径svd_typicality_ood/compute_typicality.py import numpy as np from scipy.stats import chi2 def load_basis(basis_path: str): 加载离线拟合得到的主成分信息。 data np.load(basis_path) return { mean: data[mean], components: data[components], explained_variance: data[explained_variance], n_components: int(data[n_components]), } def compute_mahalanobis_map(patch_features: np.ndarray, basis: dict) - np.ndarray: 计算每个 patch 的马氏距离返回形状为 [P] 的距离数组。 centered patch_features - basis[mean] # 投影到主成分空间Z centered components.T Z centered basis[components].T # [P, k] # 每个主成分方向上的标准差 std np.sqrt(basis[explained_variance]) # [k] Z_norm Z / std mahalanobis_dist2 np.sum(Z_norm ** 2, axis1) # [P] return mahalanobis_dist2 def compute_typicality_map(patch_features: np.ndarray, basis: dict, spatial_size: int 14) - np.ndarray: 将马氏距离转换为 Typicality Map。 dist2 compute_mahalanobis_map(patch_features, basis) df basis[n_components] typicality chi2.sf(dist2, dfdf) # 使用卡方分布生存函数 return typicality.reshape(spatial_size, spatial_size), dist2.reshape(spatial_size, spatial_size) if __name__ __main__: # 演示用法随机生成一个 patch 特征矩阵 rng np.random.default_rng(0) basis load_basis(pca_basis.npz) dummy_features rng.normal(size(196, basis[mean].shape[0])) typicality_map, dist_map compute_typicality_map(dummy_features, basis) print(typicality_map shape:, typicality_map.shape) print(typicality value range:, typicality_map.min(), typicality_map.max())chi2.sf(dist2, dfdf)返回的是卡方分布右尾概率也就是“一个来自 ID 分布的点其距离平方比当前值更大的概率”。这个概率越大代表当前点越典型概率越小代表当前点越异常。在真实评估流程中patch_features应来自待测图像的 ViT 特征提取结果而不是随机生成的演示数据。5.4 图像级 OOD 检测与 AUROC 评估有了 Typicality Map就可以聚合出图像级分数并在混合测试集上评估 OOD 检测性能。# 文件路径svd_typicality_ood/evaluate.py import numpy as np from sklearn.metrics import roc_auc_score from compute_typicality import compute_typicality_map def aggregate_score(typicality_map: np.ndarray, method: str min) - float: 从 Typicality Map 聚合图像级分数。 if method min: return typicality_map.min() elif method mean: return typicality_map.mean() elif method quantile: return np.quantile(typicality_map, 0.05) else: raise ValueError(fUnknown method: {method}) def evaluate_auc(scores: np.ndarray, labels: np.ndarray) - float: scores 是聚合后的典型性分数typicality 越低越可能是 OOD。 为了让 AUROC 大于 0.5我们使用 1 - typicality 作为 OOD 分数。 ood_scores 1.0 - scores return roc_auc_score(labels, ood_scores) def extract_features_for_eval(model, loader, device: str cuda): 为评估集批量提取 patch 特征返回特征和 label。 from extract_features import extract_features return extract_features(model, loader, device) if __name__ __main__: import argparse import torch import timm from torch.utils.data import DataLoader from torchvision import datasets, transforms parser argparse.ArgumentParser() parser.add_argument(--test_dir, typestr, requiredTrue) parser.add_argument(--basis_path, typestr, defaultpca_basis.npz) parser.add_argument(--batch_size, typeint, default64) parser.add_argument(--device, typestr, defaultcuda) args parser.parse_args() transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 测试目录结构test_dir/id、test_dir/ood dataset datasets.ImageFolder(args.test_dir, transformtransform) loader DataLoader(dataset, batch_sizeargs.batch_size, shuffleFalse) model timm.create_model(vit_base_patch16_224, pretrainedTrue) model.eval().to(args.device) X_test, y_test extract_features_for_eval(model, loader, args.device) basis np.load(args.basis_path) basis { mean: basis[mean], components: basis[components], explained_variance: basis[explained_variance], n_components: int(basis[n_components]), } scores [] for i in range(X_test.shape[0]): patch_feat X_test[i] typicality_map, _ compute_typicality_map(patch_feat, basis) scores.append(aggregate_score(typicality_map, methodmin)) scores np.array(scores) auc evaluate_auc(scores, y_test) print(fOOD 检测 AUROC{auc:.4f})测试目录建议如下组织ImageFolder会按子目录名生成类别标签test_dir/ ├── id/ # 来自 ID 分布的图像 └── ood/ # 来自分布外的图像AUROC 为 0.9 以上通常说明检测方法有很好的区分能力如果接近 0.5说明方法基本失效需要检查特征质量、主成分保留比例和聚合方式。5.5 可视化 Typicality Map最后一个代码示例是可视化。将输入图像与 Typicality Map 叠加显示可以直观地看到 OOD 区域的位置。# 文件路径svd_typicality_ood/visualize.py import matplotlib.pyplot as plt import numpy as np from torchvision import transforms from PIL import Image from compute_typicality import compute_typicality_map def denormalize(tensor: np.ndarray) - np.ndarray: 将归一化后的图像还原到 [0, 1] 区间方便显示。 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img tensor * std[:, None, None] mean[:, None, None] return np.clip(img.transpose(1, 2, 0), 0, 1) def visualize_single_image(image_path: str, patch_features: np.ndarray, basis: dict, save_path: str typicality_example.png): typicality_map, _ compute_typicality_map(patch_features, basis) image Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) img_tensor transform(image).numpy() img_show denormalize(img_tensor) fig, axes plt.subplots(1, 2, figsize(10, 5)) axes[0].imshow(img_show) axes[0].set_title(Input Image) axes[0].axis(off) im axes[1].imshow(typicality_map, cmapjet, vmin0, vmax1) axes[1].set_title(Typicality Map) axes[1].axis(off) plt.colorbar(im, axaxes[1], fraction0.046, pad0.04) plt.tight_layout() plt.savefig(save_path, dpi150, bbox_inchestight) print(f可视化结果保存到 {save_path})在 Typicality Map 中蓝色区域表示 typicality 接近 0是模型认为“最不像训练数据”的位置红色区域表示 typicality 接近 1属于典型区域。如果一张 OOD 图像上出现明显的蓝色斑块说明方法不仅完成了检测还给出了可解释的定位线索这在工业质检、医学影像等场景中非常有价值。6. 常见问题与排查思路6.1 常见问题汇总问题现象常见原因解决思路AUROC 接近 0.5检测基本失效使用的特征层不适合主成分保留过多噪声维度尝试不同层特征降低 variance_ratio 到 0.9 或 0.8Typicality Map 全部接近 1测试图像实际来自 ID 数据patch 数不足导致分布假设失效确认测试集划分增加 ID 特征数量Typicality Map 全部接近 0预处理不一致图像色调偏移特征缩放方式不同检查 ID 与测试数据的预处理是否一致显存不足batch size 过大或模型过大降低 batch size使用半精度推理PCA 拟合时报内存错误patch 数量太大特征矩阵超过内存分批 fit降低采样比例timm 版本不同导致 forward_features 报错timm API 不兼容安装统一版本查看模型 forward_features 返回结构卡方分布计算结果为 0马氏距离平方过大浮点下溢使用 log 空间或直接以马氏距离作为分数6.2 主成分数量对结果的影响保留的主成分数量直接影响检测效果。保留过多主成分会把大量属于噪声的方向也纳入距离计算导致 OOD 分数波动保留过少又会丢失关键分布信息。建议在验证集上做一次小实验分别尝试保留 90%、95%、99% 方差观察 AUROC 变化。通常 95% 是一个不错的起点但在特征噪声较大的场景中适当降低到 90% 反而更稳定。6.3 典型性图对整图评估的干扰有读者可能会遇到一个现象OOD 检测分数很高但 Typicality Map 看起来并不像预期那样有明显的异常区域。这是因为某些 OOD 样本的异常模式是“全局纹理分布不同”而不是“某一个局部区域异常”。这种情况下每个 patch 的典型性都会有一些下降但没有任何一个 patch 会极端异常。此时使用 min 聚合可能不是最优选择改用 5% 分位数或均值聚合会对全局性分布偏移更敏感。建议在实验时同时输出多种聚合方式的结果再做选择。7. 工程最佳实践7.1 特征标准化要谨慎PCA 在拟合阶段会执行中心化但不建议对 patch 特征做额外的标准化。如果对所有通道强行标准化到单位方差会抹掉不同维度之间的方差差异破坏马氏距离的数学基础。本文的做法是保留 PCA 内部的中心化逻辑只对特征本身做简单的数值类型转换。7.2 离线特征缓存在生产环境的检测流程中每次推理都重新提取特征并计算投影会带来额外开销。如果图像输入尺寸固定可以在离线阶段为 ID 数据集提取一次 patch 特征并缓存到磁盘作为 PCA 拟合的输入在线阶段则只保存模型权重和 PCA 基向量避免重复拟合。# 缓存文件建议同时保存特征版本信息避免混用 np.savez_compressed(id_features_v1.npz, featuresX, labelsy, model_namevit_base_patch16_224)7.3 阈值校准机制OOD 检测不是一次性确定阈值就结束的。ID 数据的分布会随着数据积累缓慢变化建议在系统中保留一个验证集周期性重新计算分数的分位数分布并据此调整判定阈值。常见做法是取验证集上 95% 分位数的 1 - typicality 作为初始阈值。7.4 与 Softmax 置信度结合实际工程中不建议完全抛弃模型自身的置信度输出。可以把 Softmax 置信度作为第一道防线把基于典型性图的 OOD 分数作为第二道防线两者同时超过阈值才放行。这样既能利用分类模型已有的信息又能规避 Softmax 对分布外输入过度自信的问题。7.5 注意 patch 数量的一致性Different 模型的 patch 数量不同。ViT-B/16 在 224×224 输入下是 196 个 patch也就是 14×14如果换成 ViT-L/14 或 Swin Transformer空间尺寸会变化。代码中的spatial_size参数需要与模型实际输出匹配否则 reshape 会报错。7.6 安全与授权边界如果该方法用于生产环境的自动拒识或告警系统建议输出结果始终保留人工确认入口。OOD 检测模型本身也可能误判尤其是数据分布发生漂移时不能将检测结果直接作为不可逆操作的触发条件。所有涉及阻断、删除或降级的操作都应先在测试环境完成充分演练。8. 总结与后续扩展本文围绕 SVD-Based Typicality Maps 这一思路完整讲解了从背景、原理到代码实现的闭环过程。核心要点可以概括为使用 ViT 的 patch token 作为特征来源用 SVD 拟合 ID 数据的低维主成分空间在马氏距离的基础上通过卡方分布构建逐 patch 的典型性分数最终形成可用于 OOD 检测和异常定位的 Typicality Map。接下来你可以从几个方向继续深入将 Typicality Map 与 MADMahalanobis Distance等经典方法做对比实验尝试不同层特征拼接提升对大尺度分布偏移的感知能力或者把该方法扩展到语义分割、目标检测等更复杂的视觉任务中。特别建议你亲自跑通一遍流程然后在自己的数据集上做一次小规模实验。你会很快发现模型给出一张“自信”的分类结果时很可能它的 Typicality Map 早已暴露出“它其实不认识这张图”。让模型知道何时应该承认自己不知道这可能比单纯提升准确率更有价值。