ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

自监督特征可视化:用DINOv2+PCA把模型玄学变成科学

自监督特征可视化:用DINOv2+PCA把模型玄学变成科学 你有没有过这种体验模型训了半天loss 曲线看着挺正常可换个批次数据效果就崩。你试着调学习率、换数据增强、加深网络层数每一下都是“凭感觉”运气好能救回来运气不好就是白熬几个通宵。我以前也这样直到我把 DINOv2 这种自监督模型提取出来的特征用 PCA 压到 RGB 空间里直接画成伪彩色图贴在原图上才第一次“看见”模型到底在关注什么。原来所谓“调参玄学”很大程度上是因为我们始终在盲调——没有反馈信号自然只能靠玄学。本文就用 ViT-S 和 ViT-G 两个规模的 DINOv2 模型把 PCA 可视化这条路完整走一遍让自监督特征变成你能看懂、能用来指导调参的实在工具。内容适合所有被深度学习调参折磨的人尤其是做分类、分割、检索方向、又不想光靠 TensorBoard 看曲线的朋友。不需要你有很强的数学基础PCA 部分我会用比较直观的方式讲清楚。1. 调参为什么这么“玄”因为你一直在盲调1.1 我们日常盯着的指标其实信息量很低做深度学习的人都有这种经历训练集的 loss 降到 0.3验证集准确率 91%一切看起来都正常。但你不知道模型内部发生了什么。它认出来的是狗的耳朵轮廓还是背景里那片草地的纹理它有没有把目标物体和背景边缘“糊”在一起这些信息loss 和 accuracy 一概不告诉你。你可以说这不重要我只要准确率够高就行。但问题是当你需要改进模型时你就不知道从哪里下手。比如你的分割模型在边缘处总是崩你判断不了是上采样模块的问题还是骨干网络提取的特征本身就在边缘处混淆了。又比如你微调一个预训练模型发现下游任务效果反而变差了你分不清是学习率太大把预训练特征“洗掉了”还是任务差异太大、原来的特征就不适用。这些问题的本质是同一个你缺少对特征本身的观测手段。调参调的是什么调的是模型内部的表征。你连表征长什么样都不知道调参可不就是玄学嘛。1.2 可视化才是模型的“仪表盘”我自己做嵌入式开发时养成了一个习惯凡是控制类算法一定要先看反馈信号的波形再动手调参数。PID 调参为什么有那么多“玄学”传说因为很多人不看被控量的实时响应就直接调比例、积分、微分系数那当然是玄学。但凡你先把阶跃响应曲线打出来比例过大导致的振荡、积分饱和导致的超调一眼就能看出来。深度学习调参也是同一个道理。你需要一个“过程变量反馈”——把特征映射成图像用眼睛看。特征可视化不是什么新东西CNN 时代就有人做 filter 可视化、CAM 热力图。但到了 ViT 和自监督模型这里情况变得更有意思DINOv2 这类模型学习到的 patch token 特征天然包含很强的语义结构——同一物体的区域在特征空间里离得很近不同物体的边界非常清晰。你不需要额外训练任何解释头只要把这些 token 拿来做 PCA、映射到 RGB结果本身就长得像一张语义分割图。下图这种效果就是 PCA 把高维 patch 特征降到 3 维、再归一化到 0-255 的 RGB 三个通道得到。你用肉眼看这张“伪彩色图”就能立刻判断模型有没有把猫和背景分开、有没有把同一个物体的不同部分统一成相近的颜色、有没有被不相关的高频纹理干扰。这才是“看见”模型。有了这个反馈再谈调参才谈得上科学。2. DINOv2 是如何学习特征的自蒸馏与 token 的分工想用好一个模型至少要理解它内部的几个关键机制。DINOv2 是 Meta 在 2023 年发布的自监督视觉模型它和普通 ViT 结构类似但训练方式完全不同。了解下面这几点你就知道为什么它的特征适合拿来可视化。2.1 自蒸馏的本质一个学生、一个老师还有 EMADINOv2 的核心训练方式是自蒸馏通俗理解就是“学生向老师学老师也在向学生学”。训练时有两个结构相同但权重不同的 ViT 网络一个叫 teacher一个叫 student。输入一张图的多个视角——一个全局视角和若干局部裁剪视角student 看局部小图teacher 看全局大图然后让 student 输出的特征去“匹配”teacher 对应区域的特征通过交叉熵损失来约束。teacher 的权重并不是独立训练的而是通过指数移动平均EMA从 student 那里更新来的。也就是说teacher 是 student 的历史平均状态。这种做法有两个好处一是防止模型崩塌到输出常数二是让 student 在“追赶”一个比自己更平滑、更稳定的目标特征会越来越有区分性。为什么要这么绕因为自监督没有标签模型最容易选择“偷懒”——对任何输入都输出同一个向量。自蒸馏的这个“慢老师”机制配合多视角输入逼着模型去学真正能区分不同位置、不同物体的语义特征。结果就是DINOv2 不需要标注数据也能学到非常干净、接近语义分割的特征空间。这正是我们可视化最需要的东西。2.2 CLS token 和 patch token全局与局部的分工ViT 会把图像切成固定大小的 patchDINOv2 默认 14x14每个 patch 经过 Embedding 后变成一个向量再在序列开头加一个特殊的 CLS token。Transformer 的 attention 层会在这些 token 之间互相交换信息最后输出的特征里每个 token 都携带了整个图像的信息但它们的“主视角”不同CLS token倾向于聚合整张图的全局信息适合做图像分类、图级检索它的语义抽象程度最高。patch token每个 patch 对应原图的一个具体位置保留了大量局部空间信息。这就是为什么 patch token 的特征天然适合可视化——你可以把它还原成一张跟原图分辨率成比例的“像素级”特征图。如果直接拿 384 维或 1536 维的 patch token 来画图人眼没法理解。我们需要降维把高维向量压缩到 3 个通道。PCA 就是最经典的线性降维方法下一节细说。2.3 ViT-S 与 ViT-G参数规模差在哪什么时候用哪个DINOv2 官方提供 ViT-S、ViT-B、ViT-L、ViT-G 四个规模。对做实验来说最常用的是 ViT-S 和 ViT-G一个轻量快跑一个重型上精度正好覆盖两个极端。我把关键参数整理了一下模型参数量嵌入维度patch 网格224px 输入推理成本相对ViT-S/14约 2100 万38416x16 256 个 token低CPU 可勉强跑ViT-B/14约 8600 万76816x16 256 个 token中ViT-L/14约 3 亿102416x16 256 个 token高ViT-G/14约 11 亿153616x16 256 个 token很高建议 12GB 以上显存嵌入维度直接决定了 PCA 时协方差矩阵的大小。ViT-S 是 384x384ViT-G 是 1536x1536后者在 PCA 求解时的开销会大不少但 224x224 输入下 patch 只有 256 个所以总体还是可以接受。选型的经验是先用 ViT-S 快速迭代看特征是否有基本语义结构验证流程有没有问题再用 ViT-G 出最终结论。ViT-G 的特征往往更精细对相似物体的区分度更好但代价是显存和推理时间。可视化实验大部分时候其实用 ViT-S 就够了它的特征并没有因为模型小而丢失主要语义。3. PCA 不是玄学它到底在算什么又怎么“画成图”很多人用过 sklearn 的 PCA但没想过它每一步在做什么。其实 PCA 并不难难的是用直觉去理解它。我换一种方式讲。3.1 从“高维坐标”到“差异最大的方向”想象每个 patch token 是 384 维空间里的一个点ViT-S。这 384 个维度不是平等的——有的方向上不同 patch 之间的差异很大有的方向上大家几乎一样基本是噪声。PCA 做的就是找到一组新的正交坐标轴让第一个轴指向数据波动最大的方向第二个轴在垂直于第一个轴的前提下指向波动次大的方向以此类推。这就像你站在一堆人面前想从某个角度看过去能把大家区分得最开你会选择“身高差”最明显的那个方向去看而不是随便挑个方向。PCA 找到的这些新轴叫“主成分”数据点投影到这些轴上得到的数值叫“主成分得分”。关键结论前几个主成分保留了数据里方差信息量最大的那几个方向。对 DINOv2 的 patch token 来说最大的方差通常就对应着“这是猫还是背景”“这是天空还是地面”这类最粗粒度的语义差异。这种差异不需要标签是模型自己从大量图像里学出来的。3.2 协方差矩阵、特征值、特征向量的一体关系PCA 为什么老跟协方差矩阵一起出现因为协方差矩阵本身就在回答一个问题所有维度两两之间是怎么协同变化的。比如某个 patch 在维度 1 上值很大时维度 2 是不是也倾向于值很大协方差正值表示两个维度同向变化负值表示反向接近零表示基本无关。数据矩阵 X每行是一个 patch token经过中心化减去每个维度的均值后协方差矩阵 C X^T X / (n-1)其中 n 是 patch 数量。对 C 做特征值分解会得到一组特征值和对应的特征向量。特征向量指向主成分方向特征值就是数据在该方向上的方差大小。这就是 PCA 原理里最关键的一步不是直接对原始数据做投影而是先分析维度之间的关系再选出最能代表数据差异的方向。很多朋友会卡在“为什么 PCA 要用协方差矩阵”这个问题上其实就是这个原因——协方差矩阵把所有维度之间的相关性都汇总在一起了对它做特征分解就等价于在寻找数据方差最大的正交方向。3.3 为什么取前三个主成分就能映射成 RGB主成分按特征值从大到小排列前三个主成分解释的方差通常已经能覆盖特征空间中相当大的一部分结构。于是你把每个 patch token 在前三个主成分上的得分当成 RGB 的三个通道值。这里要注意一个细节主成分得分是浮点数范围从负到正都有不能直接当 RGB。需要做一次归一化。我以前习惯用 min-max 归一化后来发现容易受极端值影响一个离群点会让整张图颜色偏灰。更稳的做法是用分位数裁剪取 2% 到 98% 的分位数作为上下界把超出部分截断到边界再做 min-max 拉伸。这样颜色对比度会有明显改善。3.4 特征值衰减曲线被大多数人忽略的“健康指标”可视化只用了前三个主成分剩下的大量主成分就被忽略了。但从所有特征值的衰减曲线里你能读出比颜色图更多的信息训练良好的自监督模型特征值应该是“头部集中、长尾平缓”的——前几十个主成分占据大部分方差但后面的维度仍然有贡献。如果前 3 个主成分就解释了超过 60% 的方差说明特征过于集中在低维子空间可能发生了特征退化如果衰减曲线非常平缓、前 100 维只解释了不到 30%说明特征分布比较分散下游分类可能要更复杂的头才能利用。这个指标在你对比不同 training recipe 时特别有用。我在做实验时经常把几个候选模型的特征值曲线叠在一张图上比对着看谁的结构更健康比单看下游 acc 更早发现问题。4. 实操全流程加载模型、提取特征、PCA 上色理论讲完直接上代码。整个流程可以在普通单卡环境跑通ViT-S 甚至 CPU 都能接受。4.1 环境与模型加载含显存/速度对比先装依赖pip install torch torchvision scikit-learn matplotlib opencv-pythonDINOv2 模型通过 torch.hub 加载最方便import torch # 想用 ViT-S 还是 ViT-G换这一行就行 model torch.hub.load(facebookresearch/dinov2, dinov2_vits14, pretrainedTrue) # model torch.hub.load(facebookresearch/dinov2, dinov2_vitg14, pretrainedTrue) model.eval() device cuda if torch.cuda.is_available() else cpu model.to(device)torch.hub 第一次运行会从 GitHub 拉取代码并下载权重ViT-S 权重约 85MBViT-G 权重约 4.4GB下载时间取决于网络。这里有一点经验如果后续要多次使用建议把权重缓存目录固定好避免每次都重新下载。缓存路径默认在~/.cache/torch/hub/可用环境变量TORCH_HOME指定到 SSD 目录。显存方面ViT-S 加 224x224 输入推理峰值不到 1GB任何带 CUDA 的机器都没问题。ViT-G 就夸张多了单卡 224x224 推理大概需要 5GB 以上显存12GB 的卡建议用 fp16 推理model model.half()注意换成 half 之后输入图像也要转成 half否则会报类型不匹配。4.2 图像预处理与特征提取DINOv2 沿用了 ImageNet 风格的归一化参数分辨率为 224x224patch size 14正好 16x16 个 patch。我用 torchvision 的 transforms 做预处理from PIL import Image from torchvision import transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) image Image.open(test.jpg).convert(RGB) x transform(image).unsqueeze(0).to(device)提取特征的写法因模型后端而异。torch.hub 加载的官方模型可用forward_features方法with torch.no_grad(): out model.forward_features(x) cls_token out[x_norm_clstoken] # [B, 384] 或 [B, 1536] patch_tokens out[x_norm_patchtokens] # [B, 256, 384] 或 [B, 256, 1536]patch_tokens的形状是 [batch, 256, 384]256 表示 16x16 个 patch。注意官方实现里返回的是归一化后的 token一般直接用就行。如果你用 HuggingFace transformers 加载facebook/dinov2-small则写法略有不同from transformers import AutoModel model AutoModel.from_pretrained(facebook/dinov2-small) last_hidden model(x).last_hidden_state # [B, 257, 384]含 CLS token patch_tokens last_hidden[:, 1:, :]两种方式效果等价按你习惯的生态来。4.3 PCA 降维与 patch 上色核心代码如下。它做的事情是把这一张图的 256 个 patch token 去做 PCA fit取前 3 个主成分归一化到 0-1重排成 16x16 的特征图再放大到原图尺寸叠加显示。import numpy as np import cv2 import matplotlib.pyplot as plt from sklearn.decomposition import PCA def pca_to_rgb(patch_tokens, image_np): patch_tokens: [N, D] numpyN每个图patch数D嵌入维度 image_np: 原始RGB图 (H, W, 3)uint8 pca PCA(n_components3) coords pca.fit_transform(patch_tokens) # [N, 3] # 分位数裁剪避免离群点把颜色拉伸灰掉 lower, upper np.percentile(coords, [2, 98], axis0) coords np.clip((coords - lower) / (upper - lower 1e-6), 0, 1) # 假设是方形网格 h w int(np.sqrt(patch_tokens.shape[0])) color_map coords.reshape(h, w, 3) # [16, 16, 3] # 插值放大到原图尺寸 color_map cv2.resize(color_map, (image_np.shape[1], image_np.shape[0]), interpolationcv2.INTER_LINEAR) # 伪彩色底图 原图半透明叠加 vis (color_map * 255).astype(np.uint8) vis cv2.cvtColor(vis, cv2.COLOR_RGB2BGR) # opencv 是 BGR overlay cv2.addWeighted(image_np, 0.4, vis, 0.6, 0) return overlay, pca调用patches patch_tokens[0].cpu().numpy() image_np np.array(image)[:, :, ::-1] # RGB - BGR因为后面要过 opencv overlay, pca pca_to_rgb(patches, image_np) plt.figure(figsize(8, 8)) plt.imshow(cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)) plt.axis(off) plt.show()这样你就能看到类似语义分割的伪彩色图。如果你只想显示纯色块图、不要原图叠加把addWeighted那行改成直接返回vis再 resize 就行。4.4 画图与解读你会在结果里看到什么我第一次跑通这个流程是在一张“草地上的狗”的照片上。colormap 出来之后代即是背景草地被染成了一种均匀的绿色调狗的身体区域是另一种颜色狗头和背景交界处颜色变化非常锐利——模型虽然是在没有任何标注的情况下训练的但它已经能区分前景和背景而且是“像素级”的区分。这就是自监督特征最惊人之处。更有意思的是如果你把同一张图分别过 ViT-S 和 ViT-G 做可视化再对比能看到 ViT-G 的色块边界更细、同类物体内部的颜色更统一。这说明更大的模型学到了更精细的语义划分。对做下游任务的人来说这个直观差异意味着如果你的任务对边缘敏感比如分割用 ViT-G 作为骨干可能先天就有优势。你也可以把 patch token 换成 CLS token 做 PCA但那样只有 1 个点没法形成二维网格画不出空间图。所以可视化空间结构必须用 patch token这是它们分工决定的。5. 把可视化变成调参依据几组真实决策场景能画出漂亮的图还不够关键是把这种可视化真正嵌到调参流程里。这里说几个我实际用下来非常有效的决策场景。5.1 判断收敛同一批图在不同 stage 的变化我微调模型时会用同一组固定图片比如 8 张覆盖不同场景的验证图在每个 epoch 结束后跑一次 PCA 可视化保存下来。注意这里有个关键细节每次都要用同一批图片来 fit PCA否则颜色含义会漂移对比没有意义。具体做法是用第一轮预训练模型或 epoch 0跑出来的 PCA 主成分作为固定投影矩阵后续所有轮次都用pca.transform()来投影而不是重新 fit。观察这个“特征演进视频”你能看到很多指标看不到的事。比如训练到第 3 个 epoch 时如果色块开始出现清晰结构、物体边界变锐利说明模型正在快速学习。如果到了第 10 个 epoch 颜色图还在剧烈抖动说明训练不稳定可能需要降低学习率或加大 warmup。5.2 判断过拟合PCA 颜色是否开始“碎”过拟合的传统判断是训练 loss 下降但验证 loss 回升。但特征可视化能提前暴露问题当模型开始过拟合训练集时我观察到 patch 特征会出现“碎裂”现象——原本大片的同色区域开始出现细碎的异色噪点尤其是背景区域因为模型开始记住训练集中的高频纹理细节。这个信号通常比验证 loss 回升早 2 到 3 个 epoch。一旦发现特征开始“碎”就果断恢复上一轮的 checkpoint或者提前停止。我后来把这个观察固化成脚本每次训练完自动输出最后 10 个 epoch 的可视化缩略图扫一眼就知道该选哪个 checkpoint不再单纯依赖 val acc 曲线。5.3 微调 DINOv2 时可视化告诉你要不要动所有层“DINOv2 怎么微调”是我被问得最多的问题之一。很多人一上来就全量微调显存烧了一半下游效果还不如线性探测。可视化能帮你判断微调策略如果你在目标数据集上的伪彩色图已经边界清晰、语义明确说明预训练特征完全够用你只需要训一个分类头或分割头就行backbone 保持冻结。如果色块能区分大类、但同类细分子类混乱说明特征需要适配建议只微调最后 2-3 个 Transformer block学习率设置在 1e-5 量级。如果整张图颜色混沌、看不出结构说明预训练特征与你的任务域差异较大这时再考虑全量微调但依然建议学习率不超过 5e-5。学习率对 DINOv2 微调特别敏感。我踩过太多次“lr1e-4 直接把特征洗没”的坑。调大学习率后可视化特征会迅速退化成一片噪声而且这种退化不可逆——降低学习率也救不回来只能重新从预训练权重开始。5.4 一个完整的微调与监控循环我现在跑实验的基本流程是这样加载 DINOv2 预训练权重用固定 8 张验证图生成初始 PCA 投影方向和基线伪彩色图。决定微调策略只调头、调最后几层、或全量从较小学习率开始。每个 epoch 结束做三件事记录 loss/acc、跑固定图集的特征可视化、计算当前特征在“固定 PCA 方向”上的主成分得分分布。对比当前伪彩色图与基线若语义保持甚至变清晰说明微调方向正确若出现碎裂或混乱立即回滚并降低学习率。最终选 checkpoint 时不只看验证集分数还会要求伪彩色图没有明显退化。这套流程看起来简单但它把调参从“事后解释”变成了“过程控制”。调参不再是猜而是有仪表盘的闭环。6. 这些坑我替你先踩过了最后分享几个我在实操中踩过的坑都是代码层面和流程层面非常容易犯的问题。6.1 PCA 在 patch 数量多时的内存问题小图 224x224 只有 256 个 tokenPCA 毫无压力。但当你把输入分辨率提到 518x518DINOv2 官方支持更大输入patch 数量变成 37x371369如果再把多张图拼在一起批量 fit样本量上万直接 fit 标准 PCA 就可能内存吃紧。解决办法有两个一是用sklearn.decomposition.IncrementalPCA分 batch 地 fit内存占用恒定二是只用一部分 patch 来 fit 主成分方向比如均匀采样 20% 的 patch然后用这个方向 transform 全部 patch。第二种方法更快而且对结果影响很小。6.2 归一化处理直接 min-max 会遇到极端值第一次做可视化时我直接对 PCA 得分做 min-max 归一化结果大部分图的颜色都偏灰“对比度”很低。原因是某些 patch 的得分特别极端常见于纯色天空区域把整个颜色范围拉得很大其他区域的差异就被压缩了。解决方案就是前面提到的分位数裁剪。我用2%和98%分位数作为上下界效果立竿见影。裁剪比例可以根据图像内容微调如果图里天空面积特别大可以把下限调到 5% 来避免大片灰色区域。6.3 单张图 fit 与批量 fit 的区别别把颜色跨图对比这是我犯过的最隐蔽的错误。一开始我用每张图单独 fit PCA然后拿两张图的伪彩色图做对比试图分析模型对不同类别图像的响应差异。后来发现这个对比毫无意义因为 PCA 对每张图重新找主成分方向两张图的“红色”很可能代表的不是同一个语义。要做跨图对比必须把多张图的 patch token 堆在一起用一个统一的 PCA 投影。更严谨的做法是先选一批代表性图片fit 一次 PCA这就固定了投影方向和特征顺序之后所有图片包括训练过程中的监控图、测试集图片都用transform()投影颜色含义就完全可比较了。6.4 一点个人体会做可视化这件事本身不复杂代码量很少但它真正改变的是我调试模型的思路。以前我依赖日志和曲线现在我会习惯性地“看一眼特征”。这一眼看起来很简单但每次都能帮我提前发现问题省下的时间远比写代码多。从 ViT-S 到 ViT-G从原始 patch token 到 PCA 伪彩色图中间隔的并不是多高深的技术而是一个朴素的道理你能不能调好一个模型取决于你对它内部状态的感知有多清晰。DINOv2 的自监督特征本身已经足够干净PCA 只是把这份干净翻译成人眼能懂的语言。翻译器就在你手边建议你今天就找一张自己的数据图跑一下大概率会有惊喜。
返回列表