如何用 DINOv3 实现零训练零样本语义分割:dino.txt 完整实战
2026/9/15 14:32:24 网站建设 项目流程

如何用 DINOv3 实现零训练零样本语义分割:dino.txt 完整实战

【免费下载链接】dinov3Reference PyTorch implementation and models for DINOv3项目地址: https://gitcode.com/GitHub_Trending/di/dinov3

你手上有几张 4K 街景图,要分出"道路、车辆、天空、行人",却一个标注像素都没有。DINOv3 的 dino.txt(DINOTxt)模型可以做到:只给它一份类别名列表,就能输出像素级分割图,不训练、不标注。

从类别名到分割图:3 分钟看懂对齐原理

dino.txt 本质上是一个双塔对比模型。左塔是 DINOv3 的 ViT-L/16 骨干,外加两个注意力块;右塔是一个 24 层因果 Transformer 文本编码器。两者联合训练:真实匹配过的图文对在特征空间里被拉近,不匹配的推远。训练结束后,图像特征和文本特征共享同一个 2048 维空间,"距离近"就意味着语义相近。

图像侧的设计与常见 CLIP 不同。它不把整张图压成一个向量,而是为每个 16×16 的图像块(patch)各输出一个特征向量。一张 512×512 的图会生成 32×32 的特征网格,这个网格的分辨率就是分割输出的原生分辨率。文本侧则把每个类别名套进若干提示模板里,编码成单个向量。

于是推理只剩一步内积:把 patch 特征和文本向量各自 L2 归一化后做余弦相似度(Cosine Similarity)计算,每个位置得到 C 个分数(C 为类别数),取 argmax 即得类别。整个过程没有需要训练的参数,输入只有类别名列表和图像本身。

从环境到首次推理:最短路径

项目自带 conda 环境文件,模型权重会在首次加载时自动下载:

git clone https://gitcode.com/GitHub_Trending/di/dinov3 cd dinov3 micromamba env create -f conda.yaml micromamba activate dinov3

Python 里一行调用即可拿到模型和分词器:

import torch import torch.nn.functional as F from dinov3.hub.dinotxt import dinov3_vitl16_dinotxt_tet1280d20h24l # 自动下载 DINOv3 骨干与 dinotxt 视觉头/文本编码器,返回 (model, tokenizer) model, tok = dinov3_vitl16_dinotxt_tet1280d20h24l() model.to("cuda", non_blocking=True).eval() tokenize = tok.tokenize # 接收字符串列表,返回 [N, 77] 的 token id

文本侧的编码函数值得单独拎出来。类别名需要套进提示模板(参考 notebook 里有近 80 条,调试时先用 3 条就够):

TEMPLATES = ("a photo of a {0}.", "a photo of many {0}.", "a close-up photo of a {0}.") def encode_text(class_name: str) -> torch.Tensor: """类别名 + 模板 -> 单个类别的文本向量""" tokens = tokenize([t.format(class_name) for t in TEMPLATES]).to("cuda") feats = model.encode_text(tokens) # [模板数, 2048] feats = feats[:, feats.shape[1] // 2:] # 丢弃 CLS 槽位,保留与 patch 对齐的半边 feats = F.normalize(feats, dim=-1).mean(dim=0) # 对所有模板取平均 return F.normalize(feats, dim=-1) # 再次归一化,便于余弦计算

这里有个第一处容易踩的坑:encode_text输出 2048 维,但做像素级对齐只用后 1024 维。原因是视觉塔的输出是"类别向量 + patch 向量"两段拼接,文本向量按同构设计输出了对应结构,而像素级任务只需要与 patch 对齐的那一半。

核心机制拆解

图像侧:补齐到 16 的倍数,取 patch 特征图

骨干按 16×16 切块,所以输入分辨率必须是 16 的整数倍,否则最后一行列无处安放。参考实现先用双三次插值把图拉伸到 patch 尺寸的倍数,再提特征:

