【YOLO系列07】YOLOv5详解——PyTorch时代的工程化典范
YOLOv5解析:PyTorch时代的工程化典范 YOLOv5是Ultralytics公司于2020年发布的基于PyTorch的目标检测框架,尽管存在命名争议,但其工程化实现、易用性和活跃社区使其成为最流行的检测框架之一。主要特点包括: 模块化架构:采用Focus模块(后改为6×6卷积)、CSP结构和SPPF模块,平衡性能与速度 多尺度模型:提供5种规模的预训练模型(n/s/m/l/x),参数从1
【YOLO系列07】YOLOv5详解——PyTorch时代的工程化典范
本文是YOLO系列博客的第七篇,深入解析YOLOv5的网络架构,包括Focus模块、CSP结构、自适应Anchor、完善的训练策略以及Ultralytics的工程化实现。
1. 引言
2020年6月,Ultralytics公司发布了YOLOv5。尽管没有正式论文,YOLOv5凭借其优秀的工程实现、易用的API和活跃的社区维护,成为目前最流行的目标检测框架之一。
项目信息:
- 作者:Glenn Jocher(Ultralytics)
- 框架:PyTorch
- 代码:https://github.com/ultralytics/yolov5
- Star:40K+
2. YOLOv5的争议与定位
2.1 命名争议
YOLOv5发布时引发了社区争议:
- 没有发表正式论文
- 性能数据受质疑
- 是否有资格称为"v5"
2.2 实际价值
尽管有争议,YOLOv5的价值不可否认:
| 优势 | 说明 |
|---|---|
| 工程化 | 完善的训练、评估、部署流程 |
| 易用性 | 简洁的API,快速上手 |
| 社区 | 活跃的维护和更新 |
| 多格式导出 | ONNX、TensorRT、CoreML等 |
| 文档 | 详尽的使用文档 |
3. 网络架构总览
3.1 整体结构
3.2 模型变体
YOLOv5提供5种规模的模型:
| 模型 | 深度倍数 | 宽度倍数 | 参数量 | FLOPs | mAP |
|---|---|---|---|---|---|
| YOLOv5n | 0.33 | 0.25 | 1.9M | 4.5G | 28.0 |
| YOLOv5s | 0.33 | 0.50 | 7.2M | 16.5G | 37.4 |
| YOLOv5m | 0.67 | 0.75 | 21.2M | 49.0G | 45.4 |
| YOLOv5l | 1.00 | 1.00 | 46.5M | 109.1G | 49.0 |
| YOLOv5x | 1.33 | 1.25 | 86.7M | 205.7G | 50.7 |
缩放公式:
channels=base_channels×width_multiple\text{channels} = \text{base\_channels} \times \text{width\_multiple}channels=base_channels×width_multiple
num_blocks=base_blocks×depth_multiple\text{num\_blocks} = \text{base\_blocks} \times \text{depth\_multiple}num_blocks=base_blocks×depth_multiple
4. Focus模块
4.1 设计动机
将空间信息重组到通道维度,在不丢失信息的情况下进行下采样。
4.2 工作原理
4.3 切片方式
将图像按照步长2进行切片:
class Focus(nn.Module):
def __init__(self, c1, c2, k=1):
super().__init__()
self.conv = Conv(c1 * 4, c2, k)
def forward(self, x):
# x: (b, c, h, w) -> (b, 4c, h/2, w/2)
return self.conv(torch.cat([
x[..., ::2, ::2], # 左上
x[..., 1::2, ::2], # 右上
x[..., ::2, 1::2], # 左下
x[..., 1::2, 1::2] # 右下
], dim=1))
4.4 Focus vs 卷积
| 方式 | 输入 | 输出 | 信息保留 |
|---|---|---|---|
| Conv 6×6/2 | 640×640×3 | 320×320×64 | 部分重叠 |
| Focus | 640×640×3 | 320×320×64 | 完全保留 |
注意:YOLOv5 v6.0后,Focus被替换为普通6×6卷积以提升部署兼容性。
5. CSP结构
5.1 C3模块
YOLOv5使用C3模块(CSP Bottleneck with 3 convolutions):
5.2 实现代码
class Bottleneck(nn.Module):
def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):
super().__init__()
c_ = int(c2 * e)
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c_, c2, 3, 1, g=g)
self.add = shortcut and c1 == c2
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class C3(nn.Module):
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__()
c_ = int(c2 * e)
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(2 * c_, c2, 1)
self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])
def forward(self, x):
return self.cv3(torch.cat([self.m(self.cv1(x)), self.cv2(x)], dim=1))
6. SPPF模块
6.1 与SPP对比
| 模块 | 结构 | 速度 |
|---|---|---|
| SPP | 并行多尺度池化 | 较慢 |
| SPPF | 串行多次5×5池化 | 快2倍 |
6.2 SPPF结构
6.3 实现
class SPPF(nn.Module):
def __init__(self, c1, c2, k=5):
super().__init__()
c_ = c1 // 2
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c_ * 4, c2, 1, 1)
self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)
def forward(self, x):
x = self.cv1(x)
y1 = self.m(x)
y2 = self.m(y1)
return self.cv2(torch.cat([x, y1, y2, self.m(y2)], dim=1))
7. 自适应Anchor
7.1 自动计算
YOLOv5在训练开始时自动为数据集计算最优Anchor:
def kmean_anchors(dataset, n=9, img_size=640, thr=4.0, gen=1000):
"""使用K-Means计算最优Anchor"""
from scipy.cluster.vq import kmeans
# 获取所有GT框的宽高
shapes = np.concatenate([
l[:, 3:5] * img_size for l in dataset.labels
], axis=0)
# K-Means聚类
anchors, _ = kmeans(shapes, n)
# 按面积排序
anchors = anchors[np.argsort(anchors.prod(1))]
return anchors
7.2 Anchor分配
YOLOv5的默认Anchor(640×640输入):
| 尺度 | 特征图 | Anchors (w, h) |
|---|---|---|
| P3/8 | 80×80 | (10,13), (16,30), (33,23) |
| P4/16 | 40×40 | (30,61), (62,45), (59,119) |
| P5/32 | 20×20 | (116,90), (156,198), (373,326) |
7.3 正样本分配
使用基于Anchor的匹配策略:
match=max(wgtwanchor,wanchorwgt)<anchor_t\text{match} = \max\left(\frac{w_{gt}}{w_{anchor}}, \frac{w_{anchor}}{w_{gt}}\right) < \text{anchor\_t}match=max(wanchorwgt,wgtwanchor)<anchor_t
同时检查宽度和高度,anchor_t默认为4.0。
8. 损失函数
8.1 总损失
L=λboxLbox+λobjLobj+λclsLcls\mathcal{L} = \lambda_{box}\mathcal{L}_{box} + \lambda_{obj}\mathcal{L}_{obj} + \lambda_{cls}\mathcal{L}_{cls}L=λboxLbox+λobjLobj+λclsLcls
默认权重:λbox=0.05\lambda_{box}=0.05λbox=0.05,λobj=1.0\lambda_{obj}=1.0λobj=1.0,λcls=0.5\lambda_{cls}=0.5λcls=0.5
8.2 边界框损失
使用CIoU Loss:
Lbox=1−CIoU\mathcal{L}_{box} = 1 - \text{CIoU}Lbox=1−CIoU
8.3 目标损失
使用BCE Loss,带有目标置信度:
Lobj=BCE(o^,o⋅IoU)\mathcal{L}_{obj} = \text{BCE}(\hat{o}, o \cdot \text{IoU})Lobj=BCE(o^,o⋅IoU)
其中 IoU\text{IoU}IoU 是预测框与GT的IoU,作为软标签。
8.4 分类损失
使用BCE Loss(多标签):
Lcls=BCE(p^,p)\mathcal{L}_{cls} = \text{BCE}(\hat{p}, p)Lcls=BCE(p^,p)
8.5 平衡权重
不同尺度的损失使用不同权重:
| 尺度 | 权重 |
|---|---|
| P3 (80×80) | 4.0 |
| P4 (40×40) | 1.0 |
| P5 (20×20) | 0.4 |
小目标尺度权重更高,以平衡样本数量。
9. 数据增强
9.1 Mosaic
def load_mosaic(self, index):
"""4图Mosaic增强"""
labels4 = []
s = self.img_size
# 随机中心点
yc, xc = [int(random.uniform(s*0.5, s*1.5)) for _ in range(2)]
# 随机选择3张其他图像
indices = [index] + random.choices(range(len(self)), k=3)
for i, idx in enumerate(indices):
img, labels = self.load_image(idx)
h, w = img.shape[:2]
# 放置位置
if i == 0: # 左上
x1a, y1a, x2a, y2a = max(xc-w, 0), max(yc-h, 0), xc, yc
x1b, y1b, x2b, y2b = w-(x2a-x1a), h-(y2a-y1a), w, h
# ... 其他位置类似
img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b]
labels4.append(adjusted_labels)
return img4, np.concatenate(labels4, 0)
9.2 MixUp
def mixup(img1, labels1, img2, labels2, alpha=0.5):
"""MixUp增强"""
r = np.random.beta(alpha, alpha)
img = (img1 * r + img2 * (1 - r)).astype(np.uint8)
labels = np.concatenate([labels1, labels2], 0)
return img, labels
9.3 Copy-Paste
def copy_paste(img, labels, segments, p=0.5):
"""Copy-Paste增强(需要分割标注)"""
n = len(segments)
if p and n:
for j in random.sample(range(n), k=int(n * p)):
# 随机缩放和翻转
# 粘贴到新位置
pass
return img, labels
9.4 增强策略总结
| 增强方法 | 默认概率 | 作用 |
|---|---|---|
| Mosaic | 1.0 | 多图融合 |
| MixUp | 0.0 | 图像混合 |
| Copy-Paste | 0.0 | 实例粘贴 |
| HSV增强 | 1.0 | 色彩变换 |
| 翻转 | 0.5 | 水平翻转 |
| 缩放 | 0.5 | 随机缩放 |
| 平移 | 0.1 | 随机平移 |
10. 训练策略
10.1 学习率调度
使用余弦退火(Cosine Annealing):
ηt=ηmin+12(ηmax−ηmin)(1+cos(tTπ))\eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})(1 + \cos(\frac{t}{T}\pi))ηt=ηmin+21(ηmax−ηmin)(1+cos(Ttπ))
10.2 Warmup
前3个epoch使用线性warmup:
# 学习率warmup
if ni < nw:
xi = [0, nw]
for j, x in enumerate(optimizer.param_groups):
x['lr'] = np.interp(ni, xi, [warmup_bias_lr if j == 2 else 0.0, x['initial_lr'] * lf(epoch)])
if 'momentum' in x:
x['momentum'] = np.interp(ni, xi, [warmup_momentum, momentum])
10.3 EMA(指数移动平均)
class ModelEMA:
def __init__(self, model, decay=0.9999):
self.ema = deepcopy(model).eval()
self.decay = decay
def update(self, model):
with torch.no_grad():
for ema_p, p in zip(self.ema.parameters(), model.parameters()):
ema_p.data.mul_(self.decay).add_(p.data, alpha=1 - self.decay)
10.4 训练超参数
# hyp.scratch-low.yaml
lr0: 0.01 # 初始学习率
lrf: 0.01 # 最终学习率 = lr0 * lrf
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3.0
warmup_momentum: 0.8
warmup_bias_lr: 0.1
box: 0.05 # 边界框损失权重
cls: 0.5 # 分类损失权重
obj: 1.0 # 目标损失权重
anchor_t: 4.0 # Anchor匹配阈值
11. 推理与部署
11.1 推理流程
from ultralytics import YOLO
# 加载模型
model = YOLO('yolov5s.pt')
# 推理
results = model('image.jpg')
# 获取结果
for result in results:
boxes = result.boxes.xyxy # 边界框
scores = result.boxes.conf # 置信度
classes = result.boxes.cls # 类别
11.2 模型导出
# 导出ONNX
python export.py --weights yolov5s.pt --include onnx
# 导出TensorRT
python export.py --weights yolov5s.pt --include engine --device 0
# 导出CoreML
python export.py --weights yolov5s.pt --include coreml
# 导出TFLite
python export.py --weights yolov5s.pt --include tflite
11.3 支持的导出格式
| 格式 | 用途 |
|---|---|
| PyTorch | 训练和研究 |
| ONNX | 跨平台部署 |
| TensorRT | NVIDIA GPU加速 |
| CoreML | iOS/macOS部署 |
| TFLite | Android/嵌入式 |
| OpenVINO | Intel硬件 |
| PaddlePaddle | 百度生态 |
12. 完整配置文件
12.1 模型配置 (yolov5s.yaml)
# YOLOv5s
nc: 80 # 类别数
depth_multiple: 0.33 # 深度倍数
width_multiple: 0.50 # 宽度倍数
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 6, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, Concat, [1]],
[-1, 3, C3, [512, False]],
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]],
[-1, 3, C3, [256, False]], # 17 (P3/8-small)
[-1, 1, Conv, [256, 3, 2]],
[[-1, 14], 1, Concat, [1]],
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]],
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)
[[17, 20, 23], 1, Detect, [nc, anchors]],
]
13. 实验结果
13.1 COCO val2017
| 模型 | 尺寸 | mAP@0.5 | mAP@0.5:0.95 | 速度(V100) |
|---|---|---|---|---|
| YOLOv5n | 640 | 45.7 | 28.0 | 0.6ms |
| YOLOv5s | 640 | 56.8 | 37.4 | 1.0ms |
| YOLOv5m | 640 | 64.1 | 45.4 | 2.2ms |
| YOLOv5l | 640 | 67.3 | 49.0 | 3.8ms |
| YOLOv5x | 640 | 68.9 | 50.7 | 6.3ms |
13.2 与其他模型对比
| 模型 | 参数量 | mAP | FPS |
|---|---|---|---|
| YOLOv4 | 64M | 43.5 | 38 |
| YOLOv5l | 47M | 49.0 | 50 |
| EfficientDet-D2 | 8M | 43.0 | 26 |
14. 总结
YOLOv5的成功在于:
| 方面 | 特点 |
|---|---|
| 架构 | CSP + SPPF + PANet的成熟组合 |
| 工程 | 完善的训练、评估、部署流程 |
| 易用 | 简洁API,丰富文档 |
| 社区 | 持续更新,活跃维护 |
| 部署 | 多格式导出,跨平台支持 |
YOLOv5虽然没有论文,但其工程化实现为目标检测的实际应用树立了标杆。
参考资源
- 官方仓库:https://github.com/ultralytics/yolov5
- 官方文档:https://docs.ultralytics.com/yolov5/
- 预训练模型:https://github.com/ultralytics/yolov5/releases
上一篇:【YOLO系列06】YOLOv4详解——Bag of Freebies与Bag of Specials
下一篇:【YOLO系列08】YOLOv6详解——美团的工业级检测器
更多推荐


所有评论(0)