ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv3目标检测实战:从数据集构建到训练评估全流程解析

YOLOv3目标检测实战:从数据集构建到训练评估全流程解析 简介基于YOLOv3的目标检测课程作业资源包面向计算机视觉方向的学生与入门开发者完整覆盖行人、自行车与机动车三类目标的检测任务适用于课程作业、实验复现或算法验证场景。包内共12个文件包括3个Python脚本用于模型训练、推理与目标检测7张结果分析图展示mAP、LAMR、检测结果及标注信息另有1份txt输出记录和1个Markdown实验报告压缩包仅158KB轻量且便于快速上手。实验报告以PaddlePaddle框架为依托按小组成员分工、网络结构、模型建立、训练迭代、YOLO与YOLO-tiny对比、参数调整、模型优化、网络性能分析、挑战集测试及实际结果等模块组织系统还原从算法原理到代码落地的完整过程。已有693人学习下载既可作为课程作业的参考模板也适合初学者动手复现目标检测实验快速掌握YOLOv3的核心思想与调优经验。1. 基于 YOLOv3 的目标检测课程作业代码、数据集与训练评估全拆解一份能跑通行人、自行车、机动车三类识别的 YOLOv3 课程作业值得花点时间把它从头到尾拆一遍。这份资源不是零散代码堆砌而是一个完整闭环PaddlePaddle 框架下的网络定义、数据集与类别文件、训练主脚本、独立推理脚本以及 mAP、LAMR、检测结果明细等一整套评估输出。对正在做目标检测课程设计、或者第一次用 YOLO 系列做实验的从业者来说最有价值的不是算法本身而是从数据准备到结果验证的完整链路。本文按我实际拆解这个项目的顺序把网络结构、标注格式、训练参数、踩坑记录和结果验证逐层展开读完你也能在自己的数据集上复现。2. YOLOv3 网络结构拆解Darknet-53 骨干与多尺度检测头2.1 从滑动窗口到回归问题YOLO 的核心思想转变在 YOLO 出现之前主流目标检测思路是滑动窗口加分类。用一个固定大小的窗口在整张图片上滑动每个窗口位置裁出来的区域送进分类网络判断里面是行人还是自行车。这个思路实现简单但计算量巨大——窗口尺寸、宽高比、滑动步长三个变量组合起来一张图要跑成千上万次前向推理。而且窗口尺寸和物体实际尺寸往往对不上小目标漏检、大目标被截断是常态。YOLOv3 把这个多阶段流程压缩成了一个回归问题单个卷积神经网络直接接收整张图像一次前向传播同时输出所有目标的位置和类别。网络把输入图像划分成 S×S 的网格每个网格负责预测固定数量的边界框每个边界框用四个描述符 (x, y, w, h) 定位再附带一个置信度分数和各类别的概率分布。用课程报告里的话说它将对象检测重新定义为一个回归问题这也是整个 YOLO 系列速度优势的根本来源——没有区域提议阶段没有二次分类一次推理全部搞定。这个设计取舍在实时检测场景里非常关键也是这套课程作业选它而不是 Faster R-CNN 的原因。2.2 Darknet-53 骨干网络残差结构与下采样节奏YOLOv3 的骨干网络叫 Darknet-53名字里的 53 来自网络层数。它借鉴了 ResNet 的残差思想在卷积块之间加了 shortcut 连接解决深层网络训练时的梯度消失问题。整条骨干由一系列 1×1 和 3×3 卷积交替堆叠而成通过步长为 2 的卷积实现逐级下采样特征图尺寸从输入分辨率一路缩到原来的 1/32。我拆 main.py 和 objectDetection.py 时发现代码里骨干网络的实现方式和官方 Darknet-53 基本一致但用 PaddlePaddle 的 paddle.nn 重写了一遍。下采样节奏是固定的每经过一组残差块特征图长宽减半通道数翻倍。这个节奏直接决定了后面三个检测头的输入特征图尺寸——如果输入是 416×416三个检测头拿到的特征图分别时 52×52、26×26、13×13。检测头各自独立如图 2-1 所示。这一层对应代码里的 darknet53 模块输出的是三个不同尺度的特征图而不是单一向量。# objectDetection.py 中的骨干网络关键结构 class Darknet53(paddle.nn.Layer): def __init__(self): super(Darknet53, self).__init__() # 第一层卷积输入图像输出32通道步长2下采样 self.conv1 ConvBNLayer(3, 32, 3, stride1, actleaky) # 后续每个 stage 都是一组残差块 步长2卷积下采样 self.stage1 self._make_stage(32, 64, 1) # 输出 208×208 self.stage2 self._make_stage(64, 128, 2) # 输出 104×104 self.stage3 self._make_stage(128, 256, 8) # 输出 52×52 self.stage4 self._make_stage(256, 512, 8) # 输出 26×26 self.stage5 self._make_stage(512, 1024, 4) # 输出 13×13这里 _make_stage 内部是多个残差块的堆叠每个残差块包含 1×1 降维卷积和 3×3 升维卷积最后加上 shortcut。stage 后面的数字是残差块重复次数YOLOv3 原文在 stage3、stage4、stage5 分别用了 8、8、4 次重复这个配置不能随意改——改少了特征提取不充分改多了训练速度和显存开销线性上涨。ConvBNLayer 封装了卷积、批归一化、LeakyReLU 激活三件套批归一化在训练时会对每个通道做归一化能显著加速收敛。2.3 多尺度检测头特征金字塔融合与三个输出分支YOLOv3 的多尺度检测是它区别于 YOLO v1/v2 的最大改进。小目标在浅层特征图52×52上信息保留完整大目标在深层特征图13×13语义最强。三个检测头各司其职但深层特征会通过上采样与浅层特征拼接让浅层也能获得语义信息。这个结构就是特征金字塔的变体代码里通过一个 FPN 模块实现。三个检测头的输出张量形状需要严格对应每个网格预测 3 个先验框每个框输出 4 个坐标偏移、1 个置信度、3 个类别概率行人/自行车/机动车单尺度输出就是 S×S×3×(413)。代码里用的是 Conv2D(256, 24, 1)24 就是 3×8 展平后的通道数。这里有一个容易混淆的点anchor 数量是 3类别数是 3于是输出通道公式是 3×(5classes)也就是 3×(53)24。如果你换成自己的数据集类别数变了这个卷积输出通道必须同步修改否则前向传播直接报维度错误。# 每个检测头的输出卷积 self.yolo_head paddle.nn.Conv2D( in_channels256, out_channels3 * (5 3), # 3个anchor × (x,y,w,h,obj 3个类别) kernel_size1, stride1, padding0 )训练时损失函数会分别计算三个尺度的损失再相加。每个尺度各自负责不同尺寸范围的目标13×13 特征图感受野最大负责大目标52×52 感受野最小负责小目标。课程作业的可视化里如果有小目标漏检优先排查 52×52 分支的权重和对应 anchor 配置而不是盲目调全局阈值。3. 数据集构建与标注格式从原图到 YOLO 坐标的转换3.1 目录组织与类别文件classes 文件的含义拆开压缩包第一个要看的是 classes 文件。这个文件每行一个类别名顺序至关重要——训练时类别 ID 按行号从 0 开始编号推理时输出的类别也是按这个顺序映射回名称。这个课程作业里的 classes 内容基本可以确定是 person、bicycle、car 三行对应行人、自行车、机动车三个检测类别。数据集目录结构和标注格式是 YOLO 系列通用的图片和标注文件分开存放每张图片对应一个同名 txt 文件文件名一致扩展名不同。图片放在 images 目录下标注放在 labels 目录下。txt 里每行描述一个目标格式是类别ID x_center y_center width height所有坐标都归一化到 0~1 区间。这个归一化坐标是 YOLO 和 VOC 标注格式最大的区别——VOC 存的是左上角和右下角的绝对像素坐标YOLO 存的是中心点和宽高的相对比例。# 数据集目录结构常见组织方式 dataset/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── 000002.jpg │ └── val/ │ ├── 000100.jpg │ └── 000101.jpg ├── labels/ │ ├── train/ │ │ ├── 000001.txt │ │ └── 000002.txt │ └── val/ │ └── 000100.txt ├── classes.txt ├── train.txt # 每行一个训练图片路径 └── val.txt # 每行一个验证图片路径train.txt 和 val.txt 是训练入口文件每行写图片的完整路径。PaddlePaddle 的 Dataset 类会读取这些路径再根据图片路径去同级 labels 目录下找对应标注。路径写相对路径还是绝对路径要看训练脚本实现有的脚本内部会拼接有的要求完整路径。我的习惯是直接写绝对路径避免在不同机器上跑的时候路径拼接出错。3.2 标注格式转换从 VOC XML 到 YOLO txt 的坐标变换如果你手上的数据是 VOC 格式XML 标注转到 YOLO 格式需要三步解析 XML 拿到目标的类别和边界框坐标把 xmin、ymin、xmax、ymax 转成中心点加宽高的形式最后除以图片宽高做归一化。转换公式不复杂但方向很容易搞反尤其是坐标归一化时忘记除以图片宽而不是高。下面这段脚本是把 VOC XML 批量转成 YOLO txt 的常用做法我用的是 xml.etree.ElementTree 解析这是标准库不需要额外安装依赖。import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_path, class_list): tree ET.parse(xml_path) root tree.getroot() # 图片实际尺寸从 XML 的 size 节点读取 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_list: continue # 过滤不在类别表中的目标 cls_id class_list.index(cls_name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 转中心点 宽高再归一化 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_path, out_name), w) as f: f.write(\n.join(lines))脚本里最容易出错的是坐标归一化时用 img_w 还是 img_h。x 相关的一律除以宽y 相关的一律除以高很多人惯性写成除以宽导致标注全部纵向偏移。另外一个容易被忽略的点是 XML 里 bndbox 的坐标可能带有小数点取值时用 float 不要用 int否则目标太小的时候精度损失会直接影响小目标检测效果。3.3 训练集与验证集划分随机采样与类别均衡数据划分看起来简单按 8:2 随机分就行但目标检测数据有个特殊要求同一个目标的多个实例可能出现在相邻帧或同一张图的不同位置如果随机划分导致验证集里出现某种类别数量极少mAP 评估会出现这类目标的 AP 为 0整个 mAP 被拉低。我一般会先统计每个类别在全部数据中的分布再按类别比例做分层采样。import random from collections import defaultdict def split_dataset(image_paths, label_dir, val_ratio0.2, seed42): random.seed(seed) # 按类别统计样本确保验证集覆盖全部三类 cls_samples defaultdict(list) for img_path in image_paths: label_path os.path.join(label_dir, os.path.splitext(os.path.basename(img_path))[0] .txt) if not os.path.exists(label_path): continue with open(label_path) as f: first_line f.readline().strip() if first_line: cls_id int(first_line.split()[0]) cls_samples[cls_id].append(img_path) train, val [], [] for cls_id, paths in cls_samples.items(): random.shuffle(paths) split_idx int(len(paths) * (1 - val_ratio)) train.extend(paths[:split_idx]) val.extend(paths[split_idx:]) return train, val这段代码按类别分组后再分别划分能保证每个类别在训练集和验证集里都有一定样本量。seed 参数固定随机数种子保证每次划分结果一致方便复现。如果你换了自己的数据建议先用脚本统计各类别样本数如果某个类别只有几十张图别急着扩大验证集比例优先保证训练样本量才是正经事。4. PaddlePaddle 训练实操代码结构与关键参数调整4.1 代码文件分工main.py 与 objectDetection.py 各管什么压缩包里的 Python 文件大致分为三类训练入口、网络定义、推理验证。objectDetection.py 承载了 YOLOv3 的网络结构定义和损失函数计算main.py 是训练主脚本负责加载数据、配置优化器、执行迭代循环。Infer.py 是独立的推理脚本读取训练好的权重对单张图片或目录做检测并输出可视化结果。这个划分方式值得学习网络定义和训练逻辑分开后续换数据集训练时只需要改数据加载部分网络结构基本不用动。main.py 里通常在开头集中定义了一批超参数——初始学习率、batch_size、迭代轮数、输入尺寸、类别数、anchor 列表。训练时这些参数全部集中在顶部方便反复调整。# main.py 中的超参数配置区 EPOCHS 100 BATCH_SIZE 8 LEARNING_RATE 0.001 INPUT_SIZE 416 # 训练时输入图片边长 NUM_CLASSES 3 # 行人、自行车、机动车 ANCHORS [[10, 13], [16, 30], [33, 23], # 52×52 检测头 [30, 61], [62, 45], [59, 119], # 26×26 检测头 [116, 90], [156, 198], [373, 326]] # 13×13 检测头ANCHORS 列表里的 9 组宽高是官方在 COCO 数据集上用 k-means 聚类出来的。很多人直接沿用但如果你的数据集目标尺寸分布和 COCO 差异很大比如全是近距离的大目标最好自己重新聚类 anchor否则训练初期 loss 会很高收敛也慢。我自己做数据集时会先跑一段 k-means聚类完对比官方的 anchor差异超过 30% 就果断换。4.2 训练脚本的关键流程数据加载、损失计算与优化器配置训练主循环的逻辑并不复杂每个 epoch 内遍历训练集取一个 batch 的图片和标注前向传播得到三个尺度的预测计算损失反向传播更新权重。但 PaddlePaddle 的实现里数据加载这块要特别注意——YOLO 的数据增强是在线做的每个 batch 的图片会被 resize 到统一尺寸同时做随机翻转、色调抖动等操作标注坐标必须同步变换。# main.py 训练循环核心逻辑 for epoch in range(EPOCHS): for batch_id, (images, targets) in enumerate(train_loader): # images: [B, 3, 416, 416] 归一化后的张量 # targets: 每个目标的类别、中心坐标、宽高 # 前向传播返回三个尺度的预测 preds model(images) # preds[0]: [B, 24, 52, 52] # preds[1]: [B, 24, 26, 26] # preds[2]: [B, 24, 13, 13] # 计算总损失 三个尺度的坐标损失 置信度损失 分类损失 loss model.loss(preds, targets) # 反向传播与参数更新 loss.backward() optimizer.step() optimizer.clear_grad() if batch_id % 10 0: print(fepoch {epoch}, batch {batch_id}, loss {loss.item():.4f})训练时打印的 loss 是三个尺度的加权和。观察 loss 曲线有讲究前几个 epoch loss 应该在几百的量级快速下降如果一开始就是个位数或者几十先检查学习率是不是太大或者 anchor 和数据集尺寸不匹配。我一般前 10 个 epoch 只看 loss 有没有下降趋势不急着调参等到 loss 降速明显放缓再考虑降学习率。4.3 训练过程中的参数调整与 YOLO/YOLO-tiny 选型课程报告的摘要里专门有一节YOLO 和 YOLO-tiny 对比这是训练调参最重要的决策点。YOLOv3 完整版用 Darknet-53 骨干推理速度慢但精度高YOLO-tiny 用轻量骨干层数大幅缩减速度可以跑到实时代价是 mAP 明显下降。这个作业选择了完整版说明对精度的要求大于速度。训练过程中我一般按三个阶段的节奏调整学习率第一阶段保持初始学习率 0.001 跑完前 30% 的 epoch让网络快速收敛到大致正确的检测模式第二阶段降到 0.0001精细调整边界框回归最后 10% 的 epoch 用 0.00001 微调防止在最优解附近震荡。此外 batch_size 影响的是梯度的稳定性显存够用就用大 batch不够就调小但同步降学习率——batch_size 减半学习率也减半这是经验法则。如果是课程作业迭代轮数不用太多100 个 epoch 在单卡上跑几个小时足够出一个能看的模型没必要追求极致 mAP。5. 避坑指南训练 YOLOv3 遇到的五个翻车现场5.1 损失值不降或高位震荡学习率与 batch_size 失配现象训练十几个 epoch 后 loss 停在 50 上下波动没有明显下降趋势。原因学习率偏大导致损失在最优解附近来回震荡或者 batch_size 太小梯度方向噪声太大。我遇到过最离谱的一次是学习率设了 0.01loss 前三个 epoch 就冲到 800之后一直下不来。解决把学习率降到 0.001 或 0.0005如果 batch_size 小于 8同步降低学习率。改完重启训练一般十个 epoch 内能看到 loss 进入稳定下降通道。排查时先打印前几个 batch 的 loss 值如果第一个 batch 就异常高优先怀疑学习率而不是网络结构。5.2 mAP 很低但训练 loss 正常anchor 与目标尺寸分布的错位现象训练 loss 曲线正常收敛但验证集 mAP 只有 0.2 左右检测出的框位置整体偏移。原因官方 anchor 是在 COCO 数据集上聚类得到的COCO 包含大量小目标而你的数据集里目标普遍偏大或偏小导致每个检测头负责的目标范围与实际分布错位。解决用 k-means 对训练集所有标注框的宽高重新聚类生成 9 组新 anchor按面积从小到大排列前 3 组给 52×52 检测头中间给 26×26最大给 13×13。改完 anchor 后建议从头训练因为之前学到的特征已经和错误 anchor 绑定了继续训效果有限。5.3 检测结果全是背景置信度阈值与 NMS 阈值的配合失误现象模型预测正确但输出图片上没有任何检测框或者同一个目标被画了七八个重叠框。原因置信度阈值设太高把正确预测全过滤掉了或者 NMS非极大值抑制阈值设太高重叠框没有合并干净。解决调 Infer.py 里的两个参数。置信度阈值从 0.5 降到 0.25 看效果NMS 阈值保持在 0.45 左右。判断是不是阈值问题有个技巧把置信度阈值设为 0直接看模型输出原始框的数量如果输出框很多但位置准确就是阈值过滤太狠了。5.4 GPU 显存溢出输入尺寸与批量大小不匹配现象训练脚本启动后几秒报 CUDA out of memory程序直接退出。原因YOLOv3 输入尺寸 416×416 只是基础实际显存占用还和 batch_size、特征图通道数成正比。三检测头设计让显存占用比单尺度模型高不少。解决显存不足时优先降 batch_size从 8 降到 4 或 2。如果降到 2 还不够再把输入尺寸从 416 降到 320——代价是小目标检测能力下降但课程作业场景下可接受。另外检查是否开了太多 DataLoader 子进程每个子进程都会拷贝一份模型也是隐性显存杀手。5.5 训练集和验证集划分不合理导致评估失真现象训练过程正常验证集 mAP 很高但实际跑一张新图片效果很差或者 mAP 曲线剧烈波动。原因数据集划分时没有保证类别均衡验证集里恰好全是某一类目标或者同一场景的相似图片被分到了两个集合里造成评估结果虚高。解决用 3.3 节的分层采样逻辑重新划分数据并按类别打印训练集和验证集的样本数做人工核对。还有一个习惯值得养成训练前手动挑几张和训练集风格差异明显的图片放进测试列表每次评估完单独看这几张的检测结果比只看 mAP 数字可靠得多。6. 结果验证与推理验证从输出文件反推模型真实水平6.1 用 output 文件和可视化指标判断模型有没有学到位压缩包里 output.txt 和三个 PNG 图——mAP.png、lamr.png、detection-results-info.png——是评估阶段的核心产出。output.txt 保存了每一张测试图片的检测结果包括目标类别、置信度分数和边界框坐标。打开后逐行看重点找两类异常一是置信度很高但框明显不对的目标二是同一区域出现多个大小相近的重叠框。前者说明类别特征没学好后者说明 NMS 没过滤干净。三个可视化指标各有分工。mAP.png 展示整体平均精度曲线越靠近右上角说明模型越可靠。lamr.png 是 Log-Average Miss Rate 曲线看的是漏检率——曲线越低说明漏检越少这个指标对行人这类安全敏感目标尤其重要。detection-results-info.png 通常展示各类别的精确率和召回率分布从中能看出模型对哪一类最拿手、哪一类最容易混淆。6.2 用 Infer.py 做单图验证参数组合与输出解读推理脚本 Infer.py 是快速检验模型效果的最短路径。加载模型权重后对输入图片执行预处理——resize 到训练尺寸、归一化、维度变换——然后前向推理最后把检测框画回原图。# Infer.py 单图推理核心逻辑 def detect(image_path, model, conf_thres0.25, nms_thres0.45): # 读取并预处理图片 img cv2.imread(image_path) orig_h, orig_w img.shape[:2] img_resized cv2.resize(img, (416, 416)) img_tensor paddle.to_tensor(img_resized.transpose(2, 0, 1)[None]).astype(float32) img_tensor img_tensor / 255.0 # 归一化到 0~1 # 前向传播 with paddle.no_grad(): preds model(img_tensor) # 后处理解码坐标、按置信度过滤、NMS 去重 boxes decode_preds(preds, conf_thres, nms_thres, orig_w, orig_h) # boxes 中每个元素: [cls_id, score, x1, y1, x2, y2] return boxes这里的解码坐标是关键模型输出的是归一化后的中心点和宽高要乘回原图尺寸再转换成左上角和右下角坐标用于画框。如果你拿到的权重是自己训练的务必确认训练时的输入尺寸是 416推理时 resize 要一致否则坐标还原会和实际位置对不上。图片里检测结果如果框的位置略微偏移检查是不是省了 letterbox 预处理——直接把图拉伸到 416 会破坏宽高比导致框偏。6.3 从课程报告的挑战集测试分析获得进阶启发课程报告里有实际结果和挑战集测试分析这两节这是最有参考价值的部分。挑战集通常包含低光照、遮挡、小目标、密集人群等困难场景。对行人检测来说LAMR 指标比 mAP 更能反映实际可用性——漏检一个行人和误检一个非行人的代价完全不同。我自己的习惯是训练完成后固定一组测试图片不参与任何训练过程每次调完参数跑一遍记录 mAP 和 LAMR 两个数的变化比只看 loss 曲线直观得多。那次做完这个课程作业之后我每次训练目标检测模型都会强制走一遍这个验证流程先看 output 文件的前 50 行检测结果再跑一遍全部测试图片生成指标最后挑三张最难的挑战图片看实际效果。这套流程看着繁琐但确实能拦住很多指标好看、实际翻车的情况。希望这篇文章能帮你在自己的数据集上少走几个弯路训练一次就拿到能用的模型。本文还有配套的精品资源点击获取
返回列表