简介:这份课程作业以YOLOv3为目标检测骨干网络,面向计算机视觉初学者或需要完成类似课程设计的学生,提供可识别行人、自行车与机动车的完整实现、可视化结果与配套数据集。压缩包共12个文件,核心为3个Python脚本(模型构建、训练与推理),另有7张检测/评估结果图、1个类别说明文本和1份Markdown实验报告,整体仅158KB,体积小巧便于快速下载。报告系统梳理了YOLO算法从滑动窗口到回归式检测的演进、网络结构、PaddlePaddle代码实现与主要参数,并给出模型建立、训练迭代、数据集基本信息,以及YOLO与YOLO-tiny的对比实验、参数调整与模型优化策略,还涵盖挑战集测试分析和实际结果展示;附带的mAP曲线、检测结果信息图等可辅助理解网络性能分析。已有693人学习浏览,适合用来复现实验、理解目标检测原理、作为课程项目参考,或在此基础上进一步拓展调优思路。
1. 为什么课程作业选yoloV3:一个能在自己笔记本上跑通的目标检测方案
把“识别行人、自行车与机动车”做成课程作业,最稳妥的方案不是从零手写神经网络,而是基于yoloV3做迁移学习。它结构足够经典——Darknet-53骨干、多尺度检测头、锚框机制——课程答辩时能讲的东西多;同时社区资料极厚,踩过的坑网上都有记录,适合在有限时间窗口内交付。更关键的是,yoloV3对硬件要求相对温和:一张8G显存的显卡就能跑训练,纯CPU也能跑得动推理。这篇笔记按“网络结构 → 数据准备 → 训练调参 → 避坑 → 推理验证”的顺序,把一套完整落地路径讲清楚,新手能照做,熟手能直接拿去改自己的任务。
2. 先弄懂yoloV3的网络骨架与检测头:Darknet-53、多尺度特征图与锚框机制
做目标检测和做分类不一样的地方在于:网络既要回答“这是什么”,也要回答“这个东西在哪、有多大”。yoloV3把这两个问题的答案都编码在特征图的每个格子里。理解它的结构,不是为了在答辩时背名词,而是为了后面改配置、调参数、看日志时有判断依据。
2.1 Darknet-53:残差块堆出来的特征提取主干
yoloV3的主干网络叫Darknet-53,名字里“53”指的是52个卷积层加1个全连接层(实际做检测时全连接层用不上)。它的核心设计是大量残差连接:把输入加到卷积块的输出上,形成跳跃连接。这样做的好处是梯度在反向传播时有一条高速公路可走,网络堆到几十层也不会轻易出现梯度消失。
Darknet-53没有用池化层做下采样,而是用步长为2的卷积替代。每个卷积块由“卷积-BN-LeakyReLU”组成。BN层是个关键:它对输入做批归一化,让每层激活值分布稳定,训练时能容忍相对大一点的学习率。很多新手在移植yoloV3代码时手动删掉BN层想加速,结果loss直接不收敛,原因就在这里——BN不仅仅是加速,它是整个训练稳定性的基石。
在PyTorch里,Darknet-53不需要自己实现,成熟的代码仓库里都有现成的类。但作为课程作业,建议至少手写一遍forward流程,理解特征图在哪个位置被分流。主干网络最终输出的是三个不同尺度的特征图,分别来自第36层(13x13)、第61层(26x26)和第74层(52x52)。这三个尺度就是yoloV3多尺度检测的“原料”。
2.2 三个尺度的检测头:为什么靠它才能同时认大车和小行人
目标检测里最烦人的问题是尺度差异。一张街景图里,机动车可能占了几百乘几百像素,行人骑的自行车可能只有几十像素。如果只用最后一层特征图做检测,小目标早就被下采样弄丢了——13x13的特征图上一个格子对应原图32x32的像素块,一个30像素高的小行人在特征图里连一个完整格子都占不满。
yoloV3的做法是“特征金字塔”式的多尺度检测:
| 特征图尺寸 | 检测头对应 | 适合的目标 |
|---|---|---|
| 13x13 | 大目标检测头 | 机动车、公交车 |
| 26x26 | 中目标检测头 | 自行车、行人(近景) |
| 52x52 | 小目标检测头 | 远景行人、自行车局部 |
这个结构对“行人、自行车、机动车”三类目标特别友好。机动车通常面积大,交给低分辨率特征图处理能减少计算量;行人是典型的小目标,必须依赖52x52的高分辨率特征图。训练你自己的数据集时,如果发现小目标漏检严重,优先怀疑的不是模型能力,而是输入图像尺寸——后面会细说。
2.3 锚框先验与预测解码:特征图上的格子怎么变成框
yoloV3不是直接回归“中心点x、中心点y、宽、高”四个数,而是基于预先设定的一组锚框做偏移预测。COCO预训练权重里内置了9组锚框,分成三组分别对应三个检测头,每个检测头负责3组锚框。以COCO默认值为例:
- 13x13大目标头: (116,90)、(156,198)、(373,326)
- 26x26中目标头: (30,61)、(62,45)、(59,119)
- 52x52小目标头: (10,13)、(16,30)、(33,23)
训练时网络对真值框做的事是反过来的:把真值框缩放到特征图坐标系,算它和每个锚框的IoU,IoU最大的锚框负责预测这个目标。所以锚框的分布必须贴合你数据集中目标的尺寸分布。课程作业如果只用默认锚框,通常问题不大,但如果你只拍俯视的自行车、或者全是远景小行人,锚框就要用k-means重新聚类。yolo系列在darknet里提供了k-means锚框聚类脚本,PyTorch复现版也遍地都是,后面训练章节会讲具体怎么用。
解码公式也值得写进作业报告:
bx = sigmoid(tx) + cx by = sigmoid(ty) + cy bw = pw * exp(tw) bh = ph * exp(th)tx、ty是网络输出的中心偏移,经过sigmoid限制在0到1之间,保证中心点落在当前格子内;bw、bh是宽高,用exp让预测值始终为正。这就是为什么训练日志里经常看到“Avg IOU:某个数值”后面跟着w、h正负波动——exp的梯度特性决定了宽高预测天然比中心点容易抖。
理解了这块,再去看配置文件里anchors、classes、filters三个参数的作用,就不会心虚了。
3. 准备数据集:从标注工具选择到VOC格式转yolo格式的落地脚本
模型结构讲得再好,没有数据一切都是空谈。课程作业常见的数据获取路线有三条:公开数据集、网上下载的街景图片、自己用手机拍。不管哪条路,最终都要变成yoloV3训练时吃的格式:一张图片对应一个txt,txt里每行是“类别id 中心点x 中心点y 宽 高”,而且全部归一化到0到1之间。
3.1 公开数据集怎么挑:COCO太大,小数据集够不够用
直接拿COCO训练集跑不现实——几十G数据加几天训练时间,课程作业等不起。更常见的选择是从COCO里抽子集,只保留person、bicycle、car、bus、truck这几类,用现成的COCO训练脚本做一次轻量训练。但更贴合课程作业的做法是自建小数据集:机动车、行人、自行车各采集500到1000张,总共2000到3000张,训练2到4个小时就能出效果。
数据集太小会不会过拟合?会。但课程作业的目标珠峰是“在测试集上有可用检出率”,不是冲刷新纪录。有正则化手段兜底:数据增强、随机裁剪、mosaic(YOLOv4才提出,yoloV3时代常用的是随机缩放、翻转、hue-saturation扰动)。小数据集训练还有一个技巧:加载COCO预训练权重再做全图微调,比从头训练收敛快得多,也更不容易过拟合。因为COCO里本身就包含person、bicycle、car,模型已经认识这些物体的大致长相,只是没适配你的街景分布而已。
3.2 标注工具:LabelImg和它的闭坑细节
自建数据集的痛点永远在标注。最常规的标注工具是LabelImg,图形界面,画框后自动生成XML文件(Pascal VOC格式)。用LabelImg有两点必须注意。第一,标注时尽量框住目标主体,不要留太多背景边;背景边会让锚框IoU计算失真,训练时物体中心点附近的负样本会变多。第二,软件默认保存的XML里有绝对路径,训练前要检查图片路径字段,否则换目录后读取会报错。
还有一个小坑:LabelImg保存的图像是原图,不管原图多大多小。yoloV3训练时对输入尺寸有统一要求(常见416或608),代码会自动resize,但强烈建议在训练前把图片统一处理到相近尺寸——直接用OpenCV批量resize到640x640以内并覆盖原文件即可。原始图片长宽悬殊(比如一张很长的横幅照片)会让resize后目标变形,检测性能肉眼可见地掉。
如果要快一点,可以考虑用半自动标注工具,但课程作业规模下,纯手标2到3千张图耗时大概2到3天,边标边检查是常态。你要做好心理准备,这部分没有捷径,标得越认真,后面训练翻车的概率越低。
3.3 VOC格式转yolo训练格式:转换脚本与类别映射
LabelImg产出的XML是VOC格式,而yoloV3吃的是txt。转换脚本每个yolo教程里都有,但真正出问题的往往是细节。下面这个脚本是我常用的版本,可以直接参考试着改成适合自己数据的样式,注意它在转的同时顺手把类别校验和异常宽高过滤也加上了:
import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_dir, out_dir, class_names): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) out_lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_names: continue class_id = class_names.index(name) box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) # 坐标归一化,并做边界裁剪,防止标注框越界 x_center = ((x1 + x2) / 2) / img_w y_center = ((y1 + y2) / 2) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h if w <= 0 or h <= 0: print(f"跳过异常框: {xml_file} 中 {name}") continue out_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if out_lines: with open(os.path.join(out_dir, xml_file.replace('.xml', '.txt')), 'w') as f: f.write('\n'.join(out_lines)) print("转换完成") class_names = ['person', 'bicycle', 'car', 'bus', 'truck'] convert_voc_to_yolo('labels_xml/', 'labels_txt/', class_names)这段代码的说明:
- class_names顺序就是训练时使用的类别ID,必须与数据加载器的类表一致,否则模型学到的类别编号会错位。最常见翻车场景是:训练时class_names是[‘person’,‘bicycle’,‘car’],测试时加载的是COCO原版80类的权重,结果模型输出的类别ID全乱套。
- 边界裁剪这里不能省:如果标注时手抖让xmax超过了图像宽度,归一化后w可能大于1,训练时loss会跳nan。
- print出异常框是必要的,别觉得烦。大量“跳过”记录意味着某个manifest或XML有系统性问题(比如某个类名写错),提早发现,避免训练到一半才发现数据坏了。
数据准备好后,目录结构建议按darknet或自己代码的约定组织,常见的是train/和valid/两个文件夹,各自下面分images/和labels/。然后生成train.txt和valid.txt,每行放图片的绝对路径。生成脚本很简单,但要确认里面没有空行、没有空格路径。Windows下路径含中文或空格几乎一定会出问题,这个坑放到避坑章节细说。
4. 跑通训练:cfg文件三个必改参数、训练命令与loss曲线判断
数据准备好之后,进入训练环节。这一章主要讲三件事:改哪些配置文件参数才能让模型认识三类目标、启动训练的命令长什么样、以及训练过程中怎么看日志判断有没有在向正确方向走。
4.1 cfg文件三个必改参数:filters、classes、anchors
yoloV3在darknet框架里用配置文件描述结构和超参数。即使是PyTorch版本的yoloV3,也存在一个解析yoloV3.cfg的工具,PyTorch模型里仍然保留cfg结构。所以你要知道至少三个参数必须改:
第一,类别数classes。在cfg里每个yolo层后面都有一个classes=,把它从80改成你的类别数,比如3。
第二,上一个卷积层的filters。yolo层前面会有一个[convolutional]块,它的filters值必须满足一个固定公式:
filters = 3 * (classes + 5)对于3类目标,filters = 3 * (3 + 5) = 24。这个5代表每个锚框预测的5个量:中心点x、中心点y、宽w、高h,外加一个objectness置信度。如果classes改了而filters没改,模型加载时shape对不上,报错几乎不可避免。
第三,anchors。前面说过COCO默认的9组锚框可以沿用,但如果你用自建数据集且目标尺寸分布差异大,建议用k-means重新聚类。在darknet官方脚本里有一个gen_anchors.py,PyTorch版本社区的yoloV3实现里也通常带。聚类脚本读dataset里的标注txt,输出新的锚框序列,用等号后面的值替换cfg里yolo层的anchors=。注意聚类时输入尺寸要与训练尺寸一致。
4.2 启动训练:命令与参数含义
用darknet命令行训练是yoloV3最直接的路径(PyTorch复现版训练方式略有不同,但核心参数大同小异)。以darknet为例,一条典型训练命令长这样:
./darknet detector train data/obj.data cfg/yolo-obj.cfg darknet53.conv.74 -gpus 0 -dont_show -mapdata/obj.data:路径、类别数量、train.txt和valid.txt列表位置、备份权重输出目录的说明文件。里面必须写classes=3、train=train.txt、valid=valid.txt、backup=backup/。cfg/yolo-obj.cfg:前面提到的配置文件。darknet53.conv.74:预训练权重,官方提供,用于初始化骨干网络参数。-gpus 0:指定显卡。没有GPU就删掉这个参数,darknet纯CPU能跑,但慢很多。课程作业选择在云端租卡训练,或借实验室机器,都是正常操作。-map:训练过程中每N轮计算一次mAP,方便监控。建议加上。-dont_show:服务器上没屏幕时使用,否则界面画框图会造成启动失败。
训练日志每隔一定轮次会输出一行,格式通常是:
Region Avg IOU: 0.79, Class: 0.88, Obj: 0.87, No Obj: 0.21, Avg Recall: 0.94, count=342这段日志是实时判断模型健康度的关键。Region Avg IOU表示当前预测框和真值框的平均交并比,理想状态是从0.3慢慢爬到0.7以上。Obj是负责预测目标的锚框的objectness分数,应该高于0.5。No Obj是不含目标的锚框预测的“空置信度”,理论上越低越好,0.2左右可以接受。如果No Obj一直高到0.5以上,说明模型把大量背景误当成目标,训练十有八九在漂移。
4.3 loss曲线怎么看:不是所有下降都是好消息
在darknet日志里直接看loss值也能判断收敛,但更科学的做法是打开训练中途保存的权重,在验证集上跑一轮mAP。只看train loss会骗人:过拟合时train loss照样往下掉,而valid loss在抬头。所以计划训练轮次时,常规做法是每100轮存一次权重,隔500轮或1000轮用验证集跑一次mAP,观察mAP趋势决定stop点。
关于学习率,yoloV3官方cfg的默认值是learning_rate=0.001,配合burn_in=1000在前1000步渐升。如果loss在早期就出现大幅震荡,常见处理是调低到0.0001再试。如果loss一直缓慢下降也可以保持默认。很多人喜欢一上来就动学习率,实际这个参数在迁移学习场景下比较稳定,真正让模型翻车的多是数据问题而不是学习率问题。
训练时还要注意权重保存的轮次节奏。backup/目录下会生成多个权重文件,训练很久后磁盘可能占满。建议训练脚本定期清理早期的middle权重,只保留每500轮一个样本权重和最后一轮权重,因为课程作业通常不需要大ensemble。
5. 避坑清单:loss变nan、框乱飘、小目标漏检的四条排查记录
训练目标检测,踩坑是常态,甚至是必要的学习环节。下面按“现象 → 原因 → 解决”的格式整理四条高频问题,都是自己训练时常遇到的。
5.1 loss变成nan:不是模型坏了,是数据或学习率出问题了
现象:训练刚开始几百步,loss直接变成nan,或者突然从个位数跳到1e38然后变成nan。
原因:最常见是标注框异常,比如某张图片的txt里出现负的宽或高;或者类别ID越界,比如class_names只有3类但txt里写了一个4。另一个原因是学习率过大,尤其是在用自定义优化器时,momentum设置不当也会让loss爆炸。
解决:先用脚本扫描整个labels目录,检查每个txt里所有坐标是否落在0到1区间内,宽高是否为正。这一步能过滤掉90%的nan问题。然后检查倒数第二个卷积层的filters是否按3*(classes+5)计算正确。排除数据问题后,再把学习率从0.001降到0.0001试试。最后,把batch_size调小,避免一次梯度里混入太多异常样本。
5.2 检测框全在图像边缘或乱飘:归一化坐标错位了
现象:训练完成后推理,框的位置要么贴在图片左上角,要么满图飘,完全没有贴合目标。
原因:多半是训练数据和推理时的图像缩放逻辑不一致。比如训练时代码把图片resize到416后再归一化标注,但推理时直接读原图尺寸归一化,坐标就错位了。更隐蔽的是,有些windows实现的letterbox处理在上下填充黑边后没同步调整标注坐标。
解决:推理和训练必须用同一套图像预处理流程。yolo标准流程是letterbox——等比缩放图片到目标尺寸,多出来的部分填0或灰色,然后归一化时把填充偏移也算进坐标。检查数据加载器里训练阶段和推理阶段是不是共用同一个resize函数,这是最容易漏的地方。配合验证集上的可视化,跑10张图,按‘预测框画在图上’的方式检查对齐情况。立刻能看出来坐标错位规律。
5.3 行人总是漏检,自行车和机动车还好:小目标在低分辨率下消失了
现象:各类别的mAP里,person类别明显低于car和truck;推理时近景行人能检出来,远景行人几乎全丢。
原因:行人属于小目标,在低分辨率特征图上信息量不足。输入图像尺寸太小,比如用320x320训练,远景行人在图上只有十几像素,对应到52x52特征图一个格子都占不满,自然检不出来。另一个原因是数据增强里没有加入多尺度训练,yoloV3官方cfg里random=1开启多尺度训练,如果代码实现里把它关了,模型对不同尺度目标的适应能力会弱很多。
解决:把训练尺寸从416提高到608或640,小目标mAP会有肉眼可见的提升,代价是显存占用上升。开启多尺度训练,让模型在每10个batch里随机变化输入尺寸,等于免费获得数据增强。同时也检查一下小目标类别(person)在训练集中是否足够多;如果采集时只标了近景行人,远景行人几乎没有,那模型学不到小尺寸行人外观也正常。多方补充一部分远景行人的训练图,效果最直接。
5.4 显存不足:一个batch都塞不进去
现象:GPU显存8G,batch_size设16训练,启动后直接OOM报错。
原因:yoloV3在608分辨率下batch_size=16需要约11G显存,8G卡跑不动。batch_size太大是新手最容易犯的错误。
解决:显存不够时先调小batch_size,再考虑调小分辨率。900万像素的氛围下,先保证能启动训练再谈参数。常见做法:
- batch_size=8,配合subdivisions=2,让优化器每次累积两个子batch再更新权重。
- 如果还不行,batch_size=4,subdivisions=1,学习率相应调低,训练轮次加长。
- 推理阶段没有这个问题,可以在更大分辨率下跑推理。
补充一个冷门坑:Python版本的yoloV3训练时,dataloader的num_workers设太大也会导致内存溢出,尤其当图片读取后还有大量中间变量时。调到2到4就能解决。
6. 模型推理与验证:把权重接到视频流,用置信度阈值和NMS收尾
训练完成,拿到最后一轮权重之后,只跑mAP不够直观。课程作业通常需要展示效果:输入一段视频或摄像头实时画面,输出带框带标签的结果。作为最后一步,我习惯写一个最简单的推理脚本,并在脚本里把两个对效果影响极大的参数明明白白放出来。
import cv2 import numpy as np import torch from models import Yolov3 # 以你自己的模型类为准 model = Yolov3(num_classes=3) model.load_state_dict(torch.load('weights/best.pt', map_location='cuda')) model.eval().cuda() cap = cv2.VideoCapture('test_video.mp4') fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter('result.mp4', fourcc, 30.0, (1280, 720)) while True: ret, frame = cap.read() if not ret: break # letterbox 缩放输入 img = letterbox(frame, new_shape=416) tensor = torch.from_numpy(img).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): # conf_thres 与 nms_thres 是影响效果的两个关键旋钮 boxes, scores, classes = model(tensor.cuda(), conf_thres=0.4, nms_thres=0.5) # 将预测框坐标映射回原图并绘制 frame = draw_boxes(frame, boxes, scores, classes) out.write(frame) out.release() cap.release()这个脚本框架几乎每套yoloV3复现版都有,关键要理解两个参数的含义:
- conf_thres(置信度阈值)控制“多确信才算检测到”。取0.5时漏检多但误检少,取0.25时误检多但不太漏。课程作业展示一般用0.35到0.4折中,如果场景里目标小而密集,阈值调低到0.25能看到更多框。
- nms_thres(NMS阈值)控制两个重叠框是否合并。值越大框越容易保留,但容易出现一个目标被框两次;值太小又可能把本来就存在的相近物体误合并。默认0.5通常够用,场景中人挨着人的时候调到0.45更稳。
自己调这两个旋钮几次,比看十篇文档都深刻。每段视频的拍摄角度不同,光照不同,最好的做法是把两个参数做成命令行入参或界面上滑杆,跑一遍测试集挑一个视觉上最平衡的。mAP和视觉观感不一定完全一致——对课程作业来说,视频效果越直观,答辩越顺利——但用mAP作为客观指标,以视觉结果为辅,两边都照顾是最稳妥的做法。
用最后这段收尾,也是我自己的习惯:做项目时总是先跑通推理脚本再回头调训练,这样每调一次参数,都能立刻看到效果变化。希望这套基于yoloV3的行人、自行车、机动车检测方案能帮你在有限时间内交出能跑、能讲、能扩展的作业成果。
本文还有配套的精品资源,点击获取