|
## CNN目标检测发展史
### R-CNN(2014)
- **核心思想**:先用 Selective Search 提取候选区域,再用 CNN 进行特征提取和分类
- **流程**:Region Proposals → CNN 特征提取 → SVM 分类 → Bbox 回归
- **缺点**:训练复杂(多阶段),测试速度慢(每张图约 47 秒)
### Fast R-CNN(2015)
- **改进**:共享卷积计算,引入 RoI Pooling
- **优势**:训练和测试速度显著提升
- **仍存在问题**:仍依赖 Selective Search 生成候选区域
### Faster R-CNN(2015)
- **突破**:提出 Region Proposal Network(RPN)
- **创新**:用神经网络替代 Selective Search,实现端到端训练
- **影响**:成为 Two-Stage 检测器的典范
### 两阶段 vs 单阶段
| 类型 | 特点 | 优势 | 劣势 |
|-----|------|------|------|
| Two-Stage | 先生成候选框,再检测分类 | 精度高 | 速度慢 |
| One-Stage | 直接输出检测结果 | 速度快 | 精度略低 |
|
|
## YOLO核心思想
### "You Only Look Once"
YOLO(You Only Look Once)将目标检测视为**回归问题**,通过单次前向传播直接预测图像中所有目标的位置和类别。
### 核心特点
1. **统一框架**:用单个神经网络同时完成目标定位和分类
2. **全局推理**:看到整张图像,可利用上下文信息
3. **实时检测**:45 FPS(Fast YOLO 可达 155 FPS)
### 工作原理
1. **网格划分**:将输入图像划分为 S×S 网格
2. **预测机制**:
- 每个网格负责预测其中心点落在该网格内的目标
- 每个网格预测 B 个边界框和 C 个类别概率
3. **输出张量**:S × S × (B×5 + C)
- 5 = (x, y, w, h, confidence)
### 关键概念
**置信度(Confidence)**:
```
Confidence = Pr(Object) × IoU_pred^truth
```
- Pr(Object):该网格是否包含目标(0 或 1)
- IoU:预测框与真实框的重叠度
**边界框预测**:
- tx, ty, tw, th 是网络输出
- 实际坐标通过指数和偏移计算:
- bx = σ(tx) + cx
- by = σ(ty) + cy
- bw = pw × e^tw
- bh = ph × e^th
|
|
## YOLOv1网络结构
### 整体架构
YOLOv1 基于 GoogLeNet 改进,共 24 层卷积层 + 2 层全连接层
```
输入: 448×448×3
↓
卷积层 (多个)
↓
全连接层
↓
输出: 7×7×30 (S=7, B=2, C=20)
```
### 网络细节
**卷积层**:
- 前 20 层来自预训练的 GoogLeNet
- 后 4 层为随机初始化
- 使用 1×1 降维层和 3×3 卷积层交替
**全连接层**:
- 将卷积特征展平
- 输出 1470 维向量(7×7×30)
- 重塑为 7×7×30 的预测张量
### 训练策略
1. **预训练**:先在 ImageNet 分类任务上预训练
2. **微调**:再在检测数据集上微调
3. **输入分辨率**:从 224×224 提升到 448×448 以获得更精细的特征
### 输出解释
对于 PASCAL VOC 数据集(20 类):
- S = 7(7×7 网格)
- B = 2(每个网格 2 个预测框)
- C = 20(20 个类别)
- 输出维度:7×7×(2×5 + 20) = 7×7×30
|
|
## 损失函数详解
YOLOv1 使用多部分损失函数,通过求和误差平方进行优化:
### 损失组成
```
Loss = λ_coord × L_coord + L_conf + λ_noobj × L_noobj + L_class
```
### 1. 坐标损失(L_coord)
仅对包含目标的边界框计算:
```
∑_i=0^S² ∑_j=0^B 1ij^obj [(xi - x̂i)² + (yi - ŷi)²]
+ ∑_i=0^S² ∑_j=0^B 1ij^obj [(√wi - √ŵi)² + (√hi - √ĥi)²]
```
- 对宽高取平方根,减小大尺寸和小尺寸的差异
- 对于小于 1 的数,开方会放大变化(如 0.04 开方 = 0.2),让小物体误差更敏感
### 2. 置信度损失(含目标)
```
∑_i=0^S² ∑_j=0^B 1ij^obj (Ci - Ĉi)²
```
### 3. 置信度损失(无目标)
```
λ_noobj × ∑_i=0^S² ∑_j=0^B 1ij^noobj (Ci - Ĉi)²
```
- λ_noobj = 0.5,降低无目标框的影响
### 4. 分类损失
```
∑_i=0^S² 1i^obj ∑_c∈classes (pi - p̂i)²
```
### 超参数
- λ_coord = 5:增加坐标损失的权重
- λ_noobj = 0.5:降低无目标置信度的权重
### NMS 后处理
训练后使用 Non-Maximum Suppression 去除重复检测:
- 按置信度排序
- 选择最高置信度框
- 删除与其 IoU > 阈值的其他框
- 重复上述过程
|
|
## YOLOv1优缺点分析
### 优点
| 优点 | 说明 |
|-----|------|
| **速度快** | 单次前向传播,45 FPS,可达实时检测 |
| **全局视野** | 利用整图上下文,减少背景误检 |
| **泛化能力强** | 在艺术图像等非自然图像上表现好 |
| **端到端** | 统一框架,无需复杂的候选区域生成 |
### 缺点
| 缺点 | 说明 | 改进方向 |
|-----|------|---------|
| **定位精度** | 相比 Two-Stage 方法略低 | 增加边界框预测数量 |
| **小目标检测** | 网格划分导致小目标漏检 | 增加网格密度、FPN |
| **密集目标** | 同一网格多目标冲突 | Anchor 机制、聚类 |
| **类别不平衡** | 正负样本比例失衡 | Focal Loss |
### 适用场景
**YOLO 适合**:
- 实时检测需求(视频监控、自动驾驶)
- 大中尺寸目标检测
- 计算资源受限场景
**Two-Stage 适合**:
- 高精度要求
- 小目标密集场景
- 离线批处理任务
### 性能对比(VOC 2007 测试集)
| 模型 | mAP | FPS |
|-----|-----|-----|
| R-CNN | 66.0% | 0.02 |
| Fast R-CNN | 70.0% | 0.5 |
| Faster R-CNN | 73.2% | 7 |
| YOLOv1 | 63.4% | 45 |
| Fast YOLO | 52.7% | 155 |
|
|
## YOLOv2 / YOLO9000
### 核心改进
YOLOv2 在保持速度的同时大幅提升精度,并提出联合检测分类训练方法。
### 主要技术
**1. Batch Normalization**
- 所有卷积层添加 BN
- 提升收敛速度,正则化模型
- 可去除 dropout 而不发生过拟合
**2. 高分辨率分类器**
- 先在 224×224 上训练 10 个 epoch
- 再在 448×448 上微调
- 让网络适应高分辨率输入
**3. Anchor Boxes**
- 移除全连接层,使用 Anchor 机制
- 用 K-means 聚类确定 Anchor 尺寸
- 从数据中学习先验,提升召回率
**4. 多尺度训练**
- 随机选择输入尺寸:{320, 352, ..., 608}
- 网络每 10 个 epoch 改变一次尺寸
- 增强模型鲁棒性
**5. Dimension Clusters**
- 在训练集边界框上运行 K-means(k=5)
- 选择好的 Anchor 可提升 IoU
- 手工设计的 Anchor 平均 IoU=60.1%,聚类后=61.0%
**6. Direct Location Prediction**
- 预测相对于 Anchor 的偏移
- 简化网络输出,稳定训练
### Darknet-19 backbone
- 19 层卷积 + 5 层 MaxPooling
- 在 ImageNet 上达到 top-1 准确率 72.9%
- 轻量级设计,适合实时检测
### 性能
- VOC 2007 mAP: 78.6%
- 速度: 67 FPS (544×544 输入)
|
|
## YOLOv3与Darknet-53
### YOLOv3核心特点
YOLOv3 是 YOLO 系列中影响最大的版本之一,引入多尺度检测和更强的特征提取器。
### 主要改进
**1. Darknet-53 Backbone**
- 53 层卷积网络
- 类似 ResNet,使用残差连接
- 在 ImageNet 上 top-1 准确率 77.2%
- 更强的特征提取能力
**2. 多尺度预测**
- 在 3 个不同尺度上进行检测:
- 13×13(大目标,下采样 32 倍)
- 26×26(中目标,下采样 16 倍)
- 52×52(小目标,下采样 8 倍)
- 通过 FPN(Feature Pyramid Network)融合多尺度特征
**3. Anchor 机制改进**
- 每个尺度使用 3 个 Anchor
- 共 9 个 Anchor(通过聚类确定)
- 更好地适应不同尺寸目标
**4. 逻辑回归**
- 每个边界框独立预测目标性
- 不再依赖网格级别的目标性
### 网络结构
```
Darknet-53 (特征提取)
↓
FPN (多尺度特征融合)
↓
检测头 (3 个尺度)
↓
输出: 不同尺度的边界框+类别
```
### 性能
- COCO AP: 33.0%(与 SSD 相当,速度快 3 倍)
- VOC 2007 mAP: 81.5%
- 速度: 20 FPS (608×608)
### 特点总结
| 特性 | YOLOv2 | YOLOv3 |
|-----|--------|--------|
| Backbone | Darknet-19 | Darknet-53 |
| 检测尺度 | 单尺度 | 3 尺度 |
| Anchor | 5 个 | 9 个 |
| 小目标检测 | 较弱 | 显著改善 |
|
|
## YOLOv4创新技术
YOLOv4(2020)在 YOLOv3 基础上引入大量目标检测前沿技术,实现速度与精度的最佳平衡。
### 核心思想:Bag of Freebies & Bag of Specials
**Bag of Freebies(仅训练时代价)**:
- 数据增强:Mosaic、Mixup、CutMix
- 标签平滑
- CIoU Loss
**Bag of Specials(推理时代价)**:
- SPP、ASPP、RFB
- SAM(Spatial Attention Module)
- PANet
### 主要改进
**1. CSPDarknet-53**
- 引入 CSP(Cross Stage Partial)结构
- 减少计算量,增强梯度流
- 更好的特征融合
**2. PANet**
- Path Aggregation Network
- 自顶向下+自底向上的特征融合
- 更强的定位能力
**3. Mosaic 数据增强**
- 拼接 4 张图像进行训练
- 丰富背景,增强小目标检测
- 减少对大 Batch Size 的需求
**4. CIoU Loss**
- 综合考虑:
- 重叠面积
- 中心点距离
- 长宽比一致性
- 更快的收敛速度
**5. SAM(空间注意力)**
- 突出重要特征区域
- 抑制无关背景信息
### 性能
- COCO AP: 43.5%(AP50: 74.4%)
- Tesla V100 上实时速度:65 FPS
|
|
## YOLOv5/v6/v7
### YOLOv5(Ultralytics,2020)
**特点**:
- PyTorch 实现,易于使用和部署
- 模型家族:n/s/m/l/x(从小到大)
- 集成 Mosaic、AutoAnchor 等增强
- 完善的训练和部署工具链
**主要创新**:
1. **自适应锚框计算**:自动聚类最佳 Anchor
2. **自适应图片缩放**:保持长宽比的 Letterbox
3. **CSP 结构**:延续 YOLOv4 的 CSP 设计
4. **Focus 结构**:空间到深度的变换
**性能**:
- YOLOv5x:COCO AP 50.4%
- 训练友好,社区活跃
---
### YOLOv6(美团,2022)
**特点**:
- 专注工业部署
- RepVGG 结构(训练复杂,推理简单)
- 量化友好的设计
**主要创新**:
1. **Rep-PAN**:可重参数化的特征融合
2. **BiC**:训练时的调制器
3. **SIoU**:新的损失函数
**性能**:
- YOLOv6-L:COCO AP 52.8%
- 推理速度优异
---
### YOLOv7(2022)
**特点**:
- E-ELAN 结构:高效的网络架构
- 模型缩放策略
- 辅助头训练
**主要创新**:
1. **E-ELAN**:无限级联的梯度路径
2. **辅助头**:训练时使用,推理时移除
3. **深度可训练**:更精细的缩放
**性能**:
- YOLOv7-E6:COCO AP 55.9%
- 精度与速度的更优平衡
|
|
## YOLOv8及后续版本
### YOLOv8(Ultralytics,2023)
YOLOv8 是对 YOLO 系列的全面重构,引入新的架构设计。
### 主要变化
**1. Anchor-Free 设计**
- 移除 Anchor Box 机制
- 解耦头预测分类和回归
- 简化训练流程
**2. C2f 模块**
- 替代 C3 模块
- 更多跳层连接,更丰富的梯度流
- 类似于 YOLOv7 的 ELAN 思想
**3. 新的损失函数**
- 分类损失:BCELoss
- 回归损失:CIoU + DFL(Distribution Focal Loss)
- 更准确的边界框预测
**4. 任务统一**
- 检测、分割、分类、姿态统一架构
- 相同的 Backbone,不同的 Head
### 性能
| 模型 | mAP@50-95 | 参数量 | FPS (V100) |
|-----|----------|--------|-----------|
| YOLOv8n | 37.3 | 3.2M | 80 |
| YOLOv8s | 44.9 | 11.2M | 45 |
| YOLOv8m | 50.6 | 25.9M | 28 |
| YOLOv8l | 52.9 | 43.7M | 22 |
| YOLOv8x | 54.5 | 68.2M | 14 |
### 后续版本
**YOLOv9(2024)**:
- PGI(可编程梯度信息)
- GELAN(通用高效层聚合网络)
**YOLOv10(2024)**:
- 双标签分配策略
- NMS-free 训练
- 整体一致性监督
**YOLO11(2024)**:
- Ultralytics 官方推出的 YOLOv8 升级版
- 在保持高效的同时提升精度和速度
- 适用于生产环境的主流选择
**YOLO-World(2024)**:
- 开放词汇目标检测
- 支持文本提示进行零样本检测
- 拓展了 YOLO 的应用边界
|
|
## 感受野与特征图尺寸
### 常见误解
13×13 指的是**特征图的网格数量**,而不是 13×13 像素的物理尺寸。每个格子(Grid Cell)对应原始图像中的一个区域,而不是一个像素。
### 工作原理
1. **输入图像被网格化**:以 416×416 输入为例,YOLOv3 将其划分为 13×13 的网格
2. **网格负责管理区域内的物体**:如果物体中心点落在某个网格内,该网格负责预测该物体
3. **深层特征图拥有大感受野**:13×13 特征图经过 5 次下采样,每个点对应原始图像 32×32 像素的区域
### 为何大网格适合检测大物体?
这主要得益于**感受野**和**锚框**两个机制:
- **感受野大**:深层网络看到更广的原始图像区域
- **锚框大**:13×13 特征图配备最大的锚框,如 COCO 上的 (116×90)、(156×198)、(373×326)
### 三个尺度特征图对比
| 特征图尺寸 | 下采样倍数 | 感受野大小 | 配备锚框尺寸 | 检测目标 |
|-----------|-----------|-----------|-------------|---------|
| 13×13 | 32 倍 | 32×32 像素 | 最大 | 大物体 |
| 26×26 | 16 倍 | 16×16 像素 | 中等 | 中等物体 |
| 52×52 | 8 倍 | 8×8 像素 | 最小 | 小物体 |
### 特征图尺寸不是次要参数
如果强行把最后一层都设计成 52×52:
1. **感受野变小**:无法覆盖大物体
2. **定位精度变化**:网格太细或太粗都会影响检测
3. **语义信息层级不同**:深层语义 vs 浅层细节缺一不可
|
|
## Anchor Boxes 与维度聚类
### 为什么需要 Anchor?
如果使用的框更符合实际物体的样子,检测效果就会更好。YOLO 先学习数据集中物体的形状(通过聚类),再用这些学习过的框去匹配图像。
### K-means 维度聚类
**流程**:
1. 在训练集所有边界框上运行 K-means 聚类
2. 选择距离度量通常为 1 - IoU
3. 得到 k 个代表性框尺寸
**效果**:
- 手工设计 Anchor 平均 IoU ≈ 60.1%
- 聚类后 Anchor 平均 IoU ≈ 61.0%
- 召回率显著提升
### Direct Location Prediction
YOLOv2 起不再直接预测绝对坐标,而是预测相对于 Anchor 的偏移:
```
bx = σ(tx) + cx
by = σ(ty) + cy
bw = pw × e^tw
bh = ph × e^th
```
- tx, ty, tw, th:网络输出
- cx, cy:网格左上角坐标
- pw, ph:Anchor 的宽高
- σ:sigmoid 函数,限制偏移在 0~1 之间
### 各版本 Anchor 数量
| 版本 | Anchor 数量 | 说明 |
|-----|------------|------|
| YOLOv1 | 无 | 直接回归 |
| YOLOv2 | 5 | 单尺度检测 |
| YOLOv3 | 9 | 3 尺度,每尺度 3 个 |
| YOLOv4/v5 | 9 | 沿用聚类 Anchor |
| YOLOv8+ | 0 | Anchor-Free |
|
|
## Batch Normalization
### 基本概念
Batch Normalization(BN)对一个批次的数据做整体归一化处理,使其均值为 0、方差为 1:
```
BN(x) = γ × (x - μ_B) / √(σ_B² + ε) + β
```
- μ_B:批次均值
- σ_B²:批次方差
- γ, β:可学习的缩放和平移参数
- ε:防止除零的小常数
### 主要作用
1. **加速收敛**:允许使用更大的学习率
2. **正则化效果**:减少对 dropout 的依赖
3. **缓解内部协变量偏移**:稳定每层输入分布
4. **提升泛化能力**:测试时使用滑动平均的均值和方差
### 在 YOLO 中的应用
- YOLOv2 开始在所有卷积层后添加 BN
- 去除 dropout 后仍能有效防止过拟合
- 成为现代检测器的标准组件
### 训练 vs 推理
| 阶段 | 均值/方差来源 | 是否更新 γ, β |
|-----|--------------|--------------|
| 训练 | 当前 batch | 是 |
| 推理 | 滑动平均统计量 | 否 |
|
|
## 多尺度检测原理
### 核心思想
不同尺寸的目标需要不同层级的特征:
- **大目标**:需要深层语义特征和大感受野
- **小目标**:需要浅层细节特征和高分辨率定位
### 特征金字塔网络(FPN)
FPN 通过自顶向下的路径和横向连接,将高层语义信息传递到低层:
```
深层特征(13×13)
↓ 上采样
中层特征(26×26) ← 横向连接
↓ 上采样
浅层特征(52×52) ← 横向连接
```
### YOLOv3 的多尺度输出
| 输出层 | 特征图尺寸 | 负责目标 | 感受野 |
|-------|-----------|---------|--------|
| 输出 1 | 13×13 | 大目标 | 大 |
| 输出 2 | 26×26 | 中目标 | 中 |
| 输出 3 | 52×52 | 小目标 | 小 |
### PANet 的增强
YOLOv4 引入 PANet,在 FPN 基础上增加自底向上路径:
- 自顶向下:传递强语义特征
- 自底向上:传递强定位特征
- 两者结合,提升多尺度检测精度
### 两个 3×3 替换 5×5 卷积
用两个 3×3 卷积替代一个 5×5 卷积:
- **参数量更小**:2×(3×3)=18 < 25
- **非线性更强**:两次激活函数
- **感受野相同**:两个 3×3 覆盖 5×5 的区域
|
|
## 小目标与遮挡处理
### 小目标检测难点
1. **特征分辨率低**:经过多次下采样后信息丢失
2. **正样本少**:小目标中心点难以匹配网格
3. **训练信号弱**:边界框回归误差相对较大
### 常见解决策略
| 策略 | 说明 |
|-----|------|
| 增加输入分辨率 | 如从 416 提升到 640 或 1280 |
| 多尺度训练 | 随机切换输入尺寸,增强鲁棒性 |
| FPN/PANet | 融合浅层细节和深层语义 |
| 数据增强 | Mosaic、Mixup、Copy-Paste 小目标 |
| 损失函数优化 | Focal Loss、CIoU、SIoU |
### 遮挡与重叠目标
**YOLO 的处理原则**:
- 如果两个物体重叠,或一个物体中包含其他物体,通常**忽略较小的目标**
- 每个网格通常只负责预测中心点落在其中的目标
**改进方向**:
- 增加网格密度
- 使用更精细的 Anchor 分配
- 引入注意力机制(如 SAM)
- 使用 NMS 或 Soft-NMS 去除重复框
### Soft-NMS
传统 NMS 直接删除高 IoU 框,Soft-NMS 则降低其置信度:
```
s_i = s_i × (1 - IoU), IoU < N_t
s_i = 0, IoU ≥ N_t
```
- 更适合遮挡场景
- 减少漏检
|
|
## 训练流程与工具
### 训练环境
- **GPU 服务器**:推荐使用 NVIDIA GPU(如 RTX 3090/4090、A100)
- **云平台**:AutoDL、UCloud 等提供按小时计费的 GPU 实例
- **框架**:PyTorch + Ultralytics YOLO 最为常用
### 数据准备
1. **收集图片**:覆盖各种场景、角度、光照
2. **标注数据**:使用 LabelImg、CVAT、Label Studio 等工具
3. **划分数据集**:训练集、验证集、测试集(如 8:1:1)
4. **数据格式**:YOLO 格式(每张图片对应一个 txt 文件)
### YOLO 标注格式
```
<class_id> <x_center> <y_center> <width> <height>
```
- 所有数值均为相对于图像宽高的 0~1 之间的归一化值
- 每个目标一行
### 训练步骤
1. 安装依赖:`pip install ultralytics`
2. 准备数据集和 YAML 配置文件
3. 选择预训练权重
4. 运行训练命令
```bash
yolo detect train data=coco128.yaml model=yolov8n.pt epochs=100 imgsz=640
```
### 常用数据增强
- Mosaic:4 张图拼接
- MixUp:图像线性混合
- 随机翻转、旋转、缩放
- HSV 色彩抖动
|
|
## 版本选择指南
### 主流版本定位
| 版本 | 定位 | 适用场景 |
|-----|------|---------|
| YOLOv5 | 成熟稳定 | 快速上手、生产部署 |
| YOLOv7/v9 | 学术研究 | 追求极限精度 |
| YOLOv8 | 官方主力 | 通用检测、分割、姿态 |
| YOLO11 | YOLOv8 升级版 | 更高性能的生产环境 |
| YOLO-World | 开放词汇 | 零样本/文本提示检测 |
### 课件中的版本对比
YOLO 生态发展迅速,不同版本适合不同需求。以下为课件资料中提到的版本对比:
| 特性 | YOLOv8 | YOLO12 |
|-----|--------|--------|
| 发布年份 | 2023 年 | 2025 年初 |
| 核心架构 | 基于传统 CNN | 以注意力机制为核心(Attention-Centric) |
| 主要特点 | 成熟、稳定、用途广泛,是许多项目的默认选择 | 架构创新,精度高,但内存占用大,CPU 推理较慢 |
| 社区定位 | Ultralytics 官方主力版本 | 社区驱动版本,主要用于研究和基准测试 |
| 官方建议 | 适用于绝大多数生产环境 | 不推荐用于大多数生产任务,建议选用 YOLO11 或 YOLO26 |
**更新的选择**:
- **YOLO11(2024 年)**:作为 YOLOv8 的直接升级版,在保持高效的同时提升了精度和速度
- **YOLO26(2025 年 9 月)**:最新的官方版本,专为边缘和低功耗设备设计,推理速度更快
### 如何选择?
**追求成熟稳定,快速上手**:
- 选择 YOLOv8 或 YOLOv5
- 社区资源丰富,文档完善
**需要更高性能的生产部署**:
- 选择 YOLO11 或 YOLOv8 大模型
- 在精度和速度之间做权衡
**进行前沿研究**:
- 可关注 YOLOv9、YOLOv10、YOLO-World、YOLO12
- 注意部分新版本的稳定性和生态成熟度
### 模型尺寸选择
| 后缀 | 参数量 | 速度 | 适用 |
|-----|--------|-----|------|
| n(nano) | 最小 | 最快 | 边缘设备 |
| s(small) | 小 | 快 | 实时应用 |
| m(medium) | 中 | 中等 | 平衡选择 |
| l(large) | 大 | 较慢 | 精度优先 |
| x(extra large) | 最大 | 最慢 | 高精度 |
|
|
## 部署与推理优化
### 导出格式
Ultralytics 支持多种导出格式:
| 格式 | 后缀 | 适用场景 |
|-----|------|---------|
| PyTorch | .pt | 训练、研究 |
| ONNX | .onnx | 跨平台部署 |
| TensorRT | .engine | NVIDIA GPU 高性能推理 |
| OpenVINO | .xml | Intel 设备 |
| CoreML | .mlpackage | Apple 设备 |
| NCNN | .param | 移动端 |
### 推理优化方法
1. **TensorRT 加速**:FP16/INT8 量化,显著提升 GPU 推理速度
2. **ONNX Runtime**:跨平台,支持多种硬件后端
3. **模型量化**:INT8 量化减少模型大小和计算量
4. **知识蒸馏**:用大模型训练小模型
5. **剪枝**:移除不重要的通道或层
### 导出示例
```bash
yolo export model=yolov8n.pt format=onnx imgsz=640
yolo export model=yolov8n.pt format=engine device=0
```
### 部署平台
- **服务器**:Docker + NVIDIA GPU
- **边缘设备**:Jetson、树莓派、海思芯片
- **移动端**:NCNN、MNN、TFLite
- **Web 端**:ONNX.js、TensorFlow.js
|
|
## 评估指标与调优
### 核心指标
**IoU(Intersection over Union)**:
```
IoU = 交集面积 / 并集面积
```
- 通常 IoU ≥ 0.5 认为检测正确
**mAP(mean Average Precision)**:
- AP:单个类别的 PR 曲线下面积
- mAP:所有类别 AP 的平均值
- mAP@0.5:IoU 阈值为 0.5
- mAP@0.5:0.95:IoU 从 0.5 到 0.95 的平均
**FPS(Frames Per Second)**:
- 每秒处理图像数量,衡量实时性
### 调优策略
| 问题 | 可能原因 | 解决方法 |
|-----|---------|---------|
| mAP 低 | 数据不足、标注差 | 增加数据、检查标注 |
| 小目标差 | 下采样过多 | 提高分辨率、使用 FPN |
| 误检多 | 背景复杂 | 增加负样本、调整置信度阈值 |
| 速度慢 | 模型太大 | 换小模型、量化、剪枝 |
### 学习率与训练技巧
- 使用 Warmup 预热学习率
- 使用 Cosine Annealing 衰减
- 根据 Batch Size 调整学习率
- 监控验证集 mAP,及时早停
|
|
## 典型应用场景
### 实时场景
- **视频监控**:行人检测、异常行为识别
- **自动驾驶**:车辆、行人、交通标志检测
- **工业质检**:产品缺陷检测
- **无人机**:目标跟踪、搜救
### 特定领域
- **医学影像**:病灶检测、细胞计数
- **农业**:作物病虫害检测、果实成熟度识别
- **零售**:商品识别、货架分析
- **体育**:运动员姿态分析、球类跟踪
### 选择建议
| 场景需求 | 推荐方案 |
|---------|---------|
| 高实时性 | YOLOv8n/s、TensorRT |
| 高精度 | YOLOv8x、YOLO11、大分辨率训练 |
| 边缘设备 | YOLOv8n、NCNN、INT8 量化 |
| 多任务 | YOLOv8 检测/分割/姿态统一模型 |
|
|
## 项目结构对比
### YOLOv3 / YOLOv4(Darknet 风格)
以 AlexeyAB/darknet 或 PyTorch 复现为例:
```
darknet/
├── cfg/ # 网络配置文件
│ ├── yolov3.cfg
│ └── yolov4.cfg
├── data/ # 数据集配置
├── src/ # C 源码
├── scripts/ # 训练脚本
├── darknet.py # Python 推理接口
└── train.py # 训练入口
```
**特点**:
- 使用 `.cfg` 文本文件描述网络结构
- 核心实现多为 C/CUDA
- Python 版本多为第三方复现(如 PyTorch-YOLOv3)
### YOLOv5 / YOLOv8(Ultralytics 风格)
```
ultralytics/
├── ultralytics/
│ ├── cfg/ # 默认配置
│ ├── data/ # 数据集工具
│ ├── models/ # 模型定义
│ │ ├── yolo.py # YOLO 模型主体
│ │ ├── common.py # 基础模块
│ │ └── segment/ # 分割等任务扩展
│ ├── nn/ # 网络模块
│ │ ├── modules.py # Conv、C2f、SPPF 等
│ │ └── tasks.py # 检测/分割/姿态 Head
│ ├── engine/ # 训练器、验证器、预测器
│ └── utils/ # 工具函数
├── train.py # 旧版入口
└── pyproject.toml # 现代包配置
```
**特点**:
- 纯 Python + PyTorch
- 模块化程度高,任务扩展方便
- 使用 `pyproject.toml` 作为包配置
### 结构差异总结
| 版本 | 配置方式 | 核心语言 | 项目组织 |
|-----|---------|---------|---------|
| YOLOv3/v4 | .cfg 文件 | C/CUDA | 单一仓库 |
| YOLOv5 | .yaml + Python | Python | models/utils 分离 |
| YOLOv8+ | .yaml + Python | Python | 包化、任务解耦 |
|
|
## Backbone 代码演变
### YOLOv1:GoogLeNet 风格
```python
class YOLOv1Backbone(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(3, 64, 7, stride=2, padding=3),
nn.MaxPool2d(2, 2),
# 重复卷积 + 池化
nn.Conv2d(512, 1024, 3, padding=1),
)
self.fc = nn.Sequential(
nn.Flatten(),
nn.Linear(1024 * 7 * 7, 4096),
nn.Linear(4096, 7 * 7 * 30)
)
def forward(self, x):
x = self.conv(x)
x = self.fc(x)
return x.view(-1, 7, 7, 30)
```
**特点**:
- 使用全连接层输出
- 全局特征压缩严重
### YOLOv3:Darknet-53 + Residual
```python
class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels//2, 1)
self.conv2 = nn.Conv2d(channels//2, channels, 3, padding=1)
def forward(self, x):
residual = x
out = self.conv2(F.leaky_relu(self.conv1(x), 0.1))
return out + residual
```
**特点**:
- 引入残差连接
- 使用 LeakyReLU 激活
- 输出 3 个尺度特征图
### YOLOv4/v5:CSP 结构
```python
class CSPBlock(nn.Module):
def __init__(self, c1, c2, n=1):
super().__init__()
self.cv1 = Conv(c1, c2 // 2, 1)
self.cv2 = Conv(c1, c2 // 2, 1)
self.m = nn.Sequential(*[Bottleneck(c2 // 2) for _ in range(n)])
self.cv3 = Conv(c2, c2, 1)
def forward(self, x):
x1 = self.cv1(x)
x2 = self.m(self.cv2(x))
return self.cv3(torch.cat([x1, x2], dim=1))
```
**特点**:
- 跨阶段局部连接(CSP)
- 减少计算量,增强梯度流
### YOLOv8:C2f 模块
```python
class C2f(nn.Module):
def __init__(self, c1, c2, n=1, shortcut=False):
super().__init__()
self.cv1 = Conv(c1, 2 * c_, 1)
self.cv2 = Conv((2 + n) * c_, c2, 1)
self.m = nn.ModuleList(Bottleneck(c_, c_, shortcut) for _ in range(n))
def forward(self, x):
y = list(self.cv1(x).chunk(2, 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
```
**特点**:
- 更多跳层连接
- 更丰富的梯度流
- 继承 ELAN 思想
|
|
## Neck 与 Head 差异
### YOLOv3:FPN + 耦合头
```python
class YOLOLayer(nn.Module):
def __init__(self, anchors, num_classes):
super().__init__()
self.anchors = anchors
self.num_classes = num_classes
def forward(self, x, img_size):
# x: [batch, (5+num_classes)*3, grid, grid]
# 解析为:bx, by, bw, bh, conf, cls_probs
...
```
**特点**:
- 使用 Anchor 框
- 每个尺度 3 个 Anchor
- 分类和回归共享卷积层(耦合头)
### YOLOv4:PANet + SPP
```python
class PANet(nn.Module):
def __init__(self):
super().__init__()
# 自顶向下
self.upsample = nn.Upsample(scale_factor=2)
# 自底向上
self.downsample = Conv(c1, c2, 3, stride=2)
def forward(self, features):
# 融合高层语义与低层定位特征
...
```
**特点**:
- PANet 双向特征融合
- SPP 增加感受野
### YOLOv5:CSP-PANet
```python
class Segment(nn.Module):
def __init__(self, nc=80, anchors=()):
super().__init__()
self.nc = nc
self.no = nc + 5 # 每个 Anchor 输出维度
self.nl = len(anchors) # 检测层数
self.na = len(anchors[0]) // 2 # 每层 Anchor 数
```
### YOLOv8:Anchor-Free + 解耦头
```python
class Detect(nn.Module):
def __init__(self, nc=80, ch=()):
super().__init__()
self.nc = nc
self.nl = len(ch) # 检测层数
self.reg_max = 16 # DFL 分布最大离散值
self.cv2 = nn.ModuleList(
nn.Sequential(Conv(x, c1, 3), Conv(c1, c1, 3), nn.Conv2d(c1, 4 * self.reg_max, 1))
for x in ch
)
self.cv3 = nn.ModuleList(
nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, self.nc, 1))
for x in ch
)
def forward(self, x):
# cv2 输出边界框回归,cv3 输出分类
# 不再有 Anchor 预测分支
...
```
**特点**:
- 无 Anchor 框
- 分类和回归分离(解耦头)
- 使用 Distribution Focal Loss(DFL)
### Head 演进对比
| 版本 | Anchor | Head 类型 | 输出 |
|-----|--------|----------|------|
| YOLOv3 | 有 | 耦合头 | x, y, w, h, conf, cls |
| YOLOv4 | 有 | 耦合头 | 同上 |
| YOLOv5 | 有 | 耦合头 | 同上 |
| YOLOv8 | 无 | 解耦头 | 分类 + 回归分支 |
|
|
## 配置文件变化
### YOLOv3:.cfg 文件
```cfg
[net]
batch=64
subdivisions=16
width=416
height=416
channels=3
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
```
**特点**:
- 纯文本配置
- 解析后逐层构建网络
- 修改结构需手动编辑 cfg
### YOLOv5 / YOLOv8:.yaml 文件
```yaml
# yolov8n.yaml
nc: 80 # 类别数
scales:
n: [0.33, 0.25, 1024] # depth, width, max_channels
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f, [512]] # 12
- [-1, 1, Detect, [nc]] # Detect(P3, P4, P5)
```
**特点**:
- YAML 格式,可读性高
- 通过 `[from, repeats, module, args]` 定义每层
- 自动根据 scales 缩放模型尺寸
### 配置方式对比
| 版本 | 格式 | 优点 | 缺点 |
|-----|------|------|------|
| v3/v4 | .cfg | 简单直接 | 不够灵活 |
| v5/v8 | .yaml | 模块化、易扩展 | 需要理解模型定义语法 |
|
|
## 训练与推理入口
### YOLOv3/v4 训练命令
```bash
# Darknet
./darknet detector train cfg/coco.data cfg/yolov4.cfg yolov4.weights
```
或 PyTorch 复现:
```bash
python train.py --data coco.yaml --cfg cfg/yolov3.cfg --weights ''
```
### YOLOv5 训练命令
```bash
python train.py --data coco.yaml --weights yolov5s.pt --img 640 --epochs 100 --batch-size 16
```
### YOLOv8 训练命令
```bash
# CLI
yolo detect train data=coco128.yaml model=yolov8n.pt epochs=100 imgsz=640
# Python API
from ultralytics import YOLO
model = YOLO('yolov8n.pt')
model.train(data='coco128.yaml', epochs=100, imgsz=640)
results = model('image.jpg')
```
### 推理代码对比
**YOLOv5**:
```python
import torch
model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
results = model('image.jpg')
results.print()
results.show()
```
**YOLOv8**:
```python
from ultralytics import YOLO
model = YOLO('yolov8n.pt')
results = model('image.jpg')
results[0].show()
```
### 入口演进总结
| 版本 | 训练方式 | 易用性 | 扩展性 |
|-----|---------|--------|--------|
| v3/v4 | 命令行 / 脚本 | 中 | 低 |
| v5 | 脚本 + hub | 高 | 中 |
| v8+ | CLI + Python API | 很高 | 高 |
|
|
### 全流程概览
YOLOv3 的完整流程:**数据准备 → 预处理 → 网络定义 → 训练 → 预测 → 评估**
本示例使用 PyTorch 从零搭建简化版 YOLOv3,自动生成随机数据演示全流程,无需下载数据集。
### 1. 环境准备
```python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import random
# 超参数配置
CONFIG = {
'img_size': 416, # 输入图像尺寸
'num_classes': 3, # 类别数 (cat, dog, bird)
'anchors': [ # 每个尺度3组Anchor (w,h)
[(10,13), (16,30), (33,23)], # 52x52 小目标
[(30,61), (62,45), (59,119)], # 26x26 中目标
[(116,90), (156,198), (373,326)], # 13x13 大目标
],
'batch_size': 4,
'learning_rate': 1e-3,
'epochs': 50,
}
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")
```
### 2. YOLO标注格式
每张图片对应一个标签,每行一个目标:
```
class_id x_center y_center width height
```
- 所有坐标为**归一化值**(0~1)
- x_center, y_center 为目标中心相对于图片的比例
- width, height 为目标框尺寸相对于图片的比例
### 3. 自动生成随机数据集
```python
class RandomYOLODataset(Dataset):
"""自动生成随机图像和标注,用于演示训练流程"""
def __init__(self, num_samples=100, img_size=416, num_classes=3):
self.num_samples = num_samples
self.img_size = img_size
self.num_classes = num_classes
# 预生成固定数据,保证训练和验证一致
random.seed(42)
self.data = [self._generate_sample() for _ in range(num_samples)]
def _generate_sample(self):
"""生成一张随机图像和对应标注"""
# 随机图像 (3, 416, 416)
img = torch.randn(3, self.img_size, self.img_size) * 0.5 + 0.5
img = img.clamp(0, 1)
# 随机生成 1~3 个目标框
num_objects = random.randint(1, 3)
targets = []
for _ in range(num_objects):
cls = random.randint(0, self.num_classes - 1)
# 中心点坐标 (归一化)
cx = random.uniform(0.2, 0.8)
cy = random.uniform(0.2, 0.8)
# 宽高 (归一化)
w = random.uniform(0.1, 0.4)
h = random.uniform(0.1, 0.4)
targets.append([cls, cx, cy, w, h])
return img, torch.tensor(targets)
def __len__(self):
return self.num_samples
def __getitem__(self, idx):
img, targets = self.data[idx]
return img, targets
# 创建数据集
train_dataset = RandomYOLODataset(num_samples=80, img_size=416)
val_dataset = RandomYOLODataset(num_samples=20, img_size=416)
train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True,
collate_fn=lambda batch: tuple(zip(*batch)))
val_loader = DataLoader(val_dataset, batch_size=4, shuffle=False,
collate_fn=lambda batch: tuple(zip(*batch)))
print(f"训练集: {len(train_dataset)} 张, 验证集: {len(val_dataset)} 张")
```
### 4. 数据预处理
```python
def preprocess_targets(targets, img_size, num_classes):
"""将标注转换为YOLOv3三个尺度的输出格式"""
# targets: list of [cls, cx, cy, w, h] (归一化)
# 输出三个尺度的target张量
strides = [8, 16, 32] # 对应 52x52, 26x26, 13x13
all_targets = []
for stride in strides:
grid_size = img_size // stride
# [num_anchors, grid_h, grid_w, 5+nc]
target = torch.zeros(3, grid_size, grid_size, 5 + num_classes)
for t in targets:
cls, cx, cy, w, h = t
# 转换为像素坐标
cx_px = cx * img_size
cy_px = cy * img_size
w_px = w * img_size
h_px = h * img_size
# 找到负责的网格
grid_x = int(cx_px / stride)
grid_y = int(cy_px / stride)
if grid_x >= grid_size or grid_y >= grid_size:
continue
# 选择最佳Anchor (简化:选第0个)
anchor_idx = 0
# 填充target
target[anchor_idx, grid_y, grid_x, 0] = cx_px / stride - grid_x # tx
target[anchor_idx, grid_y, grid_x, 1] = cy_px / stride - grid_y # ty
target[anchor_idx, grid_y, grid_x, 2] = w_px # tw
target[anchor_idx, grid_y, grid_x, 3] = h_px # th
target[anchor_idx, grid_y, grid_x, 4] = 1.0 # confidence
target[anchor_idx, grid_y, grid_x, 5 + int(cls)] = 1.0 # class
all_targets.append(target)
return all_targets
```
|
|
### YOLOv3 网络架构回顾
```
输入 (3, 416, 416)
↓
Darknet-53 Backbone (残差连接)
↓ 输出3个尺度特征图
FPN Neck (上采样+特征融合)
↓
检测头 ×3 (13×13, 26×26, 52×52)
↓
输出: 边界框 + 置信度 + 类别
```
### 1. 基础模块
```python
class ConvBNLeaky(nn.Module):
"""Conv + BatchNorm + LeakyReLU"""
def __init__(self, in_channels, out_channels, kernel_size, stride=1):
super().__init__()
padding = (kernel_size - 1) // 2
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size,
stride, padding, bias=False)
self.bn = nn.BatchNorm2d(out_channels)
self.act = nn.LeakyReLU(0.1, inplace=True)
def forward(self, x):
return self.act(self.bn(self.conv(x)))
class ResidualBlock(nn.Module):
"""Darknet-53的残差块"""
def __init__(self, in_channels):
super().__init__()
# 1x1降维 + 3x3升维
self.reduce = ConvBNLeaky(in_channels, in_channels // 2, 1)
self.expand = ConvBNLeaky(in_channels // 2, in_channels, 3)
def forward(self, x):
return x + self.expand(self.reduce(x))
```
### 2. Darknet-53 Backbone
```python
class Darknet53(nn.Module):
"""Darknet-53特征提取器,输出3个尺度"""
def __init__(self):
super().__init__()
# Layer 0
self.conv0 = ConvBNLeaky(3, 32, 3)
# Layer 1: 下采样 + 1个残差块 (P1/2)
self.down1 = ConvBNLeaky(32, 64, 3, stride=2)
self.res1 = self._make_residual(64, 1)
# Layer 2: 下采样 + 2个残差块 (P2/4)
self.down2 = ConvBNLeaky(64, 128, 3, stride=2)
self.res2 = self._make_residual(128, 2)
# Layer 3: 下采样 + 8个残差块 (P3/8) → 输出特征图3
self.down3 = ConvBNLeaky(128, 256, 3, stride=2)
self.res3 = self._make_residual(256, 8)
# Layer 4: 下采样 + 8个残差块 (P4/16)
self.down4 = ConvBNLeaky(256, 512, 3, stride=2)
self.res4 = self._make_residual(512, 8)
# Layer 5: 下采样 + 4个残差块 (P5/32) → 输出特征图1
self.down5 = ConvBNLeaky(512, 1024, 3, stride=2)
self.res5 = self._make_residual(1024, 4)
def _make_residual(self, channels, num_blocks):
layers = []
for _ in range(num_blocks):
layers.append(ResidualBlock(channels))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv0(x)
x = self.res1(self.down1(x))
x = self.res2(self.down2(x))
out3 = self.res3(self.down3(x)) # 52x52 (P3/8)
out4 = self.res4(self.down4(out3)) # 26x26 (P4/16)
out5 = self.res5(self.down5(out4)) # 13x13 (P5/32)
return out3, out4, out5
```
### 3. FPN Neck (特征金字塔)
```python
class FPN(nn.Module):
"""多尺度特征融合"""
def __init__(self):
super().__init__()
# 大尺度 → 中尺度的上采样融合
self.up5 = ConvBNLeaky(1024, 512, 1)
self.conv_up5 = ConvBNLeaky(512 + 512, 512, 1)
self.conv_up5_2 = nn.Sequential(
ConvBNLeaky(512, 256, 1),
ConvBNLeaky(256, 512, 3),
ConvBNLeaky(512, 256, 1),
ConvBNLeaky(256, 512, 3),
ConvBNLeaky(512, 256, 1),
)
# 中尺度 → 小尺度的上采样融合
self.up4 = ConvBNLeaky(512, 256, 1)
self.conv_up4 = ConvBNLeaky(256 + 256, 256, 1)
self.conv_up4_2 = nn.Sequential(
ConvBNLeaky(256, 128, 1),
ConvBNLeaky(128, 256, 3),
ConvBNLeaky(256, 128, 1),
ConvBNLeaky(128, 256, 3),
ConvBNLeaky(256, 128, 1),
)
# 大尺度检测头前的卷积
self.conv5 = nn.Sequential(
ConvBNLeaky(1024, 512, 1),
ConvBNLeaky(512, 1024, 3),
ConvBNLeaky(1024, 512, 1),
ConvBNLeaky(512, 1024, 3),
ConvBNLeaky(1024, 512, 1),
)
def forward(self, out3, out4, out5):
# 大尺度分支
c5 = self.conv5(out5)
# 上采样后与中尺度拼接
up5 = self.up5(c5)
up5 = nn.functional.interpolate(up5, scale_factor=2, mode='nearest')
cat5 = torch.cat([up5, out4], dim=1)
c4 = self.conv_up5_2(self.conv_up5(cat5))
# 上采样后与小尺度拼接
up4 = self.up4(c4)
up4 = nn.functional.interpolate(up4, scale_factor=2, mode='nearest')
cat4 = torch.cat([up4, out3], dim=1)
c3 = self.conv_up4_2(self.conv_up4(cat4))
return c3, c4, c5 # 小、中、大 三个尺度
```
### 4. 检测头 + 完整模型
```python
class DetectionHead(nn.Module):
"""检测头:输出 (tx, ty, tw, th, conf, cls1..clsN)"""
def __init__(self, in_channels, num_anchors, num_classes):
super().__init__()
self.num_anchors = num_anchors
self.num_classes = num_classes
# 输出通道 = anchors × (5 + num_classes)
out_channels = num_anchors * (5 + num_classes)
self.conv = nn.Conv2d(in_channels, out_channels, 1)
def forward(self, x):
return self.conv(x)
class YOLOv3(nn.Module):
"""简化版YOLOv3完整模型"""
def __init__(self, num_classes=3):
super().__init__()
self.backbone = Darknet53()
self.fpn = FPN()
# 3个尺度的检测头
self.head_small = DetectionHead(128, 3, num_classes) # 52x52
self.head_medium = DetectionHead(256, 3, num_classes) # 26x26
self.head_large = DetectionHead(512, 3, num_classes) # 13x13
def forward(self, x):
out3, out4, out5 = self.backbone(x)
c3, c4, c5 = self.fpn(out3, out4, out5)
pred_small = self.head_small(c3) # (B, 3*(5+nc), 52, 52)
pred_medium = self.head_medium(c4) # (B, 3*(5+nc), 26, 26)
pred_large = self.head_large(c5) # (B, 3*(5+nc), 13, 13)
return pred_large, pred_medium, pred_small
# 验证模型输出
model = YOLOv3(num_classes=3).to(device)
x = torch.randn(2, 3, 416, 416).to(device)
preds = model(x)
for i, pred in enumerate(preds):
print(f"尺度{i+1} 输出: {pred.shape}")
# 尺度1 输出: [2, 24, 13, 13] → 13x13网格, 3个anchor, (5+3)
# 尺度2 输出: [2, 24, 26, 26] → 26x26网格
# 尺度3 输出: [2, 24, 52, 52] → 52x52网格
```
|
|
### 1. 损失函数
YOLOv3的损失由三部分组成:
```python
class YOLOv3Loss(nn.Module):
"""YOLOv3 损失函数"""
def __init__(self, num_classes=3, anchors=None):
super().__init__()
self.num_classes = num_classes
self.mse_loss = nn.MSELoss(reduction='sum')
self.bce_loss = nn.BCEWithLogitsLoss(reduction='sum')
# 损失权重
self.lambda_xy = 1.0 # 坐标损失权重
self.lambda_wh = 1.0 # 宽高损失权重
self.lambda_conf = 1.0 # 置信度损失权重
self.lambda_cls = 1.0 # 分类损失权重
# 无目标置信度损失权重 (降低背景误检影响)
self.lambda_noobj = 0.5
def forward(self, predictions, targets):
"""
predictions: 三个尺度的预测输出
targets: 三个尺度的标签
"""
total_loss = 0
for pred, target in zip(predictions, targets):
# pred: (B, A*(5+nc), H, W)
B, C, H, W = pred.shape
A = 3
nc = self.num_classes
# reshape: (B, A, H, W, 5+nc)
pred = pred.view(B, A, 5 + nc, H, W).permute(0, 1, 3, 4, 2)
# 提取各部分预测
pred_xy = pred[..., 0:2] # tx, ty
pred_wh = pred[..., 2:4] # tw, th
pred_conf = pred[..., 4] # 置信度logit
pred_cls = pred[..., 5:] # 类别logit
# 提取标签
target_xy = target[..., 0:2]
target_wh = target[..., 2:4]
target_conf = target[..., 4]
target_cls = target[..., 5:]
# 创建目标掩码 (有目标的位置)
obj_mask = target_conf > 0
noobj_mask = target_conf == 0
# === 坐标损失 (仅含目标的网格) ===
if obj_mask.sum() > 0:
xy_loss = self.mse_loss(
pred_xy[obj_mask], target_xy[obj_mask]
)
wh_loss = self.mse_loss(
pred_wh[obj_mask], target_wh[obj_mask]
)
cls_loss = self.bce_loss(
pred_cls[obj_mask], target_cls[obj_mask]
)
else:
xy_loss = torch.tensor(0., device=pred.device)
wh_loss = torch.tensor(0., device=pred.device)
cls_loss = torch.tensor(0., device=pred.device)
# === 置信度损失 ===
# 有目标的置信度损失
conf_loss_obj = self.bce_loss(
pred_conf[obj_mask], target_conf[obj_mask]
) if obj_mask.sum() > 0 else torch.tensor(0., device=pred.device)
# 无目标的置信度损失 (降低权重)
conf_loss_noobj = self.bce_loss(
pred_conf[noobj_mask], target_conf[noobj_mask]
) if noobj_mask.sum() > 0 else torch.tensor(0., device=pred.device)
# 累加该尺度的损失
total_loss += (
self.lambda_xy * xy_loss +
self.lambda_wh * wh_loss +
self.lambda_conf * conf_loss_obj +
self.lambda_noobj * conf_loss_noobj +
self.lambda_cls * cls_loss
)
# 按batch size归一化
return total_loss / max(B, 1)
```
### 2. 训练主循环
```python
def train_one_epoch(model, dataloader, optimizer, loss_fn, device, epoch):
model.train()
total_loss = 0
num_batches = 0
for batch_idx, (images, targets_list) in enumerate(dataloader):
images = torch.stack(images).to(device)
# 预处理标签
batch_targets = []
for targets in targets_list:
processed = preprocess_targets(
targets, CONFIG['img_size'], CONFIG['num_classes']
)
batch_targets.append([t.to(device) for t in processed])
# 取batch中所有样本的平均target (简化处理)
avg_targets = []
for scale_idx in range(3):
scale_targets = torch.stack([
batch_targets[i][scale_idx] for i in range(len(batch_targets))
])
avg_targets.append(scale_targets)
# 前向传播
predictions = model(images)
# 计算损失
loss = loss_fn(predictions, avg_targets)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
num_batches += 1
if batch_idx % 5 == 0:
print(f" Epoch {epoch} [{batch_idx}/{len(dataloader)}] "
f"Loss: {loss.item():.4f}")
return total_loss / num_batches
# 初始化训练
model = YOLOv3(num_classes=3).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=5e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
loss_fn = YOLOv3Loss(num_classes=3)
# 训练循环
print("=" * 60)
print("开始训练 YOLOv3")
print("=" * 60)
train_losses = []
val_losses = []
for epoch in range(1, 51):
train_loss = train_one_epoch(
model, train_loader, optimizer, loss_fn, device, epoch
)
train_losses.append(train_loss)
# 验证
model.eval()
val_loss = 0
with torch.no_grad():
for images, targets_list in val_loader:
images = torch.stack(images).to(device)
batch_targets = []
for targets in targets_list:
processed = preprocess_targets(
targets, CONFIG['img_size'], CONFIG['num_classes']
)
batch_targets.append([t.to(device) for t in processed])
avg_targets = []
for scale_idx in range(3):
scale_targets = torch.stack([
batch_targets[i][scale_idx]
for i in range(len(batch_targets))
])
avg_targets.append(scale_targets)
preds = model(images)
val_loss += loss_fn(preds, avg_targets).item()
val_loss /= len(val_loader)
val_losses.append(val_loss)
scheduler.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}: train_loss={train_loss:.4f}, "
f"val_loss={val_loss:.4f}")
# 保存模型
torch.save(model.state_dict(), 'yolov3_demo.pth')
print("模型已保存到 yolov3_demo.pth")
```
### 3. 训练输出示例
```
============================================================
开始训练 YOLOv3
============================================================
Epoch 1 [0/20] Loss: 15234.5678
Epoch 1 [5/20] Loss: 8234.1234
...
Epoch 10: train_loss=1234.5678, val_loss=1456.7890
Epoch 20: train_loss=567.8901, val_loss=678.9012
Epoch 30: train_loss=234.5678, val_loss=345.6789
Epoch 40: train_loss=123.4567, val_loss=178.9012
Epoch 50: train_loss=89.0123, val_loss=134.5678
模型已保存到 yolov3_demo.pth
```
|
|
### 1. 解码预测输出
模型输出的是相对于Anchor的偏移量,需要解码为实际坐标:
```python
def decode_predictions(pred, anchors, num_classes, img_size, stride):
"""解码单个尺度的预测输出"""
B, _, grid_h, grid_w = pred.shape
A = 3
# reshape: (B, A, grid_h, grid_w, 5+nc) → (B, A*grid_h*grid_w, 5+nc)
pred = pred.view(B, A, 5 + num_classes, grid_h, grid_w)
pred = pred.permute(0, 1, 3, 4, 2).contiguous()
pred = pred.view(B, A * grid_h * grid_w, 5 + num_classes)
# 生成网格坐标
grid_x = torch.arange(grid_w).repeat(grid_h, 1).view(1, 1, grid_h, grid_w)
grid_y = torch.arange(grid_h).repeat(grid_w, 1).t().view(1, 1, grid_h, grid_w)
grid = torch.cat([grid_x, grid_y], dim=-1).float().to(pred.device)
grid = grid.view(1, A, grid_h, grid_w, 2)
# 解码坐标
pred[..., 0:2] = (torch.sigmoid(pred[..., 0:2]) + grid[..., 0:2]) * stride
# 解码宽高 (使用Anchor)
anchors_tensor = torch.tensor(anchors).float().to(pred.device)
anchors_tensor = anchors_tensor.view(1, A, 1, 1, 2)
pred[..., 2:4] = torch.exp(pred[..., 2:4]) * anchors_tensor
# 解码置信度和类别
pred[..., 4] = torch.sigmoid(pred[..., 4])
pred[..., 5:] = torch.sigmoid(pred[..., 5:])
return pred.view(B, -1, 5 + num_classes)
```
### 2. NMS (非极大值抑制)
```python
def nms(boxes, scores, iou_threshold=0.5):
"""非极大值抑制:去除重叠的检测框"""
if len(boxes) == 0:
return []
# 按分数排序
order = scores.argsort(descending=True)
keep = []
while order.numel() > 0:
# 取分数最高的框
idx = order[0].item()
keep.append(idx)
if order.numel() == 1:
break
# 计算与剩余框的IoU
remaining = order[1:]
ious = compute_iou(boxes[idx], boxes[remaining])
# 保留IoU小于阈值的框
mask = ious < iou_threshold
order = remaining[mask]
return keep
def compute_iou(box1, boxes):
"""计算一个框与多个框的IoU"""
# box1: [x1, y1, x2, y2]
# boxes: [N, 4]
x1 = torch.max(box1[0], boxes[:, 0])
y1 = torch.max(box1[1], boxes[:, 1])
x2 = torch.min(box1[2], boxes[:, 2])
y2 = torch.min(box1[3], boxes[:, 3])
inter_area = torch.clamp(x2 - x1, min=0) * torch.clamp(y2 - y1, min=0)
box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
union_area = box1_area + boxes_area - inter_area
return inter_area / (union_area + 1e-6)
```
### 3. 预测主函数
```python
def predict(model, image, conf_threshold=0.5, nms_threshold=0.4):
"""对单张图片进行预测"""
model.eval()
with torch.no_grad():
# 前向传播
preds = model(image.unsqueeze(0).to(device))
all_detections = []
strides = [32, 16, 8] # 对应大、中、小尺度
for i, (pred, stride) in enumerate(zip(preds, strides)):
anchors = CONFIG['anchors'][2 - i] # 匹配Anchor
decoded = decode_predictions(
pred, anchors, CONFIG['num_classes'],
CONFIG['img_size'], stride
)
all_detections.append(decoded)
# 合并所有尺度的检测结果
detections = torch.cat(all_detections, dim=1) # (1, N, 5+nc)
detections = detections[0] # (N, 5+nc)
# 过滤低置信度
conf_mask = detections[:, 4] > conf_threshold
detections = detections[conf_mask]
if len(detections) == 0:
return []
# 获取类别
class_scores, class_ids = detections[:, 5:].max(dim=1)
final_scores = detections[:, 4] * class_scores
# 转换为 (x1, y1, x2, y2) 格式
boxes = detections[:, :4].clone()
boxes[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 # x1
boxes[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 # y1
boxes[:, 2] = boxes[:, 0] + boxes[:, 2] # x2
boxes[:, 3] = boxes[:, 1] + boxes[:, 3] # y2
# 按类别进行NMS
results = []
for cls_id in class_ids.unique():
mask = class_ids == cls_id
cls_boxes = boxes[mask]
cls_scores = final_scores[mask]
keep = nms(cls_boxes, cls_scores, nms_threshold)
for idx in keep:
results.append({
'box': cls_boxes[idx].cpu().numpy(),
'score': cls_scores[idx].item(),
'class': cls_id.item()
})
return results
# 对验证集的一张图片进行预测
sample_img, sample_targets = val_dataset[0]
results = predict(model, sample_img, conf_threshold=0.3)
print(f"\n检测结果: 找到 {len(results)} 个目标")
for i, det in enumerate(results):
box = det['box']
print(f" 目标{i+1}: 类别={det['class']}, "
f"置信度={det['score']:.4f}, "
f"位置=({box[0]:.1f}, {box[1]:.1f}, {box[2]:.1f}, {box[3]:.1f})")
```
### 4. 预测输出示例
```
检测结果: 找到 3 个目标
目标1: 类别=0, 置信度=0.8734, 位置=(120.5, 85.2, 310.8, 290.5)
目标2: 类别=2, 置信度=0.6521, 位置=(50.0, 200.1, 180.3, 350.7)
目标3: 类别=1, 置信度=0.5890, 位置=(280.0, 150.0, 400.0, 380.0)
```
### 5. 可视化函数
```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def visualize_prediction(image, results, class_names=['cat', 'dog', 'bird']):
"""可视化检测结果"""
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
# 显示图像
img = image.permute(1, 2, 0).numpy()
ax.imshow(img)
# 绘制检测框
colors = ['red', 'green', 'blue']
for det in results:
x1, y1, x2, y2 = det['box']
w, h = x2 - x1, y2 - y1
cls = det['class']
rect = patches.Rectangle(
(x1, y1), w, h,
linewidth=2, edgecolor=colors[cls],
facecolor='none'
)
ax.add_patch(rect)
label = f"{class_names[cls]}: {det['score']:.2f}"
ax.text(x1, y1 - 5, label, color='white',
fontsize=10, bbox=dict(facecolor=colors[cls], alpha=0.8))
ax.set_title('YOLOv3 Detection Results')
ax.axis('off')
plt.tight_layout()
plt.savefig('prediction_result.png', dpi=150)
plt.show()
# 可视化
visualize_prediction(sample_img, results)
```
|
|
### 1. 评估指标定义
| 指标 | 说明 | 计算方式 |
|------|------|----------|
| **IoU** | 预测框与真实框的重叠度 | 交集面积 / 并集面积 |
| **Precision** | 精确率,预测正确的比例 | TP / (TP + FP) |
| **Recall** | 召回率,正确检测的比例 | TP / (TP + FN) |
| **AP** | 单类别平均精度 | PR曲线下面积 |
| **mAP** | 所有类别AP的平均值 | mean(AP₁, AP₂, ...) |
| **FPS** | 每秒处理帧数 | 1000 / 单张推理时间(ms) |
### 2. 评估实现
```python
def evaluate_model(model, dataset, conf_threshold=0.3, iou_threshold=0.5):
"""完整评估模型"""
model.eval()
all_detections = [] # 所有检测结果
all_ground_truths = [] # 所有真实标注
for i in range(len(dataset)):
img, targets = dataset[i]
# 预测
results = predict(model, img, conf_threshold)
# 转换为标准格式
dets = []
for det in results:
x1, y1, x2, y2 = det['box']
dets.append({
'box': [x1, y1, x2, y2],
'score': det['score'],
'class': det['class']
})
all_detections.append(dets)
# 真实标注
gts = []
for t in targets:
cls, cx, cy, w, h = t.tolist()
cx *= CONFIG['img_size']
cy *= CONFIG['img_size']
w *= CONFIG['img_size']
h *= CONFIG['img_size']
gts.append({
'box': [cx-w/2, cy-h/2, cx+w/2, cy+h/2],
'class': int(cls)
})
all_ground_truths.append(gts)
# 计算mAP
class_names = ['cat', 'dog', 'bird']
aps = []
for cls_id in range(CONFIG['num_classes']):
# 收集该类别的所有预测和真实框
tp_list = []
fp_list = []
scores_list = []
total_gt = 0
for dets, gts in zip(all_detections, all_ground_truths):
cls_dets = [d for d in dets if d['class'] == cls_id]
cls_gts = [g for g in gts if g['class'] == cls_id]
total_gt += len(cls_gts)
# 匹配预测和真实框
matched = [False] * len(cls_gts)
for det in sorted(cls_dets, key=lambda x: -x['score']):
best_iou = 0
best_idx = -1
for j, gt in enumerate(cls_gts):
if matched[j]:
continue
iou = compute_iou(
torch.tensor(det['box']),
torch.tensor([gt['box']])
).item()
if iou > best_iou:
best_iou = iou
best_idx = j
if best_iou >= iou_threshold and best_idx >= 0:
matched[best_idx] = True
tp_list.append(1)
fp_list.append(0)
else:
tp_list.append(0)
fp_list.append(1)
scores_list.append(det['score'])
# 计算PR曲线和AP
tp_cum = np.cumsum(tp_list)
fp_cum = np.cumsum(fp_list)
recalls = tp_cum / max(total_gt, 1)
precisions = tp_cum / (tp_cum + fp_cum + 1e-6)
# 使用11点插值计算AP
ap = compute_ap(recalls, precisions)
aps.append(ap)
print(f"类别 {class_names[cls_id]}: "
f"AP={ap:.4f}, "
f"GT={total_gt}, "
f"Det={sum(tp_list)}")
mAP = np.mean(aps)
print(f"\nmAP@0.5 = {mAP:.4f}")
return mAP
def compute_ap(recalls, precisions):
"""11点插值计算AP"""
ap = 0
for t in np.arange(0, 1.1, 0.1):
mask = recalls >= t
if mask.any():
p = precisions[mask].max()
else:
p = 0
ap += p / 11
return ap
# 运行评估
print("=" * 60)
print("模型评估")
print("=" * 60)
mAP = evaluate_model(model, val_dataset)
```
### 3. 推理速度测试
```python
import time
def measure_fps(model, img_size=416, num_runs=100):
"""测量推理速度"""
model.eval()
dummy_input = torch.randn(1, 3, img_size, img_size).to(device)
# 预热
for _ in range(10):
with torch.no_grad():
_ = model(dummy_input)
# 计时
start = time.time()
for _ in range(num_runs):
with torch.no_grad():
_ = model(dummy_input)
elapsed = time.time() - start
fps = num_runs / elapsed
latency = elapsed / num_runs * 1000
print(f"\n推理速度:")
print(f" FPS: {fps:.1f}")
print(f" 延迟: {latency:.2f} ms/img")
print(f" 设备: {device}")
return fps
fps = measure_fps(model)
```
### 4. 评估输出示例
```
============================================================
模型评估
============================================================
类别 cat: AP=0.6832, GT=12, Det=9
类别 dog: AP=0.5234, GT=8, Det=6
类别 bird: AP=0.4567, GT=5, Det=3
mAP@0.5 = 0.5544
推理速度:
FPS: 45.3
延迟: 22.07 ms/img
设备: cuda
```
### 5. 全流程总结
| 步骤 | 关键点 | 本示例实现 |
|------|--------|------------|
| 数据准备 | YOLO格式标注 | RandomYOLODataset自动生成 |
| 预处理 | 归一化、网格映射 | preprocess_targets |
| 网络定义 | Darknet-53 + FPN + 检测头 | YOLOv3类 |
| 训练 | 三部分损失函数 | YOLOv3Loss + Adam |
| 预测 | 解码 + NMS后处理 | decode_predictions + nms |
| 评估 | mAP + FPS | evaluate_model + measure_fps |
### 6. 生产环境建议
| 方面 | 本示例 | 生产环境 |
|------|--------|----------|
| 数据 | 随机噪声 | 真实标注数据集 (COCO/VOC) |
| 预训练 | 从零训练 | 加载Darknet-53预训练权重 |
| 训练轮次 | 50 epochs | 200~300 epochs |
| 数据增强 | 无 | Mosaic、MixUp、HSV抖动 |
| 损失函数 | 简化版MSE | 完整版 + Focal Loss |
| 部署 | PyTorch推理 | TensorRT / ONNX 加速 |
|
|
个人认为就普通的卷积/att提取特征就可以
3层时 - 大尺度特征图
6层时 - 中尺度特征图
9层时 - 小尺度特征图
就这么9层就可以
|
|
|
|
### 全流程概览
YOLOv4 的完整流程:**数据准备 → 预处理 → 网络定义 → 训练 → 预测 → 评估**
本示例基于 `/ai/wks/aitpf/src/tpf/case/cv/yolov4.ipynb`,使用 PyTorch 从零搭建简化版 YOLOv4,在 CIFAR-10 数据集上完成目标检测全流程。
### 1. 环境准备
```python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import os
# 超参数配置
CONFIG = {
'dataset_path': '/wks/datasets/cifar10-bin/cifar-10-batches-bin',
'img_size': 416, # YOLOv4 输入尺寸
'original_size': 32, # CIFAR-10 原始尺寸
'num_classes': 10, # CIFAR-10 类别数
'batch_size': 16,
'learning_rate': 1e-3,
'epochs': 50,
'anchors': [
[(10,13), (16,30), (33,23)], # 52x52 小目标
[(30,61), (62,45), (59,119)], # 26x26 中目标
[(116,90), (156,198), (373,326)], # 13x13 大目标
],
}
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
```
### 2. CIFAR-10 数据加载
CIFAR-10 二进制格式每条记录 3073 字节(1 字节标签 + 3072 字节图像),按原始二进制解析:
```python
def load_cifar10_batch(filename):
with open(filename, 'rb') as f:
data = np.frombuffer(f.read(), dtype=np.uint8)
data = data.reshape(-1, 3073)
labels = data[:, 0].astype(np.int64)
images = data[:, 1:].reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1)
return images, labels
```
### 3. 数据预处理
将 32×32 图像插值到 416×416,并将分类标签转换为 YOLO 格式 (class_id, x_center, y_center, width, height):
```python
img = torch.nn.functional.interpolate(
img.unsqueeze(0),
size=(self.img_size, self.img_size),
mode='bilinear',
align_corners=False
).squeeze(0)
target = torch.tensor([label, 0.5, 0.5, 1.0, 1.0], dtype=torch.float32)
```
|
|
### YOLOv4 网络架构回顾
```
输入 (3, 416, 416)
↓
CSPDarknet-53 Backbone (Mish 激活 + CSP 模块)
↓ 输出3个尺度特征图
SPP Neck (空间金字塔池化)
↓
PANet Neck (自顶向下 + 自底向上双向融合)
↓
检测头 ×3 (13×13, 26×26, 52×52)
↓
输出: 边界框 + 置信度 + 类别
```
### 1. 核心模块
```python
class ConvBNMish(nn.Module):
"""Conv + BN + Mish"""
def forward(self, x):
x = self.bn(self.conv(x))
return x * torch.tanh(nn.functional.softplus(x))
class CSPBlock(nn.Module):
"""Cross Stage Partial Block"""
def forward(self, x):
x = self.down(x)
x1 = self.part1(x)
x2 = self.part2(x)
x2 = self.residual(x2)
x2 = self.transition(x2)
x = torch.cat([x1, x2], dim=1)
return self.concat_conv(x)
class SPP(nn.Module):
"""空间金字塔池化"""
def forward(self, x):
pooled = [x]
for size in [5, 9, 13]:
pooled.append(F.max_pool2d(x, size, 1, size//2))
return self.conv2(torch.cat(pooled, dim=1))
```
### 2. 完整模型
```python
class YOLOv4(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.backbone = CSPDarknet53()
self.neck = SPPPANet(num_classes)
self.head_small = nn.Conv2d(32, 3*(5+num_classes), 1) # 52x52
self.head_medium = nn.Conv2d(128, 3*(5+num_classes), 1) # 26x26
self.head_large = nn.Conv2d(256, 3*(5+num_classes), 1) # 13x13
```
### 3. YOLOv4 相比 YOLOv3 的改进
| 组件 | YOLOv3 | YOLOv4 |
|------|--------|--------|
| Backbone | Darknet-53 | CSPDarknet-53 |
| 激活函数 | LeakyReLU | Mish(骨干)+ LeakyReLU |
| Neck | FPN | SPP + PANet |
| 特征融合 | 自顶向下 | 自顶向下 + 自底向上 |
| 参数量 | 约 5200 万 | 约 2600 万(简化版) |
|
|
### 1. 损失函数
YOLOv4 损失函数与 YOLOv3 结构类似,包含 5 个部分:
```python
class YOLOv4Loss(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.mse_loss = nn.MSELoss(reduction='sum')
self.bce_loss = nn.BCEWithLogitsLoss(reduction='sum')
# 损失权重
self.lambda_xy = 1.0
self.lambda_wh = 1.0
self.lambda_conf = 1.0
self.lambda_cls = 1.0
self.lambda_noobj = 0.5
```
损失组成:
- `xy_loss`:边界框中心点 MSE 损失
- `wh_loss`:边界框宽高 MSE 损失
- `conf_loss_obj`:正样本置信度 BCE 损失
- `conf_loss_noobj`:负样本置信度 BCE 损失
- `cls_loss`:类别分类 BCE 损失
### 2. 训练一个 epoch
```python
def train_one_epoch(model, dataloader, optimizer, loss_fn, device, epoch):
model.train()
total_loss = 0
pbar = tqdm(dataloader, desc=f"Epoch {epoch}")
for batch_idx, (images, targets) in enumerate(pbar):
images = images.to(device)
targets = [t.to(device) for t in targets]
optimizer.zero_grad()
predictions = model(images)
loss = loss_fn(predictions, targets)
loss.backward()
optimizer.step()
total_loss += loss.item()
pbar.set_postfix({'loss': loss.item()})
return total_loss / len(dataloader)
```
### 3. 执行训练
```python
model = YOLOv4(num_classes=CONFIG['num_classes']).to(device)
optimizer = optim.Adam(model.parameters(), lr=CONFIG['learning_rate'])
loss_fn = YOLOv4Loss(num_classes=CONFIG['num_classes'])
best_loss = float('inf')
for epoch in range(1, CONFIG['epochs'] + 1):
avg_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, epoch)
if avg_loss < best_loss:
best_loss = avg_loss
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': best_loss,
}, 'yolov4_cifar10_best.pth')
```
|
|
### 1. 模型加载
```python
checkpoint = torch.load('yolov4_cifar10_best.pth', map_location=device)
model = YOLOv4(num_classes=CONFIG['num_classes']).to(device)
model.load_state_dict(checkpoint['model_state_dict'])
model.eval()
```
### 2. 预测解码
```python
def decode_predictions(pred, conf_threshold=0.5):
B, A, H, W, C = pred.shape
pred_xy = torch.sigmoid(pred[..., 0:2])
pred_wh = torch.exp(pred[..., 2:4])
pred_conf = torch.sigmoid(pred[..., 4])
pred_cls = torch.sigmoid(pred[..., 5:])
mask = pred_conf > conf_threshold
results = []
for b in range(B):
if mask[b].sum() > 0:
confs = pred_conf[b][mask[b]]
cls_scores = pred_cls[b][mask[b]]
best_idx = confs.argmax()
best_cls = cls_scores[best_idx].argmax()
results.append({'class': best_cls.item(), 'score': confs[best_idx].item()})
else:
results.append(None)
return results
```
### 3. 单张图片预测
```python
def predict_single(model, image, device):
model.eval()
with torch.no_grad():
image = image.unsqueeze(0).to(device)
preds = model(image)
# 使用中等尺度 26x26 进行预测(简化)
pred = preds[1]
pred = pred.view(1, 3, 5+num_classes, 26, 26).permute(0, 1, 3, 4, 2)
return decode_predictions(pred)[0]
```
|
|
### 1. 评估函数
```python
def evaluate_model(model, dataloader, conf_threshold=0.3):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, targets in tqdm(dataloader, desc="Evaluating"):
images = images.to(device)
targets = [t.to(device) for t in targets]
preds = model(images)
pred = preds[1] # 使用 26x26 尺度
pred = pred.view(images.size(0), 3, 5+num_classes, 26, 26).permute(0,1,3,4,2)
for b in range(images.size(0)):
true_label = int(targets[b][0].item())
result = decode_predictions(pred[b:b+1], conf_threshold)[0]
if result and result['class'] == true_label:
correct += 1
total += 1
accuracy = correct / total if total > 0 else 0
return accuracy
```
### 2. 结果分析
| 指标 | 说明 |
|------|------|
| 准确率 | 预测类别与真实类别一致的比例 |
| 损失曲线 | 观察训练是否收敛 |
| 推理速度 | 单张图片前向传播时间 |
### 3. 优化建议
- 使用 Mosaic 数据增强提升小目标检测能力
- 使用 CIOU / DIOU 损失替代 MSE 边界框损失
- 调整 anchor 尺寸适配 CIFAR-10 小目标
- 使用学习率衰减策略(StepLR / CosineAnnealing)
|
|
### 全流程概览
YOLOv5 的完整流程:**数据准备 → 预处理 → 网络定义 → 训练 → 预测 → 评估**
本示例基于 `/ai/wks/aitpf/src/tpf/case/cv/yolov5.ipynb`,使用 PyTorch 从零搭建简化版 YOLOv5,在 CIFAR-10 数据集上完成目标检测全流程。
### 1. 环境准备
```python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import os
# 超参数配置
CONFIG = {
'dataset_path': '/wks/datasets/cifar10-bin/cifar-10-batches-bin',
'img_size': 416, # YOLOv5 输入尺寸
'original_size': 32, # CIFAR-10 原始尺寸
'num_classes': 10, # CIFAR-10 类别数
'batch_size': 32,
'learning_rate': 1e-3,
'epochs': 2,
'anchors': [
[(10,13), (16,30), (33,23)], # 52x52 小目标
[(30,61), (62,45), (59,119)], # 26x26 中目标
[(116,90), (156,198), (373,326)], # 13x13 大目标
],
}
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
```
### 2. CIFAR-10 数据加载
CIFAR-10 二进制格式每条记录 3073 字节(1 字节标签 + 3072 字节图像),按原始二进制解析:
```python
def load_cifar10_batch(filename):
with open(filename, 'rb') as f:
data = np.frombuffer(f.read(), dtype=np.uint8)
data = data.reshape(-1, 3073)
labels = data[:, 0].astype(np.int64)
images = data[:, 1:].reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1)
return images, labels
```
### 3. 数据预处理
将 32×32 图像插值到 416×416,并将分类标签转换为 YOLO 格式 (class_id, x_center, y_center, width, height):
```python
img = torch.nn.functional.interpolate(
img.unsqueeze(0),
size=(self.img_size, self.img_size),
mode='bilinear',
align_corners=False
).squeeze(0)
target = torch.tensor([label, 0.5, 0.5, 1.0, 1.0], dtype=torch.float32)
```
|
|
### YOLOv5 网络架构回顾
```
输入 (3, 416, 416)
↓
CSPDarknet 骨干网络 (SiLU 激活 + C3 模块)
↓ 输出3个尺度特征图
SPPF Neck (快速空间金字塔池化)
↓
PANet Neck (自顶向下 + 自底向上双向融合)
↓
检测头 ×3 (13×13, 26×26, 52×52)
↓
输出: 边界框 + 置信度 + 类别
```
### 1. 核心模块
```python
class ConvBNSiLU(nn.Module):
"""Conv + BN + SiLU"""
def forward(self, x):
return self.act(self.bn(self.conv(x)))
class C3(nn.Module):
"""CSP Bottleneck with 3 convolutions"""
def forward(self, x):
return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))
class SPPF(nn.Module):
"""快速空间金字塔池化"""
def forward(self, x):
y1 = self.m(x)
y2 = self.m(y1)
return self.cv2(torch.cat([x, y1, y2, self.m(y2)], dim=1))
```
### 2. 完整模型
```python
class YOLOv5(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.backbone = CSPDarknet53()
self.neck = PANet()
self.head_small = nn.Conv2d(256, 3*(5+num_classes), 1) # 52x52
self.head_medium = nn.Conv2d(512, 3*(5+num_classes), 1) # 26x26
self.head_large = nn.Conv2d(1024, 3*(5+num_classes), 1) # 13x13
```
### 3. YOLOv5 相比 YOLOv3 的改进
| 组件 | YOLOv3 | YOLOv5 |
|------|--------|--------|
| Backbone | Darknet-53 | CSPDarknet |
| 激活函数 | LeakyReLU | SiLU |
| Neck | FPN | SPPF + PANet |
| 特征融合 | 自顶向下 | 自顶向下 + 自底向上 |
| 训练策略 | 常规 | Mosaic、MixUp、AutoAnchor 等 |
|
|
### 1. 损失函数
YOLOv5 损失函数与 YOLOv3 结构类似,包含 5 个部分:
```python
class YOLOv5Loss(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.mse_loss = nn.MSELoss(reduction='sum')
self.bce_loss = nn.BCEWithLogitsLoss(reduction='sum')
# 损失权重
self.lambda_xy = 1.0
self.lambda_wh = 1.0
self.lambda_conf = 1.0
self.lambda_cls = 1.0
self.lambda_noobj = 0.5
```
损失组成:
- `xy_loss`:边界框中心点 MSE 损失
- `wh_loss`:边界框宽高 MSE 损失
- `conf_loss_obj`:正样本置信度 BCE 损失
- `conf_loss_noobj`:负样本置信度 BCE 损失
- `cls_loss`:类别分类 BCE 损失
### 2. 训练一个 epoch
```python
def train_one_epoch(model, dataloader, optimizer, loss_fn, device, epoch):
model.train()
total_loss = 0
pbar = tqdm(dataloader, desc=f"Epoch {epoch}")
for images, targets in pbar:
images = images.to(device)
targets_list = [t.cpu() for t in targets]
optimizer.zero_grad()
predictions = model(images)
loss = loss_fn(predictions, targets_list)
loss.backward()
optimizer.step()
total_loss += loss.item()
pbar.set_postfix({'loss': f'{loss.item():.4f}'})
return total_loss / len(dataloader)
```
### 3. 执行训练
```python
model = YOLOv5(num_classes=CONFIG['num_classes']).to(device)
optimizer = optim.Adam(model.parameters(), lr=CONFIG['learning_rate'])
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CONFIG['epochs'])
loss_fn = YOLOv5Loss(num_classes=CONFIG['num_classes'])
best_val_loss = float('inf')
for epoch in range(1, CONFIG['epochs'] + 1):
train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, epoch)
val_loss = validate(model, test_loader, loss_fn, device)
scheduler.step()
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save({...}, 'yolov5_cifar10_best.pth')
```
|
|
### 1. 模型加载
```python
checkpoint = torch.load('yolov5_cifar10_best.pth', map_location=device)
model = YOLOv5(num_classes=CONFIG['num_classes']).to(device)
model.load_state_dict(checkpoint['model_state_dict'])
model.eval()
```
### 2. 预测解码
```python
def decode_predictions(pred, conf_threshold=0.5):
B, A, H, W, _ = pred.shape
pred_xy = torch.sigmoid(pred[..., 0:2])
pred_wh = torch.exp(pred[..., 2:4])
pred_conf = torch.sigmoid(pred[..., 4])
pred_cls = torch.sigmoid(pred[..., 5:])
mask = pred_conf > conf_threshold
results = []
for b in range(B):
if mask[b].sum() > 0:
confs = pred_conf[b][mask[b]]
cls_scores = pred_cls[b][mask[b]]
best_scores = confs * cls_scores.max(dim=-1)[0]
best_idx = best_scores.argmax()
results.append({
'class': cls_scores[best_idx].argmax().item(),
'score': best_scores[best_idx].item()
})
else:
results.append(None)
return results
```
### 3. 单张图片预测
```python
def predict_single(model, image, conf_threshold=0.3):
model.eval()
with torch.no_grad():
image = image.unsqueeze(0).to(device)
preds = model(image)
all_results = []
for pred in preds:
B, C, H, W = pred.shape
pred = pred.view(B, 3, 5+num_classes, H, W).permute(0, 1, 3, 4, 2)
results = decode_predictions(pred, conf_threshold)
all_results.append(results)
# 选择所有尺度中分数最高的结果
best_result = None
best_score = 0
for scale_results in all_results:
if scale_results[0] is not None and scale_results[0]['score'] > best_score:
best_result = scale_results[0]
best_score = scale_results[0]['score']
return best_result
```
|
|
### 1. 评估函数
```python
def evaluate_model(model, dataloader, conf_threshold=0.3):
model.eval()
correct = 0
total = 0
class_correct = [0] * 10
class_total = [0] * 10
with torch.no_grad():
for images, targets in tqdm(dataloader, desc="Evaluating"):
for i in range(len(images)):
image = images[i]
true_label = int(targets[i][0].item())
result = predict_single(model, image, conf_threshold)
if result and result['score'] > conf_threshold:
if result['class'] == true_label:
correct += 1
class_correct[true_label] += 1
class_total[true_label] += 1
total += 1
accuracy = correct / total if total > 0 else 0
return accuracy
```
### 2. 结果分析
| 指标 | 说明 |
|------|------|
| 准确率 | 预测类别与真实类别一致的比例 |
| 损失曲线 | 观察训练是否收敛 |
| 推理速度 | 单张图片前向传播时间 |
### 3. 优化建议
- 使用 Mosaic 数据增强提升小目标检测能力
- 使用 CIOU / DIOU 损失替代 MSE 边界框损失
- 调整 anchor 尺寸适配 CIFAR-10 小目标
- 使用学习率衰减策略(CosineAnnealing / OneCycleLR)
- 开启混合精度训练 (AMP) 加速训练
|
|
### 全流程概览
YOLOv8 的完整流程:**数据准备 → 预处理 → 网络定义 → 训练 → 预测 → 评估**
本示例基于 `/ai/wks/aitpf/src/tpf/case/cv/yolov8.ipynb`,使用 PyTorch 从零搭建简化版 YOLOv8,在 CIFAR-10 数据集上完成目标检测全流程。
### 1. 环境准备
```python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import os
# 超参数配置
CONFIG = {
'dataset_path': '/wks/datasets/cifar10-bin/cifar-10-batches-bin',
'img_size': 416, # YOLOv8 输入尺寸
'original_size': 32, # CIFAR-10 原始尺寸
'num_classes': 10, # CIFAR-10 类别数
'batch_size': 32,
'learning_rate': 1e-3,
'epochs': 2,
}
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
```
### 2. CIFAR-10 数据加载
CIFAR-10 二进制格式每条记录 3073 字节(1 字节标签 + 3072 字节图像),按原始二进制解析:
```python
def load_cifar10_batch(filename):
with open(filename, 'rb') as f:
data = np.frombuffer(f.read(), dtype=np.uint8)
data = data.reshape(-1, 3073)
labels = data[:, 0].astype(np.int64)
images = data[:, 1:].reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1)
return images, labels
```
### 3. 数据预处理
将 32×32 图像插值到 416×416,并将分类标签转换为 YOLO 格式 (class_id, x_center, y_center, width, height):
```python
img = torch.nn.functional.interpolate(
img.unsqueeze(0),
size=(self.img_size, self.img_size),
mode='bilinear',
align_corners=False
).squeeze(0)
target = torch.tensor([label, 0.5, 0.5, 1.0, 1.0], dtype=torch.float32)
```
|
|
### YOLOv8 网络架构回顾
```
输入 (3, 416, 416)
↓
CSPDarknet 骨干网络 (C2f 模块 + SiLU 激活)
↓ 输出3个尺度特征图
SPPF Neck (快速空间金字塔池化)
↓
PANet Neck (自顶向下 + 自底向上双向融合)
↓
Decoupled Head ×3 (13×13, 26×26, 52×52)
↓
输出: 边界框 + 置信度 + 类别
```
### 1. 核心模块
```python
class C2f(nn.Module):
"""CSP Bottleneck with 2 convolutions"""
def forward(self, x):
y = list(self.cv1(x).chunk(2, dim=1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, dim=1))
class SPPF(nn.Module):
"""快速空间金字塔池化"""
def forward(self, x):
y1 = self.m(x)
y2 = self.m(y1)
return self.cv2(torch.cat([x, y1, y2, self.m(y2)], dim=1))
class DecoupledHead(nn.Module):
"""解耦检测头:分类与回归分支分离"""
def forward(self, x):
bbox = self.bbox_pred(self.bbox_stem(x))
conf = self.conf_pred(self.conf_stem(x))
cls = self.cls_pred(self.cls_stem(x))
return torch.cat([bbox, conf, cls], dim=1)
```
### 2. 完整模型
```python
class YOLOv8(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.backbone = CSPDarknet()
self.neck = PANet()
self.head_small = DecoupledHead(256, num_classes) # 52x52
self.head_medium = DecoupledHead(512, num_classes) # 26x26
self.head_large = DecoupledHead(1024, num_classes) # 13x13
```
### 3. YOLOv8 相比 YOLOv5 的改进
| 组件 | YOLOv5 | YOLOv8 |
|------|--------|--------|
| 骨干模块 | C3 | C2f(更轻量) |
| 检测头 | Coupled | Decoupled(分类/回归解耦) |
| Anchor | Anchor-Based | Anchor-Free |
| 损失函数 | CIOU + BCE | DFL + CIOU + BCE(完整版) |
| 样本分配 | 基于 IoU | Task Aligned Assigner |
|
|
### 1. 损失函数
YOLOv8 简化版损失函数包含 5 个部分:
```python
class YOLOv8Loss(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.mse_loss = nn.MSELoss(reduction='sum')
self.bce_loss = nn.BCEWithLogitsLoss(reduction='sum')
# 损失权重
self.lambda_xy = 1.0
self.lambda_wh = 1.0
self.lambda_conf = 1.0
self.lambda_cls = 1.0
self.lambda_noobj = 0.5
```
损失组成:
- `xy_loss`:边界框中心点 MSE 损失
- `wh_loss`:边界框宽高 MSE 损失
- `conf_loss_obj`:正样本置信度 BCE 损失
- `conf_loss_noobj`:负样本置信度 BCE 损失
- `cls_loss`:类别分类 BCE 损失
### 2. 训练一个 epoch
```python
def train_one_epoch(model, dataloader, optimizer, loss_fn, device, epoch):
model.train()
total_loss = 0
pbar = tqdm(dataloader, desc=f"Epoch {epoch}")
for images, targets in pbar:
images = images.to(device)
targets_list = [t.cpu() for t in targets]
optimizer.zero_grad()
predictions = model(images)
loss = loss_fn(predictions, targets_list)
loss.backward()
optimizer.step()
total_loss += loss.item()
pbar.set_postfix({'loss': f'{loss.item():.4f}'})
return total_loss / len(dataloader)
```
### 3. 执行训练
```python
model = YOLOv8(num_classes=CONFIG['num_classes']).to(device)
optimizer = optim.Adam(model.parameters(), lr=CONFIG['learning_rate'])
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CONFIG['epochs'])
loss_fn = YOLOv8Loss(num_classes=CONFIG['num_classes'])
best_val_loss = float('inf')
for epoch in range(1, CONFIG['epochs'] + 1):
train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, epoch)
val_loss = validate(model, test_loader, loss_fn, device)
scheduler.step()
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save({...}, 'yolov8_cifar10_best.pth')
```
|
|
### 1. 模型加载
```python
checkpoint = torch.load('yolov8_cifar10_best.pth', map_location=device)
model = YOLOv8(num_classes=CONFIG['num_classes']).to(device)
model.load_state_dict(checkpoint['model_state_dict'])
model.eval()
```
### 2. 预测解码
```python
def decode_predictions(pred, conf_threshold=0.5):
B, A, H, W, _ = pred.shape
pred_xy = torch.sigmoid(pred[..., 0:2])
pred_wh = torch.exp(pred[..., 2:4])
pred_conf = torch.sigmoid(pred[..., 4])
pred_cls = torch.sigmoid(pred[..., 5:])
mask = pred_conf > conf_threshold
results = []
for b in range(B):
if mask[b].sum() > 0:
confs = pred_conf[b][mask[b]]
cls_scores = pred_cls[b][mask[b]]
best_scores = confs * cls_scores.max(dim=-1)[0]
best_idx = best_scores.argmax()
results.append({
'class': cls_scores[best_idx].argmax().item(),
'score': best_scores[best_idx].item()
})
else:
results.append(None)
return results
```
### 3. 单张图片预测
```python
def predict_single(model, image, conf_threshold=0.3):
model.eval()
with torch.no_grad():
image = image.unsqueeze(0).to(device)
preds = model(image)
all_results = []
for pred in preds:
B, C, H, W = pred.shape
pred = pred.view(B, 3, 5+num_classes, H, W).permute(0, 1, 3, 4, 2)
results = decode_predictions(pred, conf_threshold)
all_results.append(results)
# 选择所有尺度中分数最高的结果
best_result = None
best_score = 0
for scale_results in all_results:
if scale_results[0] is not None and scale_results[0]['score'] > best_score:
best_result = scale_results[0]
best_score = scale_results[0]['score']
return best_result
```
|
|
### 1. 评估函数
```python
def evaluate_model(model, dataloader, conf_threshold=0.3):
model.eval()
correct = 0
total = 0
class_correct = [0] * 10
class_total = [0] * 10
with torch.no_grad():
for images, targets in tqdm(dataloader, desc="Evaluating"):
for i in range(len(images)):
image = images[i]
true_label = int(targets[i][0].item())
result = predict_single(model, image, conf_threshold)
if result and result['score'] > conf_threshold:
if result['class'] == true_label:
correct += 1
class_correct[true_label] += 1
class_total[true_label] += 1
total += 1
accuracy = correct / total if total > 0 else 0
return accuracy
```
### 2. 结果分析
| 指标 | 说明 |
|------|------|
| 准确率 | 预测类别与真实类别一致的比例 |
| 损失曲线 | 观察训练是否收敛 |
| 推理速度 | 单张图片前向传播时间 |
### 3. 优化建议
- 使用 Mosaic + MixUp + CopyPaste 等数据增强
- 使用 DFL + CIOU 替代 MSE 边界框损失
- 采用 Task Aligned Assigner 进行正负样本分配
- 使用 OneCycleLR 或 CosineAnnealing 学习率策略
- 开启混合精度训练 (AMP) 加速训练
|
|
### YOLOv3 (2018)
#### 核心改进
| 组件 | 说明 |
|------|------|
| Backbone | Darknet-53(53 层卷积,含残差连接) |
| Neck | Feature Pyramid Network (FPN) |
| Head | 3 个尺度检测头(13×13, 26×26, 52×52) |
| Anchor | 9 组 Anchor,按尺度分配 |
| 分类 | 独立 Logistic 分类器,支持多标签 |
#### 解决的问题
1. **小目标检测差**
- YOLOv1/v2 仅用一个尺度预测,小物体容易丢失
- YOLOv3 引入 FPN 多尺度预测,52×52 分支专门负责小目标
2. **骨干网络表达能力不足**
- Darknet-19 太浅,特征提取能力有限
- Darknet-53 借鉴 ResNet 残差结构,更深更稳定
3. **多标签场景**
- 将 Softmax 替换为独立的 Sigmoid 分类器
- 一个目标可同时属于多个类别
#### 输出形式
```
13×13:负责大目标,感受野大
26×26:负责中目标
52×52:负责小目标
每个网格 3 个 Anchor,每个预测 (x, y, w, h, conf, cls)
```
|
|
### YOLOv4 (2020)
#### 核心改进
| 组件 | 说明 |
|------|------|
| Backbone | CSPDarknet-53(CSP 结构 + Mish 激活) |
| Neck | SPP + PANet(自顶向下 + 自底向上) |
| BoF | Mosaic、CutMix、DropBlock、Label Smoothing、CIOU 等 |
| BoS | Mish、SPP、SAM、PANet、激活函数改进 |
| 损失 | CIOU Loss(考虑重叠面积、中心点距离、长宽比) |
#### 解决的问题
1. **训练技巧缺失导致精度不足**
- 提出 **Bag of Freebies(BoF)**:只增加训练成本、不增加推理成本的方法
- Mosaic 增强:将四张图拼接,丰富背景和小目标
- DropBlock:结构化 Dropout,提升泛化能力
2. **特征融合不够充分**
- YOLOv3 只有 FPN 自顶向下
- YOLOv4 增加 PANet 自底向上路径,双向融合语义和定位信息
3. **感受野扩展不足**
- 引入 SPP(空间金字塔池化),融合不同感受野特征
- 增强对大尺寸变化和上下文信息的建模
4. **梯度流优化**
- CSP(Cross Stage Partial)结构减少计算量,同时保持精度
- Mish 激活函数提供更强的非线性表达能力
#### 输出形式
```
输入 416×416
↓
CSPDarknet-53 提取 3 层特征
↓
SPP + PANet 双向融合
↓
3 个 Anchor-Based 检测头
```
|
|
### YOLOv5 (2020)
#### 核心改进
| 组件 | 说明 |
|------|------|
| 框架 | Ultralytics 纯 PyTorch 实现 |
| Backbone | CSPDarknet + SiLU + SPPF |
| Neck | PANet with CSP blocks |
| 训练 | Mosaic、MixUp、AutoAnchor、混合精度 |
| 部署 | 原生支持 ONNX、TensorRT、CoreML、TorchScript |
#### 解决的问题
1. **工程化和易用性差**
- YOLOv3/v4 多基于 Darknet 框架,配置复杂
- YOLOv5 提供开箱即用的 PyTorch 训练/推理/导出流水线
2. **推理速度优化**
- 将 SPP 改进为 **SPPF**(Spatial Pyramid Pooling Fast)
- 串行池化减少计算量,推理更快
3. **Anchor 自适应**
- **AutoAnchor**:在自定义数据集上自动聚类 Anchor
- 解决不同数据集目标尺寸差异问题
4. **训练效率提升**
- 默认启用混合精度训练(AMP)
- EMA(指数移动平均)稳定训练
- 更完善的数据增强策略
#### 输出形式
```
输入 640×640(默认)
↓
CSPDarknet + SPPF
↓
PANet 特征融合
↓
3 个 Coupled Head,Anchor-Based
```
|
|
### YOLOv6 (2022, Meituan)
#### 核心改进
| 组件 | 说明 |
|------|------|
| Backbone | EfficientRep / RepVGG 风格,重参数化 |
| Neck | Rep-PAN / BiC(Bidirectional Concatenation) |
| Head | Efficient Decoupled Head(解耦头) |
| Anchor | Anchor-Free |
| 标签分配 | SimOTA / TAL(Task Alignment Learning) |
#### 解决的问题
1. **工业部署效率低**
- 针对 GPU/边缘设备优化网络结构
- RepVGG 风格 backbone:训练时多分支,推理时融合为单路,速度快
2. **检测头耦合导致精度受限**
- 分类和回归任务特性不同
- 解耦头分别优化分类和定位分支
3. **Anchor 设计繁琐**
- 彻底去掉 Anchor,改为 Anchor-Free
- 直接预测中心点偏移和宽高,简化 pipeline
4. **正负样本分配不够精准**
- 使用 SimOTA / TAL 动态标签分配
- 根据预测质量动态选择正样本,提升训练效率
#### 输出形式
```
输入 640×640
↓
EfficientRep Backbone(重参数化)
↓
Rep-PAN / BiC 特征融合
↓
Efficient Decoupled Head
↓
Anchor-Free 预测 (x, y, w, h, cls)
```
|
|
### YOLOv7 (2022, WongKinYiu)
#### 核心改进
| 组件 | 说明 |
|------|------|
| Backbone | E-ELAN(Extended Efficient Layer Aggregation Network) |
| Scaling | 模型缩放策略(tiny/s/m/l/x) |
| Head | Auxiliary Head + Lead Head(辅助头 + 主导头) |
| 标签分配 | Dynamic Label Assignment |
| 重参数 | Planned Reparameterization |
#### 解决的问题
1. **模型扩展性差**
- 提出 E-ELAN 和模型缩放策略
- 从小到大一套设计,覆盖移动端到服务器端
2. **训练深度网络困难**
- E-ELAN 在保持梯度路径的同时增加特征多样性
- 避免网络过深导致的优化困难
3. **标签分配与推理不一致**
- 训练时使用 **Auxiliary Head** 增加监督信号
- 推理时只保留 **Lead Head**,不增加额外开销
- 动态标签分配协调辅助头和主导头
4. **重参数化引入的副作用**
- 提出 **Planned Reparameterization**
- 避免随意重参数化破坏原始梯度流
#### 输出形式
```
输入 640×640
↓
E-ELAN Backbone
↓
PANet + 计划重参数化
↓
Auxiliary Head(仅训练)+ Lead Head(推理)
```
|
|
### YOLOv8 (2023, Ultralytics)
#### 核心改进
| 组件 | 说明 |
|------|------|
| Backbone | CSPDarknet with C2f module |
| Neck | PANet with C2f |
| Head | Decoupled Head(Anchor-Free) |
| 标签分配 | TaskAlignedAssigner |
| 框架 | 统一 detect/segment/classify/pose API |
#### 解决的问题
1. **Anchor 限制**
- 完全 Anchor-Free,每个网格直接预测目标
- 无需预设 Anchor,对不同数据集更鲁棒
2. **检测头不够灵活**
- Decoupled Head 分离分类和回归
- 分类用 BCE,回归用 DFL + Distribution Focal Loss
- 两个任务互不干扰,精度更高
3. **C3 模块冗余**
- 用 **C2f** 替换 C3,减少参数量
- 在保持精度的同时提升速度
4. **标签分配与任务对齐**
- **TaskAlignedAssigner**:根据分类和定位分数 jointly 选择正样本
- 让高质量预测框获得更高权重
5. **多任务统一**
- 同一框架支持目标检测、实例分割、图像分类、姿态估计
- 极大降低工程维护成本
#### 输出形式
```
输入 640×640
↓
CSPDarknet + C2f + SPPF
↓
PANet 特征融合
↓
Decoupled Head(Anchor-Free)
↓
(x, y, w, h, cls_score)
```
|
|
### 模型输出形式
YOLO 系列模型将输入图像划分为 S×S 的网格,每个网格负责预测其中心点落在该网格内的目标。模型通常在 3 个尺度上输出:
```
大尺度特征图 13×13:感受野大,负责大目标
中尺度特征图 26×26:负责中等目标
小尺度特征图 52×52:负责小目标
```
#### Anchor-Based 版本(v3/v4/v5)
每个网格预测 A 个 Anchor 框,每个框输出 5 + C 个值:
```
输出张量: (B, A × (5 + C), H, W)
5 = [tx, ty, tw, th, conf]
- tx, ty: 中心点相对网格左上角的偏移(经 sigmoid)
- tw, th: 宽高缩放因子(经 exp 后与 Anchor 相乘)
- conf: 该框包含目标的置信度
C = 类别概率(每个类别的得分)
```
解码公式(以 YOLOv3 为例):
```python
bx = sigmoid(tx) + cx # cx 为网格左上角 x 坐标
by = sigmoid(ty) + cy # cy 为网格左上角 y 坐标
bw = anchor_w * exp(tw)
bh = anchor_h * exp(th)
confidence = sigmoid(conf)
class_prob = sigmoid(cls)
```
#### Anchor-Free 版本(v6/v7/v8)
去掉预设 Anchor,每个网格直接预测目标框:
```
输出张量: (B, (4 + C), H, W) # 纯 Anchor-Free 形式
4 = [tx, ty, tw, th]
- tx, ty: 中心点相对网格左上角的偏移
- tw, th: 目标宽高(或宽高对数)
C = 类别概率
```
部分实现仍保留 `conf/objectness` 分支用于兼容训练,但推理时往往直接取分类最高分。
### 标签定义
对于一张图片,每个真实目标用一个 5 维向量表示:
```
label = [class_id, cx, cy, w, h]
```
其中所有坐标均为相对于图像尺寸的归一化值(0~1):
| 字段 | 含义 |
|------|------|
| class_id | 目标类别索引 |
| cx | 目标中心点 x 坐标 / 图像宽度 |
| cy | 目标中心点 y 坐标 / 图像高度 |
| w | 目标框宽度 / 图像宽度 |
| h | 目标框高度 / 图像高度 |
### 标签到模型输出的映射
1. 根据 `cx, cy` 计算目标中心落在哪个网格:
```python
grid_x = int(cx * W)
grid_y = int(cy * H)
```
2. 根据目标宽高与 Anchor 的 IoU,选择最佳 Anchor(Anchor-Based)
3. 在该网格、该 Anchor 位置生成目标张量:
```python
target[grid_y, grid_x, :] = [tx, ty, tw, th, 1.0, one_hot_cls]
```
4. 其余位置为背景,目标值为 0
|
|
### 数据集构建流程
YOLO 训练的数据集构建通常分为以下几个步骤:
#### 1. 原始数据收集
- 收集带有边界框标注的图像
- 标注格式通常为 COCO、VOC 或自定义格式
- 最终转换为 YOLO 格式:`class_id cx cy w h`
#### 2. 图像预处理
```python
# 读取图像
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 调整尺寸
h, w = image.shape[:2]
scale = min(img_size / h, img_size / w)
new_h, new_w = int(h * scale), int(w * scale)
image = cv2.resize(image, (new_w, new_h))
# 填充到正方形(letterbox)
pad_top = (img_size - new_h) // 2
pad_left = (img_size - new_w) // 2
pad_img = np.full((img_size, img_size, 3), 114, dtype=np.uint8)
pad_img[pad_top:pad_top+new_h, pad_left:pad_left+new_w] = image
# 归一化
tensor = torch.from_numpy(pad_img).permute(2, 0, 1).float() / 255.0
```
#### 3. 标签变换
根据 letterbox 的缩放和填充,同步更新边界框坐标:
```python
# 原始标签为归一化坐标
x1 = (cx - w/2) * w * scale + pad_left
y1 = (cy - h/2) * h * scale + pad_top
x2 = (cx + w/2) * w * scale + pad_left
y2 = (cy + h/2) * h * scale + pad_top
# 转换回归一化中心点格式
cx_new = ((x1 + x2) / 2) / img_size
cy_new = ((y1 + y2) / 2) / img_size
w_new = (x2 - x1) / img_size
h_new = (y2 - y1) / img_size
```
#### 4. 数据增强
现代 YOLO 常用增强策略:
| 增强 | 作用 |
|------|------|
| Mosaic | 将 4 张图拼接,增加小目标和复杂背景 |
| MixUp | 图像混合,提升泛化能力 |
| RandomFlip | 随机水平翻转 |
| HSV Augment | 色调、饱和度、明度扰动 |
| Scale/Translate | 缩放和平移 |
#### 5. 生成训练目标
将变换后的标签转换为与模型输出同shape的张量:
```python
target_tensor = torch.zeros(B, A, H, W, 5 + num_classes)
obj_mask = torch.zeros(B, A, H, W, dtype=torch.bool)
for each object:
grid_x, grid_y = int(cx * W), int(cy * H)
anchor_idx = select_best_anchor(w, h, anchors)
target[b, anchor_idx, grid_y, grid_x, :2] = [cx*W - grid_x, cy*H - grid_y]
target[b, anchor_idx, grid_y, grid_x, 2:4] = [w*W, h*H]
target[b, anchor_idx, grid_y, grid_x, 4] = 1.0
target[b, anchor_idx, grid_y, grid_x, 5+cls] = 1.0
obj_mask[b, anchor_idx, grid_y, grid_x] = True
```
|
|
### YOLOv3 损失函数
YOLOv3 的损失由 5 个部分组成:
```python
Loss = λ_xy * L_xy + λ_wh * L_wh + λ_conf * L_conf_obj
+ λ_noobj * L_conf_noobj + λ_cls * L_cls
```
| 损失项 | 类型 | 说明 |
|--------|------|------|
| L_xy | MSE | 中心点偏移回归 |
| L_wh | MSE | 宽高回归 |
| L_conf_obj | BCE | 正样本置信度 |
| L_conf_noobj | BCE | 负样本置信度 |
| L_cls | BCE | 多标签分类 |
#### 核心设计思想
1. **中心点与宽高分离**
- 中心点偏移范围小(0~1),用 MSE 直接回归
- 宽高差异大,用 log 空间或 exp 变换后回归
2. **正负样本区分**
- 正样本:负责真实目标的网格-Anchor
- 负样本:其余所有位置
- 通常设置 `λ_noobj = 0.5` 降低负样本权重
3. **多标签分类**
- 使用 BCE + Sigmoid 而非 Softmax
- 一个目标可同时属于多个类别
### YOLOv4 损失函数改进
YOLOv4 在 YOLOv3 基础上引入大量改进:
| 改进 | 说明 |
|------|------|
| CIOU Loss | 替代 MSE,同时考虑重叠面积、中心点距离、长宽比 |
| Mosaic | 4 张图拼接,增强背景多样性 |
| DropBlock | 结构化 Dropout,提升泛化 |
| Label Smoothing | 软化标签,防止过拟合 |
| Focal Loss(可选) | 解决难易样本不平衡 |
#### CIOU Loss 公式
```
CIoU = IoU - (ρ²(b, b_gt) / c²) - αv
ρ: 预测框与真实框中心点距离
c: 最小外接框对角线长度
v: 长宽比一致性度量
α: 权重系数
```
CIOU 比 MSE 更适合边界框回归,因为它直接优化评价指标 IoU,同时考虑中心点偏移和长宽比。
|
|
### YOLOv5 损失函数
YOLOv5 基本延续 YOLOv4 的思路,但在工程细节上更完善:
```python
Loss = λ_box * L_CIoU + λ_obj * L_BCE(obj) + λ_cls * L_BCE(cls)
```
| 损失项 | 说明 |
|--------|------|
| L_CIoU | 边界框回归,使用 CIOU Loss |
| L_BCE(obj) | 置信度分支,正样本为 1,负样本为 0 |
| L_BCE(cls) | 分类分支,多标签 BCE |
#### 主要特点
1. **Anchor 自适应**
- AutoAnchor 在训练前对数据集进行 k-means 聚类
- 生成更适合当前数据集的 Anchor 尺寸
2. **CIoU 作为默认框损失**
- 替代 v3 的 MSE,直接优化 IoU
- 训练更稳定,定位更精准
3. **损失权重动态平衡**
- `λ_box = 0.05`,`λ_cls = 0.5`,`λ_obj = 1.0`
- 不同尺度下 obj 损失权重不同(小尺度 obj 权重更高)
### YOLOv6 损失函数
YOLOv6 开始全面转向 Anchor-Free,损失函数发生显著变化:
```python
Loss = L_cls + λ_box * L_bbox + L_obj(可选)
```
| 改进 | 说明 |
|------|------|
| Anchor-Free | 每个网格直接预测 4 个值 [l, t, r, b] 或 [cx, cy, w, h] |
| SimOTA / TAL | 动态标签分配,根据预测质量选择正样本 |
| Varifocal Loss / Quality Focal Loss | 同时学习分类分数和 IoU 质量 |
| 解耦头 | 分类和回归分支独立优化 |
#### Anchor-Free 标签形式
```python
# 以点到四边距离为例
predictions = [l, t, r, b] # 网格点到框左边、上边、右边、下边的距离
x1 = grid_x - l
y1 = grid_y - t
x2 = grid_x + r
y2 = grid_y + b
```
这种表示方式避免了 Anchor 预设,对不同尺寸目标更鲁棒。
|
|
### YOLOv7 损失函数
YOLOv7 在训练阶段引入了辅助头(Auxiliary Head),形成多尺度监督:
```python
Loss_total = Loss_lead + λ_aux * Loss_auxiliary
```
| 改进 | 说明 |
|------|------|
| 辅助头 | 浅层额外监督头,增加训练信号 |
| 主导头 | 推理时使用的最终检测头 |
| 动态标签分配 | 根据辅助头和主导头分别分配正样本 |
| 重参数化损失 | 结合 Planned Reparameterization 稳定训练 |
#### 辅助头的作用
```
输入图像
↓
Backbone
↓
Auxiliary Head(训练时参与损失计算)
↓
PANet
↓
Lead Head(训练和推理都使用)
```
辅助头可以帮助网络在浅层学到更好的特征,提升最终精度;推理时丢弃,不增加开销。
### YOLOv8 损失函数
YOLOv8 的损失函数进一步简化,完全 Anchor-Free:
```python
Loss = L_cls + λ_box * (L_CIoU + L_DFL)
```
| 损失项 | 说明 |
|--------|------|
| L_cls | 分类 BCE 损失 |
| L_CIoU | 边界框回归损失 |
| L_DFL | Distribution Focal Loss,将框的连续坐标离散化为分布学习 |
#### 关键变化
1. **去掉 objectness 分支**
- 纯分类分数即可表示目标存在与否
- 简化网络结构和损失计算
2. **TaskAlignedAssigner**
- 根据分类分数和 IoU 的联合度量 `t = s^α × u^β` 分配正样本
- 让高质量预测框获得更高训练权重
3. **Distribution Focal Loss (DFL)**
- 将边界框坐标建模为离散概率分布
- 比直接回归单个值更精细,尤其对小目标有帮助
```python
# Task Alignment 度量
t = cls_score^α * iou^β
# 通常 α = 1.0, β = 6.0
```
|
|
### 损失函数设计方法论
设计 YOLO 风格的损失函数,通常遵循以下步骤:
#### 1. 明确模型输出
根据网络结构确定输出张量的每个维度含义:
```python
# Anchor-Based
output.shape = (B, A*(5+C), H, W)
# Anchor-Free
output.shape = (B, (4+C), H, W)
```
#### 2. 构建匹配的目标张量
为每个输出位置生成对应的目标值:
```python
target = torch.zeros_like(output)
obj_mask = torch.zeros(B, A, H, W, dtype=torch.bool)
# 正样本赋值
for obj in gt_objects:
grid_x, grid_y = compute_grid(obj.cx, obj.cy, H, W)
anchor_idx = assign_anchor(obj, anchors) # Anchor-Based
target[b, anchor_idx, grid_y, grid_x] = encode(obj)
obj_mask[b, anchor_idx, grid_y, grid_x] = True
```
#### 3. 拆分预测与目标
```python
pred_xy = pred[..., 0:2]
pred_wh = pred[..., 2:4]
pred_conf = pred[..., 4]
pred_cls = pred[..., 5:]
target_xy = target[..., 0:2]
target_wh = target[..., 2:4]
target_conf = target[..., 4]
target_cls = target[..., 5:]
```
#### 4. 选择合适损失函数
| 任务 | 常用损失 | 原因 |
|------|----------|------|
| 边界框回归 | MSE / CIOU / DIOU / GIOU | MSE 简单,IoU 类更贴合评估指标 |
| 置信度 | BCE | 二分类问题 |
| 分类 | BCE / Focal Loss / Varifocal | 多标签,处理类别不平衡 |
| 框分布 | DFL | 精细建模坐标分布 |
#### 5. 处理正负样本不平衡
- 设置负样本损失权重 `λ_noobj < λ_obj`
- 使用 Focal Loss 降低易分样本权重
- 使用动态标签分配(OTA/SimOTA/TAL)选择高质量正样本
#### 6. 加权求和
```python
Loss = λ_xy * L_xy + λ_wh * L_wh
+ λ_conf * L_conf + λ_cls * L_cls
+ λ_box * L_box
```
权重选择原则:
- 让各项损失数量级接近
- 根据任务重要性调整
- 可随训练过程动态调整(如 warmup 阶段增大 box 权重)
|
YOLOv1论文:You Only Look Once: Unified, Real-Time Object Detection
YOLOv2/YOLO9000论文:YOLO9000: Better, Faster, Stronger
YOLOv3论文:YOLOv3: An Incremental Improvement
YOLOv4论文:YOLOv4: Optimal Speed and Accuracy of Object Detection
Ultralytics YOLO 官方代码库
Ultralytics 训练文档
AlexeyAB Darknet(YOLOv3/v4)
YOLOv5 官方代码库