YOLOv5目标检测实战全解析:从原理到部署的PyTorch完整指南

YOLOv5目标检测实战全解析:从原理到部署的PyTorch完整指南

文章目录


一、项目背景与意义

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 技术挑战

目标检测面临的核心技术挑战包括:

  1. 多尺度问题:同一图像中物体尺度差异巨大(从几个像素到整张图),模型需要同时检测大小目标
  2. 密集场景:拥挤场景中目标严重重叠,NMS后处理难以区分
  3. 实时性要求:工业场景要求毫秒级推理延迟
  4. 小目标检测:小目标信息量少,特征容易在深层网络中丢失
  5. 类别不平衡:前景-背景样本极度不平衡

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目标检测框架,涵盖以下核心内容:

  1. 架构解析:深入剖析了CSPDarknet骨干网络、PANet特征金字塔和YOLO检测头的设计原理
  2. 数学推导:详细推导了CIoU损失函数、Focal Loss和锚框匹配策略的数学原理
  3. 代码实战:提供了完整的训练、推理、模型导出代码,每行关键代码均有注释
  4. 避坑指南:总结了5个最常见的训练和部署错误及解决方案
  5. 性能优化:介绍了FP16推理、TensorRT加速、模型量化等部署优化方案

YOLOv5以其出色的工程化设计和丰富的工具链,成为目标检测领域最受欢迎的框架之一。掌握YOLOv5不仅是学习目标检测的捷径,更是深入理解现代深度学习工程实践的最佳案例。

下篇预告

下一篇我们将进入目标检测系列第3篇Face Mask Detection(人脸口罩检测)------使用Python、Keras、OpenCV和MobileNet实现实时视频流中的人脸口罩检测。该项目将展示如何将轻量级分类网络MobileNet应用于实际疫情防护场景,敬请期待!