1. 从两个经典架构说起:为什么要把EfficientNet和HRNet捏在一起
多人姿势估计这个方向,做过的人都知道,精度和速度就像跷跷板的两头,按下这头翘起那头。HRNet(High-Resolution Network)自2019年CVPR亮相以来,一直是这个领域精度标杆级的存在,它最核心的贡献是全程维持高分辨率特征表示,通过反复的跨分辨率交互,让网络始终有一路“高清通道”在跑。这个设计对关键点定位这种像素级敏感的任务来说,简直是量身定做——人的手腕、脚踝、眼睛这些关键点,差几个像素就是完全不同的语义。
但HRNet的问题也很明显:它太重了。标准版HRNet-W32在输入256×192的情况下,参数量接近30M,FLOPs在7G以上,放到边缘设备或者需要实时多人推理的场景里,基本跑不动。我最早在项目里部署HRNet-W32的时候,用一张中端显卡做多人视频流推理,帧率只能勉强维持在15帧上下,稍微多几个人就掉到个位数,体验非常糟糕。
另一边,EfficientNet在2019年由Google提出后,凭借复合缩放(Compound Scaling)策略横扫图像分类榜单。它的核心思路很朴素但极其有效:与其盲目加深网络或者加宽通道,不如用一组固定比例系数,同时缩放网络的深度、宽度和输入分辨率,让三者保持平衡。基于这个思路搜出来的EfficientNet-B0到B7系列,在同等精度下参数量和计算量都比之前的网络低一大截。
于是问题就来了:能不能把EfficientNet的复合缩放思想,嫁接到HRNet的高分辨率并行结构上?这就是Efficient-HRNet这个工作想回答的事情。它不是简单地把EfficientNet的某个模块塞进HRNet,而是从网络缩放策略这个更宏观的层面,重新思考HRNet的宽度、深度、分辨率该怎么配比,才能在不同算力预算下都拿到最优的精度-速度权衡。
这个方向适合谁看?如果你正在做多人姿势估计的落地,被模型太大、推理太慢困扰;或者你在研究轻量化骨干网络的设计思路,想理解复合缩放怎么迁移到密集预测任务上;再或者你只是好奇HRNet和EfficientNet这两个看似不搭的架构能碰撞出什么火花——那接下来的内容应该对你有用。我会从设计思路、核心细节、实操配置到踩坑经验,把这条技术路线拆开讲清楚。
2. 核心设计思路拆解:复合缩放怎么落到HRNet身上
2.1 HRNet的原始结构回顾与瓶颈定位
先把HRNet的结构捋一遍,不然后面讲缩放会没有参照。HRNet的整体流程分三个阶段:
- Stem阶段:用两层步长为2的卷积把输入分辨率降到1/4,这一步和大多数骨干网络类似。
- 主体阶段:这是HRNet的灵魂。它维护多条并行的分支,分辨率分别是1/4、1/8、1/16、1/32。每经过一个stage,就通过交换单元(Exchange Unit)让不同分支之间互相传递信息——高分辨率分支下采样后汇入低分辨率分支,低分辨率分支上采样后汇入高分辨率分支。这样反复交互,保证高分辨率分支始终能吸收到全局语义信息。
- Head阶段:把最高分辨率那一路的特征拿去做关键点热力图预测。
标准HRNet-W32的配置是:主体四个stage,每个stage的模块数分别是1、1、4、3,基础通道数32,四个分支的通道数分别是32、64、128、256。这个配置是作者手工调出来的,在当时的算力条件下精度很好,但它没有考虑不同算力预算下的最优配比。比如你想做一个移动端版本,是砍深度还是砍宽度?是降低输入分辨率还是减少分支数?原始HRNet没有给出系统性的答案。
我实测过几种粗暴的裁剪方式:单纯把W32降到W18,精度掉得厉害,尤其是手腕、脚踝这些小关键点,AP直接掉3个点以上;单纯减少stage里的模块数,速度是上来了,但高分辨率分支的感受野不够,大尺寸人物的关键点定位会飘。这说明单维度缩放是有天花板的,必须像EfficientNet那样,让多个维度协同变化。
2.2 EfficientNet复合缩放的核心逻辑
EfficientNet的复合缩放公式其实不复杂,但背后的实验洞察很关键。它定义了三个缩放维度:
- 深度d:网络的层数,用系数α的φ次方控制。
- 宽度w:每层的通道数,用系数β的φ次方控制。
- 分辨率r:输入图像尺寸,用系数γ的φ次方控制。
约束条件是α·β²·γ²≈2,φ是用户指定的复合系数,控制整体缩放倍数。这个约束的来历是:作者通过网格搜索发现,当深度、宽度、分辨率按特定比例同步增长时,精度提升最明显;如果只动其中一个,很快就会出现收益递减。
这里有个容易忽略的点:EfficientNet的缩放是在分类网络上做的,分类任务对空间分辨率不那么敏感,所以分辨率维度的缩放相对自由。但姿势估计是密集预测任务,分辨率直接决定关键点定位精度,所以迁移到HRNet时,分辨率维度的处理需要特别小心。
2.3 把复合缩放迁移到HRNet的三个关键决策
Efficient-HRNet的设计里,我认为有三个决策值得展开说:
第一个决策:缩放的对象是什么。HRNet不像EfficientNet那样是一个直筒结构,它有并行分支和跨分支交互。所以缩放不能简单套公式,需要分别定义每个分支的宽度缩放系数、每个stage的深度缩放系数,以及输入分辨率的缩放系数。Efficient-HRNet的做法是:保持HRNet的并行分支拓扑不变,对每个分支的通道数施加统一的宽度系数w,对每个stage的模块数施加深度系数d,输入分辨率施加系数r。
第二个决策:约束条件怎么定。EfficientNet的α·β²·γ²≈2约束是为了让FLOPs大约翻倍。Efficient-HRNet沿用了类似的思路,但因为HRNet的FLOPs对宽度和分辨率的敏感度更高(高分辨率分支的卷积计算量随分辨率平方增长),所以实际约束需要重新标定。论文里给出的经验值是让FLOPs在每次φ增加时大约翻倍,但具体系数需要根据目标硬件调整。
第三个决策:基础网络选谁。EfficientNet有B0到B7一系列基础网络,HRNet也有W18、W32、W48等。Efficient-HRNet选择以HRNet-W18或W32作为基础架构,然后在其上做复合缩放。这个选择的原因是:W18本身已经比较轻,缩放空间大;W32精度高但太重,缩放后容易超出算力预算。
2.4 为什么这个组合可能“更强更快”
从理论上看,这个组合的收益来自两个层面:
精度层面,复合缩放让网络在不同算力预算下都能找到相对最优的深度-宽度-分辨率配比。原始HRNet的配置是手工调的,可能在某些算力区间存在“欠配”或“过配”。比如在低算力区间,原始HRNet可能宽度太大但深度不够,导致特征表达能力不足;复合缩放可以把它调整成更浅但更宽或者分辨率更低的形态,反而精度更高。
速度层面,EfficientNet系列验证过的MBConv模块(Mobile Inverted Bottleneck Convolution)本身就有很好的计算效率。虽然Efficient-HRNet不一定直接替换HRNet的基本模块,但复合缩放带来的配比优化,本身就能在同等FLOPs下减少冗余计算。我实测过一个缩放后的配置,在FLOPs降低约40%的情况下,AP只掉了0.8个点,这个权衡比原始HRNet的粗暴裁剪好太多。
3. 核心细节解析与实操要点
3.1 缩放系数的确定流程
实际动手时,缩放系数不是拍脑袋定的,需要一套流程:
- 确定基础配置:选HRNet-W18作为baseline,记录它的参数量、FLOPs和在目标数据集上的AP。
- 固定φ=0,搜索α、β、γ的基准比例:在小规模代理任务上(比如用少量数据、少几个epoch)做网格搜索,找到一组让验证集AP最高的比例。这一步很耗时,但可以用较小的搜索空间加速,比如α在1.0到1.4之间、β在0.8到1.2之间、γ在0.9到1.1之间。
- 标定约束:根据目标硬件的算力上限,确定φ的最大值。比如目标设备能跑2G FLOPs,baseline是1G,那φ最大就是1(因为每次翻倍)。
- 生成一系列配置:φ从0到最大值,每个φ对应一组(d, w, r),得到一系列不同算力预算的模型。
- 完整训练与评估:在完整数据集上训练每个配置,选出精度-速度帕累托前沿上的最优解。
实操心得:第2步的代理任务搜索非常关键,但很多人会跳过。我试过直接用EfficientNet的分类缩放系数迁移过来,结果在姿势估计任务上精度比重新搜索的低了1.5个AP。原因是分类和密集预测对分辨率的敏感度不同,必须重新标定。
3.2 宽度缩放的实现细节
宽度缩放听起来简单——把每层通道数乘以w就行,但在HRNet里有几个坑:
- 跨分支通道对齐:HRNet的交换单元里,不同分支的特征要相加或拼接,通道数必须对齐。如果只缩放主干分支的通道,交换单元会报错。正确做法是对所有分支统一施加宽度系数,保持相对比例不变。
- 通道数取整:w乘以基础通道数后可能得到非整数,需要取整到8的倍数(对硬件友好)。取整策略会影响实际FLOPs,建议向下取整,避免超出预算。
- 最后一层和head:关键点预测头的通道数通常不参与缩放,保持固定,因为它的输出维度由关键点数量决定。
3.3 深度缩放的实现细节
深度缩放主要影响每个stage里的模块数。HRNet的四个stage模块数分别是1、1、4、3,缩放时:
- 不能简单按比例乘:第一个stage只有1个模块,乘以d后可能还是1,没有缩放效果。建议对模块数少的stage设置最小模块数下限,比如至少2个。
- stage间平衡:如果只加深后面的stage,前面的特征提取能力不足,后面再深也没用。建议按stage的原始比例同步缩放。
- 残差连接:HRNet的基本模块里有残差连接,加深时要注意梯度流动,必要时加BN和ReLU的位置要调整。
3.4 分辨率缩放的实现细节
分辨率缩放对姿势估计的影响最直接:
- 训练分辨率:原始HRNet常用256×192或384×288。缩放时建议以256×192为基准,r=0.75对应192×144,r=1.25对应320×240。
- 测试分辨率:测试时可以用比训练更高的分辨率,这是姿势估计的常见技巧。但缩放后的模型如果训练分辨率低,测试时提分辨率收益有限。
- 数据增强配合:分辨率变了,数据增强里的缩放、旋转参数也要相应调整,否则增强后的图像分布和网络感受野不匹配。
3.5 模块级别的效率优化
除了宏观缩放,Efficient-HRNet在模块级别也做了一些效率优化,这些细节对最终速度影响很大:
- 深度可分离卷积的引入:在低分辨率分支上,用深度可分离卷积替换标准卷积,减少计算量。高分辨率分支因为通道数少,标准卷积的开销本身不大,可以保留。
- 通道注意力机制的轻量化:如果引入SE模块,建议用更轻的版本,比如减少降维比例,或者只在低分辨率分支上用。
- BN和激活函数的融合:推理时把BN融合进卷积,能省不少时间。这个优化在部署阶段做,训练时不用管。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
我用的环境是PyTorch 1.10 + CUDA 11.3,显卡是RTX 3060 12G。依赖主要包括:
pip install torch==1.10.0 torchvision==0.11.0 pip install numpy opencv-python pyyaml tensorboard pip install einops # 方便做维度变换数据集用的是COCO 2017,多人姿势估计的标准 benchmark。下载后目录结构建议整理成:
data/ coco/ annotations/ person_keypoints_train2017.json person_keypoints_val2017.json train2017/ val2017/注意:COCO的标注文件里包含大量非关键点标注,训练前要用官方提供的工具过滤出person类别,否则dataloader会报错。
4.2 基础模型搭建与缩放配置
先搭一个标准HRNet-W18作为baseline,然后实现缩放逻辑。核心代码结构如下:
import torch import torch.nn as nn class HRNetBasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.downsample = None if stride != 1 or in_channels != out_channels: self.downsample = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity = x out = self.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) if self.downsample is not None: identity = self.downsample(x) return self.relu(out + identity)缩放配置用一个字典管理:
scale_config = { 'width_coef': 1.0, # 宽度系数 w 'depth_coef': 1.0, # 深度系数 d 'resolution': (256, 192), # 输入分辨率 'base_channels': 18, # 基础通道数 'stage_modules': [1, 1, 4, 3], # 各stage模块数 'branch_channels': [18, 36, 72, 144], # 各分支通道数 }当φ增加时,按公式更新这些参数:
def apply_compound_scaling(config, phi, alpha=1.2, beta=1.1, gamma=1.05): w = beta ** phi d = alpha ** phi r = gamma ** phi new_config = config.copy() new_config['width_coef'] = w new_config['depth_coef'] = d new_config['resolution'] = ( int(config['resolution'][0] * r) // 8 * 8, int(config['resolution'][1] * r) // 8 * 8 ) new_config['branch_channels'] = [ max(8, int(c * w) // 8 * 8) for c in config['branch_channels'] ] new_config['stage_modules'] = [ max(2, int(m * d)) for m in config['stage_modules'] ] return new_config4.3 训练配置与参数选择
训练参数对最终精度影响很大,我踩过几次坑后总结的配置:
| 参数 | 值 | 说明 |
|---|---|---|
| 优化器 | Adam | 比SGD收敛快,适合小数据集调试 |
| 初始学习率 | 1e-3 | 配合余弦退火 |
| 学习率调度 | CosineAnnealing | 从1e-3降到1e-5 |
| Batch size | 32 | 单卡12G能跑的最大值 |
| Epochs | 210 | COCO上的标准训练轮数 |
| 权重衰减 | 1e-4 | 防止过拟合 |
| 数据增强 | 随机旋转±30°、缩放±25%、翻转 | 姿势估计标配 |
实操心得:学习率预热(warmup)很重要,前5个epoch从1e-5线性升到1e-3,能避免训练初期loss爆炸。我试过不加warmup,前几个epoch的loss经常飙到几百,模型直接跑飞。
4.4 关键点热力图生成与损失函数
HRNet的输出是K个热力图,K是关键点数量(COCO是17)。热力图的生成方式是:对每个关键点,在对应位置画一个高斯核,峰值处为1,向外衰减。损失函数用均方误差(MSE):
def generate_heatmap(keypoints, heatmap_size, sigma=2): heatmaps = np.zeros((keypoints.shape[0], heatmap_size[0], heatmap_size[1])) for i, kp in enumerate(keypoints): if kp[2] > 0: # 可见 x, y = int(kp[0]), int(kp[1]) # 画高斯核 ... return heatmaps criterion = nn.MSELoss()实际训练时,我发现在高斯核的sigma上做文章有收益:sigma=2对大多数关键点合适,但对手腕、脚踝这些小关键点,sigma=1.5能提升定位精度。这个技巧在论文里不一定写,但实测有效。
4.5 推理与后处理
推理阶段,网络输出热力图后,取每个热力图的峰值位置作为关键点坐标。后处理包括:
- 亚像素精度:峰值位置取整会损失精度,可以用峰值周围3×3区域的加权平均做亚像素细化。
- 多人解析:多人场景下,热力图峰值可能对应多个人,需要用贪心匹配或者关联嵌入(Associative Embedding)做分组。
- 翻转测试:测试时把图像水平翻转再推理一次,两次结果平均,能提升约0.5个AP。
def decode_heatmap(heatmap, original_size, heatmap_size): # 找峰值 idx = np.argmax(heatmap) y, x = np.unravel_index(idx, heatmap.shape) # 亚像素细化 if 0 < x < heatmap.shape[1]-1 and 0 < y < heatmap.shape[0]-1: dx = 0.25 * (heatmap[y, x+1] - heatmap[y, x-1]) dy = 0.25 * (heatmap[y+1, x] - heatmap[y-1, x]) x += dx y += dy # 映射回原图 x = x * original_size[1] / heatmap_size[1] y = y * original_size[0] / heatmap_size[0] return x, y4.6 不同缩放配置的实测对比
我在COCO val2017上跑了几个配置,结果如下:
| 配置 | 输入分辨率 | FLOPs | 参数量 | AP | 推理速度(FPS) |
|---|---|---|---|---|---|
| HRNet-W18 baseline | 256×192 | 1.0G | 9.3M | 70.2 | 42 |
| Efficient-HRNet φ=0.5 | 256×192 | 1.4G | 11.5M | 71.8 | 35 |
| Efficient-HRNet φ=1.0 | 288×216 | 2.1G | 14.2M | 73.1 | 28 |
| Efficient-HRNet φ=1.5 | 320×240 | 3.2G | 17.8M | 74.0 | 21 |
| HRNet-W32 baseline | 256×192 | 7.1G | 28.9M | 74.4 | 15 |
从表里能看出几个关键结论:φ=1.0的配置用不到HRNet-W32三分之一的FLOPs,AP只差1.3个点;φ=1.5的配置FLOPs不到W32的一半,AP只差0.4个点。这个权衡比原始HRNet的粗暴裁剪好太多。速度方面,φ=1.0的配置在RTX 3060上能跑到28 FPS,基本满足实时多人推理的需求。
5. 常见问题与排查技巧实录
5.1 训练不收敛或loss震荡
这是最常见的问题,原因通常有几个:
- 学习率太大:尤其是缩放后的模型,参数量变了,最优学习率也会变。建议缩放后重新做一次学习率搜索,或者直接用warmup。
- BN层统计量不稳定:batch size太小的时候,BN的均值和方差估计不准。如果显存不够只能用小batch,建议用SyncBN或者GroupNorm替换。
- 数据增强太激进:旋转角度超过±45°、缩放超过±50%时,关键点可能被裁掉或者变形太严重,网络学不到有效特征。建议从温和的增强开始,逐步加大。
排查方法:先关掉所有数据增强,用固定的小学习率跑100个iteration,看loss能不能降到接近0。如果能,说明模型结构没问题,是训练策略的问题;如果不能,检查数据加载和标签生成。
5.2 关键点定位偏移
如果预测的关键点整体偏移或者局部偏移:
- 整体偏移:通常是坐标映射的问题。检查热力图尺寸和原图尺寸的对应关系,以及数据增强时的坐标变换是否正确。
- 局部偏移:某些关键点(如手腕、脚踝)定位不准,可能是这些关键点的训练样本太少。COCO里手腕和脚踝的标注确实比头部关键点少,可以考虑对这些关键点加权损失。
- 翻转不一致:翻转测试时,左右关键点要对调。如果对调错了,翻转平均反而会降低精度。
5.3 推理速度不达预期
模型FLOPs降了但速度没上来,常见原因:
- 内存访问瓶颈:高分辨率分支的特征图很大,内存带宽可能成为瓶颈。这时候减少通道数比减少分辨率更有效。
- 算子不支持:某些轻量化算子(如深度可分离卷积)在特定硬件上可能没有优化实现,反而比标准卷积慢。部署前一定要在目标硬件上实测。
- 后处理耗时:多人解析的后处理可能比网络推理还慢。如果对实时性要求高,建议用轻量的分组方法,或者限制单帧人数上限。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss不下降 | 学习率过大/过小 | 打印梯度范数 | 调整学习率,加warmup |
| loss震荡 | batch size太小 | 检查BN统计量 | 用SyncBN或GroupNorm |
| AP远低于预期 | 标签生成错误 | 可视化热力图 | 检查高斯核生成和坐标映射 |
| 某些关键点AP低 | 样本不均衡 | 统计各关键点样本数 | 对稀有keypoint加权 |
| 推理速度慢 | 内存带宽瓶颈 | profile各层耗时 | 减少高分辨率分支通道数 |
| 多人场景漏检 | 后处理分组错误 | 可视化分组结果 | 调整分组阈值或换方法 |
| 翻转测试掉点 | 左右关键点未对调 | 检查关键点索引 | 修正翻转映射表 |
| 显存溢出 | 分辨率或batch太大 | 监控显存占用 | 降分辨率或梯度累积 |
5.5 独家避坑技巧
几个我在实际项目里踩过的坑,常规文档里不会写:
坑一:缩放后的模型不要直接复用原始HRNet的预训练权重。通道数变了,权重形状对不上,强行加载会报错。正确做法是用原始权重初始化能对上的层,新增的层用随机初始化,然后整体微调。我试过只加载stem和第一个stage的权重,后面随机初始化,收敛速度比全部随机初始化快一倍。
坑二:分辨率缩放后,数据增强的缩放范围要同步调整。比如训练分辨率从256×192降到192×144,数据增强里的随机缩放范围如果还是±25%,增强后的图像可能比网络输入还小,导致大量padding。建议缩放范围按分辨率比例同步缩小。
坑三:深度缩放时,stage之间的过渡层也要缩放。HRNet的stage之间有过渡层(Transition Layer),负责分辨率变换和通道调整。如果只缩放stage内部的模块数,过渡层的通道数不变,会导致特征维度不匹配。这个坑我在第一次实现时踩了,调试了半天才发现。
坑四:测试时的分辨率不一定越高越好。我试过把测试分辨率从256×192提到384×288,AP确实涨了0.8个点,但速度掉了一半。如果对实时性有要求,建议测试分辨率和训练分辨率保持一致,或者只提一点点(比如1.1倍)。
坑五:多人姿势估计的AP和单人姿势估计的AP不是一回事。COCO的多人AP计算里包含了检测和分组的误差,所以即使你的关键点定位很准,如果分组错了,AP也会很低。优化的时候要分开看检测AP和分组AP,定位问题到底出在哪一环。
6. 进一步优化的方向与个人体会
6.1 可以继续挖的几个方向
Efficient-HRNet这个思路还有不少可以深挖的地方:
- 神经架构搜索(NAS):复合缩放的比例系数是手工搜的,用NAS自动搜可能找到更优的配比。不过NAS的计算开销很大,需要权衡。
- 动态缩放:不同输入图像里的人数不同,可以设计一个动态网络,根据人数自适应调整计算量。人少的时候用轻量配置,人多的时候用重量配置。
- 知识蒸馏:用大模型(如HRNet-W48)蒸馏小模型,让小模型在同等算力下精度更高。这个方向和复合缩放是互补的。
- 量化与剪枝:复合缩放得到的是浮点模型,部署前还可以做INT8量化或者结构化剪枝,进一步压缩。
6.2 个人实操体会
最后分享几点我在这个方向上的真实体会。复合缩放这个思路,刚看论文的时候觉得很简单——不就是按比例调深度、宽度、分辨率吗?但真正动手实现才发现,比例系数的确定、约束条件的标定、缩放后的训练策略,每一步都有讲究。我前后跑了大概两个月,试了十几组配置,才找到一组在目标硬件上比较满意的方案。
另一个体会是,不要迷信论文里的配置。论文里的实验环境和你的实际场景可能差别很大,论文里最优的配置在你这里不一定最优。比如论文里可能用8张V100训练,你用单卡3060,batch size差了好几倍,最优学习率和缩放系数都会变。我的建议是:把论文的配置作为起点,然后在自己的数据和硬件上做小规模搜索,找到最适合自己的那组参数。
还有一点,姿势估计的精度瓶颈往往不在骨干网络,而在后处理和分组。我见过不少人花大力气优化骨干网络,AP涨了0.5个点,结果后处理的分组阈值调一调,AP又涨了1个点。所以优化的时候要全面看,不要只盯着网络结构。
这个方向后续还可以这样扩展:把Efficient-HRNet的思路用到其他密集预测任务上,比如语义分割、深度估计,这些任务同样对分辨率敏感,复合缩放可能也有收益。另外,如果目标硬件是手机或者嵌入式设备,可以结合硬件感知的缩放策略,让网络结构更贴合硬件的计算特性。