如果你点进这篇文章,大概率是正打算让电脑学会“认人”——我说的不是那种高深的人脸识别,而是更粗粒度的行人检测:给一张监控画面,把画面里出现的每一个人都用框圈出来。这活儿听着不难,真做起来涉及的东西相当多:数据集怎么标、模型选哪个、训练参数怎么调、导出部署怎么整。今天这篇就把我用PaddlePaddle训练PPYOLO行人检测模型的整个过程掰开揉碎讲一遍,从环境准备到最终部署,踩过的坑和沉淀的经验都在里面。内容主要面向打算用PaddlePaddle做目标检测落地项目的开发者,不管你是刚接触检测的新手,还是从其他框架转过来想快速上手的人,这篇文章都能帮你省下不少摸索时间。
1. 项目整体设计与方案选型
1.1 为什么是PaddlePaddle配PPYOLO,而不是别的组合
我在动手之前其实纠结过一段时间。市面上目标检测的轮子太多了,PyTorch系的YOLO各种变体满天飞,随便找一个都能跑。但真到了要做一个能落地的项目,尤其是行人检测这种偏工程化的任务,我发现框架选型不能只看模型精度,更要看工具链是不是完整。
PaddlePaddle这边有一个PaddleDetection仓库,它和那种“只给模型代码”的开源项目不太一样。它把数据读取、模型结构、训练策略、评估脚本、部署导出全部做成了配置文件加命令行的形式,预训练权重也直接给好。我不用自己去拼装一个训练流水线,也不用到处找backbone的预训练模型,把配置改一改就能开训。这一点在赶项目进度的时候特别占优势。
另外还有一个现实考量:团队里其他成员不一定都熟悉同一个深度学习框架。Paddle的文档和社区内容在国内比较丰富,遇到问题排查起来效率高。而且Paddle训练出来的模型可以导出成静态图格式,之后用它的推理库在服务端部署,也可以转成轻量模型放到端侧。这条链路是闭环的。所以最终我选了PaddlePaddle,模型选型则锁定了PPYOLO。
1.2 PPYOLO在行人检测上的技术优势
PPYOLO这个模型,名字里虽然有YOLO,但它不是简单的YOLOv3换皮。它做了一堆对检测精度有明显提升的改进。我挑几个对行人检测帮助最大的说:
第一是可变形卷积DCN。普通卷积的采样点是固定的正方形网格,但行人的姿态变化特别大,站着的、弯腰的、骑车的、被挡住一半的都有。DCN让卷积核的采样位置可以根据图像内容自适应偏移,等于让网络“盯着”目标的有效区域去提取特征,对形变目标友好很多。
第二是SPP和PAN结构。SPP(空间金字塔池化)能把不同尺度的上下文信息聚合起来,让网络对大目标和小目标都有感知。PAN则在FPN的基础上增加了一条自底向上的路径,把浅层的细节信息往深层传,这对检测远处那些只有十几个像素大小的行人特别关键。
第三是训练技巧的堆叠,比如IOU Loss、Grid Sensitive、CoordConv、DropBlock这些。简单理解,IOU Loss是直接对着预测框和真实框的重叠度去优化,比只算分类损失更贴近最终目标;Grid Sensitive解决的是物体中心点正好落在网格边缘导致检测失效的问题;CoordConv则让卷积能够感知坐标信息,对行人这种位置敏感的目标有帮助。
我拿它和常见的YOLOv3做了对比,在相同数据集上,PPYOLO的AP50大概能高出六到八个点,推理耗时就多十几毫秒。对行人检测这种需要实时性但并不极致追求帧率的场景来说,这个取舍非常划算。
1.3 行人检测任务本身的特殊性
通用目标检测比赛里那种“万物皆可检测”的模型,拿来直接做行人检测往往不舒服。行人检测有几个很突出的难点:
一是尺度差异极大。同一帧画面里,近处的人可能占据画面三分之一,远处的人只有二十个像素。模型需要同时照顾到这两种极端尺度,这对特征融合的要求很高。
二是遮挡严重。地铁站、商场这种人流密集的地方,人与人之间的遮挡是常态。模型不能因为目标只露出一半就不检测,这需要训练数据里有大量遮挡样本,并且模型本身要足够鲁棒。
三是误检代价高。安防场景里漏检一个人可能比多报一个框严重得多,但误检太多又会让下游业务没法处理。所以调优时不能只看精度,要结合Recall和Precision一起看。
我在规划项目的时候就意识到,这个任务需要的不只是一个“能跑的模型”,而是一整套围绕数据、训练、评估的闭环方案。这也是我把整个流程走了一遍之后最想分享出来的东西。
2. 环境准备与数据集处理
2.1 训练环境安装与版本匹配
这一步看着简单,但很多人一开始就卡住了。装PaddlePaddle最核心的一点是版本匹配。GPU驱动、CUDA版本、Paddle版本、PaddleDetection版本这四者必须咬合,否则经常出现装好了之后import paddle直接报错或者训练时崩掉。
我的建议是先看GPU驱动支持的CUDA版本,用nvidia-smi查看机器上驱动对应的最高CUDA版本,然后选择一个比它低一点点的CUDA去装Paddle。比如驱动支持CUDA 12,那就装CUDA 11.8或者12.0版本的Paddle。命令大概是这样的:
# 以CUDA 11.8对应的paddle为例 python -m pip install paddlepaddle-gpu==2.5.2.post118 \ -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html装完之后一定要跑一下自检:
python -c "import paddle; paddle.utils.run_check()"看到提示Paddle安装成功,再进行下一步。接着装PaddleDetection,我推荐用源码方式安装,不要只装pip包,因为训练过程中经常需要看配置文件和改代码,源码目录就在手边会方便很多:
git clone https://github.com/PaddlePaddle/PaddleDetection.git cd PaddleDetection pip install -r requirements.txt python setup.py installPython版本建议用3.8到3.10,太新的版本偶尔会遇到一些依赖库没跟上导致编译出错。我一开始在Python 3.11下装就遇到某个底层库没有预编译包,折腾了半天,直接换回3.9就顺了。
2.2 数据集准备:标注格式、目录结构、划分策略
数据集是这次项目里花时间最多的一部分。我一开始用的是一套自己整理的模拟项目X行人检测数据,里面包含商场、地铁、路口等多个场景的图片,标注格式参考了最主流的两种目标检测标注格式。
一种是XML风格,每张图片对应一个XML文件,里面记录每个目标的类别和矩形框坐标;另一种是JSON风格,所有标注汇总到一个JSON文件里,按图片ID组织。PaddleDetection两种都支持,我需要根据自己的数据情况选择。因为我有一部分历史标注数据是XML格式,所以最终统一转成了JSON格式。
目录结构我建议按下面这种方式组织:
dataset/person/ ├── annotations/ │ ├── instances_train.json │ └── instances_val.json └── images/ ├── train/ └── val/这里的JSON文件里必须包含三个关键字段:images记录图片路径和尺寸,categories定义类别ID到类别名的映射,annotations记录每个目标的类别ID和bbox坐标,bbox的格式是[x, y, width, height]。
标注的时候有几个规范需要提前定好,不然后面检查数据会想哭。框要尽量贴近人体边界,不要留太多背景,但也不要卡得过死,把头发包括进去、飘起来的衣角可以适当忽略;遮挡超过一半的行人要不要标,我的经验是如果肉眼还能分辨出这是一个完整的人就标,只露出一只手或者一个头就不标;密集场景里远处的小人一定不能漏,漏标了模型就会觉得小目标不重要,后面推理时必然漏检。
数据集划分也要讲究。我一开始图省事直接用随机划分,结果评估指标虚高。后来发现原因很简单:同一段视频的连续帧被随机分到了训练集和验证集,模型在训练时见过了几乎相同的画面。正确的做法是按场景或者按视频片段划分,保证同一个场景的画面只出现在一边。这样验证集的分数才有参考价值。
2.3 数据预处理与增强策略
PaddleDetection的Reader配置里内置了一套数据增强流程。我对默认配置做了一些调整,主要针对行人检测的特点。
默认的训练增强包括随机翻转、随机裁剪、随机颜色扰动、MixUp这些。对于行人检测来说,随机翻转基本可以放心开,因为行人不分左右。但如果你的场景里行人带明显方向属性,比如都朝一个方向走,那翻转就要谨慎一点。
我强烈建议开多尺度训练。行人检测的尺度差异太大,多尺度训练让模型每个iteration随机选择一个缩放尺寸,比如从320到768之间随机取一个值作为输入尺寸。这样模型见过的目标大小更丰富,对远处小人的检测能力会明显提升。
对比度、亮度扰动也很重要。监控场景里光照变化剧烈,白天大太阳、晚上路灯昏暗,有时候还有逆光。如果在数据增强阶段让模型见过各种亮度下的行人,后面部署到真实场景就不会因为光线一变就检测不到人。
3. 模型配置与训练实操
3.1 配置文件里最关键的几个字段
PaddleDetection把训练要用到的所有东西都写在YAML配置文件里,包括数据集路径、模型结构、优化器、学习率、数据增强策略。我基于PPYOLO的ResNet50vd版本配置改了三个地方。
第一是类别数。行人检测只需要检测“人”这一类,所以num_classes要改成1。不改的话,模型最后一层输出通道和你标注的类别对不上,训练不会报错,但推理结果会乱七八糟。
第二是数据集路径。dataset_dir指向你的数据根目录,anno_path分别指向训练和验证用的JSON文件。
第三是学习率和训练轮数。默认的配置是在公开数据集上用8卡、batch_size为8的条件下设计的,基础学习率是0.01。我这次只有单卡,batch_size只能开到4,于是学习率也要跟着调低。这里有一个经验公式:学习率大约等于参考学习率乘以全局batch_size再除以参考batch_size。我用0.01 * 4 / (8 * 8)算出来大概0.0006左右,然后在这附近取整。
下面是我整理后的一个简化配置片段,方便对照:
num_classes: 1 epoch: 80 LearningRate: base_lr: 0.0006 warmup_steps: 500 warmup_start_lr: 0.00006 schedulers: - !PiecewiseDecay gamma: 0.1 milestones: [54, 68] TrainReader: batch_size: 4 inputs_def: image_shape: [3, 608, 608] sample_transforms: - !DecodeImage - !RandomFlipImage - !NormalizeImage batch_transforms: - !MultiScale - !PadBatch weights: output/person_ppyolo/model_final预训练权重这块要特别说一句。如果你是从头开始训练,收敛会非常慢,而且效果不一定好。PaddleDetection支持直接加载官方在通用数据集上预训练好的权重,命令里加一个-o pretrain_weights=https://...就行。模型会自动匹配权重里backbone部分,那些因为num_classes变化导致shape对不上的层会被忽略。我实测下来,加载预训练权重之后,前十个epoch的收敛速度肉眼可见地加快。
3.2 训练命令与超参数调整
训练命令非常简洁,但有几个参数值得讲究。最基础的是这样:
python tools/train.py \ -c configs/person_ppyolo.yml \ -o use_gpu=True \ -o batch_size=4 \ --eval--eval这个参数我特别推荐加上。它会让模型每训练一定轮数就在验证集上跑一次评估,并且把最高精度的模型单独保存为best_model.pdparams。这个文件后面导出部署用的就是它。
如果你的机器有多张显卡,可以用分布式命令:
python -m paddle.distributed.launch \ --gpus 0,1,2,3 \ tools/train.py \ -c configs/person_ppyolo.yml \ --eval多卡训练时有个容易误解的点:配置文件里的batch_size是单卡上的大小,所以实际全局batch_size是单卡batch_size乘以卡数。调整学习率时要用全局batch_size去算,不然多卡训练会因为学习率偏大或者偏小出现精度问题。
超参调整上,我踩过一次比较大的坑是epoch设得太少。第一次跑就用了默认的12个epoch,结果训练结束模型还没真正收敛,验证集AP50只有0.45。后来我把epoch加到80,配合piecewise的学习率衰减,模型在55个epoch左右开始稳定,最终AP50到了0.81。如果训练时间有限,建议至少跑到验证集指标不再明显上升之后再停,别死守着默认配置。
3.3 训练日志怎么看
训练过程中终端会输出很多日志,每行大概是这样的格式:
[04/30 15:23:45] ppdet.utils.checkpoint INFO: Saving checkpoint: output/person_ppyolo/epoch_50 [04/30 15:23:46] ppdet.engine INFO: Epoch 50/80, step 300/1200, loss: 1.524, lr: 0.000236这里面的核心是loss值。PPYOLO的loss由分类损失、框回归损失等好几项组成,不同版本日志字段不完全一样,但总的趋势是loss应该逐步下降然后趋于平稳。我习惯把loss画成曲线来看,Paddle自带的VisualDL就能干这个事。
对于行人检测,我总结出三个常见现象。第一是loss前几个epoch下降很慢,这通常是学习率太低或者没加载预训练权重。第二是loss一开始降得很快,后面突然震荡,这通常是学习率太高,或者batch_size太小导致梯度不稳定。第三是loss在某个平台期卡住,怎么调都不降,这时候问题可能出在数据上,比如标注漏标太多,让模型学到了错误信息。
还有一个很多新手会忽略的点:loss下降不等于模型好用。我见过loss曲线很漂亮,但模型把路灯柱子当成人、把地上的影子当成人。这种情况光看loss是发现不了的,必须用--eval的验证集结果去分析。
4. 模型评估与导出部署
4.1 评估指标怎么读、怎么用
模型训练完之后,用官方评估脚本跑一遍验证集,拿到的不只是一个数字,而是一组指标。评估命令:
python tools/eval.py \ -c configs/person_ppyolo.yml \ -o weights=output/person_ppyolo/best_model.pdparams输出里比较重要的是这几个指标:
| 指标 | 含义 | 我关注的原因 |
|---|---|---|
| AP50 | 在IoU阈值为0.5时的平均精度 | 行人检测主要的精度参考 |
| AP50:95 | 多个IoU阈值下的平均 | 要求更严格,用于横向对比模型 |
| Recall | 检出来占真实目标的比例 | 漏检多不多,安防场景极其看重 |
| Precision | 检出来的框里正确的比例 | 误检多不多,影响下游业务 |
我这次在验证集上最终拿到AP50大约0.81、Recall大概0.86。看起来不错,但部署到真实摄像头之后发现有个问题:线下验证集里几乎没有夜间场景,线上夜间误检明显变多。后来我又在数据集里补了一批夜间图片重新训练,情况才好转。评估指标只能代表你的测试集,不代表所有真实场景。
阈值设置也要注意。PaddleDetection部署时的NMS置信度阈值默认可能是0.5,如果验证集里的模型输出置信度普遍不高,可以适当下调。我这次调到0.3之后,Recall提升明显,但误检也会增加。建议在部署前用小批量的真实场景图片做阈值搜索,不要拍脑袋定。
4.2 模型导出与推理
训练得到的.pdparams是动态图权重,不能直接用于部署,需要先导出成静态图推理模型。这一步PaddleDetection封装好了:
python tools/export_model.py \ -c configs/person_ppyolo.yml \ -o weights=output/person_ppyolo/best_model.pdparams导出后在output_inference目录下会看到三个关键文件:model.pdmodel是模型结构,model.pdiparams是参数文件,infer_cfg.yml记录预处理和后处理的配置信息。这套文件可以被Paddle Inference直接加载,不需要依赖训练代码。
验证导出的模型是否正确,最简单的办法是用官方推理脚本跑一张真实图片:
python deploy/python/infer.py \ --model_dir=output_inference/person_ppyolo/ \ --image_file=test.jpg \ --device=gpu \ --threshold=0.3如果输出的框能正常画出来,说明导出链路是通的。这一步我建议在训练刚结束时就做,不要等到要上线了才导,万一中间哪一步配置不一致,排查起来会特别痛苦。
4.3 部署优化和性能调优
推理模型能跑只是第一步,真部署到服务器上要考虑性能。我用的是GPU服务器推理,单张960x540的监控画面,PPYOLO的推理耗时大概在40毫秒左右,能跑到接近25帧,基本满足实时需求。但如果想跑更高分辨率或者更多路视频流,就需要优化。
Paddle Inference支持开启TensorRT加速,命令里加--run_mode=trt_fp16,我实测推理耗时能降到20毫秒左右,速度提升接近一半。代价是需要安装TensorRT环境,版本要跟Paddle匹配,这一点坑比较多,建议直接用官方容器镜像或者Paddle提供的带TensorRT的安装包。
如果部署环境是CPU,可以考虑换成PPYOLO的轻量版,或者用PaddleSlim做离线量化。量化之后模型体积能缩小到原来的四分之一,推理速度提升一到两倍,精度损失通常在可接受范围内。对CPU部署的场景,这个优化非常实用。
还有一个容易忽视的点:导出模型的时候可以固定输入尺寸,比如固定成608x608,这样推理时不用每帧做动态尺寸计算,速度会有一些提升。坏处是输入图片如果不是这个尺寸,需要做resize,可能会影响小目标的检测效果。具不具备必要性,要看你的实际业务输入是什么样的视频流。
5. 常见问题与排查技巧实录
5.1 数据相关的坑:漏标、错标、脏数据
我这次项目前半段的时间几乎都花在和数据较劲上。下面这些情况我都遇到过,写出来给大家避坑。
漏标是最隐蔽的坑。模型训练的时候会把没标注的人当背景来学,等于一直在接收错误信息。我最初标注完一批数据没有做二次检查,结果训练收敛之后,模型对远处小人的召回率特别低。后来把所有图片里肉眼可见的人全部重新过了一遍,补了一批标注,效果才恢复正常。
错标则会导致模型学到错误位置信息。比如框把两个人框在一起,或者框里大部分是背景,这种标注会让训练loss一直下不去。我习惯用可视化方式抽查标注:把标注框画在图片上,随机抽几百张人工快速浏览一遍,比看数字靠谱得多。
脏图片问题。数据集里面难免混入个别损坏的JPEG图片或者通道数不对的图片,训练时会随机崩一次,让人摸不着头脑。我给数据目录写了一个小脚本统一检查,把所有打不开的、尺寸异常的图片全部清理掉再训练。
5.2 训练过程中的异常场景
loss突然变成NaN,这是我最先碰到的问题。排查思路是:先看是不是学习率太大,如果基础学习率是0.01,直接降到0.001试试;再看数据集里有没有异常的标注,比如bbox坐标超过图片边界太远或者宽高为0;最后检查一下网络里有没有数值不稳定的操作。Paddle里可以开grad_clip,能有效防止梯度爆炸。
训练了二十个epoch loss不降,别急着调参,先确认预训练权重是不是真的加载上了。训练日志启动时会打印加载的参数名列表,如果里面没有backbone相关参数,说明权重没匹配上。这时候检查pretrain_weights路径和权重格式,权重格式不对的话加载时会静默失败。
显存不足也是老问题。GPU显存只有8G的话,batch_size设小到2或者1也可以训,但学习率要跟着降。另外把输入尺寸从608降到416,显存占用会降一大截,训练速度也更快,精度损失不多。
5.3 部署阶段的典型问题
导出模型时报shape不匹配,这种多数是因为训练时用了可变输入尺寸,导出时又没指定固定尺寸。解决方法是导出命令里加上--fixed_input_shape,指定一个你训练时最常用的尺寸,我一般用608。
导出之后用推理脚本检测不到任何框,先检查--threshold是不是设得太高,再检查用来推理的图片格式是不是和训练时一致。遇到过一种情况是训练时做了Normalize,但推理脚本里没有正确应用infer_cfg.yml的预处理配置,导致输入分布完全不对。
CPU推理慢的话,除了前面说的量化和轻量模型,还可以在推理脚本里开启MKLDNN。Paddle Inference的CPU预测支持MKLDNN加速,开启之后速度能提升一倍左右。缺点是第一次运行时有预热时间,但推理阶段性能提升很可观。
下面整理了一张速查表,方便遇到问题时快速定位:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| loss为NaN | 学习率偏大、标注异常、梯度爆炸 | 降低学习率,开启grad_clip,清理异常标注 |
| 训练loss不降 | 预训练权重没加载、类别数配错 | 检查启动日志里加载的权重列表,核对num_classes |
| 训练时随机崩溃 | 图片损坏、tfrecord损坏 | 写脚本批量检查图片完整性 |
| 验证集指标高、线上差 | 数据划分不合理、场景偏移 | 按场景划分数据,补充线上场景图片 |
| 推理检测不到目标 | 阈值过高、预处理配置不一致 | 降低threshold,核对infer_cfg.yml |
| 显存不足 | 输入过大、batch过大 | 减小batch_size、输入尺寸、关闭混合精度 |
| 导出模型shape报错 | 输入尺寸动态 | 导出时指定fixed_input_shape |
最后分享一个我这次项目里最值钱的教训:别一上来就追SOTA指标。先把数据洗干净,再跑通完整流程,最后回头调参。很多人卡在中间,都是因为数据集没整理明白就急着开训。另一个小技巧是,训练中途一定要开着--eval让模型定期跑验证集,你会发现有时候loss在降,但模型其实一直在把背景当成人。这种问题光看日志是发现不了的,一定得拿可视化的检测结果去判断。希望这篇记录能帮你少走点弯路。