目录
前言
本周学习了HRNet,看了论文,并根据模型图复现代码,以及复习机器学习,计算机网络准备考试。模型目前还没有复现成功,所以下面的代码的结构是根据源码进行的解析,等下周复现了会把自己的代码再粘贴上来。
文章
标题:Deep High-Resolution Representation Learning for Human Pose Estimation
创新点
从一个高分辨率的子网络开始作为第一个阶段,逐步增加高到低分辨率的子网络,形成更多的阶段,并并行连接多分辨率的子网络。我们进行重复的多尺度融合,使得每一个高到低分辨率的表示都可以从其他并行表示中反复接收信息,从而产生丰富的高分辨率表示
实现思路
1.连续多分辨率子网络
有的位姿估计网络是通过串联高分辨率子网来建立的,每个子网形成一个stage,由一系列卷积组成,并且在相邻的子网之间有一个下样本层来将分辨率减半
2.并行多分辨率子网
在第一个 stage 开始了一个高分辨率的网络分支,然后逐步增加高分辨率到低分辨率的子网路,形成一个新的 stages,并将多分辨率子网并行连接。因此,后一阶段并行子网的分辨率由前一阶段的分辨率和一个更低的分辨率组成,一个包含4个并行子网络的网络结构示例如下
3.重复的多尺度融合
引入了平行网络信息交换单元,比如每个子网络重复接受来自其他平行子网络的信息。下面是一个例子,展示了信息交换的方案。我们将第三 stage 分为几个(例如3个)交换模块,每个模块由3个并行卷积单元和一个跨并行单元的交换单元组成,其结构如下
卷积实现
模型图
损失
均方差
评价指标
其中,di是预测值与真实值的欧式距离,vi表示真值是否可见标志。s是目标缩放的比例,ki是一个控制衰减的每个关键点长度。
Training: 按照等比例,将人体检测结果扩展到高度:宽度 = 4 :3,然后剪裁到固定尺寸256×192或者384×288。
使用了 Adam 优化器,基础学习率设置为1e-3,在迭代170个 epochs 以及 200 个 epoch 进行10倍的学习率衰减。训练过程在210个epochs 内结束。
Testing: 使用2个阶段的方式 - 使用person检测器检测person实例,然后预测检测关键点。对于验证集和测试开发集,我们使SimpleBaseline2提供的person检测器。计算了原图,和水平反转图估算出来 heatmap 的平均值。每个关键点的位置,都是通过调整最高热值来进行判断的
代码结构
1.stem
对于输入的图片进行两个3x3卷积进行初始特征提取,使其分辨率下降到1/4
2.Layer1
Layer1模块,这里的Layer1其实和之前讲的ResNet中的Layer1类似,就是重复堆叠Bottleneck,注意这里的Layer1只会调整通道个数,并不会改变特征层大小
3.transation 不同层数分支创建
每通过一个Transition结构都会新增一个branch
def _make_transation(self):
# 不同层数分支进行创建
def _make_transition_layer(self, num_channels_pre_layer, num_channels_cur_layer):
"""
:param num_channels_pre_layer: 上一个stage平行网络的输出通道数目,为一个list,
stage=2时, num_channels_pre_layer=[256]
stage=3时, num_channels_pre_layer=[32,64]
stage=4时, num_channels_pre_layer=[32,64,128]
:param num_channels_cur_layer:
stage=2时, num_channels_cur_layer = [32,64]
stage=3时, num_channels_cur_layer = [32,64,128]
stage=4时, num_channels_cur_layer = [32,64,128,256]
"""
num_branches_cur = len(num_channels_cur_layer)
num_branches_pre = len(num_channels_pre_layer)
transition_layers = []
# 对stage的每个分支进行处理
# stage1的时候,num_channels_cur_layer为2,所以有两个循环,i=0、1
for i in range(num_branches_cur):
# 如果不为最后一个分支
if i < num_branches_pre:
# 如果当前层的输入通道和输出通道数不相等,则通过卷积对通道数进行变换
# 如果branches_cur通道数=branches_pre通道数,那么这个分支直接就可以用,不用做任何变化
# 如果branches_cur通道数!=branches_pre通道数,那么就要用一个cnn网络改变通道数
# 注意这个cnn是不会改变特征图的shape
# 在stage1中,pre通道数是256,cur通道数为32,所以要添加这一层cnn改变通道数
# 所以transition_layers第一层为
# conv2d(256,32,3,1,1)
# batchnorm2d(32)
# relu
if num_channels_cur_layer[i] != num_channels_pre_layer[i]:
transition_layers.append(
nn.Sequential(
nn.Conv2d(
num_channels_pre_layer[i],
num_channels_cur_layer[i],
3, 1, 1, bias=False
),
nn.BatchNorm2d(num_channels_cur_layer[i]),
nn.ReLU(inplace=True)
)
)
else:
# 如果当前层的输入通道和输出通道数相等,则什么都不做
transition_layers.append(None)
else:
# 如果为最后一个分支,则再新建一个分支(该分支分辨率会减少一半)
# 由于branches_cur有两个分支,branches_pre只有一个分支
# 所以我们必须要利用branches_pre里的分支无中生有一个新分支
# 这就是常见的缩减图片shape,增加通道数提特征的操作
conv3x3s = []
for j in range(i + 1 - num_branches_pre):
# 利用branches_pre中shape最小,通道数最多的一个分支(即最后一个分支)来形成新分支
inchannels = num_channels_pre_layer[-1]
outchannels = num_channels_cur_layer[i] \
if j == i - num_branches_pre else inchannels
conv3x3s.append(
nn.Sequential(
nn.Conv2d(
inchannels, outchannels, 3, 2, 1, bias=False
),
nn.BatchNorm2d(outchannels),
nn.ReLU(inplace=True)
)
)
# 所以transition_layers第二层为:
# nn.Conv2d(256, 64, 3, 2, 1, bias=False),
# nn.BatchNorm2d(64),
# nn.ReLU(inplace=True)
transition_layers.append(nn.Sequential(*conv3x3s))
return nn.ModuleList(transition_layers)
3._make_stage进行特征提取和特征融合
stage对于每个尺度分支都是先通过4个BasicBlock,然后融合不同尺度上的信息,每个尺度分支上的输出都是由所有分支上的输出进行融合得到的。
# 同级stage设计,通过 HighResolutionModule
def _make_stage(self, layer_config, num_inchannels,
multi_scale_output=True):
"""
当stage=2时: num_inchannels=[32,64] multi_scale_output=Ture
当stage=3时: num_inchannels=[32,64,128] multi_scale_output=Ture
当stage=4时: num_inchannels=[32,64,128,256] multi_scale_output=False
"""
# 当stage=2,3,4时,num_modules分别为:1,4,3
# 表示HighResolutionModule(平行之网络交换信息模块)模块的数目
num_modules = layer_config['NUM_MODULES']
# 当stage=2,3,4时,num_branches分别为:2,3,4,表示每个stage平行网络的数目
num_branches = layer_config['NUM_BRANCHES']
# 当stage=2,3,4时,num_blocks分别为:[4,4], [4,4,4], [4,4,4,4],
# 表示每个stage blocks(BasicBlock或者BasicBlock)的数目
num_blocks = layer_config['NUM_BLOCKS']
# 当stage=2,3,4时,num_channels分别为:[32,64],[32,64,128],[32,64,128,256]
# 在对应stage, 对应每个平行子网络的输出通道数
num_channels = layer_config['NUM_CHANNELS']
# 当stage=2,3,4时,分别为:BasicBlock,BasicBlock,BasicBlock
block = blocks_dict[layer_config['BLOCK']]
# 当stage=2,3,4时,都为:SUM,表示特征融合的方式
fuse_method = layer_config['FUSE_METHOD']
modules = []
# 根据num_modules的数目创建HighResolutionModule
for i in range(num_modules):
#num_modules表示一个融合块中要进行几次融合,前几次融合是将其他分支的特征融合到最高分辨率的特征图上,只输出最高分辨率特征图(multi_scale_output = False)
#只有最后一次的融合是将所有分支的特征融合到每个特征图上,输出所有尺寸特征(multi_scale_output=True)
# multi_scale_output 只被用再最后一个HighResolutionModule
if not multi_scale_output and i == num_modules - 1:
reset_multi_scale_output = False
else:
reset_multi_scale_output = True
# 根据参数,添加HighResolutionModule到
modules.append(
HighResolutionModule(
num_branches, # 当前stage平行分支的数目
block, # BasicBlock,BasicBlock
num_blocks, # BasicBlock或者BasicBlock的数目
num_inchannels, # 输入通道数目
num_channels, # 输出通道数
fuse_method, # 通特征融合的方式
reset_multi_scale_output # 是否使用多尺度方式输出
)
)
# 获得最后一个HighResolutionModule的输出通道数
num_inchannels = modules[-1].get_num_inchannels()
return nn.Sequential(*modules), num_inchannels
stage搭建
stage2/3
forward函数中
# 对应论文中的stage2,在配置文件中self.stage2_cfg['NUM_BRANCHES']为2
# 其中包含了创建分支的过程,即 N11-->N21,N22 这个过程
# N22的分辨率为N21的二分之一,总体过程为:
# x[b,256,64,48] ---> y[b, 32, 64, 48] 因为通道数不一致,通过卷积进行通道数变换
# y[b, 64, 32, 24] 通过新建平行分支生成
x_list = []
for i in range(self.stage2_cfg['NUM_BRANCHES']):
if self.transition1[i] is not None:
x_list.append(self.transition1[i](x))
else:
x_list.append(x)
# 总体过程如下(经过一些卷积操作,但是特征图的分辨率和通道数都没有改变):
# x[b, 32, 32, 24] ---> y[b, 32, 32, 24]
# x[b, 64, 16, 12] ---> y[b, 64, 16, 12]
y_list = self.stage2(x_list)
# 对应论文中的stage3
# 其中包含了创建分支的过程,即 N22-->N32,N33 这个过程
# N33的分辨率为N32的二分之一,
# y[b, 32, 64, 48] ---> x[b, 32, 64, 48] 因为通道数一致,没有做任何操作
# y[b, 64, 32, 24] ---> x[b, 64, 32, 24] 因为通道数一致,没有做任何操作
# x[b, 128, 16, 12] 通过新建平行分支生成
x_list = []
for i in range(self.stage3_cfg['NUM_BRANCHES']):
if self.transition2[i] is not None:
x_list.append(self.transition2[i](y_list[-1]))
else:
x_list.append(y_list[i])
# 总体过程如下(经过一些卷积操作,但是特征图的分辨率和通道数都没有改变):
# x[b, 32, 32, 24] ---> y[b, 32, 32, 24]
# x[b, 32, 16, 12] ---> y[b, 32, 16, 12]
# x[b, 64, 8, 6] ---> y[b, 64, 8, 6]
y_list = self.stage3(x_list)
# stage4
x_list = []
for i in range(self.stage4_cfg['NUM_BRANCHES']):
if self.transition3[i] is not None:
x_list.append(self.transition3[i](y_list[-1]))
else:
x_list.append(y_list[i])
y_list = self.stage4(x_list)
x = self.final_layer(y_list[0])
return x
stage4
需要注意的是在Stage4中的最后一个Exchange Block只输出下采样4倍分支的输出(即只保留分辨率最高的特征层),然后接上一个卷积核大小为1x1卷积核个数为17(因为COCO数据集中对每个人标注了17个关键点)的卷积层。最终得到的特征层(64x48x17)就是针对每个关键点的heatma
参考
HighResolutionModule(高分辨率模块),这篇帖子写的很详细,可以作为参考
HRNet代码理解https://icver.blog.csdn.net/article/details/111867755
总结
通过看了,CPN,Simple baseline , HRNet这三篇论文,我感觉最大的收获就是学习到了人体姿态估计的研究思想,要将关键点精准的提取出来,就要将全局特征和局部特征都要保留,然后进一步思考怎样构建模型可以达到这样的效果,在考虑精准度的同时还要考虑计算量的问题。就是这次的代码我感觉和之前相比难很多,所以这周还没复现成功,下周再花点时间完成。