import math def encode_image(img: torch.Tensor) -> torch.Tensor: """img: [3, H, W] 归一化张量;返回 [h, w, D] 的 patch 特征图""" img = img.unsqueeze(0) _, _, H, W = img.shape P = model.visual_model.backbone.patch_size # DINOv3 的 patch 尺寸 = 16 new_H, new_W = math.ceil(H / P) * P, math.ceil(W / P) * P if (H, W) != (new_H, new_W): # 拉伸到 16 的倍数,保证图像能被完整切成整数个 patch img = F.interpolate(img, size=(new_H, new_W), mode="bicubic", align_corners=False) _, _, h_i, w_i = img.shape # 返回 (cls 特征, patch 特征, 骨干原始 patch token),分割只用中间那个 _, patch_tokens, _ = model.visual_model.get_class_and_patch_tokens(img) # 把序列维还原成二维网格:[1, h, w, D] return patch_tokens.reshape(1, h_i // P, w_i // P, -1).contiguous()[0]

注意get_class_and_patch_tokens返回的第二项不是骨干原始输出,而是穿过两个额外头块(head blocks)后的 patch 特征。这两个头块正是 dinotxt 真正训练过的参数(骨干全程冻结),承载了与文本对齐的语义,直接决定相似度算得准不准。这一段把图像变成"按网格排列的语义向量矩阵",是后续所有相似度计算的坐标系。

相似度计算:一条 einsum 出类别热图

两侧特征就绪后,推理的核心只有一行。text_feats形状 [C, D],特征图 [h, w, D]:

def predict_whole(img: torch.Tensor, text_feats: torch.Tensor) -> torch.Tensor: """返回 [C, h, w] 的余弦相似度:每个类别一张低分辨率热图""" patch_feats = F.normalize(encode_image(img), dim=-1) # [h, w, D] cos = torch.einsum("cd,hwd->chw", text_feats, patch_feats) # [C, h, w] return cos

两边都已 L2 归一化,点积就是余弦值,范围 [-1, 1]。每个网格点上的 C 维分数就是该位置对所有类别的"投票"。这个函数是全链路的原子单元,全图模式直接用它,滑窗模式也建立在它之上。

滑动窗口:4K 大图怎么处理

Transformer 自注意力开销随 patch 数平方增长。一张 4096 宽的图,patch 数量是 512 宽图的 64 倍,整图一次前向既放不进显存,也保不住细节——大分辨率会被压成粗网格,小目标直接糊掉。滑动窗口(Sliding Window)策略把图切成重叠窗口逐块跑predict_whole,再写回原分辨率取平均:

def predict_slide(img, text_feats, side=384, stride=192): """side: 窗口边长;stride: 步长。返回原图分辨率的 [C, H, W] 相似度""" _, H, W = img.shape C = text_feats.shape[0] probs = torch.zeros(C, H, W, device=img.device) counts = torch.zeros(H, W, device=img.device) h_grids = max(H - side + stride - 1, 0) // stride + 1 w_grids = max(W - side + stride - 1, 0) // stride + 1 for i in range(h_grids): for j in range(w_grids): y1, x1 = i * stride, j * stride y2, x2 = min(y1 + side, H), min(x1 + side, W) y1, x1 = max(y2 - side, 0), max(x2 - side, 0) # 靠近图边时把窗口拉回 cos = predict_whole(img[:, y1:y2, x1:x2], text_feats) # 从 patch 网格上采样到窗口像素分辨率 cos = F.interpolate(cos.unsqueeze(0), size=(y2 - y1, x2 - x1), mode="bilinear", align_corners=False).squeeze(0) probs[:, y1:y2, x1:x2] += cos.softmax(dim=0) # 类间 softmax,得到软概率 counts[y1:y2, x1:x2] += 1 return probs / counts # 被多个窗口覆盖的像素取均值

关键在于最后两行:重叠区域会被多个窗口"投票",平均后相当于软投票,窗口接缝被自然抹平。slide 本质是给 whole 装的"变焦镜头",用更多次前向换空间细节。

whole 与 slide 两种推理策略怎么选

两种模式覆盖绝大多数场景,选择依据是图像分辨率和你对延迟的容忍度:

场景特征推荐策略优势注意事项
短边 ≤ 1024,单图快速预览whole单次前向,延迟最低特征图低分辨率(H/16 × W/16),小目标易丢
4K 街景 / 遥感 / 细节敏感slide(side=384, stride=192)保留局部细节,显存不爆前向次数 ≈ (H/stride)×(W/stride),留意总耗时
大批量同分辨率图像whole + batchGPU 利用率高分辨率越高 batch_size 越要降,受显存约束

