目录
1.整体架构:
2.代码梳理:
3.模块详解:
1)ResNet模块【感觉不用详解大家都清楚...跳过】
2)关键的特征融合部分:HighResolutionModule
函数一和二:
函数三:_make_fuse_layers--融合模块
最后的forward部分:
3.关键点预测模块:PoseHighResolutionNet
1)函数一:_make_transition_layer
2)函数三:_make_stage
最后是forward部分:
前提要做coco关键点比赛,第一次接触到关键点的论文,没有跟其他论文对比分析之类的,单纯了解学习网络架构原理
【Deep High-Resolution Representation Learning for Human Pose Estimation】
github:https://github.com/leoxiaobin/deep-high-resolution-net.pytorch
论文:https://arxiv.org/abs/1902.09212
参考文献:https://segmentfault.com/a/1190000019167646
1.整体架构:
如下图所示经过了前期基本的特征提取网络,就进入关键的特征融合模块
从forward部分可以看到,特征融合模块可以分为两个部分,第一部分transition层生成要进行特征融合的特征层;第二部分stage层,得到特征融合后的输出。
def forward(self, x):
"""基础提取特征的前期卷积网络"""
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.conv2(x)
x = self.bn2(x)
x = self.relu(x)
x = self.layer1(x)
"""关键部分:涉及到transition层和stage层,
代码中一共有3个stage,论文图只给出了2个stage的示例"""
# stage2,从2开始
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)
y_list = self.stage2(x_list)
# stage3
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])
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
2.代码梳理:
/lib/models/pose_hrnet.py:完成功能构建hrnet网络架构
包含
1.基础框架:
1)resnet网络的两种形式:class BasicBlock(nn.Module)
class Bottleneck(nn.Module)
2)关键部分:高分辨率模块:class HighResolutionModule(nn.Module)
该模块的重要函数:def _make_one_branch
def _make_branches
def _make_fuse_layers
2.关键点预测模块【完整的网络】: class PoseHighResolutionNet(nn.Module)
该模块的重要函数:def _make_transition_layer
def _make_layer
def _make_stage
3.模块详解:
1)ResNet模块【感觉不用详解大家都清楚...跳过】
2)关键的特征融合部分:HighResolutionModule
函数一和二:
_make_one_branch:生成一个分支;def _make_branches:利用for循环函数,使用_make_one_branch函数生成所有的分支,如下图所示,框出的两个分支就是stage2需要的生成分支部分。①是经过transition层的输入【后面讲】,②是生成的分支
- (1) 首先判断是否降维或者输入输出的通道(
)是否一致,不一致使用1z1卷积进行维度升/降,后接BN,不使用ReLU;
num_inchannels[branch_index]和 num_channels[branch_index] * block.expansion(通道扩张率)
- (2) 顺序搭建
个block,第一个block需要考虑是否降维的情况,所以单独拿出来,后面
num_blocks[branch_index]
个block完全一致,使用循环搭建就行。此时注意在执行完第一个block后将
1 到 num_blocks[branch_index]
重新赋值为
num_inchannels[branch_index
。
num_channels[branch_index] * block.expansion
def _make_one_branch(self, branch_index, block, num_blocks, num_channels,
stride=1):
downsample = None
"""如果输入输出的维度不一致的话,downsample就重新构建"""
if stride != 1 or \
self.num_inchannels[branch_index] != num_channels[branch_index] * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(
self.num_inchannels[branch_index],
num_channels[branch_index] * block.expansion,
kernel_size=1, stride=stride, bias=False
),
nn.BatchNorm2d(
num_channels[branch_index] * block.expansion,
momentum=BN_MOMENTUM
),
)
layers = []
"""分支的第一个层,的输入输出维度是不同的所以单独构建"""
layers.append(
block(
self.num_inchannels[branch_index],
num_channels[branch_index],
stride,
downsample
)
)
self.num_inchannels[branch_index] = \
num_channels[branch_index] * block.expansion
for i in range(1, num_blocks[branch_index]):
layers.append(
block(
self.num_inchannels[branch_index],
num_channels[branch_index]
)
)
return nn.Sequential(*layers)
函数三:_make_fuse_layers--融合模块
- 如果分支数等于1,返回None,说明不需要使用融合模块;
- 双层循环:
的作用是,如果需要产生多分辨率的结果,就双层循环for i in range(num_branches if self.multi_scale_output else 1):
次,如果只需要产生最高分辨率的表示,就将num_branches
确定为0。i
def _make_fuse_layers(self):
"""特征融合模块,判断特征层是不是需要上采样或者下采样"""
if self.num_branches == 1:
return None
num_branches = self.num_branches
num_inchannels = self.num_inchannels
fuse_layers = []
"""两个for循环,比对分支ind,决定采样方式"""
for i in range(num_branches if self.multi_scale_output else 1):
fuse_layer = []
for j in range(num_branches):
"""如果当分支j大于分支i,则需要上采样"""
"""先使用1x1卷积将j分支的通道数变得和i分支一致,进而跟着BN,
然后依据上采样因子将j分支分辨率上采样到和i分支分辨率相同,此处使用最近邻插值"""
if j > i:
fuse_layer.append(
nn.Sequential(
nn.Conv2d(
num_inchannels[j],
num_inchannels[i],
1, 1, 0, bias=False
),
nn.BatchNorm2d(num_inchannels[i]),
nn.Upsample(scale_factor=2**(j-i), mode='nearest')
)
)
"""如果当分支j等于分支i,则不做任何操作"""
elif j == i:
fuse_layer.append(None)
"""如果当分支j小于分支i,则需要下采样,这里直接采用3×3卷积做下采样"""
else:
"""里面包含了一个双层循环,要根据下采样的尺度决定循环的次数:
当i-j > 1时,两个分支的分辨率差了不止二倍,此时还是两倍两倍往上采样,
例如i-j = 2时,j分支的分辨率比i分支大4倍,就需要上采样两次,循环次数就是2;"""
conv3x3s = []
for k in range(i-j):
"""无论经过多少次下采样,最后一次是不使用ReLU的"""
if k == i - j - 1:
num_outchannels_conv3x3 = num_inchannels[i]
conv3x3s.append(
nn.Sequential(
nn.Conv2d(
num_inchannels[j],
num_outchannels_conv3x3,
3, 2, 1, bias=False
),
nn.BatchNorm2d(num_outchannels_conv3x3)
)
)
else:
num_outchannels_conv3x3 = num_inchannels[j]
conv3x3s.append(
nn.Sequential(
nn.Conv2d(
num_inchannels[j],
num_outchannels_conv3x3,
3, 2, 1, bias=False
),
nn.BatchNorm2d(num_outchannels_conv3x3),
nn.ReLU(True)
)
)
fuse_layer.append(nn.Sequential(*conv3x3s))
fuse_layers.append(nn.ModuleList(fuse_layer))
"""最后返回的是一个存储了每个分支对应的融合模块的二维数组,比如说两个分支中,
对于分支1,fuse_layers[0][0]=None,fuse_layers[0][1]=上采样的操作"""
return nn.ModuleList(fuse_layers)
最后的forward部分:
def forward(self, x):
"""x表示每个分支输入的特征,如果有两个分支,则x就是一个二维数组,
x[0]和x[1]就是两个输入分支的特征"""
"""如果只有一个分支就直接返回,不做任何融合"""
if self.num_branches == 1:
return [self.branches[0](x[0])]
"""有多个分支的时候,对每个分支都先用_make_branch函数生成主特征网络,
再将特定的网络特征进行融合"""
for i in range(self.num_branches):
x[i] = self.branches[i](x[i])
x_fuse = []
"""这里是利用融合模块进行特征融合"""
for i in range(len(self.fuse_layers)):
"""对每个分支用_make_fuse_layer生成要进行融合操作的层,函数三的输出上面有说"""
y = x[0] if i == 0 else self.fuse_layers[i][0](x[0])
for j in range(1, self.num_branches):
"""进行特征融合,举个例子,运行到分支一的时候,
self.fuse_layers[i][0](x[0]先生成对于分支一的融合操作,
for循环得到每个分支对于分支一的采样结果【当i=j,就是分支一本身不进行任何操作直接cat,
i>j的时候就是分支2对于分支一来说要进行上采样,然后cat得到结果】,并cat得到最后的输出"""
if i == j:
y = y + x[j]
else:
y = y + self.fuse_layers[i][j](x[j])
x_fuse.append(self.relu(y))
"""输出的是最后融合的特征【例如两个分支的时候,输出分支一+分支二的上采样和分支一的下采样+分支二】"""
return x_fuse
3.关键点预测模块:PoseHighResolutionNet
1)函数一:_make_transition_layer
目的是生成要进行融合的特征,就是刚刚讲过的特征融合模块的输入特征,这里不太好解释,看图就很明白了。
框出的部分是transition1-transition2
def _make_transition_layer(
self, num_channels_pre_layer, num_channels_cur_layer):
num_branches_cur = len(num_channels_cur_layer)
num_branches_pre = len(num_channels_pre_layer)
transition_layers = []
for i in range(num_branches_cur):
if i < num_branches_pre:
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:
conv3x3s = []
for j in range(i+1-num_branches_pre):
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.append(nn.Sequential(*conv3x3s))
return nn.ModuleList(transition_layers)
2)函数三:_make_stage
因为函数二就是很普通的_make_layer函数,感觉不用解释哈
函数三的目的就是按照.yaml文件,利用刚刚讲过的HighResolutionModule类,生成融合模块;图里是给出了两个stage的示例,代码里给的是3个stage【stage2-stage4】
可以看到不是每个分支的每个输出都做了融合,而是在②部分做特征融合
def _make_stage(self, layer_config, num_inchannels,
multi_scale_output=True):
num_modules = layer_config['NUM_MODULES']
num_branches = layer_config['NUM_BRANCHES']
num_blocks = layer_config['NUM_BLOCKS']
num_channels = layer_config['NUM_CHANNELS']
block = blocks_dict[layer_config['BLOCK']]
fuse_method = layer_config['FUSE_METHOD']
"""按照给定的分支数,每个分支有多少block数,每个分支的输入输出通道数按上述模块进行特征融合"""
modules = []
for i in range(num_modules):
# multi_scale_output is only used last module
"""需要注意的是特征融合的操作不是出现在每个分支的每个block的输出特征中,
而是在每个分支的最后block的输出特征之间进行特征融合,看图就很容易明白了"""
if not multi_scale_output and i == num_modules - 1:
reset_multi_scale_output = False
else:
reset_multi_scale_output = True
modules.append(
HighResolutionModule(
num_branches,
block,
num_blocks,
num_inchannels,
num_channels,
fuse_method,
reset_multi_scale_output
)
)
num_inchannels = modules[-1].get_num_inchannels()
return nn.Sequential(*modules), num_inchannels
最后是forward部分:
def forward(self, x):
"""最初的数据特征提取基础部分"""
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.conv2(x)
x = self.bn2(x)
x = self.relu(x)
x = self.layer1(x)
"""开始stage2的时候,第一次特征融合,先使用transition层得到特征融合部分的输入特征,就是将原有的一个分支,变为两个分支啦"""
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)
"""输入transition层得到的特征,进行特征融合得到特征融合后的输出,输入给第二个transition层得到stage3的输入特征,后面就类推啦"""
y_list = self.stage2(x_list)
"""stage3部分"""
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])
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)
"""这里只要stage4部分输出特征的第一个"""
x = self.final_layer(y_list[0])
return x
差不多完全能够看懂啦,总的来说这篇论文很好的使用了并行的网络结构,将不同分辨率的特征进行融合,获得了更好的特征信息,更有利于关键点的检测。
【代码里有一点就是相同分辨率的特征,通道数也是相同的昂,我没弄明白为什么一定是相同的】