YOLO基础
## 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 |
YOLO演进
## 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 检测/分割/姿态统一模型 |
YOLO代码解析
## 项目结构对比 ### 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 | 很高 | 高 |
YOLO v3 训练预测实战
### 全流程概览 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层就可以
YOLO v4 训练预测实践
### 全流程概览 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)
YOLO v5 训练预测实践
### 全流程概览 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) 加速训练
YOLO v8 训练预测实践
### 全流程概览 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) 加速训练
YOLO 模型演变
### 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 损失函数设计
### 模型输出形式 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 官方代码库