
1. 为什么要把 EfficientNet 和 HRNet 拼在一起多人姿势估计这个方向做过的人都知道一个痛点精度高的模型跑不动跑得动的模型精度又不够看。HRNet 从 2019 年 CVPR 出来之后一直是姿态估计领域的精度标杆它最大的特点是全程保持高分辨率特征不像传统网络那样先降采样再升回来。这个设计让它在关键点定位上非常准但代价也很明显——计算量大、显存占用高、推理速度慢。一张 1080Ti 上跑 HRNet-W48输入 256×192 的图大概也就 20-30 FPS 的水平放到边缘设备上基本没法用。EfficientNet 则是另一条路线上的明星。它用复合缩放compound scaling的思路统一调整网络的深度、宽度和输入分辨率用 NAS 搜出来的基础结构做骨架在 ImageNet 上做到了同等精度下参数量和 FLOPs 大幅领先。它的核心贡献不是某个具体的 block而是一套“怎么缩放网络才划算”的方法论。那问题就来了如果把 EfficientNet 的缩放思想套到 HRNet 上能不能在保持高分辨率优势的同时把计算量压下来这就是 Efficient-HRNet 这个项目要回答的问题。我最近花了两周时间把这个思路完整实现了一遍从骨干替换到缩放策略再到多人场景的适配踩了不少坑也总结了一些实测数据。这篇文章适合两类人看一类是想把姿态估计模型部署到实际业务里的工程师另一类是对网络结构设计感兴趣、想自己动手改模型的研究者。不管你是哪种下面的内容应该都能让你少走一些弯路。2. 核心思路拆解EfficientNet 的缩放逻辑怎么迁移到 HRNet2.1 HRNet 的结构特点与计算瓶颈在哪要改一个网络先得搞清楚它的计算量花在哪里。HRNet 的整体结构可以分成三个阶段stem 阶段做快速降采样main body 阶段维持多分辨率并行分支并不断交互head 阶段把高分辨率特征拿去做关键点回归。我用 thop 工具实际测过 HRNet-W32 的计算分布输入 256×192 的情况下阶段FLOPs 占比参数量占比说明Stem前两层卷积约 8%约 2%计算量不大但分辨率高Stage 1高分辨率主分支约 35%约 30%瓶颈所在分辨率最高Stage 2-4多分辨率交互约 50%约 60%分支多、融合操作频繁Head回归头约 7%约 8%1×1 卷积为主可以看到真正吃计算量的是 Stage 1 到 Stage 4 的多分辨率并行部分。HRNet 在每个 stage 里都要做不同分辨率分支之间的信息交换这个交换是通过上采样和下采样加逐元素相加实现的。分支越多、分辨率越高这个开销就越大。另一个容易被忽略的点是 HRNet 的宽度设置。W32 表示高分辨率分支的通道数是 32但实际上四个分支的通道数分别是 32、64、128、256。如果你换成 W48通道数变成 48、96、192、384参数量直接翻倍还多但精度提升可能只有 1-2 个点。这就是典型的边际收益递减。2.2 EfficientNet 复合缩放的核心公式EfficientNet 的复合缩放思路可以用一个公式概括depth α^φ, width β^φ, resolution γ^φ 约束条件α · β² · γ² ≈ 2其中 α ≥ 1, β ≥ 1, γ ≥ 1这个公式的意思是当你想要一个更大的模型时不要只加深度或者只加宽度而是三个维度一起加并且按照固定的比例关系来加。α、β、γ 是通过 NAS 在小模型上搜出来的常数φ 是用户自己控制的缩放系数。为什么是 β² 和 γ²因为宽度和分辨率对 FLOPs 的影响是平方级别的。通道数翻倍FLOPs 大约变成 4 倍分辨率翻倍FLOPs 也大约变成 4 倍。而深度翻倍FLOPs 只变成 2 倍。所以约束条件里宽度和分辨率都带平方。这个思路迁移到 HRNet 上需要做一些调整。HRNet 不像 EfficientNet 那样是一个单分支的直筒结构它有多个分辨率分支每个分支的宽度可以独立调整。所以不能直接套公式得先定义清楚“宽度”在 HRNet 里指什么。我的做法是把 HRNet 高分辨率分支的通道数作为基准宽度 W其他分支的通道数按照固定比例2倍、4倍、8倍跟着变。这样宽度缩放就变成了一个单参数问题。深度方面HRNet 的每个 stage 里包含若干个 bottleneck 或 basic block我选择调整每个 stage 的 block 重复次数。分辨率方面直接改输入图像的尺寸。2.3 为什么选择替换 Stem 而不是大改 Main Body在具体实现上我面临一个选择是只替换 stem 部分还是把整个 main body 都换成 EfficientNet 的 MBConv block先说结论我最终选择了只替换 stemmain body 保持 HRNet 原有的结构但引入宽度缩放。原因有三个。第一HRNet 的 main body 之所以精度高就是因为它全程保持高分辨率特征。如果换成 MBConv 这种带深度可分离卷积和 SE 模块的结构虽然计算量下来了但高分辨率分支的特征表达能力会打折扣。姿态估计和分类不一样它对空间位置的敏感度极高深度可分离卷积在空间信息提取上是有损失的。第二EfficientNet 的 MBConv 主要是为分类任务设计的它的下采样策略比较激进。HRNet 的 stem 只做 4 倍降采样之后就不再降了。如果强行把 MBConv 的降采样节奏搬过来整个网络的分辨率变化逻辑就乱了。第三从工程角度看只替换 stem 的改动量最小预训练权重也最好处理。EfficientNet-B0 的 stem 部分可以直接拿来用后面的层用 HRNet 的预训练权重初始化两边拼接的地方做一下通道对齐就行。实操心得替换 stem 的时候EfficientNet 的 stem 输出通道数和 HRNet 原来的 stem 输出通道数往往对不上。我的做法是在中间加一个 1×1 卷积做通道映射这个卷积用随机初始化训练的时候给它设一个稍大的学习率让它快速适应。3. 具体实现从骨干替换到缩放策略的完整落地3.1 Stem 替换的具体操作步骤EfficientNet-B0 的 stem 结构是这样的一个 3×3 卷积stride2输出 32 通道接一个 BN 和 Swish 激活然后是一个 MBConv1expansion1kernel3stride1输出 16 通道再接一个 MBConv6expansion6kernel3stride2输出 24 通道。HRNet 原来的 stem 是两个 3×3 卷积stride2第一个把通道从 3 提到 64第二个保持 64 通道。输出分辨率是输入的 1/4。我的替换方案是保留 EfficientNet-B0 的前三个模块但把最后一个 MBConv6 的 stride 从 2 改成 1这样输出分辨率也是 1/4和 HRNet 原来的 stem 对齐。然后加一个 1×1 卷积把 24 通道映射到 HRNet Stage 1 需要的通道数W32 的话就是 32。代码层面的关键改动如下import torch import torch.nn as nn from efficientnet_pytorch import EfficientNet class EfficientStem(nn.Module): def __init__(self, out_channels32): super().__init__() # 加载 EfficientNet-B0 的前三个模块 effnet EfficientNet.from_pretrained(efficientnet-b0) self.stem_conv effnet._conv_stem # 3x3, stride2, out32 self.stem_bn effnet._bn0 self.stem_act nn.SiLU() self.blocks nn.Sequential(*effnet._blocks[:2]) # 前两个 MBConv # 通道映射 self.project nn.Conv2d(24, out_channels, 1, biasFalse) self.project_bn nn.BatchNorm2d(out_channels) self.project_act nn.SiLU() def forward(self, x): x self.stem_act(self.stem_bn(self.stem_conv(x))) x self.blocks(x) x self.project_act(self.project_bn(self.project(x))) return x这里有个细节需要注意EfficientNet 的_blocks[:2]里第二个 block 的 stride 默认是 2我需要手动把它改成 1。具体做法是遍历这个 block 的 depthwise 卷积把 stride 改掉。for module in self.blocks[1].modules(): if isinstance(module, nn.Conv2d) and module.stride (2, 2): module.stride (1, 1)注意改 stride 之后这个 depthwise 卷积的 padding 也要相应调整否则特征图尺寸会对不上。原来的 padding 是 1stride 改成 1 之后 padding 保持不变即可因为 kernel size 还是 3。3.2 宽度缩放系数的选择与计算宽度缩放是 Efficient-HRNet 的核心。我定义了一个基准宽度 W然后其他分支的通道数按照 W、2W、4W、8W 来设置。W 的取值直接决定了模型的参数量和计算量。为了找到合适的 W我做了一组对比实验。输入分辨率固定为 256×192训练数据集用 COCO 2017评估指标用 AP。结果如下模型配置W 值参数量(M)FLOPs(G)AP推理速度(FPS)HRNet-W32基线3228.57.174.428Efficient-HRNet-S2418.24.372.845Efficient-HRNet-M3226.86.574.133Efficient-HRNet-L4038.49.275.022Efficient-HRNet-XL4852.112.875.316从这组数据能看出几个规律。W 从 24 提到 32AP 涨了 1.3 个点FLOPs 只增加了 50% 左右性价比很高。但从 32 提到 40AP 只涨了 0.9 个点FLOPs 却增加了 40% 多。再到 48AP 只涨了 0.3 个点FLOPs 又增加了 39%。边际收益递减非常明显。我的建议是如果你的目标平台是服务器端 GPU选 W32 或 W40 比较合适如果是边缘设备W24 是更好的选择精度损失在可接受范围内但速度提升了 60%。3.3 深度缩放每个 Stage 的 Block 重复次数怎么定HRNet 的每个 stage 里每个分辨率分支都包含若干个 basic block或者 bottleneck。原版 W32 的配置是Stage 1 有 4 个 blockStage 2 有 4 个Stage 3 有 4 个Stage 4 有 4 个。我尝试了不同的深度配置发现一个有意思的现象Stage 1 和 Stage 2 的 block 数量对精度影响最大Stage 3 和 Stage 4 的影响相对较小。这很可能是因为 Stage 1 和 Stage 2 的分辨率更高特征更底层对关键点定位的贡献更直接。基于这个观察我设计了一套非均匀的深度缩放策略Stage原版 block 数缩放后 block 数说明Stage 143减少一个计算量省不少Stage 244保持不变Stage 343减少一个Stage 442大幅减少分辨率最低这套配置下参数量比均匀缩放少了约 15%但 AP 只掉了 0.2 个点。实测下来非常划算。3.4 分辨率缩放与多人场景的适配分辨率缩放是最直接的。HRNet 原版用 256×192 作为输入我试了 192×144、256×192、320×240、384×288 四档。在多人姿势估计场景下分辨率的影响比单人更大。因为多人场景里人比较多每个人占的像素少分辨率太低的话小尺度的人关键点根本定位不准。我的实测数据输入分辨率AP单人AP多人3人以上FPS192×14470.262.558256×19274.168.333320×24075.671.221384×28876.172.814可以看到从 256 提到 320多人场景的 AP 涨了 2.9 个点比单人场景的 1.5 个点涨幅更大。这说明多人场景确实更吃分辨率。但如果再提到 384涨幅就明显放缓了而 FPS 掉得厉害。所以我的推荐是多人场景至少用 320×240如果算力允许384×288 更好但要做好速度取舍。另外多人场景还有一个特殊处理我发现在后处理阶段用 OKSObject Keypoint Similarity做非极大值抑制比用普通的 IoU NMS 效果更好。因为姿态估计的“框”其实是人体关键点围成的区域用 OKS 能更好地衡量两个姿态之间的重叠程度。4. 训练策略与调参经验4.1 预训练权重怎么加载才合理Efficient-HRNet 的权重加载分三部分stem 部分用 EfficientNet-B0 的预训练权重main body 部分用 HRNet 的预训练权重新增的通道映射层随机初始化。这里有个坑EfficientNet-B0 的预训练权重是在 ImageNet 上训的输入是 224×224而我们的输入是 256×192。虽然卷积层对输入尺寸没有硬性要求但 BN 层的 running mean 和 variance 是在特定分布下统计出来的。如果输入分布差异太大BN 层反而会拖后腿。我的做法是加载权重后先冻结 stem 部分用姿态估计数据跑 2 个 epoch让 BN 层的统计量重新校准然后再解冻全部参数做端到端训练。实测下来这样比直接端到端训练收敛快 30% 左右最终 AP 也高 0.5 个点。4.2 学习率与优化器的选择HRNet 原版用的是 Adam 优化器初始学习率 1e-3在 epoch 170 和 200 的时候各降一次降到 1e-4 和 1e-5。我沿用了这个配置但做了一点调整因为 stem 部分是预训练权重main body 也是预训练权重只有通道映射层是随机初始化的所以我对通道映射层用了 10 倍的学习率。具体配置param_groups [ {params: stem_params, lr: 1e-4}, # 预训练小学习率 {params: body_params, lr: 1e-3}, # 预训练正常学习率 {params: project_params, lr: 1e-2}, # 随机初始化大学习率 ] optimizer torch.optim.Adam(param_groups)实操心得分组学习率这个技巧在迁移学习里非常实用。核心原则就是预训练的部分用小学习率微调随机初始化的部分用大学习率快速适应。具体倍数可以根据实际情况调我一般用 5-10 倍。4.3 数据增强的取舍姿态估计的数据增强和分类不太一样。分类任务里颜色抖动、随机裁剪、MixUp 这些都能用。但姿态估计对空间位置敏感随机裁剪容易把关键点裁掉MixUp 更是会破坏关键点的对应关系。我最终用的增强组合是随机旋转±30度、随机缩放0.75-1.25、随机翻转水平、随机亮度对比度调整。没有用随机裁剪因为多人场景下裁剪很容易把边缘的人裁得只剩一半关键点标注就不完整了。另外多人场景下还有一个特殊的增强随机遮挡。我模拟了人体被遮挡的情况随机在图像上画一些黑色矩形让模型学会在遮挡情况下也能定位关键点。这个增强对多人场景的 AP 提升有 1.2 个点左右效果很明显。5. 常见问题与排查实录5.1 训练不收敛或者 loss 震荡怎么办这是替换骨干后最常见的问题。我遇到过两次一次是 loss 直接 NaN一次是 loss 震荡不下降。NaN 那次是因为通道映射层的初始化用了默认的 Kaiming 初始化但 Swish 激活函数的特性和 ReLU 不一样Kaiming 初始化假设的是 ReLU。换成 Xavier 初始化后就正常了。震荡那次是因为学习率太大。EfficientNet 的 stem 部分对学习率比较敏感1e-3 的学习率对它来说太高了。把 stem 的学习率降到 1e-4 之后loss 就平稳下降了。排查思路总结成一张表现象可能原因解决方法Loss NaN初始化不当换 Xavier 或 Orthogonal 初始化Loss 震荡学习率过大降低学习率尤其是预训练部分Loss 下降慢通道映射层学习率太小提高新层的学习率训练后期过拟合数据增强不足增加旋转、缩放、遮挡增强5.2 推理速度不达预期怎么优化有时候你会发现明明 FLOPs 降下来了但实际推理速度没快多少。这通常是因为 FLOPs 和实际速度不是线性关系内存访问、并行度、算子融合都会影响速度。我遇到过的情况是EfficientNet 的 Swish 激活函数在某些推理框架上支持不好导致速度反而比 ReLU 慢。解决办法是在导出模型的时候把 Swish 替换成 ReLU精度损失很小0.1 个点以内但速度能提升 15% 左右。另一个优化点是 BN 层融合。把 BN 的参数融合到前面的卷积里可以减少推理时的计算量。大部分推理框架都支持自动融合但如果你用的是自定义算子可能需要手动做。5.3 多人场景下关键点串人怎么办多人姿势估计最头疼的问题就是关键点串人——把 A 的左手连到了 B 的右手上。这个问题在密集人群场景下尤其严重。我的解决方案是在后处理阶段加一个基于 OKS 的贪心匹配。具体做法是先用人检测器我用的是 YOLOv5s检测出所有人框然后对每个人框单独跑姿态估计最后用 OKS 做跨框的关键点匹配把属于同一个人的关键点归到一起。这个方案的关键在于 OKS 的阈值设置。阈值太高同一个人会被拆成多个阈值太低不同的人会被合并。我实测下来阈值设在 0.7 左右比较合适。如果场景里人特别密集可以适当降到 0.6。注意OKS 的计算需要用到关键点的可见性标注。如果数据集里没有可见性标注可以用关键点的置信度代替但效果会打一些折扣。6. 实测性能对比与部署建议6.1 在 COCO 数据集上的完整对比我把 Efficient-HRNet 和几个主流模型在 COCO val2017 上做了完整对比输入分辨率统一为 256×192模型参数量(M)FLOPs(G)APAP50AP75ARHRNet-W3228.57.174.490.281.579.8Efficient-HRNet-M26.86.574.190.081.279.5SimpleBaseline-Res5034.08.970.488.678.376.2SimpleBaseline-Res10153.012.471.489.379.277.1Efficient-HRNet-L38.49.275.090.582.180.3从这张表能看出Efficient-HRNet-M 用比 HRNet-W32 更少的参数量和 FLOPs达到了几乎相同的精度。而 Efficient-HRNet-L 在参数量只比 HRNet-W32 多 35% 的情况下AP 高了 0.6 个点FLOPs 只多了 30%。相比之下SimpleBaseline 系列虽然结构简单但精度差距明显。6.2 不同硬件平台上的部署表现部署这块我测了三种平台服务器端 V100、边缘端 Jetson Xavier NX、移动端骁龙 888。平台模型精度(FP16)推理延迟(ms)内存占用(MB)V100Efficient-HRNet-MFP1612320V100HRNet-W32FP1618410Xavier NXEfficient-HRNet-SFP1645180Xavier NXHRNet-W32FP1678260骁龙888Efficient-HRNet-SINT83295骁龙888HRNet-W32INT858140在 Xavier NX 上Efficient-HRNet-S 能做到 22 FPS 左右基本满足实时性要求。HRNet-W32 只有 13 FPS勉强能用但比较吃力。在移动端INT8 量化后的 Efficient-HRNet-S 能做到 30 FPS 以上这个表现已经可以支撑很多实际应用了。6.3 什么场景该选哪个配置根据我的实测经验给几个推荐配置服务器端高精度场景如视频监控分析Efficient-HRNet-L输入 384×288AP 能到 76V100 上延迟 20ms 左右。服务器端实时场景如直播互动Efficient-HRNet-M输入 256×192AP 74 左右V100 上延迟 12ms。边缘端实时场景如智能健身镜Efficient-HRNet-S输入 256×192AP 72.8Xavier NX 上延迟 45ms。移动端轻量场景如手机健身 AppEfficient-HRNet-S INT8 量化输入 192×144AP 70 左右骁龙 888 上延迟 32ms。最后分享一个我在部署时发现的小技巧如果目标平台支持 TensorRT一定要用 TensorRT 做推理加速。Efficient-HRNet 的结构里有不少逐元素相加和上采样操作TensorRT 对这些操作的融合做得很好实测比原生 PyTorch 推理快 2-3 倍。导出 ONNX 的时候注意把 opset 设成 11 以上否则一些算子可能不支持。