做手机端行人检测,最先卡脖子的是模型大小和推理速度。我自己的教训是,直接拿标准YOLO跑手机,别说实时,光是模型体积就可能把安装包撑到几十兆,再加上内存占用和发热,基本没法落地。后来我换了一条路子:用GhostConv把标准YOLO里那些冗余卷积换掉,再把网络整体做成极简结构,专门面向行人检测这个单一任务做裁剪。这套方案跑下来效果出乎意料——模型文件只有3~4MB,FP16推理单帧能压到30ms左右,INT8量化后十几毫秒就能出一帧,真正达到了手机端实时行人检测的实用水平。这篇文章就完整复盘这个项目的思路、原理、改法、训练和部署细节,顺便把踩过的坑一并列出来,适合正在做移动端目标检测、或者想把手里的YOLO模型做瘦身的朋友参考。
1. 方案选型:为什么是GhostConv加上极简YOLO
1.1 手机端行人检测的硬性约束
手机端的检测任务跟服务器端完全是两种玩法。服务器上可以堆显卡,模型随便大,精度刷到最高都行;手机上就一块SoC,CPU、GPU、NPU全挤在同一个散热模组里,算力天花板摆在那。内存更是金贵,App本身已经占了大量资源,留给模型的推理内存往往只有几百兆。行人检测又是一个连续视频流任务,摄像头对着路口、楼道、园区道路,每一帧都要出检测结果,一秒十几帧是底线,低于这个数框就明显跟不上人走路的速度,用户体验直接崩塌。
除了速度和体积,精度也有特殊性。行人不是普通物体,它通常是竖长形状、宽高比稳定、肢体姿态变化大,而且远距离目标在画面上可能只有三四十个像素。这就意味着网络必须在很小的特征图上依然能保留足够的位置信息,尺度融合能力不能丢。用一句话概括手机端行人检测的需求:小模型、快推理、不漏人。这三件事放在标准YOLO上很难同时满足,所以必须做定制化的轻量化改造。
1.2 GhostConv为什么能解决问题
GhostConv的思路非常聪明,它基于一个我在实践里也深有体会的观察:普通卷积生成的特征图里,有大量通道之间存在明显的相似性,这些通道就是“冗余特征图”。与其让每个通道都通过一次完整卷积硬算出来,不如先用小规模的标准卷积生成一小部分“本真特征图”,再用一组便宜的线性变换把它们膨胀成剩下的特征图。这就像画画一样,先用几笔勾勒出人物轮廓,再去填充衣纹和背景细节,最终成品看起来差不多,但颜料和功夫省了一大截。
放到YOLO里,GhostConv可以直接替换标准卷积层,而不需要动网络的主体结构。深度可分离卷积作为线性变换算子,在大多数端侧推理框架里都有原生优化,不会成为部署瓶颈。我实测下来,单纯把Backbone里的标准卷积替换成GhostConv,计算量就能下降一半左右,而精度损失通常在0.5个点以内,这个性价比在移动端场景里非常可观。
之所以选择GhostConv而不是其他轻量化手段,还有一个原因:它对原厂预训练权重的利用比较好。你在标准YOLO上做剪枝或者蒸馏也可以,但剪枝容易破坏结构,蒸馏需要额外训练一个教师网络。GhostConv只是换了卷积实现方式,主干拓扑基本不变,很多经验可以直接复用。
1.3 极简YOLO架构的设计原则
所谓“极简”,不是简单粗暴地把网络层数砍掉。层数一旦砍太多,感受野不够,大尺度行人的边缘定位就不准,反而得不偿失。我的设计原则是:保留YOLO家族最核心的多尺度特征金字塔结构,但把每一层通道数压到“够用”的底线,同时减少重复的Bottleneck堆叠次数。
通道数压到多少才算“够用”?这个需要通过实验去试。我最初试着把Backbone每层通道砍到原来的1/4,结果小目标召回率掉得一塌糊涂,因为特征表达能力确实不够了。后来采用了一个折中方案:浅层通道保持相对宽裕,深层通道多压缩。浅层特征负责边缘、颜色、小尺度纹理,低维了容易丢失小目标的细节;深层特征负责语义类别和整体轮廓,本身就存在大量冗余,压缩空间更大。
整体上看,这个极简YOLO就是在三个层面做文章:Backbone用GhostConv堆叠并缩减通道,Neck保留完整FPN/PAN路径但减少重复模块,Head用轻量化解耦头。每一层改动的理由都不同,下面我会逐个拆开讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. GhostConv核心原理拆解:到底省了多少计算量
2.1 从普通卷积到幽灵卷积的转变
先回顾普通卷积的开销是怎么产生的。假设输入特征图尺寸是C×H×W,卷积核大小是k×k,输出通道数是C',那普通卷积的计算量大约是C×C'×H×W×k×k。这个数字会随着输出通道数翻倍而翻倍,因为每个输出通道都要和所有输入通道做一次完整的k×k卷积。
既然输出特征图之间存在大量相似性,GhostConv的处理方法是分成两步:第一步,用标准卷积只生成其中一部分特征图,比如输出通道的一半,这部分称为“本真特征”;第二步,对这些本真特征逐通道做一次轻量的线性变换,比如3×3的深度可分离卷积,再用变换结果拼出剩下那一半特征图。深度可分离卷积是在单个通道内部做空间变换,不涉及跨通道的信息融合,所以计算量远小于一次完整的标准卷积。
这个设计的本质是把“通道间相关性”转化成了“空间冗余性”的计算。网络自己学会的是:哪些特征通道可以用别的通道线性变换而成,而不是每个通道都独立从零计算。实际操作中GhostConv的实现也不复杂,拆开看就是一次标准卷积、一次depthwise卷积和一个Concat,工程上非常友好。
2.2 计算量的数学对比
用数字来看更直观。假设输入通道C=256,输出通道C'=256,特征图尺寸是28×28,卷积核3×3。标准卷积的计算量是256×256×28×28×3×3,结果约1.85亿次乘加。
用GhostConv,设定本真特征占比r=0.5。第一步标准卷积只需要输出128个通道,计算量是256×128×28×28×3×3,约0.92亿次乘加。第二步用3×3的depthwise卷积生成剩下128个通道,计算量为128×28×28×3×3,约904万次乘加。总计算量约1.01亿次乘加,相比标准卷积省了45%以上。
这里的关键在于,第一项计算量和输入通道C成正比,第二项只和输出通道C'相关,C越大,省得越多。在深层网络里输入通道动辄512、1024,节省效果会进一步放大。我实测的几个阶段(输入通道从64到512不等)里,GhostConv替换后FLOPs平均下降40%~60%,而且网络越深,收益越明显。
2.3 本真特征比例怎么调
GhostConv里有一个超参数非常关键:本真特征占输出通道的比例,也就是ratio。ratio越高,越接近普通卷积,精度更高但省得少;ratio越低,省得越多但特征表达能力被压缩。这个参数不是固定的,我建议分阶段设置。
以行人检测为例。浅层特征图分辨率高,主要负责细节边缘和局部纹理,我设置了ratio=0.5,保证边缘响应足够丰富,否则小目标行人(比如远处的人头)容易被过滤掉。中深层特征图已经具备一定语义抽象,冗余程度更高,我把ratio调到0.4,压缩更多计算量。Neck部分因为涉及多尺度特征融合,信息交互频繁,我保守设置ratio=0.6,减少融合时的信息损失。
有几次我把ratio调到0.25试速度,FLOPs确实又降了一截,但量化后小目标AP掉了2个点,得不偿失。如果应用场景里行人大都在画面中占据较大面积,ratio可以激进一些;如果远距离、密集场景多,还是保持在0.5上下比较稳妥。
3. 极简YOLO架构设计:Backbone、Neck、Head怎么改
3.1 Backbone:用GhostConv堆叠并压缩通道
Backbone是网络里计算量最集中的部分,每一层的计算量随通道数平方增长。我在YOLOv8n的基础上做了两处改动:一是把C2f模块里的标准卷积全部替换成GhostConv,二是把每个阶段的输出通道数按比例缩减。
具体配置上,我用的是逐步压缩的策略。第一层输出从64降到32,第二层从128降到64,第三层从256降到128,第四层保持256,SPPF模块中间通道压到128。为什么深层不继续降?因为第4层特征图要送入Neck做尺度融合,它承载了最高层的语义特征,如果压得过狠,分类置信度会普遍下降,检测框就变得不稳定。
我做了一张结构表,方便对照:
| 阶段 | 原YOLOv8n输出通道 | 极简版输出通道 | 是否替换GhostConv |
|---|---|---|---|
| Stem | 64 | 32 | 是 |
| Stage1 | 128 | 64 | 是 |
| Stage2 | 256 | 128 | 是 |
| Stage3 | 512 | 256 | 是 |
| SPPF | 512 | 256 | 是 |
整体改完,Backbone的FLOPs相比原版下降了约60%。实测下来,前几层的通道压缩对最终检测精度几乎没有负面影响,因为这也符合冗余特征图存在的假设——早期卷积生成的很多低层特征,本来就是多余或者高度相似的。
3.2 Neck:保留双向融合,减少重复次数
Neck是FPN+PAN的结构,它的职责是把浅层的空间细节和深层的语义信息做双向融合。如果把Neck改得太激进,比如砍掉某一层尺度路径,检测精度会明显变差。原因很简单:行人检测往往同时需要高分辨率的浅层特征来定位,也需要深层语义特征来区分人和背景,缺了哪一层都会出问题。
我的做法是在不改变路径结构的前提下,把PAN路径中每个C2f模块的Bottleneck重复次数从默认的3次减少到1次。Bottleneck在Neck里的作用是增强非线性特征表达能力,但重复堆叠3次带来的精度增益已经很小,计算开销却不小。改成1次后,Neck部分的计算量差不多减了一半,精度只掉了0.2个点左右。
此外,Neck里所有跨尺度融合的1×1卷积也全部替换成GhostConv。这些1×1卷积对通道数敏感,用GhostConv处理后省得尤其多。
3.3 Head:轻量化解耦头的取舍
YOLOv8的解耦头把分类和回归分成两个分支,分类分支输出类别概率,回归分支输出边界框参数。我在这个基础上做了压缩:每个分支先用一次GhostConv把通道统一降到64,再做一次1×1卷积输出最终预测。中间不再堆叠额外的卷积层。
这样改完,Head的参数量在几十万级别,相比整个模型可以忽略不计。有人可能会问,Head这么简单,精度会不会崩?我的实测结果是,分类分支压缩到64通道之后,AP下降不到0.3个点,但推理速度提升了约5%。因为Head通常在三个尺度上各执行一次,累计起来开销不小,这个取舍是划算的。
整体结构改完之后,模型的参数量只有原YOLOv8n的40%左右。从工程角度看,这个“极简”不是拍脑袋定的,而是经过多轮精度-速度权衡后的结果。我建议你们也不要照抄我这个配置,最好用自己的数据集跑一轮“通道压缩敏感性”测试,看看哪一层砍掉后精度掉得厉害,再针对性调整。
4. 训练与调优:如何保住行人检测精度
4.1 数据准备:行人场景的特殊性
行人检测的数据集不能直接按通用目标检测的思路来准备。我一开始直接拿公开通用数据集的person类训练,效果一般,后来经过分析发现,通用数据集里行人尺度分布比较均匀,大目标占比多,而实际监控、车载、园区场景里,大量行人是小目标,有的只有几十像素高。所以我做了一个数据整理:以通用检测数据集的person类为主,再加上几个公开行人数据库的样本,过滤掉重复图片后,凑了接近5万张有效训练图。
数据增强策略上,我用了Mosaic和MixUp。Mosaic对提升小目标检测特别有帮助,因为它能把四张图拼在一起,让每个目标相对变小,相当于变相增加了小目标样本。但是有一个坑:Mosaic在训练后期会产生尺度分布偏移,导致最后几个epoch的评估指标波动很大。我的做法是最后20个epoch关闭Mosaic,只用平移、缩放、色彩抖动这些常规增强,让模型在真实尺度分布上做微调。
4.2 训练策略:从预训练权重迁移还是从零训练
换掉主干卷积结构之后,最棘手的问题是能不能复用原版预训练权重。GhostConv和标准卷积的输入输出维度一致,所以理论上可以加载原版YOLOv8n的权重,但GhostConv内部的标准卷积分支只对应一部分输出通道,从原版权重里直接读取会造成维度不匹配。
我踩过几次坑后找到一种稳妥的初始化方式。做法是:不直接用预训练权重初始化整个网络,而是在加载权重时,只加载Backbone中未改变形状的那部分参数,对GhostConv的线性变换分支初始化成恒等映射的等价形式。也就是说,让线性变换一开始不对特征图产生干扰,网络先以接近原版的行为开始训练,随着训练推进再逐渐让GhostConv分支发挥作用。这样做了之后,训练初期的损失下降比完全随机初始化快得多,最终精度也更高。
训练轮数我设置了160轮。如果时间紧张用100轮也够,但精度会少0.5个点左右。优化器用AdamW,初始学习率0.001,配合余弦退火,批大小在单卡上取16,输入分辨率统一缩放到320×320。分辨率的选择也是权衡过的,如果放到640×640,小目标检测会好一些,但手机端推理速度直接翻倍下降,实时性就保不住了。
4.3 损失函数与NMS后处理
损失函数我没有做大的改动,继续沿用YOLOv8默认的分类损失、GIoU损失和DFL(Distribution Focal Loss)的加权组合。这套损失本身就是为目标检测任务设计的,强行改反而容易崩。
比较需要调的是NMS参数。行人场景中人常常密集聚集,比如地铁闸机口、十字路口,如果用默认的NMS阈值,重叠的检测框会被大量抑制,导致漏检。我把IoU阈值调到0.45,置信度阈值设成0.25。这两个参数一个负责抑制冗余框,一个负责过滤低置信度预测,需要平衡。调低置信度阈值能捞回一部分远距离的小目标行人,但也会引入少量误检。
我在自测数据集上做过对比:
| 置信度阈值 | IoU阈值 | Recall | 误检率 |
|---|---|---|---|
| 0.15 | 0.45 | 0.87 | 0.08 |
| 0.25 | 0.45 | 0.83 | 0.03 |
| 0.35 | 0.50 | 0.74 | 0.02 |
| 0.50 | 0.50 | 0.61 | 0.01 |
最终选了置信度0.25和IoU0.45的折中组合,在误检率和召回率之间取得了平衡。这个参数在不同数据集上会有差异,建议你们用自己验证集跑一遍小网格搜索。
5. 手机端部署:模型转换、量化与实测帧率
5.1 从PyTorch到端侧推理格式
模型训练完成只是第一步,真正难的是让它跑在手机上。我的部署链路是:先把PyTorch权重导出ONNX格式,再做计算图优化,最后转成端侧推理框架的格式。GhostConv的两个关键算子——depthwise卷积和通道拼接(Concat)——在多数主流端侧框架里都有原生算子支持,但转换过程中还是要注意几个细节。
导出ONNX之前,要把模型里的动态维度固定住。比如输入尺寸就是1×3×320×320,不要留动态轴,否则转端侧格式时有些框架会报算子不支持,或者生成的计算图特别绕。导出完成之后,先跑一遍ONNX Simplifier做简化,它会合并多余的Transpose、Reshape节点,把计算图梳理得更清爽。除非你对算子融合特别有把握,否则这一步不要跳。
转成端侧格式后,我通常在手机端先用CPU跑一遍纯推理,确认结果和PyTorch的输出误差在可接受范围内,再去考虑NPU加速。这里有个经验:对比输出结果时别只看最高的置信度类别,要把所有类别的置信度向量整体对比,差一两个百分点的浮动是正常的,但如果某个通道输出出现明显偏置,说明算子转换出了问题。
5.2 INT8量化:精度损失的防御性策略
手机端要跑实时,量化几乎是必须走的路。最常见的做法是训练后量化(PTQ),采集一些校准图片,计算出每一层的激活值分布,然后把权重和激活从FP32映射到INT8。听起来简单,实际踩坑很多。
行人检测这类任务有一个特殊性:小目标在整个图像里占比小,对应的特征激活值可能非常小或者分布不均匀,PTQ量化时容易把这些细微信号截断掉,导致漏检。我在PTQ之后做了验证,小目标行人的召回率下降了差不多2个点。为了解决这个问题,我改用逐通道量化,并且对GhostConv的depthwise卷积分支单独观察激活分布。因为depthwise卷积每个通道只处理一条特征图,统计量少,很容易被量化噪声干扰。
如果逐通道量化之后精度仍不达标,就要上量化感知训练(QAT)。QAT的本质是在训练过程中模拟量化的舍入误差,让网络权重适应低精度表示。我的经验是,QAT只需要跑20~30轮,学习率调低到原来的1/10,就能把PTQ掉点的大部分补回来。对GhostConv这种轻量模块来说,QAT的收益比较明显,因为它的权重本身就少,每一个bit都显得很重要。
5.3 实测性能对比
我在几台不同档位的手机上做了实测,统一使用同一套INT8量化后的模型文件。测试场景是手持手机拍摄园区道路,目标为路上行人,每帧做完整检测。测试数据见下表:
| 设备档位 | 推理框架 | 单帧耗时(ms) | 是否达到实时 |
|---|---|---|---|
| 中高端SoC | INT8 | 12~18 | 是,约60FPS |
| 中高端SoC | FP16 | 20~35 | 是,约30FPS |
| 中端SoC | INT8 | 18~25 | 是,约40FPS |
| 中端SoC | FP16 | 35~50 | 勉强,20FPS上下 |
| 低端SoC | INT8 | 25~40 | 勉强,15~20FPS |
低端机跑INT8虽然能到25ms左右一帧,但如果后台同时开其他App,实际帧率会掉到15FPS以下,体验已经不太行了。对于量产级别的要求,我建议至少中端以上SoC才考虑纯CPU推理,低端机最好切到NPU路径。
模型文件大小最终是3.7MB,其中权重占大头,打包进APK后新增体积不到4MB,相比标准YOLOv8n的21MB已经非常轻量。
6. 常见问题与排查技巧实录
6.1 换了GhostConv之后精度掉得厉害怎么办
最常见的问题是,把标准卷积替换成GhostConv之后,mAP掉了1~2个点甚至更多。遇到这种情况,先不要怀疑GhostConv本身,按下面的顺序排查。
先检查是不是所有卷积都用相同ratio替换的。我之前为了省事全网络统一用0.5,最后发现Neck的融合层掉点最明显。后来把Neck部分的ratio调回0.6,问题就好多了。再检查浅层Stem,这个位置最好保留标准卷积,因为它负责最底层的颜色和边缘提取,输入是原始RGB三通道,通道数少,用GhostConv省不了多少计算量,反而容易引入噪声。
如果还是掉点,可以试试在某个特定尺度层把GhostConv换回标准卷积。我自己的最终模型里,Backbone第一层Stem保留了标准卷积,Neck顶层融合也保留了一路标准卷积,其他全部用GhostConv。这个组合在速度和精度之间最平衡。
6.2 手机推理掉帧:先查算子再查内存
手机端推理掉帧可能是几种原因叠加在一起的。第一个要查的是算子是否走了优化实现。有些端侧推理框架对depthwise卷积的优化只覆盖特定通道数,比如当通道数为4的倍数时才走SIMD加速路径。如果你的网络层输出通道数是66、130这种奇奇怪怪的数字,框架可能回退到通用实现,速度直接掉一大截。解决办法很简单:把通道对齐到8的整数倍,我一般保持每一层能被4整除,尽量对齐64、128、256这种经典数值。
第二个要查的是内存分配。推理时如果每一帧都重新分配输入输出Tensor,内存碎片会导致GC频繁触发,帧率就会像心电图一样忽高忽低。解决方法是复用缓冲池,预先分配好固定大小的内存块,每一帧推理都从池里取,用完归还。
如果两层都没问题,再检查是不是后台运行了多个模型实例。我踩过一次坑,测试时把热启动和预加载做了两次模型加载,结果内存翻倍,中端机直接掉到5FPS。最终只保留一个全局单例,才恢复正常。
6.3 量化后检测框偏移怎么办
量化后最常见的现象是检测框整体偏移,尤其是行人的脚底位置对不上。这个问题多半出在回归分支。分类分支的置信度输出对量化比较皮实,因为在最后有Sigmoid归一化到0~1,误差被压得比较小。但回归分支输出的是相对坐标的偏移量,数值范围波动大,量化时一个量步长太大就可能导致框位移几个像素。
我的处理办法有两步。第一步,在QAT阶段对回归分支的量化尺度单独做细粒度校准,不跟分类分支共用同一个Scale。第二步,如果校准后仍有偏移,就在线把回归分支的中间层保留为FP16混合精度,只对输入和输出做INT8,这样速度和精度都能兼顾。实测下来,这个调整后脚底中心点误差从平均6个像素降到了2个像素左右,已经满足实际使用需求。
6.4 漏检远距离小行人的排查方向
训练完发现远处的小行人总漏检,原因多半不在模型结构,而在于三个环节。先看输入分辨率,320×320下,一个身高40像素的行人只占整个画面的1/480,特征非常稀疏,如果你对稀疏目标要求高,需要把浅层特征图的分辨率保持住。再看Anchor分配策略,我采用的是无Anchor的分配方法,但距离远的目标匹配到的正样本可能偏少,这会降低小目标的学习强度。最后看损失权重,如果分类损失权重偏低,模型会倾向于把远处的模糊目标判为背景。
我最有用的一招是:在训练时做多尺度训练,虽然最终部署固定为320×320,但训练过程中随机缩放图片到256~480之间。这样模型见过不同尺度下的行人特征,泛化能力明显变好,远处小目标的漏检率降低了约3个点。
最后再分享一个我做项目时的判断思路。GhostConv这套方案最核心的价值不只是省了多少计算量,而是它验证了一件事:深度模型里的冗余远比我们想象中多,网络真正需要的可能只是一小部分核心特征加一些便宜的变换。你在自己的项目里不必照抄我的配置,可以先拿标准YOLO跑一个基线,再按30%、50%、70%的比例逐层替换成GhostConv,记录每一层的精度和速度曲线,找到属于你自己模型的甜点。我后续还打算把剩余的标准卷积全部替换成对NPU更友好的组合,再做一次结构重参数化,把推理时间进一步往下压。希望这篇内容能给你在走路人检测落地时一些真正能上手的参考。
