接到一个在某园区做人流统计的需求时,我第一反应是直接拿现成的检测模型跑起来。但真正把模型搬到那台低功耗设备上之后,问题才一个接一个冒出来——内存只有几GB、CPU占用一上去温度立刻报警、延迟卡在200多毫秒让人没法看。也就是从那时候开始,我把“基于深度学习的轻量级行人检测”从一句口号变成了必须落地的工程问题。这篇内容是我把整个流程完整走一遍之后的总结,覆盖模型选型、剪枝量化、推理部署和实测调优,适合正在为嵌入式设备、工控机或移动端选择行人检测方案的开发者参考。不管你用的是Jetson系列、树莓派,还是普通的x86小主机,里面提到的取舍思路和踩坑记录应该都能用上。
1. 轻量级行人检测:为什么成了“不得不做”的方向
1.1 算力约束与场景需求的错位
行人检测本身算不上新问题,在服务器上用大模型刷精度已经是成熟套路。可一旦要求把模型放到边缘设备上实时跑,事情就完全变了。我手头那台设备属于典型的低功耗工控机,CPU是四核低功耗型号,没有独立显卡可用,内存也不宽裕。把常规尺寸的检测网络直接放上去,单帧推理时间轻轻松松超过300毫秒,根本谈不上“实时”。
这里有个经常被忽略的事实:安防、机器人、客流统计这类应用,对延迟的要求往往比想象中更苛刻。机器人避障需要几十毫秒级别的响应,否则碰撞预警就是摆设;出入口人流统计如果帧率太低,行人快速通过时直接漏检,数据完全不可信。所以我理解的“轻量级行人检测”,不是单纯把模型换小,而是让模型在算力受限的前提下,仍然能保持足够快的速度和可用的精度。
1.2 “轻量级”的三个层次:模型小、计算低、内存可控
很多人一提到轻量级,第一反应是参数量小。但实际部署时你会发现,参数量只是最浅的一层。我在项目里会从三个维度同时卡指标。
第一是模型体积,直接决定存储和加载速度。一个几十MB的模型在服务器上无所谓,但在嵌入式设备上,从Flash读到内存的过程本身就会带来延迟,而且Flash空间也可能不够。第二是计算量,通常看FLOPs。这个指标决定了在CPU或者NPU上推理的理论耗时上限,FLOPs越高,设备越吃力。第三是峰值内存占用,这个最容易被忽略。一些模型在推理过程中会同时维持巨大的中间特征图,即使参数量很小,内存也可能被瞬间打满。我记得第一次做轻量化尝试时,选了个参数量很小的网络,结果推理时因为特征图叠加导致内存超限,程序直接崩溃。从那之后,我把“峰值内存可控”和“模型够小”放到了同等重要的位置。
这三层约束放到一起,才是真正的“轻量级”工程目标:模型文件在10MB上下甚至更小,单帧计算量控制在几G FLOPs以内,推理峰值内存在设备承受范围内。剩下的问题,就变成了在满足这些硬指标的前提下,精度还能保住多少。
2. 模型选型实战:拿硬件算力反推候选网络
2.1 几类主流行人检测模型的取舍
选模型之前,先要明确一个原则:不是模型越轻越好,而是在“硬件跑得动”的区间里,挑精度最高的。我用一张表把自己实际比较过的几个网络的公开指标整理了一下,测算条件统一为输入尺寸640x640左右。
| 模型 | 参数量 | 计算量(FLOPs) | 特点 | 适合场景 |
|---|---|---|---|---|
| YOLOv5n | 约1.9M | 约4.5G | 生态成熟,部署资料多,anchor-based | 快速出原型,全流程踩坑少 |
| YOLOX-Nano | 约0.9M | 约3.0G | anchor-free,后处理稍简单,结构规整 | 对延迟极敏感的边缘设备 |
| NanoDet-Plus | 约1.5M | 约3.4G | 轻量检测专用,极小模型 | 极致轻量但自定义算子多 |
| PP-PicoDet | 约3.3M | 约5.0G | 综合精度/速度平衡好,有针对性优化 | 对精度有要求且算力中等 |
单看公开数据,好像每一款都合适,但放进行人检测这个具体任务里,需要额外考虑三个因素。
第一是行人目标尺寸偏小。监控摄像头视野里,行人往往只占图像很小一块区域,小目标对下采样倍数和特征金字塔设计很敏感。第二是密集场景下遮挡严重,模型需要足够的上下文信息才能区分重叠行人,网络太浅会直接丢掉这些信息。第三是后处理复杂度。NMS(非极大值抑制)在稠密检测里尤其费时,而anchor-free结构在这方面的天然优势在边缘端会被放大。
2.2 我选定的基线模型与具体理由
综合比较之后,我的基线选择是YOLOX-Nano,而不是看起来生态更成熟的YOLOv5n。理由有几点:它是anchor-free结构,省掉了anchor聚类这一步,意味着换场景时少了一个调参变量;它的解耦头在行人这类重叠较多的目标上表现更好,虽然这会带来少量额外计算,但在我的设备上完全能承受;最关键的是,它的结构非常规整,卷积层基本都是标准组件,这对后面做剪枝和量化特别友好。
这里多说一句,选型时一定要先确认设备支持什么算子。我见过有人选了带特殊注意力模块的模型,看起来指标不错,结果在推理引擎上硬是不支持,最后只能回头换模型。所以我的建议是:先跑通“模型到设备”的最小闭环,再考虑优化精度,顺序反了会浪费大量时间。
3. 落到“轻”处去:剪枝、量化、蒸馏三步走
3.1 通道剪枝:砍哪里、砍多少、砍完怎么办
选好基线模型后,我并没有直接用原版,而是先做了通道剪枝。原理不复杂:训练过程中,我对每个卷积层后面的BN(批归一化)层的缩放因子施加稀疏化约束,让一部分通道的缩放因子趋近于零,然后把贡献接近零的通道从网络里移除。
实际操作上,我用的是SPP(占用不同感受野的池化结构)之外的全部标准卷积层作为剪枝候选。设了一个稀疏系数,在训练损失里加上对缩放因子的L1正则惩罚。稀疏化训练大约跑了几十个epoch后,模型里大概有一半通道的缩放因子降到了0.01以下,这些就是可剪的冗余通道。
真正考验人的是剪完之后的恢复。裁剪模型结构简单,难点在于微调策略。我踩过的坑是:剪完直接用原学习率微调,精度掉得凶。后来改成先冻结前几层,用较低学习率专门训练深层,分阶段解冻,效果才稳住。另外,剪枝比例不能一刀切,浅层特征跟小目标检测强相关,剪多了行人漏检率立刻上升。我最终浅层只剪20%,深层剪到40%以上,整体模型计算量下降了接近一半。
3.2 PTQ量化与QAT量化:精度与速度的平衡
剪枝之后下一步是量化。很多人在这一步容易犯迷糊,以为只要把FP32模型转成INT8就完事。实际上量化的收益要看设备的计算特性。如果设备有专门的INT8加速单元,量化后速度提升非常明显;如果只是普通CPU,有时候INT8计算本身并不比FP32快多少,省下的反而是内存带宽占用。
我先是走了PTQ(训练后量化)路线:准备几百张有代表性的校准图片,统计每层激活值的分布,然后确定量化参数。PTQ的好处是快,不用重新训练,但对小目标检测不太友好。行人检测里大量目标只有十几个像素宽,量化误差很容易把关键特征抹掉。实测下来,PTQ后mAP掉了将近5个百分点,而边缘设备上本来精度就比较紧,这个跌幅我接受不了。
后来切换到QAT(量化感知训练),在训练过程中模拟量化误差,让模型自己去适应。做法是在网络里插入伪量化节点,用同样的数据重新训练几个epoch。QAT比PTQ麻烦,但收益直观:相同场景下精度损失被压到了1个百分点左右,INT8推理速度比FP32提升了约40%。如果没有严格的延迟要求,我其实不建议一上来就做INT8,FP16往往够用且省事得多。
3.3 知识蒸馏:让剪枝量化后的模型“补课”
剪枝加量化做完,精度多少还是受了点伤。为了把精度再拉回来,我用了知识蒸馏。思路是拿一个精度高但体积大的模型当老师,让轻量模型模仿老师的输出。当时我选了同类型中一个更大的模型作为教师,用教师模型的预测结果作为软标签,让学生模型的输出去逼近它。
蒸馏实现上有两个细节值得注意。一是蒸馏温度,温度设得太低,软标签携带的“类间相似信息”就体现不出来;设得太高,又会让训练发散。我试下来温度设在4左右效果比较稳定。二是损失函数配比,硬标签损失和蒸馏损失的权重需要反复调,我当时用的比例大约是七比三,学生模型兼顾真实标签和老师经验,最终比纯硬标签训练高了一截。
不过蒸馏不是万能的。如果教师模型本身在密集行人场景下就表现一般,学生再怎么学也学不出新东西。所以我更愿意把蒸馏看成“锦上添花”,而不是基础精度不足时的救命稻草。
4. 边缘部署踩坑记录:从PyTorch权重到稳定上线的路程
4.1 部署链路选型:推理引擎确定的坑
模型优化的终点是部署,而部署的第一个坑就是推理引擎选型。我同时考虑了两条路线:一条面向NVIDIA平台,走TensorRT;另一条面向纯CPU平台,走OpenVINO。它们各有各的好处,但共同点是都要求先把PyTorch模型转成ONNX。
ONNX导出这一步看似简单,实际最容易埋雷。我的模型里有一些自定义的NMS操作和动态尺寸相关的节点,导出时经常报算子不支持。解决办法是:把NMS等后处理逻辑留在推理框架外部,用原生Python或者C++实现,ONNX里只保留纯卷积网络部分。虽然这样写起来麻烦一些,但换来的是导出稳定和后续调试方便。
另一个容易出错的是动态输入。边缘设备上,为了效率我最后固定了输入尺寸,没有用动态batch。原因很简单,批量推理在低功耗设备上几乎没有意义,固定尺寸能省下不少动态分配的额外开销。如果有人非要用动态尺寸,我建议先在推理引擎里充分测试,因为动态shape在转换和优化时的潜在问题比固定shape多一个数量级。
4.2 预处理和后处理才是隐藏的大头
把模型本身优化到很快以后,你会发现真正吃时间的是模型前后那两段“原始代码”。先说预处理。模型需要letterbox缩放保持比例,还需要归一化,这些操作在CPU上执行,帧率一高就变成瓶颈。我实测过一次,在没有优化预处理的情况下,单帧处理时间里有接近三分之一花在图像缩放和颜色格式转换上。
解决办法是把能并行的工作都并行。图像缩放用高效库里的resize函数替代自己写循环,归一化合到卷积的第一层里做掉,甚至可以把BGR转RGB的步骤也融合进输入层。这些改动听起来小而杂,累计省下的时间很可观。
后处理里的NMS同样是个隐藏大户。行人密集时,一个画面里可能同时产生几十上百个候选框,NMS在单线程下能轻松吃掉好几毫秒。如果部署的推理引擎不支持高效的NMS算子,我建议自己写一个带阈值过滤的简化版本,先按得分过滤掉大量低质量框,再做NMS,能明显提速。精度上几乎无损失,因为那些低得分框本来就会被丢掉。
4.3 精度对齐:从“数值乱跳”到“完全对不上”的排查
部署时最让人崩溃的是模型在PyTorch里好好的,转换之后结果完全不对。我第一次遇到这个问题时,第一反应是检查网络结构转换是不是出了问题,结果查了半天没查到。后来才意识到,问题出在输入上——训练时我用的是RGB顺序加特定归一化系数,而部署代码里默认按BGR读取图像。推理引擎拿到的输入和训练时完全不一样,输出自然一塌糊涂。
排查这类问题,我的习惯是走数值比对流程:先随机挑几帧图片,用同一份输入分别在PyTorch模型和部署后的模型上做推理,逐层比较中间特征图的差异。如果前面几层就对不上,基本就是预处理顺序或者归一化参数的问题;如果前面层完全一致,后面才出现偏差,那就要怀疑量化精度损失或算子实现差异。数值比对虽然枯燥,但比瞎猜高效太多。
还有一个容易忽略的细节:推理引擎的默认线程数、显存分配策略也可能影响最终输出顺序。多batch时尤其容易出现结果排列错位的情况,排查时不要忘了把输入输出的维度顺序一起核对。
5. 实测数据与调优心得:从75% mAP和30 FPS说起
5.1 一个低算力设备的整链路性能结果
整个优化流程走完,我在自己的测试设备上做了一轮完整对比。测试数据集的难度在通用行人检测里面算中等,目标尺寸跨度比较大,包含部分密集场景。结果如下:
| 版本 | 模型体积 | 单帧耗时(CPU) | mAP51 | 说明 |
|---|---|---|---|---|
| 原始YOLOX-Nano(FP32) | 约8MB | 约90ms | 约77% | 基线,尚未优化 |
| 剪枝后(FP32) | 约4MB | 约55ms | 约74% | 计算量下降明显,精度小跌 |
| 剪枝+量化(INT8) | 约1.5MB | 约36ms | 约72% | 综合速度与精度较平衡 |
单看精度,剪枝加量化后掉了大约5个百分点,但换来的是运行帧率从11 FPS提升到了27 FPS左右,模型体积缩小到原来五分之一。对那台低功耗设备而言,这已经是“能干活”和“干不了活”的区别。后来我在数据处理和模型微调上再追了一下,mAP恢复到了75%上下,同时保持每秒30帧附近。这个结果谈不上惊艳,但作为边缘实时方案,已经可以上线了。
5.2 实际项目里比mAP更重要的三个指标
做这个项目的过程中,我越来越觉得,做行人检测不能只盯着mAP看,至少还有三个指标在实践中更关键。
第一个是漏检率。在客流统计场景里,漏掉一个行人比偶尔误检一个更致命,因为漏检直接导致人数偏差。我会专门把漏检率拆出来统计,尤其在人群遮挡区域单独评测。第二个是夜间鲁棒性。很多场景白天表现不错,一到晚上就崩,问题不只在模型,也可能出在摄像头参数和图像增强环节。提前在模型训练时加入亮度扰动,会比事后调曝光更有效。第三个是姿态多样性。行人不是永远站着的,骑车的、蹲着的、抱小孩的,都可能被当成非行人过滤掉。训练数据里如果缺少这些长尾姿态,模型在实际中就会时不时“看不见人”。
这些小指标不会直接反映在mAP总量上,但直接决定项目能不能通过验收。我建议方案评估阶段就把这些维度明确出来,不要等问题上线了再回头补。
5.3 几个值得分享的落地小经验
最后说几条我在这个项目里反复印证过的经验。第一,不要一上来就做INT8量化,先让模型在FP16或原精度下跑通全流程,确认预处理、后处理、精度对齐都没问题,量化才不会背上一堆糊涂账。第二,选型时把硬件算力指标当成硬约束,先确定可接受的最大FLOPs,再筛选模型,不然很容易被高精度的模型带偏。第三,剪枝和量化都做完之后,一定要重新测一波夜间、雨雾和密集场景,因为这些情况下模型的安全余量更小,容易暴露出白天测试看不出的问题。
我做这个项目最大的体会是,轻量级不是一个模型的事,而是模型、优化手段、推理框架和后处理一起协作的结果。每一个环节省下的几毫秒,最后汇聚起来,才是设备上那稳定又流畅的30帧。如果你正卡在某个具体环节上,不妨对照这篇的内容逐个排查,很多时候问题就藏在你以为没问题的地方。