whole 模式的链路最短,从相似度到 mask 只需两次变换(下面示例中img为已做 ImageNet 归一化的 [3, H, W] 张量):

cos = predict_whole(img, text_feats) # [C, h, w] 低分辨率相似度 mask = F.interpolate(cos.unsqueeze(0), size=img.shape[1:], mode="bilinear", align_corners=False).squeeze(0).argmax(0) # mask: [H, W],每个值是类别 id

slide 模式只是多传两个参数,输出直接就是原图分辨率,省掉最后的插值:

probs = predict_slide(img, text_feats, side=384, stride=192) # [C, H, W] mask = probs.argmax(0) # 像素级类别图

实战:Cityscapes 19 类零样本评估

下面走通完整任务:类别定义到 mIoU(mean Intersection over Union,平均交并比)报表。Cityscapes 有 19 个前景类别,其官方标签 0 号是有效背景类,所以不做偏移;ADE20K 的 0 号是无意义背景,需要映射到 255 再整体减 1,参考 notebook 里的IGNORE_ZERO_LABEL就是处理这件事的。

CLASS_NAMES = ( "road", "sidewalk", "building", "wall", "fence", "pole", "traffic light", "traffic sign", "vegetation", "terrain", "sky", "person", "rider", "car", "truck", "bus", "train", "motorcycle", "bicycle", ) # 文本特征只算一次,全部图像复用 text_feats = torch.stack([encode_text(c) for c in CLASS_NAMES]) # [19, D/2]

每张图的流水线是:短边缩放到 512 并做 ImageNet 归一化,推理,插值回目标分辨率,argmax,更新指标。数据集按返回 (PIL 图像, mask) 的 torch Dataset 包成 dataloader:

from torchmetrics.classification import MulticlassJaccardIndex miou = MulticlassJaccardIndex(len(CLASS_NAMES), average="macro", ignore_index=255).to("cuda") for img, target in dataloader: # img: [3, H, W] 归一化张量,target: [H, W] 标签 img, target = img.cuda(), target.cuda() probs = predict_slide(img, text_feats, side=384, stride=192) # 插值到标注分辨率再取 argmax probs = F.interpolate(probs.unsqueeze(0), size=target.shape, mode="bilinear", align_corners=False).squeeze(0) miou.update(probs.argmax(0).unsqueeze(0), target.unsqueeze(0)) print(f"mIoU: {100 * miou.compute().item():.2f}")

两个细节决定指标的可比性:输入必须先按短边缩放并用 ImageNet 的均值方差归一化,否则特征与训练时不在同一分布;评测循环里文本特征绝不重复编码,一次编码的开销摊到每张图上基本为零。

调优与进阶技巧

  • 模板数量:参考 notebook 用近 80 条 CLIP 风格模板,越多文本向量越稳;快速调试保留 3~5 条核心的即可。
  • 类别名措辞:写成独立物体名词,"sky" 优于 "the sky in the image";多词名称保留空格("traffic light")。
  • resize 短边:512 快速验证,正式评测上 768 以上,它直接决定特征网格的基础分辨率。
  • side 与 stride:stride 取 side 的一半(50% 重叠)是默认选择;赶时间可提到 0.75 倍 side,注意边界伪影。
  • 显存紧张:先降 side 而不是砍类别数;文本编码只跑一次,压力全在图像前向。
  • 混合精度:推理可整体套torch.autocast("cuda", dtype=torch.bfloat16),新一代 GPU 上提速明显。

项目关键文件导航

  • hub 加载入口:dinov3_vitl16_dinotxt_tet1280d20h24l()一次调用下载骨干与 dinotxt 权重,返回模型和分词器
  • 双塔模型定义:DINOTxt 本体,提供encode_image/encode_text与联合前向
  • 视觉塔:ViT 骨干与两个额外头块的组装,负责提取 cls/patch 特征
  • 文本塔:文本编码器与线性投影,把每条文本池化成 2048 维向量
  • 零样本分割 notebook:完整参考实现,含 Cityscapes/ADE20K 数据集类、全量提示模板与两种推理模式

把一份类别名列表直接变成像素级分割图,全程无需训练。先跑通 whole 模式验证环境,再切到 slide 对自己的数据集出一版 mIoU。

【免费下载链接】dinov3Reference PyTorch implementation and models for DINOv3项目地址: https://gitcode.com/GitHub_Trending/di/dinov3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询