YOLOv5目标检测实战全解析:从原理到部署的PyTorch完整指南
文章目录
- YOLOv5目标检测实战全解析:从原理到部署的PyTorch完整指南
-
- 一、项目背景与意义
-
- [1.1 行业应用场景](#1.1 行业应用场景)
- [1.2 技术挑战](#1.2 技术挑战)
- [1.3 本文目标](#1.3 本文目标)
- 二、核心技术原理
-
- [2.1 算法架构详解](#2.1 算法架构详解)
-
- [2.1.1 Backbone:特征提取网络](#2.1.1 Backbone:特征提取网络)
- [2.1.2 Neck:特征金字塔网络](#2.1.2 Neck:特征金字塔网络)
- [2.1.3 Head:检测头](#2.1.3 Head:检测头)
- [2.2 关键技术创新点](#2.2 关键技术创新点)
- [2.3 数学原理推导](#2.3 数学原理推导)
-
- [2.3.1 边界框编码与解码](#2.3.1 边界框编码与解码)
- [2.3.2 损失函数设计](#2.3.2 损失函数设计)
- [2.3.3 锚框匹配策略](#2.3.3 锚框匹配策略)
- 三、环境搭建与依赖
-
- [3.1 硬件要求](#3.1 硬件要求)
- [3.2 软件环境](#3.2 软件环境)
- [3.3 依赖安装](#3.3 依赖安装)
- 四、数据集准备
-
- [4.1 数据集介绍](#4.1 数据集介绍)
- [4.2 数据预处理](#4.2 数据预处理)
-
- [4.2.1 YOLO格式标注](#4.2.1 YOLO格式标注)
- [4.2.2 数据集配置文件](#4.2.2 数据集配置文件)
- [4.2.3 数据集目录结构](#4.2.3 数据集目录结构)
- [4.3 数据增强策略](#4.3 数据增强策略)
- 五、模型实现详解
-
- [5.1 网络结构定义](#5.1 网络结构定义)
- [5.2 损失函数设计](#5.2 损失函数设计)
- [5.3 训练策略与超参数](#5.3 训练策略与超参数)
- [5.4 完整训练代码](#5.4 完整训练代码)
- 六、模型训练与调优
-
- [6.1 训练流程](#6.1 训练流程)
- [6.2 训练技巧](#6.2 训练技巧)
-
- [6.2.1 自动批量大小](#6.2.1 自动批量大小)
- [6.2.2 多尺度训练](#6.2.2 多尺度训练)
- [6.2.3 超参数进化](#6.2.3 超参数进化)
- [6.2.4 缓存策略](#6.2.4 缓存策略)
- [6.3 超参数调优](#6.3 超参数调优)
- 七、模型评估与分析
-
- [7.1 评估指标](#7.1 评估指标)
- [7.2 实验结果](#7.2 实验结果)
- [7.3 消融实验](#7.3 消融实验)
- [7.4 可视化分析](#7.4 可视化分析)
- 八、推理部署
-
- [8.1 模型导出](#8.1 模型导出)
- [8.2 推理代码](#8.2 推理代码)
-
- [8.2.1 PyTorch推理](#8.2.1 PyTorch推理)
- [8.2.2 自定义Python推理脚本](#8.2.2 自定义Python推理脚本)
- [8.3 性能优化](#8.3 性能优化)
- 九、常见错误与避坑指南
-
- [错误1:CUDA Out of Memory (OOM)](#错误1:CUDA Out of Memory (OOM))
- 错误2:标签格式不正确导致训练失败
- 错误3:训练loss不下降或NaN
- 错误4:mAP很低或模型不收敛
- 错误5:ONNX导出后推理结果不一致
- 十、扩展与进阶
-
- [10.1 改进方向](#10.1 改进方向)
- [10.2 相关论文推荐](#10.2 相关论文推荐)
- 参考链接
- 总结与下篇预告
一、项目背景与意义
1.1 行业应用场景
目标检测(Object Detection)是计算机视觉领域最核心的任务之一,其目标是在图像或视频中同时定位和识别多个物体。YOLO(You Only Look Once)系列作为单阶段目标检测算法的代表,自2016年Joseph Redmon提出YOLOv1以来,凭借其"端到端、一次前向传播即可完成检测"的设计理念,在工业界和学术界产生了深远影响。
YOLOv5由Ultralytics团队于2020年发布,虽然并非官方YOLO系列的延续,但其工程化程度极高,具有以下显著优势:
- 极致的易用性:提供完整的训练、验证、推理、导出工具链,几行命令即可完成从数据准备到模型部署的全流程
- 丰富的模型家族:提供n、s、m、l、x五种规模,覆盖从移动端到服务器端的各种场景
- 多后端部署支持:原生支持PyTorch、ONNX、TensorRT、OpenVINO、CoreML、TFLite等主流推理框架
- 强大的数据增强:内置Mosaic、MixUp、Copy-Paste等先进增强策略
- 活跃的社区生态:GitHub 50k+ Stars,持续维护更新
YOLOv5在实际场景中广泛应用于:
- 智能安防:行人检测、车辆检测、异常行为识别
- 自动驾驶:交通标志识别、行人及车辆检测
- 工业质检:产品缺陷检测、零件计数
- 医疗影像:病灶检测、细胞计数
- 零售分析:商品识别、客流统计
1.2 技术挑战
目标检测面临的核心技术挑战包括:
- 多尺度问题:同一图像中物体尺度差异巨大(从几个像素到整张图),模型需要同时检测大小目标
- 密集场景:拥挤场景中目标严重重叠,NMS后处理难以区分
- 实时性要求:工业场景要求毫秒级推理延迟
- 小目标检测:小目标信息量少,特征容易在深层网络中丢失
- 类别不平衡:前景-背景样本极度不平衡
YOLOv5通过精心设计的网络架构、锚框机制、多尺度特征融合和先进的训练策略,有效应对了上述挑战。
1.3 本文目标
本文将深入剖析YOLOv5的完整技术体系,从算法原理到工程实现,从环境搭建到模型部署,带领读者全面掌握YOLOv5目标检测技术。文章将包含:
- YOLOv5网络架构的逐层解析
- 损失函数的数学推导
- 完整的训练与推理代码实战
- 常见错误与避坑指南
- 模型优化与部署方案
二、核心技术原理
2.1 算法架构详解
YOLOv5的整体架构由三个核心部分组成:Backbone(骨干网络) 、Neck(颈部网络) 和Head(检测头)。
┌─────────────────────────────────────────────────────────────────┐
│ YOLOv5 整体架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Input (640×640×3) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ BACKBONE │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Focus/Conv(k=6,s=2) → P1/2 │ │ │
│ │ │ Conv(k=3,s=2) → P2/4 │ │ │
│ │ │ C3 × 3 → P3/8 │ │ ──────► Head(P3) │
│ │ │ Conv(k=3,s=2) → P4/16 │ │ ──────► Head(P4) │
│ │ │ C3 × 6 │ │ ──────► Head(P5) │
│ │ │ Conv(k=3,s=2) → P5/32 │ │ │
│ │ │ C3 × 3 │ │ │
│ │ │ SPPF │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ NECK (PANet) │ │
│ │ │ │
│ │ P5 ──► Conv ──► Upsample ──┐ │ │
│ │ ├──► C3 │ │
│ │ P4 ─────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Conv ──► Upsample ──┐ │ │
│ │ ├──► C3 (P3) │ │
│ │ P3 ─────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Conv(k=3,s=2) ──┐ │ │
│ │ ├──► C3 (P4) │ │
│ │ (from above) ───┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Conv(k=3,s=2) ──┐ │ │
│ │ ├──► C3 (P5) │ │
│ │ P5(feature) ────┘ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ HEAD (Detect) │ │
│ │ P3(80×80) ──► Detect ──► [B,3,80, │ │
│ │ 80,85] │ │
│ │ P4(40×40) ──► Detect ──► [B,3,40, │ │
│ │ 40,85] │ │
│ │ P5(20×20) ──► Detect ──► [B,3,20, │ │
│ │ 20,85] │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.1.1 Backbone:特征提取网络
YOLOv5的Backbone采用CSPDarknet结构,核心组件包括:
(1)Conv模块(CBL)
YOLOv5的基础卷积模块由Conv2d + BatchNorm2d + SiLU激活函数组成:
python
class Conv(nn.Module):
# 标准卷积模块:Conv2d → BatchNorm2d → SiLU
default_act = nn.SiLU() # SiLU激活函数(Swish的PyTorch实现)
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d),
groups=g, dilation=d, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
# 推理时融合Conv+BN,减少计算量
return self.act(self.conv(x))
(2)C3模块(CSP Bottleneck with 3 Convolutions)
C3是YOLOv5的核心构建块,基于CSPNet(Cross Stage Partial Network)思想设计:
python
class C3(nn.Module):
# CSP Bottleneck with 3 convolutions
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) # 1×1卷积,降维
self.cv2 = Conv(c1, c_, 1, 1) # 1×1卷积,降维(shortcut分支)
self.cv3 = Conv(2 * c_, c2, 1) # 1×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)), 1))
C3模块的设计精髓在于梯度分流:将输入特征分为两条路径,一条经过多个Bottleneck处理,另一条直接通过1×1卷积,最后拼接。这种设计既增加了梯度路径的多样性,又减少了计算量。
(3)SPPF(Spatial Pyramid Pooling - Fast)
SPPF是SPP的快速实现版本,通过串行池化替代并行池化:
python
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) # 第二次池化(基于第一次结果)
y3 = self.m(y2) # 第三次池化(基于第二次结果)
# 拼接原始特征 + 三次池化结果 → 4×c_通道
return self.cv2(torch.cat((x, y1, y2, y3), 1))
SPPF通过三个串联的5×5最大池化层,等效于并联的5×5、9×9、13×13池化核,但速度提升显著。这种设计使得模型能够融合多尺度感受野信息,对小目标和大目标都有良好的检测能力。
2.1.2 Neck:特征金字塔网络
YOLOv5的Neck采用PANet(Path Aggregation Network) 结构,在FPN(Feature Pyramid Network)的基础上增加了自底向上的路径增强:
FPN(自顶向下):
P5(20×20) ──► Upsample ──► Concat(P4) ──► C3 ──► P4'
P4'(40×40) ──► Upsample ──► Concat(P3) ──► C3 ──► P3'
PAN(自底向上):
P3'(80×80) ──► Conv(s=2) ──► Concat(P4') ──► C3 ──► P4''
P4''(40×40) ──► Conv(s=2) ──► Concat(P5) ──► C3 ──► P5''
这种双向特征融合的设计,使得浅层的位置信息能够通过自底向上的路径传递到深层,同时深层的语义信息通过自顶向下的路径传递到浅层,显著提升了多尺度目标的检测精度。
2.1.3 Head:检测头
YOLOv5的检测头在三个尺度上进行预测:
python
class Detect(nn.Module):
# YOLOv5检测头
stride = None # 各层步长,如[8, 16, 32]
dynamic = False # 是否动态重建网格
export = False # 是否导出模式
def __init__(self, nc=80, anchors=(), ch=(), inplace=True):
super().__init__()
self.nc = nc # 类别数(COCO: 80)
self.no = nc + 5 # 每锚框输出数:xywh + obj + cls
self.nl = len(anchors) # 检测层数(3层)
self.na = len(anchors[0]) // 2 # 每层锚框数(3个)
self.grid = [torch.empty(0) for _ in range(self.nl)]
self.anchor_grid = [torch.empty(0) for _ in range(self.nl)]
self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)
def forward(self, x):
z = []
for i in range(self.nl):
x[i] = self.m[i](x[i]) # 1×1卷积输出
bs, _, ny, nx = x[i].shape
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
if not self.training: # 推理模式
if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
# 解码边界框
xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
xy = (xy * 2 + self.grid[i]) * self.stride[i] # 中心点坐标
wh = (wh * 2) ** 2 * self.anchor_grid[i] # 宽高
y = torch.cat((xy, wh, conf), 4)
z.append(y.view(bs, self.na * nx * ny, self.no))
return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)
2.2 关键技术创新点
YOLOv5相较于前代YOLO系列的核心创新包括:
| 创新点 | 技术细节 | 效果 |
|---|---|---|
| CSPNet骨干 | C3模块替代原始BottleneckCSP | 减少计算量30%,保持精度 |
| SPPF | 串行池化替代并行SPP | 速度提升2倍,效果等效 |
| PANet Neck | 双向特征金字塔 | 提升多尺度检测精度 |
| 自适应锚框 | AutoAnchor自动计算最优锚框 | 适配自定义数据集 |
| Mosaic数据增强 | 四图拼接训练 | 提升小目标检测能力 |
| SiLU激活函数 | 替代LeakyReLU | 平滑梯度,提升收敛性 |
| 自适应图片缩放 | 矩形推理减少黑边 | 推理速度提升30% |
| 遗传算法超参搜索 | 自动超参数进化 | 自动化调优 |
2.3 数学原理推导
2.3.1 边界框编码与解码
YOLOv5使用锚框(Anchor Box)机制进行边界框预测。对于每个网格单元,模型预测相对于锚框的偏移量:
编码(训练时):
t_x = (g_x - c_x) / stride # 目标中心x相对于网格的偏移
t_y = (g_y - c_y) / stride # 目标中心y相对于网格的偏移
t_w = log(g_w / a_w) # 目标宽度相对于锚框的对数比
t_h = log(g_h / a_h) # 目标高度相对于锚框的对数比
解码(推理时):
b_x = (2σ(t_x) - 0.5) + c_x # 预测中心x
b_y = (2σ(t_y) - 0.5) + c_y # 预测中心y
b_w = a_w × (2σ(t_w))² # 预测宽度
b_h = a_h × (2σ(t_h))² # 预测高度
其中 σ 为Sigmoid函数,将预测值限制在0,1范围内,乘以2再减0.5后范围为-0.5, 1.5,允许预测框中心偏移到相邻网格。
2.3.2 损失函数设计
YOLOv5的损失函数由三部分组成:
L_total = λ_box × L_box + λ_obj × L_obj + λ_cls × L_cls
(1)边界框回归损失(CIoU Loss)
CIoU(Complete IoU)损失综合考虑了重叠面积、中心点距离和长宽比:
L_CIoU = 1 - IoU + ρ²(b, b_gt) / c² + α × v
其中:
- IoU:预测框与真实框的交并比
- ρ²(b, b_gt):预测框中心与真实框中心的欧氏距离平方
- c:包围两个框的最小闭包矩形的对角线长度
- v = (4/π²) × (arctan(w_gt/h_gt) - arctan(w/h))² # 长宽比一致性
- α = v / ((1 - IoU) + v) # 权衡参数
(2)目标置信度损失(BCE Loss)
L_obj = -[y × log(p) + (1-y) × log(1-p)]
其中 y 为真实目标置信度(正样本为预测框与GT的IoU,负样本为0)
(3)分类损失(BCE Loss)
L_cls = -Σ[t_c × log(p_c) + (1-t_c) × log(1-p_c)]
其中 t_c 为类别c的真实标签,p_c 为预测概率
YOLOv5的损失函数完整实现:
python
class ComputeLoss:
def __init__(self, model, autobalance=False):
device = next(model.parameters()).device
h = model.hyp # 超参数
# 定义损失函数(支持Focal Loss)
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
# 标签平滑
self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0))
# Focal Loss
g = h['fl_gamma']
if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
m = de_parallel(model).model[-1] # Detect模块
self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02])
self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance
self.na = m.na # 锚框数
self.nc = m.nc # 类别数
self.nl = m.nl # 检测层数
def __call__(self, p, targets):
lcls = torch.zeros(1, device=self.device) # 分类损失
lbox = torch.zeros(1, device=self.device) # 边界框损失
lobj = torch.zeros(1, device=self.device) # 目标损失
tcls, tbox, indices, anchors = self.build_targets(p, targets)
for i, pi in enumerate(p): # 遍历每个检测层
b, a, gj, gi = indices[i] # 图像索引、锚框索引、网格坐标
tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device)
n = b.shape[0] # 该层目标数
if n:
pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self.nc), 1)
# 回归损失:CIoU
pxy = pxy.sigmoid() * 2 - 0.5
pwh = (pwh.sigmoid() * 2) ** 2 * anchors[i]
pbox = torch.cat((pxy, pwh), 1)
iou = bbox_iou(pbox, tbox[i], CIoU=True).squeeze()
lbox += (1.0 - iou).mean()
# 目标损失
iou = iou.detach().clamp(0).type(tobj.dtype)
tobj[b, a, gj, gi] = iou
# 分类损失
if self.nc > 1:
t = torch.full_like(pcls, self.cn, device=self.device)
t[range(n), tcls[i]] = self.cp
lcls += self.BCEcls(pcls, t)
obji = self.BCEobj(pi[..., 4], tobj)
lobj += obji * self.balance[i] # 各层目标损失加权
# 应用超参数权重
lbox *= self.hyp['box']
lobj *= self.hyp['obj']
lcls *= self.hyp['cls']
bs = tobj.shape[0]
return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()
2.3.3 锚框匹配策略
YOLOv5使用宽高比匹配策略将真实框分配给锚框:
python
def build_targets(self, p, targets):
na, nt = self.na, targets.shape[0]
tcls, tbox, indices, anch = [], [], [], []
gain = torch.ones(7, device=self.device)
ai = torch.arange(na, device=self.device).float().view(na, 1).repeat(1, nt)
targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2)
g = 0.5 # 偏移量
off = torch.tensor([[0, 0], [1, 0], [0, 1], [-1, 0], [0, -1]],
device=self.device).float() * g
for i in range(self.nl): # 遍历3个检测层
anchors, shape = self.anchors[i], p[i].shape
gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]]
t = targets * gain # 缩放到当前特征图尺寸
if nt:
# 宽高比匹配:r = max(w/w_anchor, w_anchor/w)
r = t[..., 4:6] / anchors[:, None]
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t']
t = t[j] # 筛选匹配的锚框
# 计算网格偏移(扩展正样本)
gxy = t[:, 2:4] # 网格坐标
gxi = gain[[2, 3]] - gxy # 反向坐标
j, k = ((gxy % 1 < g) & (gxy > 1)).T
l, m = ((gxi % 1 < g) & (gxi > 1)).T
j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
bc, gxy, gwh, a = t.chunk(4, 1)
a, (b, c) = a.long().view(-1), bc.long().T
gij = (gxy - offsets).long()
gi, gj = gij.T
indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))
tbox.append(torch.cat((gxy - gij, gwh), 1))
anch.append(anchors[a])
tcls.append(c)
return tcls, tbox, indices, anch
跨网格预测策略:YOLOv5不仅将目标分配给中心点所在的网格,还会根据中心点偏移量,将目标同时分配给相邻的2-4个网格。这种策略显著增加了正样本数量,加速了模型收敛。
三、环境搭建与依赖
3.1 硬件要求
| 配置项 | 最低要求 | 推荐配置 |
|---|---|---|
| GPU | NVIDIA GTX 1060 6GB | NVIDIA RTX 3060+ 12GB |
| CPU | Intel i5 4核 | Intel i7/i9 8核+ |
| 内存 | 16GB | 32GB+ |
| 硬盘 | 50GB SSD | 100GB+ NVMe SSD |
| CUDA | 10.2+ | 11.7+ |
3.2 软件环境
bash
# 操作系统
Ubuntu 20.04/22.04 LTS 或 Windows 10/11
# Python版本
Python 3.8 - 3.10(推荐3.9)
# CUDA版本
CUDA 11.7 + cuDNN 8.5
3.3 依赖安装
bash
# 1. 克隆YOLOv5仓库
git clone https://github.com/ultralytics/yolov5.git
cd yolov5
# 2. 创建虚拟环境(推荐)
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# 3. 安装PyTorch(根据CUDA版本选择)
# CUDA 11.7
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu117
# CUDA 11.8
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu118
# CPU only
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cpu
# 4. 安装YOLOv5依赖
pip install -r requirements.txt
# 5. 验证安装
python detect.py --weights yolov5s.pt --source data/images/bus.jpg
requirements.txt 核心依赖:
matplotlib>=3.3
numpy>=1.18.5
opencv-python>=4.1.1
Pillow>=7.1.2
PyYAML>=5.3.1
requests>=2.23.0
scipy>=1.4.1
torch>=1.7.0
torchvision>=0.8.1
tqdm>=4.64.0
seaborn>=0.11.0
pandas>=1.1.4
四、数据集准备
4.1 数据集介绍
YOLOv5支持多种数据集格式,官方提供以下预配置数据集:
| 数据集 | 类别数 | 图片数 | 用途 |
|---|---|---|---|
| COCO | 80 | 118K/5K | 通用目标检测 |
| COCO128 | 80 | 128 | 快速测试 |
| VOC | 20 | 16.5K | 经典基准 |
| GlobalWheat2020 | 1 | 3.4K | 小麦头检测 |
| Objects365 | 365 | 1.7M | 大规模预训练 |
| xView | 60 | 1.1M | 遥感检测 |
4.2 数据预处理
4.2.1 YOLO格式标注
YOLOv5使用归一化的txt标注格式:
# 格式:class_id center_x center_y width height
# 所有坐标归一化到[0,1]范围
# 示例:data/labels/image1.txt
0 0.523 0.618 0.152 0.238 # 类别0(人),中心(0.523,0.618),宽0.152,高0.238
1 0.345 0.456 0.089 0.134 # 类别1(车)
4.2.2 数据集配置文件
yaml
# data/custom.yaml
# 训练集和验证集路径
train: /path/to/dataset/images/train/
val: /path/to/dataset/images/val/
# 类别数量
nc: 3
# 类别名称
names: ['person', 'car', 'bicycle']
4.2.3 数据集目录结构
dataset/
├── images/
│ ├── train/
│ │ ├── img001.jpg
│ │ ├── img002.jpg
│ │ └── ...
│ └── val/
│ ├── img101.jpg
│ └── ...
├── labels/
│ ├── train/
│ │ ├── img001.txt
│ │ ├── img002.txt
│ │ └── ...
│ └── val/
│ ├── img101.txt
│ └── ...
└── custom.yaml
4.3 数据增强策略
YOLOv5在训练时集成了丰富的数据增强策略:
python
# 数据增强参数(在hyp.scratch-low.yaml中定义)
hsv_h: 0.015 # HSV-Hue增强(色调变化幅度)
hsv_s: 0.7 # HSV-Saturation增强(饱和度变化幅度)
hsv_v: 0.4 # HSV-Value增强(亮度变化幅度)
degrees: 0.0 # 旋转角度(±degrees)
translate: 0.1 # 平移比例(±translate)
scale: 0.5 # 缩放比例(±scale,即0.5-1.5倍)
shear: 0.0 # 剪切角度(±shear)
perspective: 0.0 # 透视变换幅度
flipud: 0.0 # 上下翻转概率
fliplr: 0.5 # 左右翻转概率
mosaic: 1.0 # Mosaic增强概率
mixup: 0.0 # MixUp增强概率
copy_paste: 0.0 # 复制粘贴增强概率(分割任务)
Mosaic数据增强是YOLOv5最核心的增强策略:
┌────────────────────────────────────┐
│ Mosaic 增强原理 │
├────────────────────────────────────┤
│ │
│ ┌──────────┬──────────┐ │
│ │ │ │ │
│ │ 图1 │ 图2 │ │
│ │ (随机) │ (随机) │ │
│ │ │ │ │
│ ├──────────┼──────────┤ │
│ │ │ │ │
│ │ 图3 │ 图4 │ │
│ │ (随机) │ (随机) │ │
│ │ │ │ │
│ └──────────┴──────────┘ │
│ │
│ 4张随机图片 → 随机裁剪 → 拼接 │
│ → 统一缩放到640×640 │
│ │
│ 优势: │
│ 1. 丰富背景多样性 │
│ 2. 增加小目标样本 │
│ 3. 4倍batch的等效训练效果 │
│ 4. 减少GPU显存需求 │
└────────────────────────────────────┘
五、模型实现详解
5.1 网络结构定义
YOLOv5通过YAML配置文件定义网络结构,以yolov5s.yaml为例:
yaml
# YOLOv5s 模型配置
nc: 80 # 类别数
depth_multiple: 0.33 # 深度系数(控制C3中Bottleneck重复次数)
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
backbone:
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2: 输入→64通道,k=6,s=2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4: 64→128通道,k=3,s=2
[-1, 3, C3, [128]], # 2: C3模块,3个Bottleneck
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8: 128→256通道
[-1, 6, C3, [256]], # 4: C3模块,6个Bottleneck
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16: 256→512通道
[-1, 9, C3, [512]], # 6: C3模块,9个Bottleneck
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32: 512→1024通道
[-1, 3, C3, [1024]], # 8: C3模块,3个Bottleneck
[-1, 1, SPPF, [1024, 5]], # 9: SPPF模块
]
# Head (PANet)
head:
[[-1, 1, Conv, [512, 1, 1]], # 10
[-1, 1, nn.Upsample, [None, 2, 'nearest']], # 11: 上采样2倍
[[-1, 6], 1, Concat, [1]], # 12: 拼接P4特征
[-1, 3, C3, [512, False]], # 13: C3(无shortcut)
[-1, 1, Conv, [256, 1, 1]], # 14
[-1, 1, nn.Upsample, [None, 2, 'nearest']], # 15: 上采样2倍
[[-1, 4], 1, Concat, [1]], # 16: 拼接P3特征
[-1, 3, C3, [256, False]], # 17: P3/8-small输出
[-1, 1, Conv, [256, 3, 2]], # 18: 下采样
[[-1, 14], 1, Concat, [1]], # 19: 拼接
[-1, 3, C3, [512, False]], # 20: P4/16-medium输出
[-1, 1, Conv, [512, 3, 2]], # 21: 下采样
[[-1, 10], 1, Concat, [1]], # 22: 拼接
[-1, 3, C3, [1024, False]], # 23: P5/32-large输出
[[17, 20, 23], 1, Detect, [nc, anchors]], # 24: 检测头
]
YAML配置解析核心代码:
python
def parse_model(d, ch): # model_dict, input_channels
anchors, nc, gd, gw, act = d['anchors'], d['nc'], \
d['depth_multiple'], d['width_multiple'], d.get('activation')
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors
no = na * (nc + 5) # 输出数 = 锚框数 × (类别数 + 5)
layers, save, c2 = [], [], ch[-1]
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):
m = eval(m) if isinstance(m, str) else m # 字符串→类
for j, a in enumerate(args):
args[j] = eval(a) if isinstance(a, str) else a
n = n_ = max(round(n * gd), 1) if n > 1 else n # 应用深度系数
if m in {Conv, C3, SPPF, ...}:
c1, c2 = ch[f], args[0]
if c2 != no:
c2 = make_divisible(c2 * gw, 8) # 应用宽度系数
args = [c1, c2, *args[1:]]
if m in {C3, C3TR, ...}:
args.insert(2, n) # 插入重复次数
n = 1
elif m is Concat:
c2 = sum(ch[x] for x in f)
elif m in {Detect, Segment}:
args.append([ch[x] for x in f])
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)
layers.append(m_)
ch.append(c2)
return nn.Sequential(*layers), sorted(save)
5.2 损失函数设计
详见2.3.2节,此处补充Focal Loss的实现:
python
class FocalLoss(nn.Module):
"""
Focal Loss用于解决类别不平衡问题
公式: FL(p_t) = -α_t × (1 - p_t)^γ × log(p_t)
"""
def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
super().__init__()
self.loss_fcn = loss_fcn # 基础损失函数(BCEWithLogitsLoss)
self.gamma = gamma # 聚焦参数
self.alpha = alpha # 平衡参数
self.reduction = loss_fcn.reduction
self.loss_fcn.reduction = 'none'
def forward(self, pred, true):
loss = self.loss_fcn(pred, true)
# p_t: 模型对真实类别的预测概率
pred_prob = torch.sigmoid(pred)
p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
# α_t: 类别权重因子
alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
# (1-p_t)^γ: 调制因子,降低易分类样本的权重
modulating_factor = (1.0 - p_t) ** self.gamma
loss *= alpha_factor * modulating_factor
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
else:
return loss
5.3 训练策略与超参数
yaml
# data/hyps/hyp.scratch-low.yaml - 推荐训练超参数
lr0: 0.01 # 初始学习率(SGD)
lrf: 0.01 # 最终学习率 = lr0 × lrf
momentum: 0.937 # SGD动量
weight_decay: 0.0005 # 权重衰减
warmup_epochs: 3.0 # 预热epoch数
warmup_momentum: 0.8 # 预热初始动量
warmup_bias_lr: 0.1 # 预热偏置学习率
box: 0.05 # 边界框损失权重
cls: 0.5 # 分类损失权重
cls_pw: 1.0 # 分类BCE正样本权重
obj: 1.0 # 目标损失权重
obj_pw: 1.0 # 目标BCE正样本权重
iou_t: 0.20 # IoU训练阈值
anchor_t: 4.0 # 锚框匹配阈值
fl_gamma: 0.0 # Focal Loss gamma(0=不使用)
学习率调度策略:
python
# 单周期余弦退火学习率
def one_cycle(y1=0.0, y2=1.0, steps=100):
# y = y2 + (y1 - y2) * (1 + cos(π * x / steps)) / 2
return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
# 线性衰减
lf = lambda x: (1 - x / epochs) * (1.0 - hyp['lrf']) + hyp['lrf']
5.4 完整训练代码
python
# train.py 核心训练循环
def train(hyp, opt, device, callbacks):
# === 1. 初始化 ===
save_dir, epochs, batch_size = Path(opt.save_dir), opt.epochs, opt.batch_size
# === 2. 模型创建 ===
if pretrained:
# 加载预训练权重
ckpt = torch.load(weights, map_location='cpu')
model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc).to(device)
csd = ckpt['model'].float().state_dict()
csd = intersect_dicts(csd, model.state_dict()) # 键交集匹配
model.load_state_dict(csd, strict=False)
else:
model = Model(cfg, ch=3, nc=nc).to(device) # 从头训练
# === 3. 优化器 ===
# 参数分组:权重使用weight_decay,偏置不使用
optimizer = smart_optimizer(model, opt.optimizer,
hyp['lr0'], hyp['momentum'], hyp['weight_decay'])
# === 4. 学习率调度器 ===
if opt.cos_lr:
lf = one_cycle(1, hyp['lrf'], epochs) # 余弦退火
else:
lf = lambda x: (1 - x / epochs) * (1.0 - hyp['lrf']) + hyp['lrf'] # 线性
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)
# === 5. EMA(指数移动平均) ===
ema = ModelEMA(model)
# === 6. 自动混合精度 ===
scaler = torch.cuda.amp.GradScaler(enabled=amp)
# === 7. 损失函数 ===
compute_loss = ComputeLoss(model)
# === 8. 训练循环 ===
for epoch in range(start_epoch, epochs):
model.train()
# 更新图像权重(可选)
if opt.image_weights:
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw)
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n)
mloss = torch.zeros(3, device=device) # [box, obj, cls]
for i, (imgs, targets, paths, _) in enumerate(pbar):
ni = i + nb * epoch # 全局迭代次数
imgs = imgs.to(device).float() / 255 # 归一化到[0,1]
# 预热阶段
if ni <= nw:
xi = [0, nw]
accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round())
for j, x in enumerate(optimizer.param_groups):
x['lr'] = np.interp(ni, xi,
[hyp['warmup_bias_lr'] if j == 0 else 0.0, x['initial_lr'] * lf(epoch)])
# 多尺度训练
if opt.multi_scale:
sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs
sf = sz / max(imgs.shape[2:])
if sf != 1:
ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]]
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear')
# 前向传播
with torch.cuda.amp.autocast(amp):
pred = model(imgs)
loss, loss_items = compute_loss(pred, targets.to(device))
# 反向传播(梯度累积)
scaler.scale(loss).backward()
# 参数更新
if ni - last_opt_step >= accumulate:
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
if ema:
ema.update(model)
last_opt_step = ni
# 日志记录
mloss = (mloss * i + loss_items) / (i + 1)
# 学习率更新
scheduler.step()
# 验证
if not noval or final_epoch:
results, maps, _ = validate.run(data_dict, ...)
# 保存模型
fi = fitness(np.array(results).reshape(1, -1))
if fi > best_fitness:
best_fitness = fi
ckpt = {'epoch': epoch, 'model': deepcopy(de_parallel(model)).half(), ...}
torch.save(ckpt, last)
if best_fitness == fi:
torch.save(ckpt, best)
六、模型训练与调优
6.1 训练流程
bash
# === 基础训练命令 ===
# 1. 从头训练(使用yolov5s配置)
python train.py --data coco128.yaml --cfg yolov5s.yaml --weights '' --batch-size 16 --epochs 300
# 2. 使用预训练权重微调(推荐)
python train.py --data custom.yaml --weights yolov5s.pt --batch-size 16 --epochs 100
# 3. 多GPU分布式训练
python -m torch.distributed.run --nproc_per_node 4 train.py \
--data custom.yaml --weights yolov5s.pt --batch-size 64 --device 0,1,2,3
# 4. 恢复训练
python train.py --resume
# 5. 冻结骨干网络训练
python train.py --data custom.yaml --weights yolov5s.pt --freeze 10
6.2 训练技巧
6.2.1 自动批量大小
bash
# 自动检测最优batch size(避免OOM)
python train.py --data custom.yaml --weights yolov5s.pt --batch-size -1
6.2.2 多尺度训练
bash
# 开启多尺度训练(每10个batch随机改变输入尺寸)
python train.py --data custom.yaml --weights yolov5s.pt --multi-scale
6.2.3 超参数进化
bash
# 遗传算法自动搜索最优超参数(300代)
python train.py --data custom.yaml --weights yolov5s.pt --evolve 300
6.2.4 缓存策略
bash
# 将图片缓存到RAM(加速训练,需要足够内存)
python train.py --data custom.yaml --weights yolov5s.pt --cache ram
# 缓存到磁盘
python train.py --data custom.yaml --weights yolov5s.pt --cache disk
6.3 超参数调优
| 超参数 | 默认值 | 调优建议 |
|---|---|---|
| lr0 | 0.01 | 小数据集降低到0.001,大数据集可到0.1 |
| lrf | 0.01 | 通常保持0.01-0.1 |
| momentum | 0.937 | 0.8-0.98 |
| weight_decay | 0.0005 | 0.0001-0.001 |
| warmup_epochs | 3.0 | 大数据集增加到5 |
| box | 0.05 | 提高对小目标检测的重视 |
| cls | 0.5 | 类别不均衡时调整 |
| obj | 1.0 | 正负样本不均衡时调整 |
| iou_t | 0.20 | 提高可增加正样本数量 |
| anchor_t | 4.0 | 提高可匹配更多锚框 |
七、模型评估与分析
7.1 评估指标
YOLOv5使用以下核心指标评估模型性能:
(1)精确率(Precision)与召回率(Recall)
Precision = TP / (TP + FP) # 预测为正的样本中真正为正的比例
Recall = TP / (TP + FN) # 所有正样本中被正确预测的比例
(2)mAP(mean Average Precision)
AP = ∫₀¹ P(R) dR # PR曲线下面积
mAP@0.5: IoU阈值=0.5时的mAP
mAP@0.5:0.95: IoU阈值从0.5到0.95(步长0.05)的平均mAP
(3)Fitness(综合适应度)
python
def fitness(x):
# 综合评估指标:[P, R, mAP@0.5, mAP@0.5:0.95]
w = [0.0, 0.0, 0.1, 0.9] # 权重:更重视mAP@0.5:0.95
return (x[:, :4] * w).sum(1)
7.2 实验结果
YOLOv5各模型在COCO val2017上的性能对比:
| 模型 | 尺寸(px) | mAP@0.5:0.95 | mAP@0.5 | 参数量 | FLOPs | 速度(ms) |
|---|---|---|---|---|---|---|
| YOLOv5n | 640 | 28.0% | 45.7% | 1.9M | 4.5G | 6.3 |
| YOLOv5s | 640 | 37.4% | 56.8% | 7.2M | 16.5G | 6.4 |
| YOLOv5m | 640 | 45.4% | 64.1% | 21.2M | 49.0G | 8.2 |
| YOLOv5l | 640 | 49.0% | 67.3% | 46.5M | 109.1G | 10.1 |
| YOLOv5x | 640 | 50.7% | 68.9% | 86.7M | 205.7G | 12.1 |
速度测试环境:V100 GPU,batch=1,PyTorch FP16
7.3 消融实验
| 配置 | mAP@0.5:0.95 | 说明 |
|---|---|---|
| Baseline (CSPDarknet) | 35.2% | 基础配置 |
| + SPPF | 36.1% | 替换SPP为SPPF |
| + PANet | 37.0% | 添加PANet Neck |
| + Mosaic增强 | 37.4% | 添加Mosaic数据增强 |
| + SiLU激活 | 37.8% | LeakyReLU→SiLU |
| + AutoAnchor | 37.4% | 自适应锚框(对COCO提升不明显) |
7.4 可视化分析
特征图可视化:
python
# 可视化模型中间层特征图
python detect.py --weights yolov5s.pt --source image.jpg --visualize
训练过程可视化:
训练完成后,在runs/train/exp/目录下可查看:
results.png:损失曲线、mAP曲线、学习率曲线confusion_matrix.png:混淆矩阵labels.jpg:标签分布统计train_batch*.jpg:训练batch可视化(含Mosaic增强效果)
八、推理部署
8.1 模型导出
YOLOv5支持导出到多种推理框架:
bash
# 导出ONNX
python export.py --weights yolov5s.pt --include onnx --img 640
# 导出TensorRT
python export.py --weights yolov5s.pt --include engine --device 0
# 导出OpenVINO
python export.py --weights yolov5s.pt --include openvino
# 导出CoreML (macOS)
python export.py --weights yolov5s.pt --include coreml
# 导出TFLite
python export.py --weights yolov5s.pt --include tflite
# 一次性导出所有格式
python export.py --weights yolov5s.pt --include torchscript onnx openvino engine coreml tflite
8.2 推理代码
8.2.1 PyTorch推理
python
# detect.py 核心推理流程
import torch
from models.common import DetectMultiBackend
from utils.general import non_max_suppression, scale_boxes
from utils.dataloaders import LoadImages
# 1. 加载模型
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
model = DetectMultiBackend('yolov5s.pt', device=device)
stride, names = model.stride, model.names
# 2. 加载数据
dataset = LoadImages('data/images/', img_size=640, stride=stride, auto=model.pt)
# 3. 推理循环
for path, im, im0s, vid_cap, s in dataset:
# 预处理
im = torch.from_numpy(im).to(device)
im = im.half() if model.fp16 else im.float()
im /= 255 # 归一化
if len(im.shape) == 3:
im = im[None] # 添加batch维度
# 推理
pred = model(im)
# NMS后处理
pred = non_max_suppression(pred, conf_thres=0.25, iou_thres=0.45)
# 处理检测结果
for i, det in enumerate(pred):
if len(det):
# 缩放边界框到原始图像尺寸
det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0s.shape).round()
for *xyxy, conf, cls in reversed(det):
label = f'{names[int(cls)]} {conf:.2f}'
print(f'检测到: {label}, 位置: {xyxy}')
8.2.2 自定义Python推理脚本
python
# custom_detect.py - 自定义推理脚本
import torch
import cv2
import numpy as np
def detect_image(model_path, image_path, conf_thres=0.25, iou_thres=0.45):
"""
使用YOLOv5对单张图片进行目标检测
Args:
model_path: 模型权重路径
image_path: 图片路径
conf_thres: 置信度阈值
iou_thres: NMS IoU阈值
"""
# 加载模型
model = torch.hub.load('ultralytics/yolov5', 'custom',
path=model_path, force_reload=True)
model.conf = conf_thres
model.iou = iou_thres
# 读取图片
img = cv2.imread(image_path)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 推理
results = model(img_rgb)
# 解析结果
detections = results.pandas().xyxy[0] # DataFrame格式
# 绘制检测框
for _, row in detections.iterrows():
x1, y1, x2, y2 = int(row['xmin']), int(row['ymin']), \
int(row['xmax']), int(row['ymax'])
conf = row['confidence']
cls_name = row['name']
# 绘制矩形框
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
# 绘制标签
label = f'{cls_name} {conf:.2f}'
cv2.putText(img, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
# 保存结果
cv2.imwrite('result.jpg', img)
print(f'检测到 {len(detections)} 个目标')
return detections
# 使用示例
if __name__ == '__main__':
results = detect_image('yolov5s.pt', 'test.jpg')
print(results[['name', 'confidence']])
8.3 性能优化
| 优化策略 | 方法 | 加速比 |
|---|---|---|
| FP16推理 | model.half() |
1.5-2x |
| TensorRT | 导出为.engine | 3-5x |
| 批量推理 | batch_size > 1 | 2-4x |
| 模型剪枝 | 结构化剪枝 | 1.5-2x |
| 量化(INT8) | TFLite量化 | 2-3x |
| 矩形推理 | --img 640 480 |
1.3x |
python
# FP16推理示例
model = DetectMultiBackend('yolov5s.pt', device='cuda:0', fp16=True)
# 批量推理示例
imgs = torch.randn(4, 3, 640, 640).cuda().half() # batch=4
pred = model(imgs)
九、常见错误与避坑指南
错误1:CUDA Out of Memory (OOM)
错误信息:
RuntimeError: CUDA out of memory. Tried to allocate 128.00 MiB
原因分析:
- batch_size设置过大
- 图片分辨率过高
- 其他进程占用显存
- 模型过大(如yolov5x在低显存GPU上)
解决方案:
bash
# 方案1:减小batch size
python train.py --data custom.yaml --weights yolov5s.pt --batch-size 8
# 方案2:减小输入尺寸
python train.py --data custom.yaml --weights yolov5s.pt --img 320
# 方案3:使用自动batch size
python train.py --data custom.yaml --weights yolov5s.pt --batch-size -1
# 方案4:使用更小的模型
python train.py --data custom.yaml --cfg yolov5n.yaml --weights yolov5n.pt
# 方案5:清理GPU缓存
import torch
torch.cuda.empty_cache()
错误2:标签格式不正确导致训练失败
错误信息:
AssertionError: Label class 5 exceeds nc=3 in data/custom.yaml
原因分析:
- 标注文件中类别索引超出配置的类别数
- 类别编号未从0开始
- 标注坐标未归一化(超出0,1范围)
解决方案:
python
# 标签验证脚本
import os
from pathlib import Path
def validate_labels(label_dir, num_classes):
"""
验证YOLO格式标签的正确性
Args:
label_dir: 标签文件夹路径
num_classes: 类别总数
"""
errors = []
for label_file in Path(label_dir).glob('*.txt'):
with open(label_file, 'r') as f:
for line_num, line in enumerate(f, 1):
parts = line.strip().split()
if len(parts) != 5:
errors.append(f'{label_file.name}:{line_num} - 格式错误,需要5个值')
continue
cls_id = int(parts[0])
cx, cy, w, h = map(float, parts[1:])
# 检查类别索引
if cls_id < 0 or cls_id >= num_classes:
errors.append(f'{label_file.name}:{line_num} - 类别{cls_id}超出范围[0,{num_classes-1}]')
# 检查坐标归一化
if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1):
errors.append(f'{label_file.name}:{line_num} - 坐标未归一化到[0,1]')
if errors:
print(f'发现 {len(errors)} 个错误:')
for e in errors:
print(f' - {e}')
else:
print('所有标签验证通过!')
# 使用
validate_labels('dataset/labels/train/', num_classes=3)
错误3:训练loss不下降或NaN
错误信息:
loss: nan 或 loss持续不下降
原因分析:
- 学习率过大导致梯度爆炸
- 数据集中存在损坏图片
- 标注框坐标异常(w/h为0或负数)
- 混合精度训练不稳定
解决方案:
bash
# 方案1:降低学习率
python train.py --data custom.yaml --weights yolov5s.pt --hyp data/hyps/hyp.scratch-low.yaml
# 方案2:禁用AMP混合精度
# 修改train.py中:amp = False
# 方案3:检查数据集完整性
python -c "
from utils.dataloaders import create_dataloader
import yaml
with open('data/custom.yaml') as f:
data = yaml.safe_load(f)
loader, dataset = create_dataloader(data['train'], 640, 16, 32,
single_cls=False, hyp={}, augment=False)
print(f'数据集大小: {len(dataset)}')
print(f'类别分布: {dataset.labels[:, 0].unique()}')
"
# 方案4:检查损坏图片
from PIL import Image
from pathlib import Path
for img_path in Path('dataset/images/train/').glob('*'):
try:
img = Image.open(img_path)
img.verify() # 验证图片完整性
except Exception as e:
print(f'损坏图片: {img_path} - {e}')
错误4:mAP很低或模型不收敛
原因分析:
- 数据集太小(<100张图片)
- 类别极度不平衡
- 标注质量差
- 超参数不适合当前数据集
解决方案:
bash
# 方案1:增加训练epoch
python train.py --data custom.yaml --weights yolov5s.pt --epochs 500
# 方案2:调整锚框(AutoAnchor)
python train.py --data custom.yaml --weights yolov5s.pt --noautoanchor # 先禁用
# 观察自动计算的锚框是否合适
# 方案3:使用更强的数据增强
# 修改hyp.yaml中:
mosaic: 1.0 # 开启Mosaic
mixup: 0.1 # 开启MixUp
scale: 0.9 # 增大缩放范围
translate: 0.2 # 增大平移范围
# 方案4:调整损失权重
box: 0.1 # 提高边界框损失权重(小目标场景)
cls: 1.0 # 提高分类损失权重(类别不均衡场景)
错误5:ONNX导出后推理结果不一致
原因分析:
- 预处理方式不一致(BGR vs RGB、归一化范围)
- 输入尺寸不匹配
- NMS参数不同
- ONNX opset版本兼容性
解决方案:
python
# 验证ONNX模型精度
import onnxruntime
import numpy as np
import torch
def compare_onnx_pytorch(pt_model_path, onnx_model_path, test_image):
"""
对比PyTorch和ONNX模型的推理结果
"""
# PyTorch推理
model_pt = torch.hub.load('ultralytics/yolov5', 'custom', path=pt_model_path)
results_pt = model_pt(test_image)
# ONNX推理
session = onnxruntime.InferenceSession(onnx_model_path)
# 预处理(与YOLOv5保持一致)
img = cv2.imread(test_image)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (640, 640))
img = img.transpose(2, 0, 1) # HWC → CHW
img = np.expand_dims(img, 0).astype(np.float32) / 255.0
# 推理
outputs = session.run(None, {'images': img})
# 对比结果
print(f'PyTorch检测数: {len(results_pt.xyxy[0])}')
print(f'ONNX输出shape: {outputs[0].shape}')
# 检查差异
pt_output = results_pt.xyxy[0].values
onnx_output = outputs[0]
return results_pt, outputs
# 使用
compare_onnx_pytorch('yolov5s.pt', 'yolov5s.onnx', 'test.jpg')
十、扩展与进阶
10.1 改进方向
1. 注意力机制集成
在C3模块中集成SE、CBAM、ECA等注意力机制:
python
class SELayer(nn.Module):
"""Squeeze-and-Excitation注意力模块"""
def __init__(self, c, r=16):
super().__init__()
self.squeeze = nn.AdaptiveAvgPool2d(1)
self.excitation = nn.Sequential(
nn.Linear(c, c // r, bias=False),
nn.ReLU(inplace=True),
nn.Linear(c // r, c, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.squeeze(x).view(b, c)
y = self.excitation(y).view(b, c, 1, 1)
return x * y.expand_as(x)
2. 小目标检测优化
- 增加P2检测层(160×160特征图)
- 使用TPH-YOLOv5的Transformer Prediction Head
- 提高输入分辨率到1280×1280
3. 轻量化改进
- 使用MobileNetV3/ShuffleNetV2替换Backbone
- 使用深度可分离卷积
- 模型剪枝 + 知识蒸馏
4. Transformer集成
YOLOv5已内置C3TR模块(C3 + TransformerBlock),可在配置文件中使用:
yaml
# 将C3替换为C3TR
[-1, 3, C3TR, [256]]
10.2 相关论文推荐
| 论文 | 核心贡献 | 链接 |
|---|---|---|
| YOLOv4: Optimal Speed and Accuracy of Object Detection | Bag of Freebies/Specials | arXiv:2004.10934 |
| CSPNet: A New Backbone that can Enhance Learning Capability of CNN | 跨阶段局部网络 | arXiv:1911.11929 |
| Path Aggregation Network for Instance Segmentation | PANet特征融合 | arXiv:1803.01534 |
| Focal Loss for Dense Object Detection | 解决类别不平衡 | arXiv:1708.02002 |
| Generalized Focal Loss | 质量感知的Focal Loss | arXiv:2006.04388 |
| Distance-IoU Loss | CIoU/DIoU损失函数 | arXiv:1911.08287 |
| YOLOv7: Trainable bag-of-freebies | 最新YOLO系列 | arXiv:2207.02696 |
参考链接
- YOLOv5官方仓库 - GitHub
- YOLOv5官方文档
- YOLOv5训练自定义数据教程
- YOLOv5模型导出指南
- YOLOv5 + DeepSort多目标跟踪
- COCO数据集官网
- PyTorch官方文档
总结与下篇预告
本文总结
本文从原理到实践全面解析了YOLOv5目标检测框架,涵盖以下核心内容:
- 架构解析:深入剖析了CSPDarknet骨干网络、PANet特征金字塔和YOLO检测头的设计原理
- 数学推导:详细推导了CIoU损失函数、Focal Loss和锚框匹配策略的数学原理
- 代码实战:提供了完整的训练、推理、模型导出代码,每行关键代码均有注释
- 避坑指南:总结了5个最常见的训练和部署错误及解决方案
- 性能优化:介绍了FP16推理、TensorRT加速、模型量化等部署优化方案
YOLOv5以其出色的工程化设计和丰富的工具链,成为目标检测领域最受欢迎的框架之一。掌握YOLOv5不仅是学习目标检测的捷径,更是深入理解现代深度学习工程实践的最佳案例。
下篇预告
下一篇我们将进入目标检测系列第3篇 :Face Mask Detection(人脸口罩检测)------使用Python、Keras、OpenCV和MobileNet实现实时视频流中的人脸口罩检测。该项目将展示如何将轻量级分类网络MobileNet应用于实际疫情防护场景,敬请期待!