Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)

1.前言

这篇文章是ECCV2016的论文,Jia Deng组的工作,是top-down算法,非常经典,当时也是在各种公开数据集上霸榜。在FLIC和MPII上都是第一名(在当时),是sota算法。现在很多关于姿态估计的论文都有参考SHN或者会拿他作对比,可以说是比较典型的姿态估计算法。

 

2. 网络结构

SHN网络名字起的很不错,级联的沙漏网络,顾名思义,沙漏网络就表示该网络具有高度对称性,多个沙漏网络进行级联,其实不级联也是可以检测的,只是检测效果会差一些,作者认为人体关节点之间有较强的相关性,前面沙漏检测出的关键点对后面的检测有帮助,所以前面的输出可以作为后面的输入的一部分,见下图的虚线部分,这个后面再讨论。

级联的沙漏网络

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第1张图片

 

2.1单个沙漏网络

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第2张图片

单个沙漏网络如上图所示,这是一个4阶版本的沙漏网络,表示有四次下采样和四次下采样。方块大小表示feature maps大小,方块变小方式下采样,方块变大是上采样,加号表示按元素相加。其他全部都是残差模块,上方的连线方式也是一些残差模块,但是没有改变feature maps的大小,只是改变通道数,变成与下面相同,然后才可以按元素相加。

看一下更加具体的版本

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第3张图片

该图来自https://blog.csdn.net/shenxiaolu1984/article/details/51428392。浅绿色部分是一些残差模块。看上去很明朗,就是一些残差模块,先下采样然后上采样,这样的网络结构提取特征很充分,在不同的分辨率有进行卷积,然后还有特征融合。但是也有一些弊端,不能使用pretrained model,因为它不像cpn那样,GlobalNet是resnet50或者resnet101,可以直接使用在ImageNet上预训练的模型进行初始化。没有预训练模型用来初始化,一般需要训练更久然后效果会更差一些,但是没有预训练的情况下,当数据很充分,训练也很充分,合理使用BN或者GN,炼丹能力较好的情况下,是可以达到预训练的效果(Kaiming He的最新论文的结论,Rethinking)。

2.2看一下Pytorch版本实现

class HourglassNet(nn.Module):
    '''Hourglass model from Newell et al ECCV 2016'''
    def __init__(self, block, num_stacks=2, num_blocks=4, num_classes=16):
        """
        参数解释
        :param block: hg块元素
        :param num_stacks: 有几个hg
        :param num_blocks: 在两个hg之间有几个block块
        :param num_classes: keypoint个数,也就是最后的heatmap个数
        """
        super(HourglassNet, self).__init__()

        self.inplanes = 64
        self.num_feats = 128
        self.num_stacks = num_stacks
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
                               bias=True)   # 第一次下采样
        self.bn1 = nn.BatchNorm2d(self.inplanes) 
        self.relu = nn.ReLU(inplace=True)
        self.layer1 = self._make_residual(block, self.inplanes, 1)  #self.planes = 64,有downsample(只是改变channel数)
        self.layer2 = self._make_residual(block, self.inplanes, 1)  #有downsample(只是改变channel数)
        # 这一次的bottleneck没有downsample,因为self.planes == planes(self.num_feats=128)*2 = 256
        self.layer3 = self._make_residual(block, self.num_feats, 1)
        self.maxpool = nn.MaxPool2d(2, stride=2)   #第二次下采样
        # build hourglass modules
        ch = self.num_feats*block.expansion   #128*2=256
        hg, res, fc, score, fc_, score_ = [], [], [], [], [], []
        for i in range(num_stacks):
            hg.append(Hourglass(block, num_blocks, self.num_feats, 4))  #block, num_blocks, planes, depth=4
            res.append(self._make_residual(block, self.num_feats, num_blocks))
            fc.append(self._make_fc(ch, ch))
            score.append(nn.Conv2d(ch, num_classes, kernel_size=1, bias=True))
            if i < num_stacks-1:
                fc_.append(nn.Conv2d(ch, ch, kernel_size=1, bias=True))
                score_.append(nn.Conv2d(num_classes, ch, kernel_size=1, bias=True))
        self.hg = nn.ModuleList(hg)
        self.res = nn.ModuleList(res)
        self.fc = nn.ModuleList(fc)
        self.score = nn.ModuleList(score)
        self.fc_ = nn.ModuleList(fc_) 
        self.score_ = nn.ModuleList(score_)

    def _make_residual(self, block, planes, blocks, stride=1):  #planes = 64,blocks=4
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            # 这里的downsample只有改变通道数的功能,并没有下采样的功能,因为调用时stride固定为1
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=True),
            )

        layers = []
        # 只在每个block的第一个bottleneck做downsample,因为channel数不相同
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion  #self.planes是改变的,从最开始的64,128,256
        for i in range(1, blocks):   #因为blocks=1 ,后面都不会执行
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def _make_fc(self, inplanes, outplanes):
        bn = nn.BatchNorm2d(inplanes)
        conv = nn.Conv2d(inplanes, outplanes, kernel_size=1, bias=True)
        return nn.Sequential(
                conv,
                bn,
                self.relu,
            )

    def forward(self, x):
        out = []
        x = self.conv1(x)  #下采样
        x = self.bn1(x)
        x = self.relu(x) 

        x = self.layer1(x)  
        x = self.maxpool(x)  #下采样
        x = self.layer2(x)  
        x = self.layer3(x)  

        for i in range(self.num_stacks):
            y = self.hg[i](x)
            y = self.res[i](y)
            y = self.fc[i](y)
            score = self.score[i](y)
            out.append(score)
            if i < self.num_stacks-1:
                fc_ = self.fc_[i](y)
                score_ = self.score_[i](score)
                x = x + fc_ + score_

        return out

