输入输出

DINOv3 ViT 作为 Backbone 的输入输出笔记

对应代码:

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_size16每个 patch 覆盖 16×16 像素
embed_dim384token 特征维度
depth12Transformer block 数量
num_heads6attention 头数(384/6 = 64 dim/head)
ffn_ratio4FFN 隐层 = 4×384 = 1536
n_storage_tokens4DINOv3 特有的"寄存器 token"
layerscale_init1e-5LayerScale 初始值(详见 layerscale理解.md)
norm_layerlayernormbf16bf16 精度的 LayerNorm
训练输入尺寸224×224预训练标准尺寸(RoPE 可外推到任意尺寸)

ViT-S/16 = “384 维 / 12 层 / patch 16”——21M 参数版本,比 ViT-Base (86M) 小 4×。

其他常用 size

名字embed_dimdepthnum_heads参数量
vits16384126~21M
vitb167681212~86M
vitl1610242416~300M
vit_so400m11522718~400M
vit7b1640964032~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/decoder1 个 [B, 384, 14, 14]
实例分割 / 检测(类 Mask2Former)单 1/16 特征 + transformer decoder1 个 [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 + storage1 + 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](不用)
实际 stride16(只有这一个)

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 × 22414 × 14[B, 384, 14, 14]
384 × 38424 × 24[B, 384, 24, 24]
512 × 76832 × 48[B, 384, 32, 48]
1024 × 102464 × 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。