天天看点

【论文阅读笔记】HRNet--从代码来看论文1.整体架构:2.代码梳理:3.模块详解:

目录

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.整体架构:

如下图所示经过了前期基本的特征提取网络,就进入关键的特征融合模块

【论文阅读笔记】HRNet--从代码来看论文1.整体架构:2.代码梳理:3.模块详解:

从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模块【感觉不用详解大家都清楚...跳过】

【论文阅读笔记】HRNet--从代码来看论文1.整体架构:2.代码梳理:3.模块详解:

2)关键的特征融合部分:HighResolutionModule

函数一和二:

_make_one_branch:生成一个分支;def _make_branches:利用for循环函数,使用_make_one_branch函数生成所有的分支,如下图所示,框出的两个分支就是stage2需要的生成分支部分。①是经过transition层的输入【后面讲】,②是生成的分支

【论文阅读笔记】HRNet--从代码来看论文1.整体架构:2.代码梳理:3.模块详解:
  • (1) 首先判断是否降维或者输入输出的通道(

    num_inchannels[branch_index]和 num_channels[branch_index] * block.expansion(通道扩张率)

    )是否一致,不一致使用1z1卷积进行维度升/降,后接BN,不使用ReLU;
  • (2) 顺序搭建

    num_blocks[branch_index]

    个block,第一个block需要考虑是否降维的情况,所以单独拿出来,后面

    1 到 num_blocks[branch_index]

    个block完全一致,使用循环搭建就行。此时注意在执行完第一个block后将

    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

    次,如果只需要产生最高分辨率的表示,就将

    i

    确定为0。
  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

【论文阅读笔记】HRNet--从代码来看论文1.整体架构:2.代码梳理:3.模块详解:
  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】

可以看到不是每个分支的每个输出都做了融合,而是在②部分做特征融合

【论文阅读笔记】HRNet--从代码来看论文1.整体架构:2.代码梳理:3.模块详解:
   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
           

差不多完全能够看懂啦,总的来说这篇论文很好的使用了并行的网络结构,将不同分辨率的特征进行融合,获得了更好的特征信息,更有利于关键点的检测。

【代码里有一点就是相同分辨率的特征,通道数也是相同的昂,我没弄明白为什么一定是相同的】

继续阅读