1. 项目缘起与整体设计思路行人检测这个方向我在实际项目里接触过不少次从最早的 HOGSVM 一路做到现在的深度学习方案踩过的坑可以说能写满一个笔记本。这次要聊的是用 YOLOv3 来训练一个专门的行人检测模型数据来源是从 VOC2012 数据集里抽取出来的行人标注数据。为什么选这个组合说白了就是两个字实用。YOLOv3 虽然已经不是最新的检测框架了但它的工程成熟度、社区资源丰富程度、以及对中小规模数据集的友好性至今仍然让它在很多实际落地场景里有一席之地。而 VOC2012 作为一个经典数据集里面本身就包含大量带标注的行人样本直接抽取出来做专项训练省去了从零标注的麻烦。先说说这个项目到底能做什么。简单讲就是给你一个输入图片或者视频流模型能框出画面中所有行人的位置并给出置信度。听起来简单但实际做起来涉及的东西不少数据抽取与清洗、格式转换、锚框聚类、模型配置、训练调参、推理部署每一步都有讲究。适合谁来参考我觉得有两类人最合适一是刚入门目标检测、想拿一个完整项目练手的同学二是在实际业务中需要快速搭建行人检测基线、不想一上来就搞太复杂方案的工程师。前者可以跟着走一遍全流程后者可以直接拿这套配置当起点再根据自己场景微调。为什么不用更新的 YOLOv5 或者 v8这个问题我被问过很多次。我的回答通常是看你手头的硬件和部署环境。YOLOv3 的 Darknet 框架用纯 C 写的依赖极少交叉编译到嵌入式设备上相对省心而且它的权重文件小、推理速度快在算力有限的边缘设备上反而比一些新模型更实用。当然如果你追求极致精度且硬件充足换更新的版本也没问题但本文的流程和思路是通用的。整个项目的设计思路可以概括为四个阶段数据准备、模型配置、训练调优、推理验证。数据准备阶段的核心是从 VOC2012 的 XML 标注里筛选出所有包含行人的样本并转换成 YOLO 需要的 txt 格式模型配置阶段要根据行人数据的特点重新聚类锚框调整网络结构中的类别数训练调优阶段涉及学习率策略、数据增强、批次大小等超参的选择推理验证阶段则要关注置信度阈值、NMS 参数对最终效果的影响。下面我会逐个拆开讲把每个环节的“为什么”和“怎么做”都说清楚。2. 数据准备从VOC2012到YOLO格式的完整转换2.1 VOC2012数据集的构成与行人样本分布VOC2012 一共包含 20 个类别其中 person 这一类就是我们要的行人。整个数据集有 17125 张图片训练集和验证集加起来大概 11540 张标注文件是 XML 格式每张图对应一个 XML。行人样本的分布有个特点大部分图片里行人数量不多且尺寸变化很大。有站在远处的几十像素高的小目标也有占据画面大半身的大目标。这种尺度分布对检测器来说是个挑战也是为什么后面要专门聚类锚框的原因。我实际统计过VOC2012 里包含行人的图片大约有 4000 多张去掉一些标注质量差、遮挡严重到几乎不可辨认的样本能用的大概在 3500 张左右。这个量级对于 YOLOv3 来说偏小所以数据增强是必须的后面会细说。另外要注意VOC 的标注里有个difficult字段标记为 1 的目标表示难以识别训练时通常忽略这些样本否则会拉低模型的学习效果。2.2 抽取行人标注的脚本实现抽取的核心逻辑就是遍历所有 XML 文件找到name为person的 object把它的边界框坐标和图片路径记录下来。这里有个细节VOC 的坐标是xmin, ymin, xmax, ymax而 YOLO 需要的是归一化后的中心点坐标和宽高。转换公式如下x_center (xmin xmax) / 2.0 / image_width y_center (ymin ymax) / 2.0 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height用 Python 实现的话大概长这样import xml.etree.ElementTree as ET import os def convert_voc_to_yolo(xml_path, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name ! person: continue difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue 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_width y_center (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height lines.append(f0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines注意类别索引我写的是 0因为行人检测是单类别任务不需要保留 VOC 原来的 20 类索引。图片宽高可以从 XML 的size节点里读也可以用 OpenCV 直接读图片获取后者更可靠因为偶尔会遇到 XML 里尺寸和实际图片不一致的情况。2.3 数据清洗与难例处理抽取完之后别急着训练先做一轮清洗。我一般会检查这几项边界框是否越界坐标超出图片范围、宽高是否为零或负数、图片是否损坏无法读取。越界的框要裁剪到图片范围内宽高异常的框直接丢弃。另外如果一张图里所有行人框都小于 8x8 像素我建议整张图都不要因为这种目标在训练中基本是噪声。还有一个容易被忽略的点重复框。VOC 标注里偶尔会出现同一个目标被标了两次的情况两个框的 IoU 超过 0.9 就认为是重复保留置信度高的那个这里没有置信度就保留面积大的。清洗完之后把图片和对应的 txt 文件按 8:2 划分训练集和验证集文件名保持一致方便后续加载。提示划分数据集时一定要随机打乱但固定随机种子保证每次实验的划分一致否则不同轮次的结果没法对比。3. 锚框聚类与网络配置调整3.1 为什么必须重新聚类锚框YOLOv3 原版的锚框是在 COCO 数据集上聚类得到的那里面有各种尺度的目标行人的比例和 COCO 整体分布并不一致。如果你直接拿原版锚框训练行人数据会发现小目标召回率特别低因为原版最小的锚框是 (10,13)而 VOC 里很多行人只有十几像素宽匹配不上。所以用自己的数据重新跑一遍 K-means 聚类是必须的这一步能明显提升召回率。聚类的距离度量不能用欧氏距离要用1 - IoU因为框的大小和位置是耦合的。K-means 的 K 取 9和 YOLOv3 的三个尺度、每个尺度三个锚框对应。聚类前把所有训练集的宽高归一化到 0-1 之间聚类完再乘回 416YOLOv3 默认输入尺寸得到实际像素值。3.2 K-means聚类实操与结果解读聚类脚本的核心是自定义距离函数import numpy as np def iou(box, clusters): x np.minimum(clusters[:, 0], box[0]) y np.minimum(clusters[:, 1], box[1]) intersection x * y box_area box[0] * box[1] cluster_area clusters[:, 0] * clusters[:, 1] iou_ intersection / (box_area cluster_area - intersection) return iou_ def kmeans(boxes, k9, max_iter100): n boxes.shape[0] distances np.empty((n, k)) last_clusters np.zeros((n,)) np.random.seed(42) clusters boxes[np.random.choice(n, k, replaceFalse)] for _ in range(max_iter): for i in range(n): distances[i] 1 - iou(boxes[i], clusters) nearest distances.argmin(axis1) if (nearest last_clusters).all(): break for j in range(k): if (nearest j).any(): clusters[j] boxes[nearest j].mean(axis0) last_clusters nearest return clusters跑完之后按面积从小到大排序最小的三个给第一个尺度13x13 特征图中间三个给第二个尺度26x26最大的三个给第三个尺度52x52。我实测在 VOC 行人数据上聚类出来的锚框大概是 (8,18)、(15,32)、(25,48)、(38,72)、(55,95)、(78,130)、(110,180)、(160,250)、(230,340) 这个量级和原版差别挺大尤其是小框明显更小。3.3 配置文件的关键修改点YOLOv3 的配置文件是.cfg格式需要改的地方有类别数classes1、最后一个卷积层的 filters公式是(classes 5) * 3 18、三个 yolo 层的 anchors换成聚类结果、三个 yolo 层的 classes。另外如果显存不够可以把batch和subdivisions调小比如batch32, subdivisions8等效批次是 4。学习率方面初始learning_rate0.001burn_in1000max_batches根据数据量定一般 3500 张图跑 20000 到 30000 批次差不多。注意改完 cfg 后一定要用darknet partial或者直接从头训练不要拿原版权重做迁移学习时只改类别数那样最后一层的权重形状对不上会报错。如果要用预训练权重需要先跑一次darknet partial把最后一层之前的权重抽出来。4. 训练过程与调参经验4.1 数据增强策略的选择数据量只有 3500 张左右不做增强肯定过拟合。YOLOv3 的 Darknet 框架自带几种增强angle旋转、saturation、exposure、hue、flip。我的经验是行人检测场景下旋转角度不要太大±10 度就够了因为行人通常是直立的大角度旋转会产生不真实的样本。flip1开启水平翻转没问题但垂直翻转不要开。饱和度和曝光度可以适当调模拟不同光照条件。另外mosaic增强在 YOLOv3 原版里没有需要自己改代码或者用支持该功能的 fork 版本如果不想折腾用基础的几种也够。还有一个技巧是随机缩放通过random1开启多尺度训练每 10 个批次换一次输入尺寸范围在 320 到 608 之间。这对提升模型对不同尺度行人的适应能力很有帮助代价是训练时间会稍微长一点。4.2 训练过程中的监控与早停训练时盯着几个关键指标loss 曲线、验证集 mAP、召回率。Darknet 每 1000 批次会输出一次 mAP如果连续几次 mAP 不升反降说明过拟合了该停了。我一般会保存每 1000 批次的权重最后挑验证集上表现最好的那个而不是用最后一个。另外avg loss在 0.5 以下且不再明显下降时基本就收敛了。有个坑要提醒学习率衰减的时机。YOLOv3 默认在max_batches的 80% 和 90% 时各衰减一次如果你中途改了max_batches记得同步调整steps参数否则学习率可能一直不降loss 震荡下不去。4.3 显存不足时的应对方案显存不够是训练中最常见的问题。除了调小batch和subdivisions还可以把输入尺寸从 416 降到 320或者用width和height参数改小。如果还是不够可以冻结前几层不训练只训后面的层但这会牺牲一些精度。我的建议是优先降输入尺寸因为行人检测对分辨率还是比较敏感的批次太小会导致 BN 层统计不准反而影响效果。5. 推理验证与效果分析5.1 推理参数对结果的影响训练完拿到权重文件后用darknet detector test跑推理。这里有两个参数直接决定最终效果置信度阈值-thresh和NMS 阈值-nms。行人检测场景下我一般把thresh设在 0.3 到 0.5 之间太低会有一堆误检太高会漏掉远处的小目标。nms设在 0.45 左右因为行人之间经常有重叠NMS 太激进会把挨着的两个人合并成一个框。实测下来VOC 行人数据训练出的模型在验证集上 mAP0.5 大概能到 0.75 到 0.82 之间具体取决于训练轮数和增强策略。小目标的召回率是短板如果业务场景里远处行人很重要可以考虑用更高分辨率的输入比如 608或者用 tiled inference 把大图切块检测。5.2 常见误检与漏检的排查误检主要集中在两类类似行人形状的物体比如消防栓、柱子和人群密集处的重复框。前者可以通过增加负样本不含行人的图片来缓解后者调 NMS 参数。漏检则多发生在遮挡严重和极小目标上遮挡问题靠数据增强里的随机裁剪可以部分解决极小目标只能靠提高输入分辨率或者用专门的小目标检测策略。我踩过的一个坑是验证集里有些图片的标注框特别大几乎覆盖整张图这种样本会让模型学到“大框就是行人”的错误关联。后来我把面积占比超过 0.8 的框都过滤掉了mAP 反而升了两个点。5.3 部署时的性能优化如果要在实际业务里用推理速度是个绕不开的问题。Darknet 原生支持 OpenCV 的 DNN 模块加载也可以用 TensorRT 加速。我实测在同等硬件下TensorRT 比原生 Darknet 快 2 到 3 倍。另外如果输入是视频流可以用跳帧检测加跟踪的方式没必要每帧都跑检测这样能大幅降低算力消耗。6. 实操心得与避坑清单6.1 数据层面的经验标注质量比数量重要VOC 里有些行人框标得比较松包含了很多背景训练时会让模型学到无关特征。如果条件允许手动过一遍关键样本的标注。类别不平衡问题行人检测是单类别不存在类别不平衡但正负样本比例要注意。背景区域远多于行人区域YOLOv3 的损失函数里对正负样本做了加权一般不用额外处理但如果发现模型倾向于把什么都判成背景可以调高objectness的损失权重。验证集要独立不要从训练集里随便抽几张当验证集一定要随机划分且保证验证集里各种尺度、各种场景的行人都有。6.2 训练层面的经验学习率预热很重要前 1000 批次用较小的学习率预热能避免初期 loss 爆炸。权重保存策略每 1000 批次存一个最后用验证集挑最好的不要只看最后一个。多尺度训练开启random1后训练时间会增加约 20%但泛化能力提升明显值得。6.3 推理层面的经验阈值要按场景调安防场景宁可误检不可漏检阈值调低统计人流量的场景可以调高减少重复计数。输入尺寸匹配训练用的什么尺寸推理最好也用相近的否则精度会掉。后处理不能省NMS 之后还可以加一个基于宽高比的过滤把明显不像行人的框去掉比如宽高比超过 1:3 或 3:1 的。这个项目后续还可以这样扩展把单类别行人检测扩展到多类别比如加上骑行者、推车的人或者引入时序信息做行人跟踪再或者用知识蒸馏把大模型的能力迁移到更小的网络上方便部署到边缘设备。我自己在实际操作中的体会是行人检测的难点从来不在模型本身而在数据质量和场景适配。同一个模型换一个场景可能效果天差地别所以每次上新场景先花时间看看数据比盲目调参有用得多。
阅读完成 · 觉得有帮助?