
工业质检线上有一个反复被验证的规律:大目标拼框得准,小目标拼检得出。一个 40×40 的斑块,模型闭着眼睛也能框出来;可一条两三像素宽、混在钢板纹理里的微裂纹(crazing),传统 CNN 骨干偏偏就"看不见"——不是框不准,是根本检不出。
漏检,才是小目标检测的真正矛盾。
这篇我们把两件事讲清楚:DINOv3 接入 YOLO26 之后,小目标的收益到底从哪来、账怎么算;以及这套"冻结视觉基座 + 实时检测器"的组合,还能往哪些场景衍生。

伪代码如下:
# -*- coding: utf-8 -*-
"""
DINOv3 × YOLO26 小目标检测方案 —— 伪代码
==========================================
对应文章:《DINOv3 助力 YOLO26 攻小目标:召回 +6.2pt、参数零增加》
本文伪代码覆盖两条技术路线:
A. 骨干替换路线(NEU-DET 实测):DINOv3 ViT-B/16 替换 YOLO26 原有骨干,
冻结前 8 层,get_intermediate_layers(n=4) 取 4 层中间特征,
经 FPN 重建 P3/P4/P5 三尺度,接 YOLO26 检测头(DFL 解码)。
B. 并行融合路线(yaml 驱动):DINOv3 作为冻结并行分支与原图并行,
轻量投影后注入骨干 P4(Single)或 P3+P4(Dual)。
NEU-DET 实测锚点(640×640,300 epochs):
原始 YOLO26n : mAP50 0.722 / Recall 0.643 / 2,376,006 参数 / 5.2 GFLOPs / 3.0 it/s
+ DINOv3 : mAP50 0.743 / Recall 0.705 / 2,376,006 参数 / 5.3 GFLOPs / 3.1 it/s
crazing 召回 : 0.215 → 0.356(相对 +66%)
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Tuple
import torch
import torch.nn as nn
# ============================================================
# 0. 配置区
# ============================================================
class FusionMode(Enum):
"""融合模式开关(--fusion 的四种取值)"""
NONE = "none" # 原始 YOLO26 基线
P4 = "p4" # 并行融合 · Single P4
P34 = "p34" # 并行融合 · Dual P3+P4
AUTO = "auto" # 原样使用 --model 指定的 yaml
@dataclass
class DINOv3Config:
"""DINOv3 基座配置(骨干替换 / 并行融合共用)"""
arch: str = "vitb16" # vits16 / vits16plus / vitb16 / vitl16 ...
img_size: int = 640 # 输入尺寸(RoPE 支持任意分辨率)
embed_dim: int = 768 # ViT-B/16 输出通道
num_blocks: int = 12 # transformer block 数
freeze_blocks: int = 8 # 冻结前 8 层(骨干替换路线)
n_intermediate: int = 4 # get_intermediate_layers(n=4)
weights: Optional[str] = None # 官方预训练权重(需向 Meta 申请)
lora_r: int = 0 # attn.qkv 上的 LoRA 秩,0 = 关闭
unfreeze_tail: int = 0 # 末尾 N 个 block 保持可训练,0 = 全冻结
@dataclass
class NeckConfig:
"""FPN 多尺度配置(NEU-DET 640 输入下的三尺度)"""
p3_size: Tuple[int, int] = (40, 40) # stride 16
p3_ch: int = 256
p4_size: Tuple[int, int] = (20, 20) # stride 32
p4_ch: int = 512
p5_size: Tuple[int, int] = (10, 10) # stride 64
p5_ch: int = 512
@dataclass
class TrainConfig:
"""训练配置"""
data: str = "data/NEU-DET.yaml"
imgsz: int = 640
epochs: int = 300
batch: int = 16
close_mosaic: int = 10
workers: int = 2
device: str = "0"
lr0: float = 0.01 # 开启 unfreeze / LoRA 时建议降到 1e-3 量级
# NEU-DET 六类缺陷
nc: int = 6
names: List[str] = field(default_factory=lambda: [
"crazing", "inclusion", "patches",
"pitted_surface", "rolled-in_scale", "scratches",
])
# ============================================================
# 1. DINOv3 基座(骨干替换路线)
# ============================================================
class DINOv3Backbone(nn.Module):
"""
DINOv3 ViT-B/16 骨干。
要点:
- 加载官方预训练权重,冻结前 freeze_blocks 层(no_grad,0 梯度);
- get_intermediate_layers(n=4) 提取 4 层中间特征,供 FPN 多尺度融合;
- ViT 原生输出为单尺度(640 输入下 40×40),多尺度由 Neck 负责;
- RoPE 无绝对位置编码,640/1280 任意尺寸直接推理。
"""
def __init__(self, cfg: DINOv3Config):
super().__init__()
self.cfg = cfg
self.blocks = nn.ModuleList([
self._build_transformer_block(i) for i in range(cfg.num_blocks)
])
self.cls_token = nn.Parameter(torch.zeros(1, 1, cfg.embed_dim))
self.register_tokens = nn.Parameter(
torch.zeros(1, 4, cfg.embed_dim) # 4 个 register token
)
self.rope = self._build_rope() # 轴向旋转位置编码
self._load_pretrained(cfg.weights)
self._freeze_first_n_blocks(cfg.freeze_blocks)
# ---------- 冻结策略:冻结层走 no_grad,不建反向图 ----------
def _freeze_first_n_blocks(self, n: int) -> None:
for i, blk in enumerate(self.blocks):
requires_grad = (i >= n)
for p in blk.parameters():
p.requires_grad = requires_grad
if not requires_grad:
blk.eval() # 冻结层永远处于 eval(无 BN/DO 也保持习惯一致)
@torch.no_grad()
def _frozen_forward(self, x: torch.Tensor) -> torch.Tensor:
"""冻结 block 的前向(no_grad,显存几乎不增)"""
for blk in self.blocks[: self.cfg.freeze_blocks]:
x = blk(x)
return x
def forward(self, x: torch.Tensor) -> List[torch.Tensor]:
"""
输入 x: [B, 3, 640, 640]
输出 : 4 层中间 patch token 特征 [B, N, C](供 FPN 使用)
"""
x = self._patch_embed(x) # [B, N=1600, 768]
x = self._prepend_special_tokens(x) # [cls, reg×4, patches]
x = self._frozen_forward(x) # 前 8 层:冻结
feats: List[torch.Tensor] = []
for blk in self.blocks[self.cfg.freeze_blocks:]:
x = blk(x)
feats.append(x) # 收集后 4 层
return self._get_intermediate_layers(feats, n=self.cfg.n_intermediate)
# ============================================================
# 2. Multi-Scale Neck(FPN):单尺度 → P3/P4/P5 三尺度
# ============================================================
class MultiScaleFPN(nn.Module):
"""
将 DINOv3 的 4 层中间特征做 C2/C3/C4 多层融合,
重建 YOLO 检测头需要的三尺度金字塔。
P3: 40×40, 256 通道(stride 16,小目标主战场)
P4: 20×20, 512 通道(stride 32)
P5: 10×10, 512 通道(stride 64)
"""
def __init__(self, cfg: NeckConfig, in_dim: int = 768):
super().__init__()
self.cfg = cfg
# C2/C3/C4 融合的轻量卷积组(示意)
self.lateral_c2 = nn.Conv2d(in_dim, cfg.p3_ch, 1)
self.lateral_c3 = nn.Conv2d(in_dim, cfg.p4_ch, 1)
self.lateral_c4 = nn.Conv2d(in_dim, cfg.p5_ch, 1)
self.fuse_p3 = nn.Sequential(nn.Conv2d(cfg.p3_ch, cfg.p3_ch, 3, padding=1), nn.BatchNorm2d(cfg.p3_ch), nn.SiLU())
self.fuse_p4 = nn.Sequential(nn.Conv2d(cfg.p4_ch, cfg.p4_ch, 3, padding=1), nn.BatchNorm2d(cfg.p4_ch), nn.SiLU())
self.fuse_p5 = nn.Sequential(nn.Conv2d(cfg.p5_ch, cfg.p5_ch, 3, padding=1), nn.BatchNorm2d(cfg.p5_ch), nn.SiLU())
def forward(self, intermediate_feats: List[torch.Tensor]) -> Dict[str, torch.Tensor]:
"""
intermediate_feats: DINOv3 的 4 层中间 token 特征
返回: {"P3": ..., "P4": ..., "P5": ...} 三个尺度的特征图
"""
c2, c3, c4, _ = [self._tokens_to_featmap(f) for f in intermediate_feats]
p5 = self.fuse_p5(self.lateral_c4(c4)) # 10×10, 512
p4 = self.fuse_p4(self.lateral_c3(c3) + self._up(p5))
p3 = self.fuse_p3(self.lateral_c2(c2) + self._up(p4))
return {"P3": p3, "P4": p4, "P5": p5}
# ============================================================
# 3. YOLO26 检测头(DFL 解码)
# ============================================================
class YOLO26DetectHead(nn.Module):
"""
三个尺度分别预测边界框与类别;
采用 DFL(Distribution Focal Loss)解码策略优化框回归精度。
"""
def __init__(self, nc: int, ch: Tuple[int, int, int] = (256, 512, 512), reg_max: int = 16):
super().__init__()
self.nc = nc
self.reg_max = reg_max
self.box_branches = nn.ModuleList([nn.Conv2d(c, 4 * reg_max, 1) for c in ch])
self.cls_branches = nn.ModuleList([nn.Conv2d(c, nc, 1) for c in ch])
def forward(
self, pyramid: Dict[str, torch.Tensor], training: bool = True
):
"""
训练时输出字典 dict(boxes, scores, feats);
推理时输出已解码的边界框张量 [B, anchors, 4 + nc]。
"""
feats = [pyramid["P3"], pyramid["P4"], pyramid["P5"]]
boxes, scores, raw_feats = [], [], []
for i, f in enumerate(feats):
box_dist = self.box_branches[i](f) # DFL 分布
score = self.cls_branches[i](f).sigmoid()
raw_feats.append(f)
if training:
boxes.append(box_dist)
scores.append(score)
else:
boxes.append(self._dfl_decode(box_dist)) # 分布期望解码
scores.append(score.flatten(2).permute(0, 2, 1))
if training:
return {"boxes": boxes, "scores": scores, "feats": raw_feats}
return torch.cat([torch.cat([b, s], dim=-1) for b, s in zip(boxes, scores)], dim=1)
def _dfl_decode(self, box_dist: torch.Tensor) -> torch.Tensor:
"""DFL:对 reg_max 个 bin 的分布取期望,得到 4 条边距离"""
B, C, H, W = box_dist.shape
prob = box_dist.view(B, 4, self.reg_max, H, W).softmax(dim=2)
bins = torch.arange(self.reg_max, device=box_dist.device, dtype=prob.dtype)
return (prob * bins).sum(dim=2) # [B, 4, H, W]
# ============================================================
# 4. 路线 A:骨干替换版模型(NEU-DET 实测路线)
# ============================================================
class YOLO26_DINOv3_BackboneReplaced(nn.Module):
"""
DINOv3 ViT-B/16 整体替换 YOLO26 原有骨干 + FPN 重建三尺度 + YOLO26 检测头。
收益锚点(NEU-DET):
mAP50 0.722 → 0.743(+2.1pt)
Recall 0.643 → 0.705(+6.2pt)
参数量 零增加 / GFLOPs +0.1 / 推理速度 3.0 → 3.1 it/s
"""
def __init__(self, dcfg: DINOv3Config, ncfg: NeckConfig, train_cfg: TrainConfig):
super().__init__()
self.backbone = DINOv3Backbone(dcfg) # 冻结前 8 层
self.neck = MultiScaleFPN(ncfg, in_dim=dcfg.embed_dim)
self.head = YOLO26DetectHead(nc=train_cfg.nc)
def forward(self, img: torch.Tensor, training: bool = True):
intermediate = self.backbone(img) # 4 层中间特征
pyramid = self.neck(intermediate) # P3/P4/P5
return self.head(pyramid, training=training)
# ============================================================
# 5. 路线 B:并行融合模块(yaml 驱动,Single P4 / Dual P3+P4)
# ============================================================
class LightProjection(nn.Module):
"""轻量投影:1×1 卷积降通道 + 可选上采样 + 3×3 深度可分离卷积"""
def __init__(self, in_ch: int, out_ch: int, upsample: bool = False):
super().__init__()
self.conv1x1 = nn.Conv2d(in_ch, out_ch, 1)
self.up = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False) if upsample else nn.Identity()
self.dwconv = nn.Conv2d(out_ch, out_ch, 3, padding=1, groups=out_ch)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.dwconv(self.up(self.conv1x1(x)))
class DINOv3(nn.Module):
"""
并行融合分支:DINOv3 完全冻结,与原图并行前向。
按 levels 取指定 transformer block 的 patch token
(丢掉 cls / register token),reshape 成 stride 16 特征图。
yaml 一行挂载(对应文章第 02 节):
- [0, 1, DINOv3, [256, "vits16", ["P3", "P4"], None, 12, 1.0]]
# c2 arch levels w depth scale
"""
def __init__(self, c2: int, arch: str = "vits16", levels: List[str] = ["P3", "P4"],
weights: Optional[str] = None, depth: int = 12, scale: float = 1.0):
super().__init__()
self.levels = levels
self.depth = depth # 只保留前 N 个 block(降算力开关)
self.scale = scale # ViT 输入降采样倍率(0.5 = 半分辨率)
self.vit = self._load_frozen_vit(arch, weights, depth)
for p in self.vit.parameters():
p.requires_grad = False # 全冻结:no_grad 前向,显存几乎不变
# 每个 level 一个轻量投影头(P3 需上采样 ×2 对齐 stride 8)
self.projs = nn.ModuleDict({
lv: LightProjection(self.vit.embed_dim, c2, upsample=(lv == "P3"))
for lv in levels
})
@torch.no_grad()
def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:
tokens = self.vit.forward_tokens(x, n=self.depth) # 冻结前向
# P4 取末层 block,P3 取中间 block(Simple Feature Pyramid 思路,代价为零)
feats = self.vit.get_intermediate_layers(tokens, blocks=self._block_map())
return {lv: self.projs[lv](feats[lv]) for lv in self.levels}
def _block_map(self) -> Dict[str, int]:
"""depth=12 时:P3 取 block 5(细节),P4 取 block 11(语义)"""
return {"P3": self.depth // 2, "P4": self.depth - 1}
class YOLO26_DINOv3_Parallel(nn.Module):
"""
并行融合版:保留 YOLO26 原生骨干,DINOv3 作为冻结并行分支,
在骨干 P4(Single)或 P3+P4(Dual)处 Concat 融合,1×1 卷积降回原通道。
可训练参数仅 +0.05M(Single)/ +0.09M(Dual);
neck / head 完全不动;检测 / 分割 / 分类共用同一套接法。
"""
def __init__(self, base_yolo26: nn.Module, fusion: FusionMode, c2: int = 256):
super().__init__()
assert fusion in (FusionMode.P4, FusionMode.P34)
self.base = base_yolo26 # 原生 YOLO26(CNN 骨干)
self.fusion = fusion
levels = ["P4"] if fusion == FusionMode.P4 else ["P3", "P4"]
self.dinov3 = DINOv3(c2=c2, levels=levels) # 冻结并行分支
# 融合层:Concat 后 1×1 卷积降回原通道
self.fuse_p4 = nn.Conv2d(c2 + 512, 512, 1)
self.fuse_p3 = nn.Conv2d(c2 + 256, 256, 1) if fusion == FusionMode.P34 else None
def forward(self, img: torch.Tensor, training: bool = True):
dino_feats = self.dinov3(img) # {"P3": ..., "P4": ...}
cnn_p4 = self.base.backbone_p4(img) # 骨干 P4(stride 16)
p4 = self.fuse_p4(torch.cat([cnn_p4, dino_feats["P4"]], dim=1))
if self.fusion == FusionMode.P34:
cnn_p3 = self.base.backbone_p3(img) # 骨干 P3(stride 8)
p3 = self.fuse_p3(torch.cat([cnn_p3, dino_feats["P3"]], dim=1))
else:
p3 = self.base.backbone_p3(img)
return self.base.neck_and_head(p3, p4, training=training)
# ============================================================
# 6. 训练:--fusion 一个开关跑三组对照实验
# ============================================================
def build_model(fusion: FusionMode, tcfg: TrainConfig) -> nn.Module:
"""按融合模式构建模型(--fusion none / p4 / p34 / auto)"""
if fusion == FusionMode.NONE:
return YOLO26_Base(nc=tcfg.nc) # 原始 YOLO26 基线
if fusion == FusionMode.AUTO:
return YOLO26_Base(nc=tcfg.nc) # 原样使用 --model 的 yaml
if fusion in (FusionMode.P4, FusionMode.P34):
return YOLO26_DINOv3_Parallel(YOLO26_Base(nc=tcfg.nc), fusion)
raise ValueError(f"unknown fusion mode: {fusion}")
def train(fusion: FusionMode, tcfg: TrainConfig) -> Dict[str, float]:
"""
训练入口。断点续训 / 多卡 DDP 略(多卡时 DINOv3 仓库路径
需用环境变量 DINOV3_REPO 传给 spawn 子进程)。
"""
model = build_model(fusion, tcfg)
# 学习率策略:开启 unfreeze / LoRA 时 lr0 降到 1e-3 量级;
# 全冻结时默认 lr0 即可(DINOv3 侧不产生梯度)
optimizer = torch.optim.AdamW(
filter(lambda p: p.requires_grad, model.parameters()), lr=tcfg.lr0
)
for epoch in range(tcfg.epochs):
for img, target in dataloader(tcfg.data, tcfg.imgsz, tcfg.batch):
out = model(img, training=True)
loss = detection_loss(out, target) # DFL + cls loss
optimizer.zero_grad(); loss.backward(); optimizer.step()
if epoch >= tcfg.epochs - tcfg.close_mosaic:
close_mosaic() # 最后 10 轮关闭 mosaic
return validate(model) # 返回 mAP50 / Recall 等指标
def run_ablation(tcfg: TrainConfig) -> Dict[str, Dict[str, float]]:
"""
三组对照实验(文章第 03 节的出数命令):
python scripts/dinov3/train.py --model yolo26n --fusion none --data data/NEU-DET.yaml --name base
python scripts/dinov3/train.py --model yolo26n --fusion p4 --data data/NEU-DET.yaml --name p4
python scripts/dinov3/train.py --model yolo26n --fusion p34 --data data/NEU-DET.yaml --name p34
"""
results = {}
for mode in (FusionMode.NONE, FusionMode.P4, FusionMode.P34):
results[mode.value] = train(mode, tcfg)
return results
# 期望锚点(NEU-DET):
# base : mAP50 0.722 / Recall 0.643
# p4 : 小步验证 DINOv3 特征有效性
# p34 : mAP50 0.743 / Recall 0.705(crazing 召回 0.215 → 0.356)
# ============================================================
# 7. 可视化三件套:验证"融合是否真的起作用"
# ============================================================
def visualize(weights: str, source: str, mode: str, out: Optional[str] = None) -> str:
"""
--mode det : 带框结果图,与基线做直观对比
--mode feat : 原图 + DINOv3 P3/P4 响应热力图 + 融合后骨干特征热力图
(典型现象:缺陷区在 CNN 特征里几乎无响应,DINOv3 上有明显激活)
--mode attn : CLS token 对 patch 的注意力热力图(ViT 已"看到"前景/缺陷)
"""
model = load_weights(weights)
img = load_image(source)
if mode == "det":
return draw_boxes(img, model.predict(img))
if mode == "feat":
heatmaps = model.dinov3_feature_maps(img) # 需融合权重,原始 YOLO 会报"找不到 DINOv3 层"
return concat_horizontally([img, *heatmaps, fused_feature_map(model, img)])
if mode == "attn":
return draw_heatmap(img, model.dinov3_cls_attention(img))
raise ValueError(mode)
# ============================================================
# 8. 部署导出:ONNX + 边缘 checklist
# ============================================================
def export(weights: str, imgsz: int = 640, opset: int = 17) -> str:
"""
边缘部署 checklist(文章第 07 节):
1. 固定 imgsz,关闭 dynamic(多数 NPU 不支持动态 shape);
2. ONNX opset ≥ 17(RoPE 的 sin/cos、LayerNorm、SDPA 需要较新算子);
3. TensorRT FP16 建议 TRT ≥ 10.6,上线前用真实图对数值;
4. 导出后跑一遍 predict 确认端到端一致;
5. 延迟不达标:yaml 改 depth: 8 + scale: 0.5,ViT 侧算力约降到 1/3。
"""
model = load_weights(weights)
return export_onnx(model, imgsz=imgsz, opset=opset, dynamic=False)
def benchmark(models: List[str], imgsz: int = 640) -> str:
"""参数量 / GFLOPs / 延迟对比,一次出一张 Markdown 表"""
rows = []
for name in models:
m = load_model(name)
rows.append({
"model": name,
"params_M": count_params(m) / 1e6,
"trainable_M": count_trainable(m) / 1e6,
"GFLOPs": calc_flops(m, imgsz),
"latency_ms": measure_latency(m, imgsz),
})
return to_markdown_table(rows)
# ============================================================
# 9. 主流程:NEU-DET 复现实验
# ============================================================
if __name__ == "__main__":
tcfg = TrainConfig() # NEU-DET, 640, 300 epochs, batch 16
# Step 1: 三组对照实验(none / p4 / p34)
results = run_ablation(tcfg)
# Step 2: 打印收益结构(文章第 03 节的核心解读)
# 召回 +6.2pt,约为 mAP50 涨幅(+2.1pt)的三倍
# → 收益来自"原本检不出的现在检出来了"
print(results)
# Step 3: 可视化验证(det / feat / attn 三件套)
visualize("runs/dinov3/train/weights/best.pt", "neu_det_sample.jpg", mode="feat")
# Step 4: 导出部署
export("runs/dinov3/train/weights/best.pt", imgsz=640, opset=17)
# Step 5: 场景衍生:更换 data 与检测头即可切入新领域
# 光伏 EL / PCB / 医学微小病灶 / 遥感小目标 / 农业视觉
for scenario in ("pv_el", "pcb_defect", "pathology", "remote_sensing", "agri_vision"):
cfg = TrainConfig(data=f"data/{scenario}.yaml")
train(FusionMode.P34, cfg)
YOLO 系列的骨干靠卷积堆叠,归纳偏置是"局部性 + 平移等变"——这在日常大目标上效率极高,但遇到两类样本就容易失效:
更棘手的是,这类任务的标注数据往往只有几百到几千张,从头训练一个大模型并不现实。常规解法是数据增强加码,但对"对比度接近背景"的目标,增强只是杯水车薪。
换一个思路:借一个已经在亿级无标注图片上学过"什么是物体、什么是边界"的视觉基座,把它冻结后接进检测器。基座负责"看得懂",检测器负责"检得快"——这就是 DINOv3 × YOLO26 组合的分工。

DINOv3 是 Meta 于 2025 年发布的自监督视觉基础模型,DINO 系列(Distillation with No Labels)的第三代。仅依靠无标签图像,通过教师-学生蒸馏框架获得通用视觉表征。几个关键数字:
真正对小目标检测有价值的,是它的四个技术特性:
特性 | 含义 | 对小目标的价值 |
|---|---|---|
Gram Anchoring | 抑制长训练下的密集特征退化,patch 级特征持续提升 | 深层特征仍保留空间结构,上采样后依然有定位能力 |
RoPE 轴向旋转位置编码 | 支持 256×256 到 4096×4096 可变分辨率 | 任意输入尺寸直接跑,小目标可以靠提高 imgsz 硬解 |
Register Tokens | [CLS] 前注入 4 个寄存器令牌,正则化 patch 范数 | 全局-局部特征交换更顺畅,背景中的弱显著目标不被"平均掉" |
密集特征质量高 | 每个 patch 位置都有语义清晰的向量 | 弱纹理、低对比目标也有响应——这正是微裂纹场景最需要的 |
一个直观的类比:CNN 像用放大镜一寸寸扫画面,ViT 则一开始就看着整张图做全局关系建模。目标越小、越融于背景,全局建模的优势越大。
实验在 NEU-DET 钢材表面缺陷数据集上进行(六类缺陷:crazing 裂纹、inclusion 夹杂、patches 斑块、pitted_surface 点蚀、rolled-in_scale 氧化铁皮压入、scratches 划痕),统一 640×640 输入、300 epochs。方案为 DINOv3 ViT-B/16 替换 YOLO26 原有骨干:加载官方预训练权重并冻结前 8 层,通过 get_intermediate_layers(n=4) 提取 4 层中间特征,经 FPN 多尺度融合生成 P3/P4/P5 三尺度,再接 YOLO26 检测头(DFL 解码)。
指标 | 原始 YOLO26n | YOLO26n + DINOv3 | 变化 |
|---|---|---|---|
mAP50 | 0.722 | 0.743 | +2.1pt |
mAP50-95 | 0.407 | 0.413 | +0.6pt |
Recall | 0.643 | 0.705 | +6.2pt |
参数量 | 2,376,006 | 2,376,006 | 零增加 |
GFLOPs | 5.2 | 5.3 | +0.1 |
推理速度 | 3.0 it/s | 3.1 it/s | 不降反升 |
三个值得注意的结构性信号:
第一,召回是全场涨得最快的指标。 Recall +6.2pt,接近 mAP50 涨幅(+2.1pt)的三倍——收益主要来自"原本检不出的现在检出来了",而不是"原本能检的框得更准了"。这正是小目标场景的核心诉求:在工业质检线上,漏检意味着不合格品流入下游,召回回收的价值直接兑现为质量成本。
第二,收益高度集中在纹理型、低对比缺陷上。
缺陷类型 | 目标特点 | mAP50 变化 | Recall 变化 |
|---|---|---|---|
scratches 划痕 | 细长、低对比 | 0.850 → 0.896(+4.6pt) | 0.818 → 0.861 |
rolled-in_scale 氧化铁皮 | 暗弱、与背景接近 | 0.615 → 0.641(+2.6pt) | 0.538 → 0.658(相对 +22%) |
crazing 微裂纹 | 微米级纹理型 | 0.367 → 0.392(+2.5pt) | 0.215 → 0.356(相对 +66%) |
pitted_surface 点蚀 | 小斑点 | 0.788 → 0.814(+2.6pt) | 0.716 → 0.738 |
patches 斑块 | 大面积、高对比 | 0.918 → 0.927(+0.9pt) | 0.848 → 0.860 |
最典型的是 crazing:召回率从 0.215 拉到 0.356,相对提升约 66%——这类微裂纹此前近八成被漏检,是产线上最容易被投诉的漏检源。而作为对照,大面积高对比的 patches 只涨了 0.9pt。收益分布精确地落在"CNN 盲区"上:越是小、越是融进背景的目标,基座特征的价值越大;大而清晰的目标,原本的骨干就够用。这给使用者一个清晰的预判方法——先看自己的难例是不是"纹理型 + 低对比 + 小尺寸",对症再下药。
第三,代价几乎为零。 冻结的 ViT-B/16 走 no_grad 前向、0 梯度,模型账面参数量零增加;GFLOPs 仅 +0.1;推理速度 3.1 it/s,甚至略快于原版的 3.0 it/s。语义增强没有牺牲实时性,产线级部署成立。对中小团队而言,这意味着不需要重算力就能复现,迭代周期从"周"压缩到"天"。
把 NEU-DET 的账本抽象一层,这套组合解决的是三类问题:
1. "检不出"——纹理型低对比小目标的漏检。 这是最直接的收益。DINOv3 的密集特征让弱显著目标在特征空间里"浮出水面",召回曲线的抬升就是证据。难例类型:微裂纹、细划痕、暗弱点蚀、与基材近色的瑕疵。
2. "训不动"——标注稀缺下的冷启动。 每类几十张标注即可起步:基座的通用表征已经在 17 亿张无标签图上学完,微调只需要让检测头适应具体缺陷形态。小样本冷启动从"玄学"变成"工程问题"。
3. "泛化差"——跨场景掉点。 自监督表征对域偏移更鲁棒:换产线、换光照、换材质时,冻结基座提供的特征底座不变,掉点明显小于纯监督训练的骨干。换检测头即可切入新领域,迁移成本极低。

DINOv3 进 YOLO26 不只有"替换骨干"一条路。目前验证过的有两条路线,适用面不同:
维度 | 骨干替换(NEU-DET 实测路线) | 并行融合(yaml 驱动路线) |
|---|---|---|
接法 | DINOv3 ViT-B/16 整体替换 CNN 骨干,FPN 重建 P3/P4/P5 | DINOv3 作为冻结并行分支与原图并行,轻量投影后注入骨干 P4(Single)或 P3+P4(Dual) |
侵入性 | 骨干整体更换 | 零侵入,yaml 几行挂载,不写额外代码 |
训练显存 | 冻结层走 no_grad,压力可控 | 整支 ViT 冻结前向,显存几乎不变 |
新增可训练参数 | 账面零增加 | 仅 +0.05M(Single)/ +0.09M(Dual) |
任务覆盖 | 检测 | 检测 / 分割 / 分类共用同一套接法 |
上限 | 更高(DEIMv2 同款思路,COCO 实时检测 SOTA) | 略低,但多任务、快速验证更灵活 |
适用 | 检测任务为主、追求性能天花板 | 多任务并行、要在原生链路上快速对照实验 |
选择建议很直接:只做检测、追性能上限,走骨干替换;检测分割分类都要、或者想在原生 ultralytics 链路里零成本做 A/B 对照,走并行融合。 后者甚至可以用 --fusion none / p4 / p34 一个开关在同一脚本里跑基线对照,先验证 DINOv3 特征对自己的数据集有效,再决定投入方向。
以 NEU-DET 的验证结果为锚点,按适配优先级往外衍生:
场景 | 适配理由 | 优先级 |
|---|---|---|
工业表面质检 | 纹理缺陷检测已验证:裂纹、划痕、点蚀、夹杂全覆盖 | ★★★★★ |
小样本冷启动 | 每类几十张标注即可起步,预训练权重兜底 | ★★★★★ |
边缘端实时检测 | 参数/算力零负担,适合产线嵌入式部署 | ★★★★☆ |
细小/弱显著目标 | 遥感小目标、医学微小病灶、半导体晶圆瑕疵 | ★★★★☆ |
跨域快速迁移 | 更换检测头即可切入农业、安防、交通 | ★★★★☆ |
几个值得优先试水的衍生方向:

DINOv3 负责"看得懂",YOLO26 负责"检得快"——这一组合将在"标注稀缺 + 目标细小 + 要求实时"的工业与垂直场景中,成为高性价比的标配方案。
在这个判断之上,还有几条明确的延伸路径:
lora_r 与 unfreeze 的性价比,找出最小代价的最优配置;小目标检测的竞赛,正在从"把骨干挖得更深"转向"站在更强表征的肩膀上"。DINOv3 把亿级无标注数据的知识冻进权重,YOLO26 把它以零参数代价接进实时检测——这条路的性价比,NEU-DET 上那 +2.1 个 mAP50 和 +6.2 个 Recall,只是开始。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。