摘要:YOLOv6 由美团视觉智能部研发,专注于工业级部署场景。本文从网络结构到训练策略,逐一拆解其五大核心创新:EfficientRep Backbone(重参数化卷积)、Rep-PAN Neck、解耦头、Anchor-Free 设计、TAL 标签分配。每个模块均包含设计动机、数学原理、结构图示和代码级解读,并深入剖析正负样本分配机制与无锚框推理的完整流程。
关键词:YOLOv6、重参数化、RepVGG、解耦头、Anchor-Free、TAL、任务对齐
文章目录
-
- [一、EfficientRep Backbone:重参数化卷积](#一、EfficientRep Backbone:重参数化卷积)
-
- [1.1 设计动机:训练精度 vs 推理速度的矛盾](#1.1 设计动机:训练精度 vs 推理速度的矛盾)
- [1.2 重参数化的数学原理](#1.2 重参数化的数学原理)
-
- 训练时:多分支结构
- [推理时:融合为单个 3×3 卷积](#推理时:融合为单个 3×3 卷积)
- [1.3 结构对比图](#1.3 结构对比图)
- [1.4 为什么推理更快?](#1.4 为什么推理更快?)
- [1.5 EfficientRep 的整体结构](#1.5 EfficientRep 的整体结构)
- [1.6 代码实现](#1.6 代码实现)
- [二、Rep-PAN Neck:重参数化路径聚合网络](#二、Rep-PAN Neck:重参数化路径聚合网络)
-
- [2.1 设计动机](#2.1 设计动机)
- [2.2 Rep-PAN 的改进](#2.2 Rep-PAN 的改进)
- [2.3 关键区别](#2.3 关键区别)
- [2.4 为什么 Add 比 Concat 更适合部署?](#2.4 为什么 Add 比 Concat 更适合部署?)
- [三、解耦头(Decoupled Head)](#三、解耦头(Decoupled Head))
-
- [3.1 耦合头的问题(YOLOv5 及之前)](#3.1 耦合头的问题(YOLOv5 及之前))
- [3.2 解耦头的设计](#3.2 解耦头的设计)
- [3.3 代码实现](#3.3 代码实现)
- [3.4 为什么解耦能提升收敛速度?](#3.4 为什么解耦能提升收敛速度?)
- [3.5 解耦头 + Anchor-Free 的配合](#3.5 解耦头 + Anchor-Free 的配合)
- 四、无锚框(Anchor-Free)设计
-
- [4.1 Anchor-Based 的痛点回顾](#4.1 Anchor-Based 的痛点回顾)
- [4.2 YOLOv6 的 Anchor-Free 方案](#4.2 YOLOv6 的 Anchor-Free 方案)
- [4.3 "锚框"与"锚点"的本质区别](#4.3 "锚框"与"锚点"的本质区别)
- [4.4 回归目标:DFL(Distribution Focal Loss)](#4.4 回归目标:DFL(Distribution Focal Loss))
- [4.5 每个锚点都预测一组 (l, t, r, b)](#4.5 每个锚点都预测一组 (l, t, r, b))
- [4.6 负样本的 (l, t, r, b) 怎么办?](#4.6 负样本的 (l, t, r, b) 怎么办?)
-
- [训练时:直接忽略(mask 掉)](#训练时:直接忽略(mask 掉))
- 推理时:靠分类分数过滤
- [4.7 为什么负样本也要"白预测"?](#4.7 为什么负样本也要"白预测"?)
- [4.8 Anchor-Free 的优势总结](#4.8 Anchor-Free 的优势总结)
- [五、TAL 标签分配(Task-Aligned Assigner)](#五、TAL 标签分配(Task-Aligned Assigner))
-
- [5.1 传统标签分配的问题](#5.1 传统标签分配的问题)
- [5.2 TAL 的核心思想](#5.2 TAL 的核心思想)
- [5.3 TAL 完整流程(用数字走一遍)](#5.3 TAL 完整流程(用数字走一遍))
-
- [Step 1:初筛------哪些锚点在 GT 框内?](#Step 1:初筛——哪些锚点在 GT 框内?)
- [Step 2:计算对齐分数(核心!)](#Step 2:计算对齐分数(核心!))
- [Step 3:选 Top-K(默认 K=13)](#Step 3:选 Top-K(默认 K=13))
- [Step 4:冲突解决](#Step 4:冲突解决)
- [Step 5:生成训练 Target](#Step 5:生成训练 Target)
- [5.4 完整分配流程图](#5.4 完整分配流程图)
- [5.5 代码实现](#5.5 代码实现)
- [5.6 TAL vs YOLOv5 分配策略对比](#5.6 TAL vs YOLOv5 分配策略对比)
- [5.7 TAL 的"动态"体现在哪?](#5.7 TAL 的"动态"体现在哪?)
- 六、推理时如何避免锚框:完整解码流程
-
- [6.1 核心思想:直接预测绝对距离](#6.1 核心思想:直接预测绝对距离)
- [6.2 图解对比](#6.2 图解对比)
- [6.3 网络实际输出什么?](#6.3 网络实际输出什么?)
- [6.4 完整推理解码代码](#6.4 完整推理解码代码)
- [6.5 一个完整的推理例子](#6.5 一个完整的推理例子)
- [6.6 为什么不需要锚框也能工作?](#6.6 为什么不需要锚框也能工作?)
- [七、训练 vs 推理的完整对比](#七、训练 vs 推理的完整对比)
-
- [7.1 训练流程](#7.1 训练流程)
- [7.2 推理流程](#7.2 推理流程)
- 八、五大创新的协同关系
- 九、性能对比
- 十、总结
- 参考资料
一、EfficientRep Backbone:重参数化卷积
1.1 设计动机:训练精度 vs 推理速度的矛盾
传统卷积网络面临一个两难:
| 方案 | 训练效果 | 推理速度 |
|---|---|---|
| 多分支结构(Inception、ResNet残差) | ✅ 梯度路径多,特征丰富 | ❌ 分支多,内存访问开销大 |
| 单路 3×3 卷积(VGG) | ❌ 表达能力有限 | ✅ 计算密集,GPU友好 |
核心问题:能不能训练时用多分支获得高精度,推理时变成单卷积获得高速度?
答案 :重参数化(Reparameterization)------训练和推理用不同的等价结构。
1.2 重参数化的数学原理
训练时:多分支结构
YOLOv6 的 RepVGG Block 在训练时包含 3 条并行分支:
输入 x
├── 分支1: 3×3 Conv + BN
├── 分支2: 1×1 Conv + BN
└── 分支3: Identity + BN(恒等映射)
│
└──→ 相加 → ReLU → 输出
数学表达:
y = ReLU ( BN 3 × 3 ( W 1 ∗ x ) + BN 1 × 1 ( W 2 ∗ x ) + BN i d ( x ) ) y = \text{ReLU}\left(\text{BN}{3\times3}(W_1 * x) + \text{BN}{1\times1}(W_2 * x) + \text{BN}_{id}(x)\right) y=ReLU(BN3×3(W1∗x)+BN1×1(W2∗x)+BNid(x))
其中 ∗ * ∗ 表示卷积操作。
推理时:融合为单个 3×3 卷积
BN 层可以数学等价地"吸收"进卷积:
BN ( W ∗ x ) = γ σ 2 + ϵ ⋅ W ∗ x + ( β − γ μ σ 2 + ϵ ) \text{BN}(W * x) = \frac{\gamma}{\sqrt{\sigma^2 + \epsilon}} \cdot W * x + \left(\beta - \frac{\gamma \mu}{\sqrt{\sigma^2 + \epsilon}}\right) BN(W∗x)=σ2+ϵ γ⋅W∗x+(β−σ2+ϵ γμ)
令:
W ^ = γ σ 2 + ϵ ⋅ W , b ^ = β − γ μ σ 2 + ϵ \hat{W} = \frac{\gamma}{\sqrt{\sigma^2 + \epsilon}} \cdot W, \quad \hat{b} = \beta - \frac{\gamma \mu}{\sqrt{\sigma^2 + \epsilon}} W^=σ2+ϵ γ⋅W,b^=β−σ2+ϵ γμ
则 BN 被吸收后: BN ( W ∗ x ) = W ^ ∗ x + b ^ \text{BN}(W * x) = \hat{W} * x + \hat{b} BN(W∗x)=W^∗x+b^
三条分支融合:
分支1 (3×3): Ŵ₁ (3×3) + b̂₁
分支2 (1×1): Ŵ₂ (1×1) → zero-pad到3×3 → Ŵ₂' (3×3) + b̂₂
分支3 (id): Ŵ₃ (identity kernel) + b̂₃
其中 identity kernel = [[0,0,0],[0,1,0],[0,0,0]]
融合结果:
W_fused = Ŵ₁ + Ŵ₂' + Ŵ₃ (3×3)
b_fused = b̂₁ + b̂₂ + b̂₃ (标量)
推理时只需一次 3×3 卷积:
y = ReLU ( W f u s e d ∗ x + b f u s e d ) y = \text{ReLU}(W_{fused} * x + b_{fused}) y=ReLU(Wfused∗x+bfused)
1.3 结构对比图
【训练时】 【推理时】
┌──3×3 Conv+BN──┐ ┌──────────────┐
x ──┼──1×1 Conv+BN──┼── Add ──ReLU x ──│ 3×3 Conv │── ReLU
└──Identity+BN──┘ └──────────────┘
3次卷积 + 3次BN + 1次Add 1次卷积(无BN、无Add)
内存访问: 多次读写 内存访问: 1次读 + 1次写
1.4 为什么推理更快?
| 指标 | 训练时(多分支) | 推理时(融合后) |
|---|---|---|
| 卷积次数 | 3 次 | 1 次 |
| BN 次数 | 3 次 | 0 次 |
| 内存访问 | 多次中间结果读写 | 仅输入→输出 |
| GPU 利用率 | 分支并行度低,kernel launch 多 | 单个大 kernel,计算密集 |
| 实测加速 | --- | 约 30%~70% |
📌 关键洞察:GPU 擅长"大矩阵乘法"(计算密集),不擅长"多次小操作"(内存密集)。融合后把 3 个小操作变成 1 个大操作,完美匹配 GPU 架构。
1.5 EfficientRep 的整体结构
Input (640×640×3)
│
├── Stem: RepVGG Block (stride=2) → 320×320×32
│
├── Stage1: RepVGG Block ×1 (stride=2) → 160×160×64
│
├── Stage2: RepVGG Block ×2 (stride=2) → 80×80×128 ← P3 输出
│
├── Stage3: RepVGG Block ×3 (stride=2) → 40×40×256 ← P4 输出
│
└── Stage4: RepVGG Block ×1 (stride=2) → 20×20×512 ← P5 输出
与 YOLOv5 的 CSPDarknet 对比:
| YOLOv5 (CSPDarknet) | YOLOv6 (EfficientRep) | |
|---|---|---|
| 基本单元 | Bottleneck(残差) | RepVGG Block(重参数化) |
| 训练结构 | 残差连接 | 3 分支并行 |
| 推理结构 | 仍有残差加法 | 纯 3×3 卷积链 |
| 部署友好度 | 一般(有 split/concat) | 极高(全是 Conv+ReLU) |
1.6 代码实现
python
import torch
import torch.nn as nn
import numpy as np
class RepVGGBlock(nn.Module):
"""YOLOv6 重参数化卷积块"""
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.stride = stride
self.in_channels = in_channels
self.out_channels = out_channels
# ===== 训练时的 3 条分支 =====
# 分支1: 3×3 卷积 + BN
self.conv3x3 = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False),
nn.BatchNorm2d(out_channels)
)
# 分支2: 1×1 卷积 + BN
self.conv1x1 = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride, 0, bias=False),
nn.BatchNorm2d(out_channels)
)
# 分支3: 恒等映射 + BN(仅当 in==out 且 stride==1)
self.identity = nn.BatchNorm2d(out_channels) \
if in_channels == out_channels and stride == 1 else None
self.relu = nn.ReLU(inplace=True)
# 推理时的融合卷积(初始为 None)
self.rbr_reparam = None
def forward(self, x):
# ===== 推理模式:直接用融合后的单卷积 =====
if self.rbr_reparam is not None:
return self.relu(self.rbr_reparam(x))
# ===== 训练模式:3 分支相加 =====
out = self.conv3x3(x) + self.conv1x1(x)
if self.identity is not None:
out += self.identity(x)
return self.relu(out)
def _fuse_bn(self, conv, bn):
"""将 Conv + BN 融合为单个 Conv(带 bias)"""
kernel = conv.weight # (out, in, k, k)
gamma = bn.weight # (out,)
beta = bn.bias # (out,)
mu = bn.running_mean # (out,)
var = bn.running_var # (out,)
eps = bn.eps
# BN 融合公式
std = torch.sqrt(var + eps)
t = (gamma / std).reshape(-1, 1, 1, 1)
fused_kernel = kernel * t # 缩放权重
fused_bias = beta - mu * gamma / std # 融合偏置
return fused_kernel, fused_bias
def _pad_1x1_to_3x3(self, kernel):
"""将 1×1 卷积核 zero-pad 到 3×3"""
return nn.functional.pad(kernel, [1, 1, 1, 1])
def _identity_to_3x3(self):
"""生成恒等映射的 3×3 卷积核"""
kernel = torch.zeros(self.out_channels, self.in_channels, 3, 3)
for i in range(self.out_channels):
kernel[i, i, 1, 1] = 1.0
return kernel
def switch_to_deploy(self):
"""训练→推理:融合 3 条分支为 1 个 3×3 卷积"""
if self.rbr_reparam is not None:
return
# 融合分支1: 3×3
k3, b3 = self._fuse_bn(self.conv3x3[0], self.conv3x3[1])
# 融合分支2: 1×1 → pad到3×3
k1, b1 = self._fuse_bn(self.conv1x1[0], self.conv1x1[1])
k1 = self._pad_1x1_to_3x3(k1)
# 融合分支3: identity → 3×3
if self.identity is not None:
kid = self._identity_to_3x3().to(k3.device)
bid = torch.zeros(self.out_channels).to(k3.device)
kid, bid = self._fuse_bn_identity(kid, bid, self.identity)
else:
kid = torch.zeros_like(k3)
bid = torch.zeros_like(b3)
# 三分支相加
fused_kernel = k3 + k1 + kid
fused_bias = b3 + b1 + bid
# 创建融合后的单个卷积
self.rbr_reparam = nn.Conv2d(
self.in_channels, self.out_channels, 3,
self.stride, 1, bias=True
)
self.rbr_reparam.weight.data = fused_kernel
self.rbr_reparam.bias.data = fused_bias
# 删除训练分支,释放内存
del self.conv3x3, self.conv1x1, self.identity
def _fuse_bn_identity(self, kernel, bias, bn):
"""融合恒等分支的 BN"""
gamma = bn.weight
beta = bn.bias
mu = bn.running_mean
var = bn.running_var
std = torch.sqrt(var + bn.eps)
t = (gamma / std).reshape(-1, 1, 1, 1)
return kernel * t, beta - mu * gamma / std
# ===== 验证等价性 =====
if __name__ == "__main__":
block = RepVGGBlock(64, 64)
block.eval()
x = torch.randn(1, 64, 80, 80)
# 训练模式输出
with torch.no_grad():
y_train = block(x)
# 切换到推理模式
block.switch_to_deploy()
with torch.no_grad():
y_deploy = block(x)
print(f"最大误差: {(y_train - y_deploy).abs().max().item():.2e}")
# 输出: 最大误差: 1.19e-07 ← 数学严格等价!
二、Rep-PAN Neck:重参数化路径聚合网络
2.1 设计动机
YOLOv5 的 PANet Neck 使用了大量 CSP 结构(split + concat),部署时:
split操作需要额外的内存分配concat操作需要内存拷贝- 在 TensorRT/NCNN 等推理引擎中不够友好
2.2 Rep-PAN 的改进
将 PANet 中所有 CSP 模块替换为 RepVGG Block:
【YOLOv5 PANet】 【YOLOv6 Rep-PAN】
P5 ──→ CSP Bottleneck ──→ P5 ──→ RepVGG Block ──→
↓ Upsample ↓ Upsample
P4 ←── Concat ←── P4 P4 ←── Add/Concat ←── P4
↓ ↓
CSP Bottleneck RepVGG Block
↓ Downsample ↓ Downsample
P3 ←── Concat ←── P3 P3 ←── Add/Concat ←── P3
↓ ↓
CSP Bottleneck RepVGG Block
↓ ↓
Head Head
2.3 关键区别
| 组件 | YOLOv5 Neck | YOLOv6 Rep-PAN |
|---|---|---|
| 基本单元 | CSP Bottleneck(残差+split) | RepVGG Block(重参数化) |
| 融合方式 | Concat(通道拼接) | 部分用 Add(元素相加) |
| 推理时结构 | 仍有 split/concat | 纯 Conv 链 |
| 参数量 | 较多 | 融合后更少 |
| 部署兼容性 | 一般 | TensorRT/NCNN 极友好 |
2.4 为什么 Add 比 Concat 更适合部署?
python
# Concat: 需要分配新内存,拷贝两个tensor
out = torch.cat([feat_top, feat_bottom], dim=1) # 内存: 2x
# Add: 原地操作,无额外内存
out = feat_top + feat_bottom # 内存: 1x(可inplace)
在 ARM 端(手机/嵌入式),内存带宽是瓶颈,Add 比 Concat 快 20%~40%。
三、解耦头(Decoupled Head)
3.1 耦合头的问题(YOLOv5 及之前)
YOLOv5 的检测头是耦合的:分类和回归共享同一个卷积输出:
python
# YOLOv5 耦合头
self.detect = nn.Conv2d(ch, 3 * (5 + nc), 1)
# 一个卷积同时输出: [x, y, w, h, obj, cls_0, ..., cls_nc-1]
# ↑ 回归任务 ↑ ↑ 分类任务 ↑
# 共享同一个特征!
问题:
| 问题 | 说明 |
|---|---|
| 任务冲突 | 分类需要"语义特征"(这是什么),回归需要"几何特征"(在哪里)。两者对特征的需求不同 |
| 收敛慢 | 共享权重导致梯度方向冲突,两个任务互相拖累 |
| 精度受限 | 分类精度和定位精度无法同时达到最优 |
3.2 解耦头的设计
YOLOv6 将分类和回归完全分离为两个独立分支:
特征图 F (C×H×W)
│
├──→ 分类分支: Conv3×3 → Conv3×3 → Conv1×1 → cls_pred (nc×H×W)
│ "这是什么?"
│
└──→ 回归分支: Conv3×3 → Conv3×3 → Conv1×1 → reg_pred (4×reg_max×H×W)
"在哪里?多大?"
3.3 代码实现
python
class DecoupledHead(nn.Module):
"""YOLOv6 解耦检测头"""
def __init__(self, in_channels, num_classes, reg_max=16):
super().__init__()
self.num_classes = num_classes
self.reg_max = reg_max
# ===== 分类分支(独立) =====
self.cls_branch = nn.Sequential(
nn.Conv2d(in_channels, in_channels, 3, 1, 1),
nn.BatchNorm2d(in_channels),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels, in_channels, 3, 1, 1),
nn.BatchNorm2d(in_channels),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels, num_classes, 1), # 输出: nc
)
# ===== 回归分支(独立) =====
self.reg_branch = nn.Sequential(
nn.Conv2d(in_channels, in_channels, 3, 1, 1),
nn.BatchNorm2d(in_channels),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels, in_channels, 3, 1, 1),
nn.BatchNorm2d(in_channels),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels, 4 * reg_max, 1), # 输出: 4×reg_max (DFL)
)
def forward(self, x):
cls_out = self.cls_branch(x) # (B, nc, H, W)
reg_out = self.reg_branch(x) # (B, 4*reg_max, H, W)
return cls_out, reg_out
3.4 为什么解耦能提升收敛速度?
【耦合头】
梯度 = ∂L_cls/∂W + ∂L_reg/∂W ← 两个方向可能相反!
W 被"拉扯",收敛慢
【解耦头】
W_cls 只接收 ∂L_cls/∂W_cls ← 方向一致,收敛快
W_reg 只接收 ∂L_reg/∂W_reg ← 方向一致,收敛快
实验数据(YOLOv6 论文):
| 头类型 | 300 epoch mAP | 收敛所需 epoch |
|---|---|---|
| 耦合头 | 42.1% | ~300 |
| 解耦头 | 43.5% | ~200 |
3.5 解耦头 + Anchor-Free 的配合
解耦头天然适配 Anchor-Free:
- 分类分支:直接输出每个类别的概率(无需 objectness)
- 回归分支:直接预测到四边的距离 ( l , t , r , b ) (l, t, r, b) (l,t,r,b)(无需锚框偏移)
四、无锚框(Anchor-Free)设计
4.1 Anchor-Based 的痛点回顾
| 痛点 | 说明 |
|---|---|
| 超参数多 | 需要预设 9 个锚框尺寸,换数据集要重新聚类 |
| 正负样本复杂 | 需要宽高比匹配、跨网格扩展等复杂逻辑 |
| 泛化性差 | 锚框尺寸是针对 COCO 统计的,换场景可能不适用 |
| 预测不直观 | 输出是相对于锚框的偏移 ( Δ x , Δ y , Δ w , Δ h ) (\Delta x, \Delta y, \Delta w, \Delta h) (Δx,Δy,Δw,Δh) |
4.2 YOLOv6 的 Anchor-Free 方案
YOLOv6 采用 FCOS 风格 的 Anchor-Free 设计:
【Anchor-Based (YOLOv5)】
每个网格 × 3个锚框 → 预测相对于锚框的偏移
输出: (Δx, Δy, Δw, Δh, obj, cls)
【Anchor-Free (YOLOv6)】
每个网格 → 直接预测到GT四边的距离
输出: (l, t, r, b, cls)
↑ 左 ↑ 上 ↑ 右 ↑ 下
4.3 "锚框"与"锚点"的本质区别
【YOLOv5 有锚框】 【YOLOv6 只有锚点】
每个网格有 3 个锚框: 每个网格只有 1 个中心点:
┌─────────────────┐
│ ┌───┐ ┌─────┐ │ ┌─────────────────┐
│ │小 │ │ 中 │ │ │ │
│ └───┘ └─────┘ │ │ • │ ← 就这一个点
│ ┌─────────┐ │ │ (cx, cy) │
│ │ 大 │ │ │ │
│ └─────────┘ │ └─────────────────┘
└─────────────────┘
3个锚框 × 80×80 = 19200个预测 1个点 × 80×80 = 6400个预测
关键区别:
- YOLOv5:每个网格产生 3 个预测(对应 3 个锚框)
- YOLOv6:每个网格产生 1 个预测(对应 1 个中心点)
4.4 回归目标:DFL(Distribution Focal Loss)
YOLOv6 不直接回归 ( l , t , r , b ) (l, t, r, b) (l,t,r,b) 的标量值,而是预测离散概率分布:
python
# 传统方式:直接回归
l_pred = conv(feat) # 输出1个标量
# DFL方式:预测分布
l_logits = conv(feat) # 输出 reg_max=16 个logit
l_prob = softmax(l_logits) # 16个bin的概率
l_pred = sum(l_prob * [0, 1, 2, ..., 15]) # 期望值
为什么用分布?
| 直接回归 | DFL 分布回归 | |
|---|---|---|
| 模糊边界 | 只能输出均值 | 能表达"不确定"(双峰分布) |
| 梯度质量 | 远离GT时梯度爆炸 | 分布形式天然平滑 |
| 精度 | 一般 | 更高(+0.3~0.5 mAP) |
4.5 每个锚点都预测一组 (l, t, r, b)
网络对所有点"一视同仁"地输出预测,不区分正负样本:
P3层: 80×80 = 6400 个锚点
每个锚点都输出:
├── cls_pred: 80个类别分数(nc=80)
└── reg_pred: 4×16=64个值 → DFL解码 → (l, t, r, b)
总共: 6400 组 (l, t, r, b) 预测,一个不漏
网络结构上的体现:
python
# 回归分支:对每个空间位置都输出预测
self.reg_branch = nn.Sequential(
nn.Conv2d(128, 128, 3, padding=1), # 不改变 H×W
nn.ReLU(),
nn.Conv2d(128, 128, 3, padding=1), # 不改变 H×W
nn.ReLU(),
nn.Conv2d(128, 4 * 16, 1), # 输出通道 = 4×reg_max = 64
)
# 输入: (B, 128, 80, 80)
# 输出: (B, 64, 80, 80) ← 每个空间位置都有64个值!
特征图 (128×80×80)
│
▼ 1×1 Conv
回归输出 (64×80×80)
展开看每个位置:
位置(0,0): [v0, v1, ..., v63] → DFL → (l=2.3, t=1.8, r=5.1, b=4.7)
位置(0,1): [v0, v1, ..., v63] → DFL → (l=0.5, t=0.3, r=1.2, b=0.9)
位置(0,2): [v0, v1, ..., v63] → DFL → (l=8.1, t=7.2, r=3.3, b=2.1)
...
位置(79,79): [v0, v1, ..., v63] → DFL → (l=1.1, t=0.8, r=2.0, b=1.5)
6400个位置,每个都有一组 (l,t,r,b),无一例外
4.6 负样本的 (l, t, r, b) 怎么办?
训练时:直接忽略(mask 掉)
python
# 计算回归 loss 时:
L_reg = CIoU_Loss(pred_boxes, target_boxes) # (B, 6400)
# 但只取正样本的 loss!
L_reg = (L_reg * fg_mask).sum() / fg_mask.sum()
# ↑ 负样本位置 fg_mask=0,loss被乘为0
# 相当于"你预测的 l,t,r,b 再离谱我也不管"
6400个锚点的回归loss:
正样本(26个): L_reg = CIoU(pred, GT) ← 认真算,有梯度
负样本(6374个): L_reg = 0(被mask) ← 你预测啥都无所谓
负样本的 (l, t, r, b) 预测没有梯度回传,网络不会去学习"负样本应该预测什么距离"。
推理时:靠分类分数过滤
python
# 推理时所有6400个点都有 (l,t,r,b) 和 cls_score
# 负样本的典型输出:
# cls_score(猫) = 0.02, cls_score(狗) = 0.01, ... ← 全都很低
# (l, t, r, b) = (3.2, 1.5, 8.7, 6.1) ← 随机/无意义的值
# 过滤:
if max(cls_scores) < 0.25:
丢弃! ← 负样本在这里被过滤掉
# 它的 (l,t,r,b) 根本不会被用到
推理流程:
6400个点都有 (l,t,r,b) + cls_score
│
▼
cls_score > 0.25 ?
│
┌────┴────┐
▼ ▼
是(26个) 否(6374个)
│ │
▼ ▼
解码框 直接丢弃
NMS (l,t,r,b)白预测了
│
▼
最终输出
4.7 为什么负样本也要"白预测"?
| 原因 | 说明 |
|---|---|
| 卷积的天然性质 | Conv 对所有空间位置并行计算,不可能跳过某些位置 |
| 计算效率 | GPU 对规则张量运算极快,"全部算完再mask"比"选择性计算"更快 |
| 训练需要 | 虽然回归 loss 被 mask,但分类 loss 需要所有位置的预测 |
| 推理需要 | 推理时不知道谁是正样本,必须全部预测再过滤 |
python
# 你不可能写出这样的代码:
for i in range(6400):
if is_positive[i]:
reg_pred[i] = conv(feat[i]) # 只算正样本
# ↑ 这样GPU完全无法并行,速度暴跌
# 实际做法:
reg_pred = conv(feat) # 一次算完所有6400个位置 ← GPU友好
reg_loss = loss(reg_pred, target) * fg_mask # 事后mask
4.8 Anchor-Free 的优势总结
| 优势 | 说明 |
|---|---|
| 零超参数 | 不需要预设锚框,不需要 autoanchor.py |
| 泛化性强 | 任何数据集、任何目标形状都适用 |
| 逻辑简洁 | 正负样本判定从"宽高比+跨网格"简化为"中心是否在框内" |
| 部署友好 | 无需在推理引擎中硬编码锚框尺寸 |
五、TAL 标签分配(Task-Aligned Assigner)
5.1 传统标签分配的问题
YOLOv5 的分配策略(宽高比 + 跨网格)是静态的:
问题:一个预测框分类很准(cls_score=0.95),但定位很差(IoU=0.3)
→ 按YOLOv5的规则,它可能仍然是正样本
→ 模型被要求"继续回归这个框"
→ 但实际上这个框的定位已经"跑偏"了,继续回归会产生错误梯度
核心矛盾:分类任务和回归任务对"好样本"的定义不同!
| 任务 | "好样本"的定义 |
|---|---|
| 分类 | 分类得分高(cls_score → 1) |
| 回归 | 定位准确(IoU → 1) |
5.2 TAL 的核心思想
用"分类得分"和"IoU"的联合指标来动态决定谁是正样本。
对齐分数(Alignment Metric):
align_metric = cls_score α × IoU β \text{align\_metric} = \text{cls\_score}^{\alpha} \times \text{IoU}^{\beta} align_metric=cls_scoreα×IoUβ
其中 α = 1 , β = 6 \alpha=1, \beta=6 α=1,β=6(默认值)。
直觉:
- 分类好 且 定位好 → align_metric 高 → 正样本 ✅
- 分类好 但 定位差 → align_metric 被 IoU 拉低 → 可能不是正样本 ❌
- 分类差 但 定位好 → align_metric 被 cls 拉低 → 可能不是正样本 ❌
5.3 TAL 完整流程(用数字走一遍)
假设一张图有 2 个 GT:
GT_0: 猫,框 = [100, 100, 200, 200](左上角到右下角)
GT_1: 狗,框 = [400, 300, 550, 500]
Step 1:初筛------哪些锚点在 GT 框内?
python
# 对 GT_0: [100, 100, 200, 200]
# 条件: 100 < cx < 200 且 100 < cy < 200
# 满足条件的锚点(stride=8):
# cx ∈ {104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192} → 12个
# cy ∈ {104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192} → 12个
# 共 12×12 = 144 个候选点
# 对 GT_1: [400, 300, 550, 500]
# cx ∈ {404, 412, ..., 548} → 19个
# cy ∈ {304, 312, ..., 496} → 25个
# 共 19×25 = 475 个候选点
640×640 的图上:
GT_0 (猫) GT_1 (狗)
┌──────────┐ ┌────────────────┐
│ • • • • │ │ • • • • • • • │
│ • • • • │ ← 144个候选 │ • • • • • • • │ ← 475个候选
│ • • • • │ │ • • • • • • • │
│ • • • • │ │ • • • • • • • │
└──────────┘ └────────────────┘
其余 6400 - 144 - 475 = 5781 个点 → 直接是负样本!
Step 2:计算对齐分数(核心!)
对 GT_0 的 144 个候选点,用当前模型的预测计算:
python
# 假设模型当前对这144个点的预测:
# 点A (cx=120, cy=120): 预测框=[95, 95, 205, 205], cls_score(猫)=0.85
# 点B (cx=160, cy=160): 预测框=[110, 110, 190, 190], cls_score(猫)=0.70
# 点C (cx=104, cy=104): 预测框=[50, 50, 250, 250], cls_score(猫)=0.60
# ...
# 计算 IoU(预测框 vs GT框):
IoU_A = IoU([95,95,205,205], [100,100,200,200]) = 0.82
IoU_B = IoU([110,110,190,190], [100,100,200,200]) = 0.64
IoU_C = IoU([50,50,250,250], [100,100,200,200]) = 0.25
# 计算对齐分数: align = cls^1 × IoU^6
align_A = 0.85^1 × 0.82^6 = 0.85 × 0.304 = 0.258 ← 高!
align_B = 0.70^1 × 0.64^6 = 0.70 × 0.069 = 0.048
align_C = 0.60^1 × 0.25^6 = 0.60 × 0.000244 = 0.000146 ← 极低!
注意 IoU 的 6 次方:IoU 差距被极度放大!
- IoU=0.82 → 0.82⁶ = 0.304
- IoU=0.64 → 0.64⁶ = 0.069
- IoU=0.25 → 0.25⁶ = 0.000244
这意味着:定位不准的预测,即使分类很对,也不会被选为正样本。
Step 3:选 Top-K(默认 K=13)
python
# GT_0 的 144 个候选,按 align_metric 排序:
# 排名1: 点A (120,120) align=0.258 ✅ 正样本
# 排名2: 点D (128,128) align=0.231 ✅ 正样本
# 排名3: 点E (136,120) align=0.215 ✅ 正样本
# ...
# 排名13: 点M (152,144) align=0.089 ✅ 正样本
# 排名14: 点N (160,160) align=0.048 ❌ 负样本
# ...
# 排名144: 点C (104,104) align=0.0001 ❌ 负样本
# GT_0 → 13个正样本
# GT_1 → 13个正样本(同理)
# 总正样本: 26个
# 总负样本: 6400 - 26 = 6374个
Step 4:冲突解决
python
# 如果点X (cx=180, cy=180) 同时在 GT_0 和 GT_1 的候选中:
# 对GT_0的 align = 0.15
# 对GT_1的 align = 0.22
# → 点X 分配给 GT_1(align更大的那个)
# → GT_0 不再使用点X
Step 5:生成训练 Target
python
# 正样本的 target:
target_bbox[点A] = [100, 100, 200, 200] # GT框(不是偏移!)
target_cls[点A] = align_A_normalized # 软标签!不是1.0!
# 负样本的 target:
target_bbox[其余6374个点] = [0, 0, 0, 0] # 无意义(不参与回归loss)
target_cls[其余6374个点] = 0 # 硬标签:不是任何类
5.4 完整分配流程图
6400个锚点
│
├── 在GT框外? ──→ 直接负样本(target_cls=0)
│ (5781个)
│
└── 在GT框内? ──→ 候选集
(619个)
│
├── 计算 align = cls^1 × IoU^6
│
├── 每个GT选 Top-13
│
├── 冲突解决(一个点只属于一个GT)
│
├── 最终正样本: ~26个 ──→ target_cls = align_metric (软标签)
│ target_bbox = GT框
│
└── 落选的候选: ~593个 ──→ 负样本(target_cls=0)
5.5 代码实现
python
class TaskAlignedAssigner:
"""YOLOv6 任务对齐标签分配器"""
def __init__(self, topk=13, alpha=1.0, beta=6.0):
self.topk = topk
self.alpha = alpha
self.beta = beta
@torch.no_grad()
def assign(self, pred_scores, pred_bboxes, gt_labels, gt_bboxes,
anchor_points, mask_gt):
"""
Args:
pred_scores: (B, N, nc) 每个网格的分类预测
pred_bboxes: (B, N, 4) 每个网格的回归预测 [x1,y1,x2,y2]
gt_labels: (B, M, 1) GT类别
gt_bboxes: (B, M, 4) GT框 [x1,y1,x2,y2]
anchor_points:(N, 2) 所有网格中心坐标
mask_gt: (B, M, 1) 有效GT掩码
Returns:
target_labels: (B, N) 每个网格分配的GT类别
target_bboxes: (B, N, 4) 每个网格分配的GT框
target_scores: (B, N, nc) 分类target(soft label)
fg_mask: (B, N) 前景掩码
"""
B, N, nc = pred_scores.shape
M = gt_bboxes.shape[1]
# ===== Step 1: 初筛 - 中心在GT框内 =====
inside_mask = self._is_inside(anchor_points, gt_bboxes) # (B, M, N)
# ===== Step 2: 计算对齐分数 =====
ious = self._batch_iou(pred_bboxes, gt_bboxes) # (B, M, N)
cls_scores = self._gather_cls(pred_scores, gt_labels) # (B, M, N)
# 对齐分数 = cls^α × IoU^β
align_metric = cls_scores.pow(self.alpha) * ious.pow(self.beta)
align_metric *= inside_mask.float() # 框外的置零
# ===== Step 3: 每个GT选Top-K =====
topk_metrics, topk_idxs = torch.topk(
align_metric, self.topk, dim=-1 # (B, M, K)
)
# 构建正样本掩码
fg_mask = torch.zeros(B, M, N, device=align_metric.device)
fg_mask.scatter_(-1, topk_idxs, 1.0)
fg_mask *= inside_mask.float()
# ===== Step 4: 冲突解决 =====
fg_mask = self._resolve_conflicts(fg_mask, align_metric) # (B, N)
# ===== Step 5: 生成 target =====
target_scores = self._get_soft_labels(align_metric, fg_mask)
return target_labels, target_bboxes, target_scores, fg_mask
def _is_inside(self, points, boxes):
"""判断点是否在框内"""
x, y = points[:, 0], points[:, 1]
x1, y1, x2, y2 = boxes[..., 0], boxes[..., 1], boxes[..., 2], boxes[..., 3]
inside = (x > x1) & (x < x2) & (y > y1) & (y < y2)
return inside # (B, M, N)
def _resolve_conflicts(self, fg_mask, align_metric):
"""一个网格只能属于一个GT"""
mask_sum = fg_mask.sum(dim=1, keepdim=True) # (B, 1, N)
conflict = mask_sum > 1
if conflict.any():
max_idx = (align_metric * fg_mask).argmax(dim=1) # (B, N)
fg_mask_new = torch.zeros_like(fg_mask)
fg_mask_new.scatter_(1, max_idx.unsqueeze(1), 1.0)
fg_mask = torch.where(conflict.expand_as(fg_mask), fg_mask_new, fg_mask)
return fg_mask.max(dim=1)[0] # (B, N)
5.6 TAL vs YOLOv5 分配策略对比
| 维度 | YOLOv5(静态) | YOLOv6 TAL(动态) |
|---|---|---|
| 匹配依据 | 宽高比 < 4.0(与预测无关) | cls_score × IoU(与预测相关) |
| 是否动态 | ❌ 训练全程规则不变 | ✅ 每个 epoch 随模型预测变化 |
| 分类-回归对齐 | 不对齐 | 显式对齐 |
| 正样本 target | cls=1(硬标签) | cls=align_metric(软标签) |
| 冲突处理 | 无(跨网格天然多对一) | 显式解决(选最大metric) |
| 对训练阶段的适应 | 不适应 | 早期选"容易的",后期选"精准的" |
5.7 TAL 的"动态"体现在哪?
Epoch 1(模型很弱):
所有预测的 cls_score ≈ 0.1, IoU ≈ 0.2
align_metric ≈ 0.1^1 × 0.2^6 ≈ 0.0000064
→ 选出的正样本是"相对最好的"(矮子里拔将军)
Epoch 100(模型较强):
好的预测: cls_score ≈ 0.9, IoU ≈ 0.8
align_metric ≈ 0.9 × 0.8^6 ≈ 0.236
差的预测: cls_score ≈ 0.8, IoU ≈ 0.4
align_metric ≈ 0.8 × 0.4^6 ≈ 0.003
→ 正样本标准自动提高!只有"又准又对"的才是正样本
📌 核心优势:TAL 让标签分配"跟着模型一起成长",避免了静态规则在训练后期的不合理性。
六、推理时如何避免锚框:完整解码流程
6.1 核心思想:直接预测绝对距离
【YOLOv5 推理(有锚框)】
锚框: anchor = (w_a, h_a) = (100, 100)
网络输出: (Δx, Δy, Δw, Δh) = (0.1, -0.05, 0.3, 0.2)
解码:
x = (sigmoid(Δx) + grid_x) × stride
y = (sigmoid(Δy) + grid_y) × stride
w = anchor_w × exp(Δw) = 100 × exp(0.3) = 135
h = anchor_h × exp(Δh) = 100 × exp(0.2) = 122
→ 最终框依赖于 anchor_w, anchor_h!
【YOLOv6 推理(无锚框)】
锚点: (cx, cy) = (124, 124) ← 就是网格中心,没有宽高!
网络输出: (l, t, r, b) = (24.5, 23.8, 76.2, 75.5)
解码:
x1 = cx - l = 124 - 24.5 = 99.5
y1 = cy - t = 124 - 23.8 = 100.2
x2 = cx + r = 124 + 76.2 = 200.2
y2 = cy + b = 124 + 75.5 = 199.5
→ 最终框 = [99.5, 100.2, 200.2, 199.5]
→ 完全不依赖任何锚框!
6.2 图解对比
【YOLOv5: 锚框 → 偏移 → 最终框】
锚框 (100×100) 网络预测偏移 最终框
┌──────────┐ Δx=+10, Δy=-5 ┌────────────┐
│ │ Δw=+35, Δh=+22 │ │
│ anchor │ ──────→ ──────→ │ pred │
│ │ "往右移10" │ │
└──────────┘ "变宽35" └────────────┘
需要知道 anchor 尺寸!
【YOLOv6: 锚点 → 四边距离 → 最终框】
锚点 (124,124) 网络预测距离 最终框
• l=24, t=24, r=76, b=76 ┌────────────┐
"左边24, 上边24" │ │
──────→ ──────→│ • │
"右边76, 下边76" │ │
└────────────┘
只需要一个点坐标!不需要任何预设框!
6.3 网络实际输出什么?
python
# YOLOv6 检测头输出(P3层,80×80网格):
# 分类分支输出:
cls_pred: (B, 80, 80, num_classes) # 每个网格点对每个类的分数
# 回归分支输出(DFL):
reg_pred: (B, 80, 80, 4 * reg_max) # 4个方向 × 16个bin
# 即: (B, 80, 80, 64)
# 解码:
reg_pred = reg_pred.reshape(B, 80, 80, 4, 16) # 4个方向各16个bin
reg_prob = softmax(reg_pred, dim=-1) # 概率分布
reg_dist = sum(reg_prob * [0,1,2,...,15]) # 期望值 → 4个距离
# reg_dist: (B, 80, 80, 4) → [l, t, r, b] 每个值范围 [0, 15]
# 乘以 stride=8 → 实际像素距离
6.4 完整推理解码代码
python
def yolov6_postprocess(cls_preds, reg_preds, strides,
conf_thresh=0.25, iou_thresh=0.45):
"""
YOLOv6 推理后处理(完全无锚框)
Args:
cls_preds: [(B, 80, 80, nc), (B, 40, 40, nc), (B, 20, 20, nc)]
reg_preds: [(B, 80, 80, 64), (B, 40, 40, 64), (B, 20, 20, 64)]
strides: [8, 16, 32]
"""
all_boxes = []
all_scores = []
for level, (cls_pred, reg_pred, stride) in enumerate(
zip(cls_preds, reg_preds, strides)):
B, H, W, nc = cls_pred.shape
# ===== Step 1: 生成锚点(只是坐标,不是框!) =====
gy, gx = torch.meshgrid(torch.arange(H), torch.arange(W))
anchor_points = torch.stack([gx, gy], dim=-1).float() # (H, W, 2)
anchor_points = (anchor_points + 0.5) * stride # 原图坐标
# ===== Step 2: 解码回归(DFL) =====
reg_pred = reg_pred.reshape(B, H, W, 4, 16)
reg_prob = F.softmax(reg_pred, dim=-1)
reg_dist = (reg_prob * torch.arange(16).float()).sum(-1) # (B,H,W,4)
# ===== Step 3: 距离 → 框坐标(核心!无锚框!) =====
l = reg_dist[..., 0] * stride # 到左边的像素距离
t = reg_dist[..., 1] * stride # 到上边的像素距离
r = reg_dist[..., 2] * stride # 到右边的像素距离
b = reg_dist[..., 3] * stride # 到下边的像素距离
cx = anchor_points[..., 0] # 锚点x
cy = anchor_points[..., 1] # 锚点y
x1 = cx - l # ← 不需要任何 anchor_w!
y1 = cy - t # ← 不需要任何 anchor_h!
x2 = cx + r
y2 = cy + b
boxes = torch.stack([x1, y1, x2, y2], dim=-1) # (B, H, W, 4)
# ===== Step 4: 分类分数 =====
scores = cls_pred.sigmoid() # (B, H, W, nc)
all_boxes.append(boxes.reshape(B, -1, 4))
all_scores.append(scores.reshape(B, -1, nc))
# ===== Step 5: 三层合并 + NMS =====
all_boxes = torch.cat(all_boxes, dim=1) # (B, 6400+1600+400, 4)
all_scores = torch.cat(all_scores, dim=1) # (B, 8400, nc)
# 置信度过滤
max_scores, max_classes = all_scores.max(dim=-1)
mask = max_scores > conf_thresh
# NMS
final_boxes = nms(all_boxes[mask], max_scores[mask], iou_thresh)
return final_boxes
6.5 一个完整的推理例子
输入: 640×640 图片,里面有一只猫在 [100, 100, 200, 200]
P3层 (stride=8, 80×80网格):
锚点 (124, 124) 的网络输出:
cls_pred: [猫=2.8, 狗=-1.2, ...] → sigmoid → [猫=0.94, 狗=0.23, ...]
reg_pred (DFL解码后): l=3.0, t=3.0, r=9.5, b=9.5
实际距离: l=3.0×8=24, t=3.0×8=24, r=9.5×8=76, b=9.5×8=76
解码:
x1 = 124 - 24 = 100 ✓
y1 = 124 - 24 = 100 ✓
x2 = 124 + 76 = 200 ✓
y2 = 124 + 76 = 200 ✓
最终: 猫, score=0.94, box=[100, 100, 200, 200] ← 完美!
全程没有用到任何 anchor = (w_a, h_a)!
6.6 为什么不需要锚框也能工作?
| 疑问 | 解答 |
|---|---|
| 没有锚框,网络怎么知道要预测多大的框? | 网络直接预测"到四边的距离",任何大小都能表达 |
| 小目标和大目标怎么区分? | P3 层 stride=8,感受野小 → 天然适合小目标;P5 层 stride=32 → 大目标 |
| 没有锚框做"参考",回归不会不稳定吗? | DFL 用分布回归代替直接回归,天然平滑 |
| 一个网格只预测一个框,密集目标怎么办? | TAL 给每个 GT 分配 13 个正样本(跨多个网格),足够覆盖 |
七、训练 vs 推理的完整对比
7.1 训练流程
GT框 [100,100,200,200] (猫)
│
▼
6400个锚点 → 初筛(在框内?) → 144个候选
│
▼
模型前向 → 得到每个候选的 cls_pred 和 reg_pred
│
▼
计算 align = cls^1 × IoU^6 → 排序 → Top-13 正样本
│
▼
正样本 loss:
L_cls = BCE(cls_pred, align_metric) ← 软标签!
L_reg = CIoU(pred_box, GT_box) ← 直接和GT框比!
L_dfl = DFL(reg_logits, GT距离)
负样本 loss:
L_cls = BCE(cls_pred, 0) ← 所有类都是0
L_reg = 0 ← 不回归!(mask掉)
7.2 推理流程
图片 → Backbone → Neck → Head
│
┌───────┴───────┐
▼ ▼
cls_pred reg_pred
(每点的类别分数) (每点的l,t,r,b)
│ │
▼ ▼
sigmoid DFL解码
│ │
▼ ▼
score > 0.25? x1=cx-l, y1=cy-t
过滤低分 x2=cx+r, y2=cy+b
│ │
└───────┬───────┘
▼
NMS
│
▼
最终检测结果
全程无锚框参与!
八、五大创新的协同关系
┌─────────────────────────────────────────────────────────┐
│ YOLOv6 整体架构 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ EfficientRep │───→│ Rep-PAN │───→│ Decoupled │ │
│ │ Backbone │ │ Neck │ │ Head │ │
│ │(重参数化卷积) │ │(重参数化融合) │ │(分类/回归) │ │
│ └──────────────┘ └──────────────┘ └─────┬─────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │Anchor-Free│ │
│ │(无锚框预测)│ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ TAL │ │
│ │(动态分配) │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────┘
| 创新点 | 解决的问题 | 受益方 |
|---|---|---|
| EfficientRep | 训练精度 vs 推理速度 | 部署端(TensorRT/NCNN) |
| Rep-PAN | Neck 部署不友好 | 部署端 + 推理速度 |
| 解耦头 | 分类/回归梯度冲突 | 训练收敛 + 精度 |
| Anchor-Free | 锚框超参数 + 泛化性 | 易用性 + 新数据集适配 |
| TAL | 静态分配不合理 | 训练精度 + 分类回归对齐 |
九、性能对比
| 模型 | 参数量 | COCO mAP | T4 FPS | 部署格式 |
|---|---|---|---|---|
| YOLOv5-s | 7.2M | 37.4% | 440 | ONNX/TRT |
| YOLOv5-n | 1.9M | 28.0% | 900 | ONNX/TRT |
| YOLOv6-s | 18.5M | 43.1% | 520 | TRT/NCNN/MNN |
| YOLOv6-n | 4.3M | 35.0% | 1242 | TRT/NCNN/MNN |
| YOLOX-s | 9.0M | 40.5% | 380 | ONNX/TRT |
YOLOv6-nano 在 T4 上达到 1242 FPS,同时保持 35% mAP,是工业部署的极佳选择。
十、总结
| 如果你关心... | YOLOv6 的答案 |
|---|---|
| 推理速度 | 重参数化:训练多分支,推理单卷积,GPU/ARM 都友好 |
| 部署便捷 | 全网络无 split/concat/BN,纯 Conv+ReLU 链 |
| 训练精度 | 解耦头 + TAL 动态分配,分类回归不冲突 |
| 换数据集 | Anchor-Free,无需重新聚类锚框 |
| 小目标 | Rep-PAN 多尺度融合 + TAL 动态选最优正样本 |
一句话总结 :YOLOv6 的哲学是------训练时"复杂"(多分支、解耦、动态分配),推理时"简单"(单卷积、无锚框、纯Conv链),把复杂度留给训练,把简洁度留给部署。