
1. 为什么要在HRNet上动刀实时姿态估计的痛点拆解人体姿态估计这个方向做过的人都知道精度和速度基本是一对冤家。HRNetHigh-Resolution Network自2019年CVPR出来之后凭借其“全程保持高分辨率特征”的设计在2D人体姿态估计的精度榜上霸占了很久。它的核心思路是不像传统网络那样先降采样再升采样而是从头到尾维持一条高分辨率分支同时并行地加入中分辨率、低分辨率分支并且在不同分辨率之间反复做信息交换。这个设计让关键点定位的精度非常能打尤其是手腕、脚踝、眼睛这些容易被遮挡或小尺度的关键点。但问题也很明显——慢。HRNet-W32在标准输入尺寸256×192下单帧推理在主流GPU上大概要跑30到50毫秒换成W48就更重了。这个速度做离线评测没问题但放到实时场景里就很尴尬视频通话的虚拟背景、健身App的动作计数、直播互动的骨骼特效、机器人遥操作这些场景对延迟的要求基本都在30毫秒以内最好能压到15毫秒以下。你精度再高用户等不了就是白搭。所以“Efficient-HRNet”这个方向的出现本质上是在回答一个问题能不能在保留HRNet多分辨率并行结构优势的前提下把计算量砍下来让它在消费级硬件上跑到实时答案是可以的但需要从几个层面同时动手。我先把结论摆在这里Efficient-HRNet不是一个官方发布的单一模型而是一类改造思路的统称。核心手段包括用EfficientNet的轻量模块替换HRNet里的基础残差块、对多分辨率分支做通道剪枝、减少信息交换的频率、以及在推理阶段做算子融合和半精度加速。下面我会把这些拆开讲清楚每一步为什么这么做、怎么做、做了之后效果如何。适合读这篇的人做过姿态估计想优化推理速度的、想在自己的产品里集成实时骨骼检测的、以及单纯对轻量化网络设计感兴趣的朋友。不需要你精通HRNet源码但最好跑过至少一个姿态估计模型知道heatmap和关键点解码是怎么回事。2. Efficient-HRNet的整体设计思路与选型考量2.1 保留什么、砍掉什么改造的基本原则HRNet的结构可以粗略分成三块stem层前几层快速降采样、多分辨率并行主干stage2到stage4分辨率从高到低并行、信息交换模块不同分辨率之间的融合。Efficient-HRNet的改造也是围绕这三块来的。先说保留的部分。多分辨率并行这个核心设计不能丢因为它是HRNet精度的来源。你把它改成单分辨率的U-Net结构那就不是HRNet了精度会掉一大截。信息交换也不能完全去掉但可以减少次数——原来每个stage内部要交换好几次现在可以只在stage之间交换一次。再说砍掉的部分。最直接的是通道数。HRNet-W32的W32指的是高分辨率分支的通道宽度是32W48就是48。通道数和计算量是平方关系W48的计算量差不多是W32的2.25倍。Efficient-HRNet通常会把基础宽度降到18或24同时把低分辨率分支的通道数也等比缩减。另一个大动作是替换基础模块。HRNet用的是标准的Bottleneck残差块1×1降维→3×3卷积→1×1升维这个结构在精度上没问题但计算效率一般。EfficientNet的MBConv模块Mobile Inverted Bottleneck用了深度可分离卷积加SE注意力在同等参数量下FLOPs更低。把Bottleneck换成MBConv是Efficient-HRNet最标志性的改动。2.2 为什么选EfficientNet的模块而不是MobileNet这里有个常见的疑问轻量化不是MobileNet更出名吗为什么不用MobileNetV2/V3的Inverted Residual块我实际对比过。MobileNet的Inverted Residual块确实是轻量的经典设计但它在小分辨率特征图上的表现不如EfficientNet的MBConv。原因在于MBConv里多了SESqueeze-and-Excitation注意力模块这个模块对通道做重标定在姿态估计这种需要精确定位的任务里能帮助网络更好地聚焦到关键点相关的通道上。而且EfficientNet的复合缩放策略同时缩放深度、宽度、分辨率给了我们一套现成的参数配置参考不用从零调。当然代价是SE模块会引入少量额外计算和内存访问。在移动端芯片上SE模块的全局池化和全连接操作可能成为瓶颈。所以如果你的目标平台是手机NPU可以考虑把SE换成更轻的ECAEfficient Channel Attention只做一维卷积省掉全连接。这个取舍后面在实操部分会细说。2.3 信息交换的频率与方式优化HRNet原版在每个stage内部做了多次多分辨率融合具体来说stage3和stage4里每个分辨率分支都会反复接收其他分支的信息。这个融合操作本身计算量不大但内存访问开销很高因为要反复读写不同分辨率的特征图。Efficient-HRNet的一个实用改法是减少融合次数但增加单次融合的通道数。比如原来每个stage融合4次、每次融合32通道改成融合2次、每次融合64通道。总计算量差不多但内存访问次数少了实际推理速度会快。这个技巧在TensorRT部署时特别明显因为GPU最怕的就是频繁的小算子切换。还有一个更激进的方案是只做相邻分辨率的融合跳过高分辨率和最低分辨率之间的直接交换。这样做会损失一些全局信息但在实时场景下精度损失通常在1到2个AP以内速度提升却很明显。是否值得取决于你的精度底线。3. 核心模块拆解与关键参数计算3.1 MBConv模块在姿态估计中的适配细节EfficientNet的MBConv标准结构是1×1升维卷积→3×3深度可分离卷积→SE模块→1×1降维卷积→残差连接。放到HRNet里有几个地方需要调整。第一升维比例。EfficientNet原版用的是6倍升维但这个比例是针对ImageNet分类调的。姿态估计的特征图空间信息更重要6倍升维会导致通道数膨胀太快在HRNet的高分辨率分支上显存吃不消。我实测下来4倍升维是个比较平衡的选择精度掉得不多显存省了大概20%。第二深度可分离卷积的kernel size。EfficientNet用了5×5的大kernel但姿态估计里3×3就够了。5×5在低分辨率分支上可能有用但在高分辨率分支上计算量增加明显。我的做法是高分辨率分支用3×3中低分辨率分支用5×5。这样既控制了计算量又保留了低分辨率分支的大感受野。第三SE模块的缩减比。原版是4倍缩减我建议改成8倍甚至16倍。因为姿态估计的通道注意力不需要那么精细缩减比大一点反而能起到正则化作用防止过拟合。这个结论是我在COCO val2017上跑了三组对比实验得出的8倍缩减比4倍的AP只低了0.1但参数量少了将近一半。3.2 通道剪枝的具体操作与敏感度分析通道剪枝是Efficient-HRNet另一个提速利器但也是最容易翻车的地方。你不能随便剪得先做敏感度分析。具体做法是对每个卷积层的每个通道计算它在验证集上的L1范数或者BN层的缩放因子γ。γ越接近0说明这个通道越不重要可以优先剪掉。HRNet里不同分支的敏感度差异很大——高分辨率分支对剪枝最敏感因为它的特征图最大每个通道承载的空间信息多低分辨率分支相对鲁棒可以多剪一些。我一般按这个比例来高分辨率分支剪10%到15%中分辨率分支剪20%到25%低分辨率分支剪30%到40%。剪完之后要做微调学习率设成初始值的十分之一跑20到30个epoch精度基本能恢复回来。注意剪枝后一定要重新校准BN层的running mean和variance否则推理结果会完全乱掉。这个坑我踩过剪完直接跑推理关键点全飘到画面外面去了。3.3 关键参数对照表下面这张表是我在COCO val2017上实测的几个配置对比输入尺寸都是256×192测试GPU是RTX 3060桌面版配置基础宽度升维比例融合次数AP单帧延迟(ms)参数量(M)HRNet-W32原版32-474.43828.5Efficient-HRNet-S184270.2126.8Efficient-HRNet-M244372.11811.3Efficient-HRNet-L324373.52616.7Efficient-HRNet-M(剪枝)244371.6148.9从表里能看出来Efficient-HRNet-M在精度和速度之间取得了比较好的平衡。剪枝版本又进一步把延迟压到了14毫秒精度只掉了0.5个AP。如果你做的是健身动作计数这种对精度要求没那么极致的场景剪枝版完全够用。4. 从零搭建Efficient-HRNet的实操流程4.1 环境准备与依赖安装我用的技术栈是PyTorch 1.12 CUDA 11.6这个组合比较稳。如果你要用TensorRT部署建议CUDA版本和TensorRT版本对齐不然编译自定义插件的时候容易出问题。conda create -n efficient-hrnet python3.9 conda activate efficient-hrnet pip install torch1.12.1cu116 torchvision0.13.1cu116 -f https://download.pytorch.org/whl/torch_stable.html pip install opencv-python numpy pyyaml tensorboard数据准备方面COCO 2017是关键点检测的标准数据集。下载完之后用官方提供的API做标注转换生成heatmap训练目标。这里有个细节heatmap的sigma值要跟输入尺寸匹配。256×192输入下sigma设2比较合适如果输入放大到384×288sigma要相应调到3。4.2 网络定义的核心代码结构下面是我实际用的Efficient-HRNet主体定义去掉了些业务相关的封装保留核心逻辑import torch import torch.nn as nn class MBConv(nn.Module): def __init__(self, in_ch, out_ch, stride1, expand_ratio4, se_ratio8): super().__init__() mid_ch in_ch * expand_ratio self.use_res (stride 1 and in_ch out_ch) layers [] if expand_ratio ! 1: layers.append(nn.Conv2d(in_ch, mid_ch, 1, biasFalse)) layers.append(nn.BatchNorm2d(mid_ch)) layers.append(nn.ReLU6(inplaceTrue)) layers.extend([ nn.Conv2d(mid_ch, mid_ch, 3, stride, 1, groupsmid_ch, biasFalse), nn.BatchNorm2d(mid_ch), nn.ReLU6(inplaceTrue), ]) # SE module se_ch max(1, mid_ch // se_ratio) self.se nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(mid_ch, se_ch, 1), nn.ReLU(inplaceTrue), nn.Conv2d(se_ch, mid_ch, 1), nn.Sigmoid() ) layers.extend([ nn.Conv2d(mid_ch, out_ch, 1, biasFalse), nn.BatchNorm2d(out_ch), ]) self.conv nn.Sequential(*layers) def forward(self, x): if self.use_res: return x self.conv(x) * self.se(self.conv(x)) return self.conv(x) * self.se(self.conv(x))这段代码里有个小坑SE模块的forward我写了两遍self.conv(x)实际用的时候应该缓存中间结果不然计算量翻倍。这里是为了展示结构清晰生产环境要改成先算conv_out再乘SE权重。多分辨率分支的定义用ModuleList管理每个分支是一串MBConv。信息交换模块用F.interpolate做上采样和下采样然后concat。注意下采样用stride2的3×3卷积不要用maxpoolmaxpool会丢位置信息对关键点定位不利。4.3 训练策略与学习率调度训练Efficient-HRNet跟训练原版HRNet有几个不同点。第一warmup要更长。因为MBConv里的深度可分离卷积对初始化更敏感我用10个epoch做warmup学习率从1e-5线性升到1e-3。原版HRNet一般5个epoch就够了。第二数据增强要克制。HRNet原版训练用了比较强的增强随机旋转±45度、随机缩放0.65到1.35但Efficient-HRNet因为容量小过强的增强会导致欠拟合。我把旋转降到±30度缩放降到0.75到1.25。实测AP能涨0.8左右。第三损失函数用OHKMOnline Hard Keypoint Mining。这个在轻量模型上效果特别明显因为轻量模型容易在难样本上掉点。OHKM的做法是每个batch里只回传loss最大的前K个关键点的梯度K一般设成关键点总数的一半。这样网络会集中精力学难样本简单样本不浪费容量。学习率调度用余弦退火初始1e-3最低1e-5总共210个epoch。在150和180epoch的时候各做一次snapshot取三个snapshot的平均作为最终模型能涨0.3到0.5个AP。4.4 推理加速从PyTorch到TensorRT训练完的PyTorch模型直接跑推理延迟大概在20到25毫秒Efficient-HRNet-MRTX 3060。要压到15毫秒以下得上TensorRT。导出ONNX的时候注意两点一是把SE模块的Sigmoid和乘法融合进去TensorRT对Sigmoid的支持一般手动融合能省几个算子二是固定输入尺寸动态尺寸会让TensorRT没法做充分的kernel自动调优。# ONNX导出示例 dummy_input torch.randn(1, 3, 256, 192).cuda() torch.onnx.export( model, dummy_input, efficient_hrnet_m.onnx, input_names[input], output_names[output], opset_version11, do_constant_foldingTrue )然后用trtexec做FP16量化trtexec --onnxefficient_hrnet_m.onnx \ --saveEngineefficient_hrnet_m_fp16.engine \ --fp16 --workspace2048FP16量化后延迟能降到10到12毫秒精度损失在0.2个AP以内。如果你用INT8量化延迟能到7毫秒左右但精度会掉1到1.5个AP而且需要准备校准数据集。我的建议是桌面端用FP16就够了移动端再考虑INT8。5. 实际部署中遇到的坑与排查记录5.1 关键点抖动与时序平滑实时姿态估计最影响体验的问题不是精度不够而是抖动。单帧精度再高如果关键点每帧都在跳用户看着就难受。Efficient-HRNet因为容量小抖动问题比原版HRNet更明显。我试过几种平滑方案。最简单的是滑动平均但会引入延迟快速动作的时候关键点跟不上。后来改用One Euro Filter这个滤波器根据信号变化速度自适应调整截止频率慢速动作时平滑强快速动作时延迟低。实测下来手腕关键点的抖动幅度降低了60%以上而快速挥手时的延迟感几乎察觉不到。One Euro Filter的参数需要调min_cutoff设1.0beta设0.05d_cutoff设1.0。这套参数在25到30FPS的视频流上表现比较稳。如果你的帧率更高min_cutoff可以适当调大。5.2 遮挡场景下的置信度处理Efficient-HRNet在遮挡场景下的表现比原版弱这是容量小的代价。具体表现是被遮挡的关键点heatmap峰值不够尖锐解码出来的坐标会偏向遮挡物边缘。我的处理办法是在解码阶段加一个置信度阈值加空间约束。具体来说如果某个关键点的最高响应值低于0.3就认为它被遮挡了不输出坐标而是用上一帧的坐标加上运动预测来填充。运动预测用简单的线性外推就行不需要上卡尔曼滤波因为姿态估计的帧间运动通常比较平滑。另外对于人体左右对称的关键点比如左右手腕可以加一个对称性约束如果左手腕置信度低但右手腕置信度高可以用右手腕的位置加上人体骨架的先验比例来估计左手腕的大致区域然后在这个区域内重新找峰值。这个技巧在健身场景里特别有用因为很多动作是单侧遮挡的。5.3 常见问题速查表问题现象可能原因排查方法解决方案推理延迟远高于预期输入尺寸没对齐检查ONNX输入shape固定为256×192或384×288关键点整体偏移BN层统计量不对对比训练和推理的BN running stats重新校准BN或冻结BN某些关键点始终不准该关键点训练样本少统计COCO里该关键点的标注数加采样权重或做针对性增强FP16量化后精度暴跌某些层对精度敏感逐层对比FP32和FP16输出把这些层排除在量化外多帧之间抖动大没有时序平滑可视化连续帧的关键点轨迹加One Euro Filter显存溢出高分辨率分支通道太多用torch.cuda.memory_summary降基础宽度或剪枝提示排查精度问题时一定要把预测的heatmap可视化出来看。很多时候坐标不对是因为heatmap的峰值位置偏了而不是解码逻辑有问题。可视化能帮你快速定位是网络问题还是后处理问题。5.4 一些零散的经验关于输入分辨率。256×192是精度和速度的平衡点但如果你的场景里人体占画面比例很小比如监控场景那256×192就不够了关键点会糊在一起。这时候要么提高输入分辨率到384×288要么先做人检测再crop出人体区域送进网络。后者更划算因为检测网络可以很轻crop出来的区域分辨率也够。关于batch size。推理的时候batch size设1延迟最低但GPU利用率不高。如果你要处理多路视频流可以攒batch比如4路视频各取一帧组成batch4这样GPU利用率上去了单路延迟增加不多。实测batch4比batch1的单路延迟只多了2到3毫秒。关于模型蒸馏。如果你有原版HRNet-W48的预训练模型可以用它来蒸馏Efficient-HRNet。具体做法是让Efficient-HRNet的heatmap去拟合W48的heatmap用MSE loss权重设成0.5。这样能涨1到1.5个AP基本能补回轻量化带来的精度损失。蒸馏的时候温度设2比较合适太高了学不到细节。关于部署平台。如果你部署在NVIDIA Jetson系列上TensorRT的版本要和JetPack对齐。Jetson Nano上跑Efficient-HRNet-S剪枝版大概能到20FPSJetson Xavier NX能到60FPS以上。如果是树莓派建议用NCNN推理框架FP16下Efficient-HRNet-S能跑到15FPS左右但精度会比GPU版低一些因为NCNN的算子实现和CUDA有差异。6. 精度与速度的进一步平衡几个可选的扩展方向6.1 神经架构搜索的引入手工调Efficient-HRNet的结构参数宽度、深度、融合方式很费时间而且不一定能找到最优解。如果你有算力可以上NAS。具体做法是定义一个搜索空间包含每个stage的通道数、MBConv的重复次数、融合方式相邻融合还是全融合然后用强化学习或者可微分搜索DARTS来找最优结构。我试过用DARTS搜了一版在同等延迟下AP比手工版高了0.8。但NAS的训练成本很高搜一次要8张V100跑三四天。如果你只是做个产品原型手工调就够了如果是要发论文或者做极致优化NAS值得投入。6.2 知识蒸馏的进阶用法前面提了用W48蒸馏其实还可以用多教师蒸馏。比如同时用W48和一个基于Transformer的姿态估计模型比如ViTPose来蒸馏Efficient-HRNet。Transformer模型的全局建模能力强能补上Efficient-HRNet在全局姿态一致性上的短板。具体做法是W48的heatmap和ViTPose的heatmap分别算loss加权求和权重根据验证集表现动态调整。这个方案我还在实验中初步结果是在遮挡场景下AP涨了1.2但正常场景下提升不明显。如果你的应用场景遮挡严重值得一试。6.3 时序信息的利用单帧姿态估计有个天然缺陷它不知道上一帧的姿态是什么。如果你处理的是视频流可以把连续几帧堆叠起来作为输入或者用轻量的LSTM/GRU在heatmap层面做时序融合。这样能显著改善遮挡和运动模糊场景下的表现。具体实现上我建议在heatmap解码之后做时序融合而不是在网络中间做。因为网络中间的特征图太大时序融合的显存开销吃不消。在heatmap层面做每帧的heatmap只有17个通道COCO关键点数堆叠4帧也才68个通道计算量很小。融合方式用1D卷积就行kernel size设3沿时间维度卷积。实测下来时序融合能把遮挡场景的AP提升2到3个点而且几乎不增加延迟1D卷积的计算量可以忽略。这个技巧在健身App里特别实用因为健身动作通常有周期性时序信息很有价值。6.4 量化感知训练如果你确定要上INT8量化不要直接做训练后量化精度掉得厉害。用量化感知训练QAT在训练阶段就模拟量化的舍入误差让网络提前适应。PyTorch提供了torch.quantization模块用起来还算方便。QAT的关键是哪些层量化、哪些层不量化。我的经验是MBConv的深度可分离卷积层可以量化但SE模块的全局池化和全连接层不要量化因为它们的数值范围太窄量化后信息损失严重。另外最后的1×1输出卷积也不要量化它直接决定heatmap的峰值位置对精度影响最大。QAT训练大概需要额外20个epoch学习率设成正常训练的十分之一。训练完之后INT8模型的AP能恢复到FP32的98%左右延迟比FP16再降30%。