在上面,Bottleneck是使用expansion=2的版本的残差Bottleneck,通常是是使用4阶版本的沙漏网络,结构就跟上图一样,很多残差模块加下采样和上采样。这个实现使用了递归实现,这么短的代码就实现了那么长的网络结构,PyTorch真香,呵呵~

 

2.3完整网络结构

看了上面的单个Hourglass结构,下面看下完整的网络结构。很简单,前面加了几层卷积,后面就是Hourglass的级联模式,Hourglass之间的级联稍微有一些特殊处理。

网络的从一个7*7的卷积开始,然后接着3个残差模块,这一共会经过两次下采样,如果输入是256*256的,那么经过这个前端网络处理feature maps变为64*64的尺寸。后面就开始级联多个Hourglass部分接口,只与多少个可能要根据实际情况确定。作者实验试过2,4,8,好像是越多越好,然后越到后面输出预测越准,符合直觉预期,说明经过级联是有效的,前面的输出对后面的训练是有帮助的。

下面是Hourglass之间的连接结构图,有一些特征融合在里面。

 

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第4张图片

图来自https://blog.csdn.net/wangzi371312/article/details/81174452

如上图,N1代表第一个沙漏网络,提取出的混合特征经过1个1x1全卷积网络后,分成上下两个分支,上部分支继续经过1x1卷积后,进入下一个沙漏网络。下部分支先经过1x1卷积后,生成heat map,就是图中蓝色部分.

上图中蓝色方块比其他三个方块要窄一些,这是因为heat map矩阵的depth与训练数据里的节点数一致,比如 [1x64x64x16],其他几个则具有较高的depth,如 [1x64x64x256]

heat_map继续经过1x1卷积,将depth调整到与上部分支一致,如256,最后与上部分支合并,一起作为下一个沙漏网络的输入。

前面提到过,由于人体关节点之间的较强相关性,作者认为前面检测出的heat maps对后面的预测是有帮助的,最初的输入,heatmaps经过1*1卷积调整channels数,以及上一级Hourglass的输出三个做按元素相加,作为下一级Hourglass的输入。

2.4 完整网络结构PyTorch实现

