简介这份课程作业以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)训练时网络对真值框做的事是反过来的把真值框缩放到特征图坐标系算它和每个锚框的IoUIoU最大的锚框负责预测这个目标。所以锚框的分布必须贴合你数据集中目标的尺寸分布。课程作业如果只用默认锚框通常问题不大但如果你只拍俯视的自行车、或者全是远景小行人锚框就要用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训练时吃的格式一张图片对应一个txttxt里每行是“类别id 中心点x 中心点y 宽 高”而且全部归一化到0到1之间。3.1 公开数据集怎么挑COCO太大小数据集够不够用直接拿COCO训练集跑不现实——几十G数据加几天训练时间课程作业等不起。更常见的选择是从COCO里抽子集只保留person、bicycle、car、bus、truck这几类用现成的COCO训练脚本做一次轻量训练。但更贴合课程作业的做法是自建小数据集机动车、行人、自行车各采集500到1000张总共2000到3000张训练2到4个小时就能出效果。数据集太小会不会过拟合会。但课程作业的目标珠峰是“在测试集上有可用检出率”不是冲刷新纪录。有正则化手段兜底数据增强、随机裁剪、mosaicYOLOv4才提出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_okTrue) 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、anchorsyoloV3在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.pyPyTorch版本社区的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列表位置、备份权重输出目录的说明文件。里面必须写classes3、traintrain.txt、validvalid.txt、backupbackup/。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, count342这段日志是实时判断模型健康度的关键。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_rate0.001配合burn_in1000在前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*(classes5)计算正确。排除数据问题后再把学习率从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里random1开启多尺度训练如果代码实现里把它关了模型对不同尺度目标的适应能力会弱很多。解决把训练尺寸从416提高到608或640小目标mAP会有肉眼可见的提升代价是显存占用上升。开启多尺度训练让模型在每10个batch里随机变化输入尺寸等于免费获得数据增强。同时也检查一下小目标类别person在训练集中是否足够多如果采集时只标了近景行人远景行人几乎没有那模型学不到小尺寸行人外观也正常。多方补充一部分远景行人的训练图效果最直接。5.4 显存不足一个batch都塞不进去现象GPU显存8Gbatch_size设16训练启动后直接OOM报错。原因yoloV3在608分辨率下batch_size16需要约11G显存8G卡跑不动。batch_size太大是新手最容易犯的错误。解决显存不够时先调小batch_size再考虑调小分辨率。900万像素的氛围下先保证能启动训练再谈参数。常见做法batch_size8配合subdivisions2让优化器每次累积两个子batch再更新权重。如果还不行batch_size4subdivisions1学习率相应调低训练轮次加长。推理阶段没有这个问题可以在更大分辨率下跑推理。补充一个冷门坑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_classes3) model.load_state_dict(torch.load(weights/best.pt, map_locationcuda)) 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_shape416) 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_thres0.4, nms_thres0.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_thresNMS阈值控制两个重叠框是否合并。值越大框越容易保留但容易出现一个目标被框两次值太小又可能把本来就存在的相近物体误合并。默认0.5通常够用场景中人挨着人的时候调到0.45更稳。自己调这两个旋钮几次比看十篇文档都深刻。每段视频的拍摄角度不同光照不同最好的做法是把两个参数做成命令行入参或界面上滑杆跑一遍测试集挑一个视觉上最平衡的。mAP和视觉观感不一定完全一致——对课程作业来说视频效果越直观答辩越顺利——但用mAP作为客观指标以视觉结果为辅两边都照顾是最稳妥的做法。用最后这段收尾也是我自己的习惯做项目时总是先跑通推理脚本再回头调训练这样每调一次参数都能立刻看到效果变化。希望这套基于yoloV3的行人、自行车、机动车检测方案能帮你在有限时间内交出能跑、能讲、能扩展的作业成果。本文还有配套的精品资源点击获取