HRNet是微软亚洲研究院的王井东老师领导的团队完成的,打通图像分类、图像分割、目标检测、人脸对齐、姿态识别、风格迁移、Image Inpainting、超分、optical flow、Depth estimation、边缘检测等网络结构。
王老师在ValseWebinar《物体和关键点检测》中亲自讲解了HRNet,讲解地非常透彻。以下文章主要参考了王老师在演讲中的解读,配合论文+代码部分,来为各位读者介绍这个全能的Backbone-HRNet。
1. 引入
在人体姿态识别这类的任务中,需要生成一个高分辨率的heatmap来进行关键点检测。这就与一般的网络结构比如VGGNet的要求不同,因为VGGNet最终得到的feature map分辨率很低,损失了空间结构。
获取高分辨率的方式大部分都是如上图所示,采用的是先降分辨率,然后再升分辨率的方法。U-Net、SegNet、DeconvNet、Hourglass本质上都是这种结构。
2. 核心
普通网络都是这种结构,不同分辨率之间是进行了串联
王井东老师则是将不同分辨率的feature map进行并联:
在并联的基础上,添加不同分辨率feature map之间的交互(fusion)。
具体fusion的方法如下图所示:
- 同分辨率的层直接复制。
- 需要升分辨率的使用bilinear upsample + 1x1卷积将channel数统一。
- 需要降分辨率的使用strided 3x3 卷积。
- 三个feature map融合的方式是相加。
至于为何要用strided 3x3卷积,这是因为卷积在降维的时候会出现信息损失,使用strided 3x3卷积是为了通过学习的方式,降低信息的损耗。所以这里没有用maxpool或者组合池化。
另外在读HRNet的时候会有一个问题,有四个分支的到底如何使用这几个分支呢?论文中也给出了几种方式作为最终的特征选择。
(a)图展示的是HRNetV1的特征选择,只使用分辨率最高的特征图。
(b)图展示的是HRNetV2的特征选择,将所有分辨率的特征图(小的特征图进行upsample)进行concate,主要用于语义分割和面部关键点检测。
©图展示的是HRNetV2p的特征选择,在HRNetV2的基础上,使用了一个特征金字塔,主要用于目标检测网络。
再补充一个(d)图
(d)图展示的也是HRNetV2,采用上图的融合方式,主要用于训练分类网络。
总结一下HRNet创新点:
- 将高低分辨率之间的链接由串联改为并联。
- 在整个网络结构中都保持了高分辨率的表征(最上边那个通路)。
- 在高低分辨率中引入了交互来提高模型性能。
3. 效果
3.1 消融实验
- 对交互方法进行消融实验,证明了当前跨分辨率的融合的有效性。
- 证明高分辨率feature map的表征能力
1x代表不进行降维,2x代表分辨率变为原来一半,4x代表分辨率变为原来四分之一。W32、W48中的32、48代表卷积的宽度或者通道数。
3.2 姿态识别任务上的表现
以上的姿态识别采用的是top-down的方法。
在参数和计算量不增加的情况下,要比其他同类网络效果好很多。
在19年2月28日时的PoseTrack Leaderboard,HRNet占领两个项目的第一名。
3.3 语义分割任务中的表现
3.4 目标检测任务中的表现
3.5 分类任务上的表现
ps: 王井东老师在这部分提到,分割的网络也需要使用分类的预训练模型,否则结果会差几个点。
以上是HRNet和ResNet结果对比,同一个颜色的都是参数量大体一致的模型进行的对比,在参数两差不多甚至更少的情况下,HRNet能够比ResNet达到更好的效果。
4. 代码
HRNet( https://github.com/HRNet )工作量非常大,构建了六个库涉及语义分割、人体姿态检测、目标检测、图片分类、面部关键点检测、Mask R-CNN等库。全部内容如下图所示:
笔者对HRNet代码构建非常感兴趣,所以以HRNet-Image-Classification库为例,来解析一下这部分代码。
先从简单的入手,BasicBlock
def conv3x3(in_planes, out_planes, stride=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
Bottleneck:
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes