输入输出
DINOv3 ViT 作为 Backbone 的输入输出笔记
对应代码:
dinov3/hub/backbones.py—dinov3_vits16/dinov3_vitb16等构造器dinov3/models/vision_transformer.py—DinoVisionTransformer.forward/get_intermediate_layersdinov3/hub/segmentors.py—dinov3_vit7b16_ms(M2F decoder 的官方用法)dinov3/layers/patch_embed.py—PatchEmbed(Conv2d 16×16)
0. 关键结论
DINOv3 ViT-S/16 只有"一个空间尺度"——所有输出都是 stride 16。 所谓"多级输出"在 DINOv3 ViT 里只有两种取法,都不是传统 CNN 那种 stride 4 / 8 / 16 / 32 的层级。
1. ViT-S/16 的"硬规格"
调用 dinov3_vits16(pretrained=True)(backbones.py:201-237)实际构造的参数:
| 参数 | 值 | 含义 |
|---|---|---|
patch_size | 16 | 每个 patch 覆盖 16×16 像素 |
embed_dim | 384 | token 特征维度 |
depth | 12 | Transformer block 数量 |
num_heads | 6 | attention 头数(384/6 = 64 dim/head) |
ffn_ratio | 4 | FFN 隐层 = 4×384 = 1536 |
n_storage_tokens | 4 | DINOv3 特有的"寄存器 token" |
layerscale_init | 1e-5 | LayerScale 初始值(详见 layerscale理解.md) |
norm_layer | layernormbf16 | bf16 精度的 LayerNorm |
| 训练输入尺寸 | 224×224 | 预训练标准尺寸(RoPE 可外推到任意尺寸) |
ViT-S/16 = “384 维 / 12 层 / patch 16”——21M 参数版本,比 ViT-Base (86M) 小 4×。
其他常用 size
| 名字 | embed_dim | depth | num_heads | 参数量 |
|---|---|---|---|---|
| vits16 | 384 | 12 | 6 | ~21M |
| vitb16 | 768 | 12 | 12 | ~86M |
| vitl16 | 1024 | 24 | 16 | ~300M |
| vit_so400m | 1152 | 27 | 18 | ~400M |
| vit7b16 | 4096 | 40 | 32 | ~7B |
2. Token 流水线
输入 [B, 3, 224, 224] 怎么变成 token 序列(vision_transformer.py:190-220):
输入图像 [B, 3, 224, 224]
│
├─ PatchEmbed (Conv2d 16×16, stride 16)
▼
patch tokens [B, 14, 14, 384] ← H=224/16=14, W=14
│ flatten(1,2)
▼
patch tokens [B, 196, 384] ← 196 个 patch
│
├─ cat(cls_token) # 1 个
├─ cat(storage_tokens × 4) # 4 个(DINOv3 特有)
▼
完整序列 [B, 1+4+196, 384] = [B, 201, 384]
│
├─ 12 × SelfAttentionBlock (RoPE + LayerScale)
▼
最终 token 序列 [B, 201, 384]
│
├─ 切分:[:, 0] → CLS, [:, 1:5] → storage, [:, 5:] → patches
└─ self.norm(...) # 最后再过一层 LayerNorm关键常数:
- patch token 数 =
H/16 × W/16 - 完整序列长度 =
1 (CLS) + 4 (storage) + H/16 · W/16 - patch token 索引区间:
[:, n_storage_tokens + 1 :] = [:, 5:]
3. 单级输出(最简形态)
用 get_intermediate_layers(x, n=1)(vision_transformer.py:285-322)取最后一层,reshape=True 后:
from dinov3.hub.backbones import dinov3_vits16
import torch
backbone = dinov3_vits16(pretrained=True).eval()
x = torch.randn(1, 3, 224, 224)
# 方式 A:只取 patch tokens 的空间特征
feat = backbone.get_intermediate_layers(x, n=1, reshape=True)[0]
# feat.shape == [1, 384, 14, 14] ← BCHW,stride=16三种可用输出
| 字段 | 形状 | 用途 |
|---|---|---|
feat (patch tokens) | [B, 384, 14, 14] | 空间特征图,直接做分割/检测 |
cls_token | [B, 384] | 全局语义向量,做分类/检索 |
storage_tokens | [B, 4, 384] | 不用,是训练时的"寄存器" |
一次拿全三种
(patches, cls, storage) = backbone.get_intermediate_layers(
x, n=1, reshape=True, return_class_token=True, return_extra_tokens=True
)[0]
# patches: [1, 384, 14, 14]
# cls: [1, 384]
# storage: [1, 4, 384]注意 storage_tokens:DINOv3 论文发现 patch token 偶尔会出现"高 norm 异常 token"(携带全局信息、干扰局部注意力),加 4 个可学习 storage token 让它们"泄洪"到寄存器里。下游使用时直接忽略这 4 个 token,也忽略 CLS,只用 patch tokens 部分。
4. 多级输出——两种取法
4.1 方式 A:取不同 block 的输出(仍是 stride 16)
DINOv3 ViT 没有 stride 4/8/32 这种降采样。要拿"多级特征",最直接就是在最后 N 个 block 里挑几个:
# 取 4 个 block 的输出
feats = backbone.get_intermediate_layers(
x, n=[3, 6, 9, 11], # 0-indexed:第 4、7、10、12 个 block
reshape=True,
norm=True, # 每个特征都过最终的 self.norm
)
for i, f in enumerate(feats):
print(i, f.shape)
# 0 torch.Size([1, 384, 14, 14]) ← stride 16, 浅层
# 1 torch.Size([1, 384, 14, 14]) ← stride 16
# 2 torch.Size([1, 384, 14, 14]) ← stride 16
# 3 torch.Size([1, 384, 14, 14]) ← stride 16, 最深层| 阶段 | block 索引 | 形状 | 典型语义 |
|---|---|---|---|
| 浅层 | 4 (idx 3) | [B, 384, 14, 14] | 边缘、纹理、底层模式 |
| 中层 | 7 (idx 6) | [B, 384, 14, 14] | 局部结构、部件 |
| 中深层 | 10 (idx 9) | [B, 384, 14, 14] | 物体部分、上下文 |
| 深层 | 12 (idx 11) | [B, 384, 14, 14] | 高级语义、整体 |
本质:4 个特征图空间分辨率完全相同(都是 14×14),只在语义抽象层级上有差别。不是传统 FPN 那种 stride 4/8/16/32 的"金字塔"。
4.2 方式 B:自己加 CNN decoder 做上采样(真·多尺度)
如果你的下游任务真的需要 stride 4/8/16 的多尺度输出(类 Mask2Former、UPerNet 的检测/分割头),在 ViT 输出基础上自己加:
import torch.nn as nn
class ViTMultiScale(nn.Module):
def __init__(self, backbone, embed_dim=384):
super().__init__()
self.backbone = backbone
# 4 个 1/16 特征 → 4 个不同上/下采样倍率
self.up_4x = nn.Sequential( # 1/16 → 1/4
nn.ConvTranspose2d(embed_dim, embed_dim//2, 4, 2, 1),
nn.ConvTranspose2d(embed_dim//2, embed_dim//4, 4, 2, 1),
nn.GELU(),
nn.Conv2d(embed_dim//4, 256, 3, 1, 1),
)
self.up_2x = nn.Sequential( # 1/16 → 1/8
nn.ConvTranspose2d(embed_dim, embed_dim//2, 4, 2, 1),
nn.GELU(),
nn.Conv2d(embed_dim//2, 256, 3, 1, 1),
)
self.up_1x = nn.Sequential( # 1/16(保持)
nn.Conv2d(embed_dim, 256, 3, 1, 1),
)
self.down_2x = nn.Sequential( # 1/16 → 1/32
nn.Conv2d(embed_dim, 256, 3, 2, 1), # stride 2
nn.GELU(),
nn.Conv2d(256, 256, 3, 1, 1),
)
def forward(self, x):
feats = self.backbone.get_intermediate_layers(
x, n=[3, 6, 9, 11], reshape=True, norm=True
)
f_shallow, f_mid1, f_mid2, f_deep = feats
return {
"stride_4": self.up_4x(f_shallow), # [B, 256, 56, 56]
"stride_8": self.up_2x(f_mid1), # [B, 256, 28, 28]
"stride_16": self.up_1x(f_mid2), # [B, 256, 14, 14]
"stride_32": self.down_2x(f_deep), # [B, 256, 7, 7]
}这就是 DINOv3 官方 dinov3_vit7b16_ms(hub/segmentors.py:25-63)的工作方式——它不依赖 backbone 给多尺度,而是拿 1/16 特征 + 一个有上采样的 Mask2Former decoder。
5. 任务 → 方案对照表
| 你的下游任务 | 推荐方案 | 取法 |
|---|---|---|
| 图像分类 / 检索 | 单 CLS 或全局平均池 | 1 个 [B, 384] 向量 |
| 语义分割 (per-pixel 预测) | 单 1/16 特征 + UNet/decoder | 1 个 [B, 384, 14, 14] |
| 实例分割 / 检测(类 Mask2Former) | 单 1/16 特征 + transformer decoder | 1 个 [B, 384, 14, 14] |
| 密集预测 + 需要 P2/P3/P4/P5 金字塔 | 多 block 输出 + 自建上/下采样 | 4 个 1/16 → 1/4, 1/8, 1/16, 1/32 |
| 多任务 / 特征融合 | 取 4 个 block 输出(1/16) | 4 个 [B, 384, 14, 14] |
经验法则:
- Mask2Former / SAM 风格下游:直接吃 1/16,decoder 自己会处理多尺度
- RetinaNet / Faster R-CNN 那种经典 FPN:得自己加 ConvTranspose / stride-2 conv 拼出 1/4, 1/8, 1/16, 1/32
- 单尺度任务(小目标检测、深度估计):1/16 + 一个反卷积头就够了
6. ViT-S/16 @ 224×224 的数字清单
| 量 | 数值 |
|---|---|
| 输入尺寸 | [B, 3, 224, 224] |
| Patch 网格 | H=14, W=14 |
| Patch token 数 | 196 |
| CLS + storage | 1 + 4 = 5 |
| 完整 token 序列长度 | 201 |
| 特征维度 | 384 |
| Block 数 | 12 |
| 单级特征形状(reshape=True) | [B, 384, 14, 14] |
| 多级(4 block)特征形状 | 4 × [B, 384, 14, 14] |
| CLS token 形状 | [B, 384] |
| Storage token 形状 | [B, 4, 384](不用) |
| 实际 stride | 16(只有这一个) |
7. 输入尺寸的灵活性
虽然预训练用 224×224,但 RoPE 是连续的(详见 rope笔记.md),所以可以处理任意尺寸的输入:
x = torch.randn(1, 3, 512, 768) # 任意尺寸都行
feat = backbone.get_intermediate_layers(x, n=1, reshape=True)[0]
# feat.shape == [1, 384, 32, 48] ← H/16, W/16| 输入尺寸 | Patch 网格 | 单级特征形状 |
|---|---|---|
| 224 × 224 | 14 × 14 | [B, 384, 14, 14] |
| 384 × 384 | 24 × 24 | [B, 384, 24, 24] |
| 512 × 768 | 32 × 48 | [B, 384, 32, 48] |
| 1024 × 1024 | 64 × 64 | [B, 384, 64, 64] |
小贴士:推理时把 backbone 转成
bf16/fp16能省一半显存:
backbone = backbone.to(memory_format=torch.channels_last)
backbone = backbone.half() # 或 .bfloat16()8. 一句话总结
DINOv3 ViT-S/16 = 1 个空间尺度(stride 16)+ 384 维 × 12 个 block。所谓"多级输出"要么是"同尺度不同 block 抽 4 个"(语义层级差,无空间层级差),要么是"自己加 CNN decoder 把 1/16 推到 1/4~1/32"(真·多尺度)。CLS 和 4 个 storage tokens 不要送进下游任务的空间 head——只用 patch tokens。