class HourglassNet(nn.Module):
    '''Hourglass model from Newell et al ECCV 2016'''
    def __init__(self, block, num_stacks=2, num_blocks=4, num_classes=16):
        """
        参数解释
        :param block: hg块元素
        :param num_stacks: 有几个hg
        :param num_blocks: 在两个hg之间有几个block块
        :param num_classes: keypoint个数,也就是最后的heatmap个数
        """
        super(HourglassNet, self).__init__()

        self.inplanes = 64
        self.num_feats = 128
        self.num_stacks = num_stacks
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
                               bias=True)
        self.bn1 = nn.BatchNorm2d(self.inplanes) 
        self.relu = nn.ReLU(inplace=True)
        self.layer1 = self._make_residual(block, self.inplanes, 1)  #self.planes = 64
        self.layer2 = self._make_residual(block, self.inplanes, 1)
        self.layer3 = self._make_residual(block, self.num_feats, 1)  #这一次的bottleneck没有downsample,因为self.planes == planes(self.num_feats=128)*2 = 256
        self.maxpool = nn.MaxPool2d(2, stride=2)   #TODO 这个maxpool需不需要。论文里是有2次下采样,从256降到64,

        # build hourglass modules
        ch = self.num_feats*block.expansion   #128*2=256
        hg, res, fc, score, fc_, score_ = [], [], [], [], [], []
        for i in range(num_stacks):
            hg.append(Hourglass(block, num_blocks, self.num_feats, 4))  #block, num_blocks, planes, depth=4
            res.append(self._make_residual(block, self.num_feats, num_blocks))
            fc.append(self._make_fc(ch, ch))
            score.append(nn.Conv2d(ch, num_classes, kernel_size=1, bias=True))
            if i < num_stacks-1:
                fc_.append(nn.Conv2d(ch, ch, kernel_size=1, bias=True))
                score_.append(nn.Conv2d(num_classes, ch, kernel_size=1, bias=True))
        self.hg = nn.ModuleList(hg)
        self.res = nn.ModuleList(res)
        self.fc = nn.ModuleList(fc)
        self.score = nn.ModuleList(score)
        self.fc_ = nn.ModuleList(fc_) 
        self.score_ = nn.ModuleList(score_)

    def _make_residual(self, block, planes, blocks, stride=1):  #planes = 64,blocks=4
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=True),
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))  #只在每个block的第一个bottleneck做下采样,因为channel数不相同
        self.inplanes = planes * block.expansion  #self.planes是改变的,从最开始的64,128,256
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def _make_fc(self, inplanes, outplanes):
        bn = nn.BatchNorm2d(inplanes)
        conv = nn.Conv2d(inplanes, outplanes, kernel_size=1, bias=True)
        return nn.Sequential(
                conv,
                bn,
                self.relu,
            )

    def forward(self, x):
        out = []
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x) 

        x = self.layer1(x)  
        x = self.maxpool(x)
        x = self.layer2(x)  
        x = self.layer3(x)  

        for i in range(self.num_stacks):
            y = self.hg[i](x)
            y = self.res[i](y)
            y = self.fc[i](y)
            score = self.score[i](y)
            out.append(score)
            if i < self.num_stacks-1:
                fc_ = self.fc_[i](y)
                score_ = self.score_[i](score)
                x = x + fc_ + score_

        return out

fc和score分别表示hourglass的输出两个支路,score是得到heatmaps,经过的卷积的channel 数好keypoints个数相同。fc_和score_分别表示当后面还需要级联Hourglass时,需要做一些1*1的卷积改变featuremaps的通道数,这样后面才能做按元素相加,然后作为后面的输入。

 

3. 中继监督

通过端到端地堆叠多个沙漏,我们将网络架构进一步细化,将一个沙漏的输出作为输入提供给下一个沙漏,但是每个Hourglass都会输出heatmaps,然后也会计算loss。这提供了具有用于重复自底向上、自顶向下的推理的机制的网络,允许在整个图像上重新评估初始估计和特征。这种方法的关键是预测我们可以应用损失的中间群体。预测是在通过每个沙漏之后生成的,其中网络有机会在本地和全局上下文中处理特性。随后的沙漏模块允许再次处理这些高级特征,以进一步评估和重新评估高阶空间关系。这与其他姿态估计方法类似,这些姿态估计方法在多个迭代阶段和中间监督下表现出很强的性能。

下面这个图挺有意思的,这个Ablation实验部分。作者实验了不同的级联方式对准确率的影响,和中间Hourglass输出heatmaps的准确率规律,在参数量几乎相同的情况下,每个残差模块有不同的个数,这样网络的总层数几乎相同。可以看到,小的Hourglass多级联几次有利于准确率提升,后面层的输出比前面的输出效果好非常多,在小的Hourglass上看的尤其明显,级联了8次,前面2级的效果很差。

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第5张图片

作者还做了一些有趣的实验,loss计算位置,在网络结构相似的情况下,loss影响不是特别大,在每个Hourglass的单独输出上计算loss效果是最好的。

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第6张图片

 

4. 训练设置

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第7张图片

5. 结论与结果

  1. 设计了一个新的单人姿态估计网络Hourglass,效果也是棒棒的,如果用于多人需要单独的行人检测作为前端预处理。
  2. 中继监督的作用很大,
  3. 级联的Hourglass效果非常好,当时sota方法
  4. 但对一些遮挡问题难以处理,这是绝大部分算法的难题

Paperreading之五 Stacked Hourglass Networks(SHN)和源码阅读(PyTorch版本)_第8张图片

 

参考文献:

[1] Newell A , Yang K , Deng J . Stacked Hourglass Networks for Human Pose Estimation[J]. 2016.

[2] https://github.com/bearpaw/pytorch-pose

[3]https://blog.csdn.net/wangzi371312/article/details/81174452

[4] https://blog.csdn.net/shenxiaolu1984/article/details/51428392

你可能感兴趣的:(人体姿态估计)