基于HRNet的人脸特征点检测
摘要
在本篇文章中我们主要研究人脸特征点检测,使用的网络结构为中科大与微软亚洲研究院,发布的HRNet。HRNet着重于输出可靠的高分辨率表征(reliable highresolution representation)。目前现有的大多数方法都是从高分辨率到低分辨率网络(high-to-low resolution network)产生的低分辨率表征中恢复高分辨率表征。相反,HRNet能在整个过程中都保持高分辨率的表征。
HRNet从高分辨率子网络(high-resolution subnetwork)作为第一阶段开始,逐步增加高分辨率到低分辨率的子网,形成更多的阶段,并将多分辨率的子网并行连接。之后通过多次多尺度融合,使得每一个高分辨率到低分辨率的表征都从其他并行表示中反复接收信息,从而得到丰富的高分辨率表征。因此预测的人脸特征可能更准确,在空间上也更精确。论文地址:High-Resolution Representations for Labeling Pixels and Regions
简介
纵观深度学习的发展表明,深度卷积神经网络已经取得非常优越的性能。大多数现有的方法,通过一个网络(通常由高分辨率到低分辨率的子网串联而成)传递输入,然后提高分辨率。例如,Hourglass通过对称的低到高分辨率(symmetric low-to-high process)过程恢复高分辨率。SimpleBaseline采用少量的转置卷积层(transposed convolution layers)来生成高分辨率的表示。此外,dilated convolutions还被用于放大高分辨率到低分辨率网络(high-to-low resolution network)的后几层(如VGGNet或ResNet)。
HRNet(High-Resolution Representaions)高分辨率网络,它能够在整个过程中维护高分辨率的表示。从高分辨率子网作为第一阶段开始,逐步增加高分辨率到低分辨率的子网(gradually add high-to-low resolution subnetworks),形成更多的阶段,并将多分辨率子网并行连接。在整个过程中,通过在并行的多分辨率子网络上反复交换信息来进行多尺度的重复融合。通过网络输出的高分辨率表示来估计关键点。生成的网络如图所示。
与现有的网络相比,HRNet有以下两个好处:
- HRNet是并行连接高分辨率到低分辨率的子网,而不是像大多数现有解决方案那样串行连接。因此,HRNet能够保持高分辨率,而不是通过一个低到高的过程恢复分辨率,因此预测的结果可能在空间上更精确。
- 大多数现有的融合方案都将低层和高层的表示集合起来。相反,HRNet使用重复的多尺度融合,利用相同深度和相似级别的低分辨率表示来提高高分辨率表示。因此,其预测的结果可能更准确。
1. 网络结构解析
目前,深度卷积神经网络提供了主流的解决方案。主要有两种方法:回归关键点位置regressing the position of keypoints和估算关键点热图estimating keypoint heatmaps,然后选择热值最高的位置作为关键点。
1.1 High-to-low and low-to-high
high-to-low 的目标是生成低分辨率和高分辨率的表征,low-to-high 的目标是生成高分辨率的表征。这两个过程可能会重复多次,以提高性能。增加多尺度信息之间的融合是非常有效的,例如原图像和模糊图像进行联合双边滤波可以得到介于两者之间的模糊程度的图像,而RGF滤波就是重复将联合双边滤波的结果作为那张模糊的引导图,这样得到的结果会越来越趋近于原图。同理,不同分辨率的图像采样到相同的尺度反复的融合,加之网络的学习能力,会使得多次融合后的结果更加趋近于正确的表示。
现有的网络设计模式有:
- 对称结构,先下采样,再上采样,同时使用跳层连接恢复下采样丢失的信息;
- 级联多字金字塔;
- 先下采样,转置卷积上采样,不使用跳层连接进行数据融合;
- 扩张卷积,减少下采样次数,不使用跳层连接进行数据融合;
如下图所示:
1.2 多分辨率子网络
并行高分辨率子网
以高分辨率子网为第一阶段,逐步增加高分辨率到低分辨率的子网,形成新的阶段,并将多分辨率子网并行连接。因此,后一阶段并行子网的分辨率由前一阶段的分辨率和下一阶段的分辨率组成。一个包含4个并行子网的网络结构示例如下:
重复多尺度融合
HRNet中引入了跨并行子网的交换单元,使每个子网重复接收来自其他并行子网的信息。下面是一个展示信息交换方案的示例。将第三阶段划分为若干个交换块,每个块由3个并行卷积单元与1个交换单元跨并行单元进行卷积,得到:
输入为s响应映射:。输出为s响应图:,其分辨率和宽度与输入相同。每个输出都是输入映射的集合。各阶段的交换单元有额外的输出图。
函数从分辨率i到k对上采样或下采样组成。我们采用步长为3×3的卷积做下采样。例如,向一个步长为3×3卷积做步长为2x2的下采样。两个连续的步长为3×3的卷积使用步长为2的4被上采样。对于上采样,我们采用最简单的最近邻抽样,从1×1卷积校准通道的数量。如果i = k,则只是一个识别连接:。
2. 代码解析
2.1 ResNet模块
ResNet(Residual Neural Network)由微软研究院的Kaiming He等四名华人提出,通过使用ResNet Unit成功训练出了152层的神经网络,并在ILSVRC2015比赛中取得冠军,在top5上的错误率为3.57%,同时参数量比VGGNet低,效果非常突出。ResNet的结构可以极快的加速神经网络的训练,模型的准确率也有比较大的提升。同时ResNet的推广性非常好,甚至可以直接用到InceptionNet网络中。
ResNet的主要思想是在网络中增加了直连通道,即Highway Network的思想。此前的网络结构是性能输入做一个非线性变换,而Highway Network则允许保留之前网络层的一定比例的输出。ResNet的思想和Highway Network的思想也非常类似,允许原始输入信息直接传到后面的层中。如下的左图对应于resnet-18/34使用的基本块,右图是50/101/152所使用的,由于他们都比较深,所以右图相比于左图使用了1x1卷积来降维。
关于ResNet此处不再赘述
(a) conv3x3:将原有的pytorch函数固定卷积和尺寸为3重新封装了一次
def conv3x3(in_planes, out_planes, stride=1):
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
(b) BasicBlock:搭建上图左边的模块。
- 每个卷积块后面连接BN层进行归一化;
- 残差连接前的3x3卷积之后只接入BN,不使用ReLU,避免加和之后的特征皆为正,保持特征的多样;
- 跳层连接:两种情况,当模块输入和残差支路(3x3->3x3)的通道数一致时,直接相加;当两者通道不一致时(一般发生在分辨率降低之后,同分辨率一般通道数一致),需要对模块输入特征使用1x1卷积进行升/降维(步长为2,上面说了分辨率会降低),之后同样接BN,不用ReLU。
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
(c) Bottleneck:搭建上图右边的模块。
- 使用1x1卷积先降维,再使用3x3卷积进行特征提取,最后再使用1x1卷积把维度升回去;
- 每个卷积块后面连接BN层进行归一化;
- 残差连接前的1x1卷积之后只接入BN,不使用ReLU,避免加和之后的特征皆为正,保持特征的多样性。
- 跳层连接:两种情况,当模块输入和残差支路(1x1->3x3->1x1)的通道数一致时,直接相加;当两者通道不一致时(一般发生在分辨率降低之后,同分辨率一般通道数一致),需要对模块输入特征使用1x1卷积进行升/降维(步长为2,上面说了分辨率会降低),之后同样接BN,不用ReLU。
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, momentum=BN_MOMENTUM)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1,
bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion,
momentum=BN_MOMENTUM)
self.relu = nn.ReLU(inplace=True)
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)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
2.2 High Resolution Module (高分辨率模块)
当仅包含一个分支时,生成该分支,没有融合模块,直接返回;当包含不仅一个分支时,先将对应分支的输入特征输入到对应分支,得到对应分支的输出特征;紧接着执行融合模块。
(a) _check_branches: 判断num_branches (int) 和 num_blocks, num_inchannels, num_channels (list) 三者的长度是否一致,否则报错;
(b) _make_one_branch: 搭建一个分支,单个分支内部分辨率相等,一个分支由num_blocks[branch_index]个block组成,block可以是两种ResNet模块中的一种;
- (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。
(c) _make_branches: 循环调用_make_one_branch函数创建多个分支;
- (1) 如果分支数等于1,返回None,说明此事不需要使用融合模块;
- (2) 双层循环:for i in range(num_branches if self.multi_scale_output else 1):的作用是,如果需要产生多分辨率的结果,就双层循环num_branches 次,如果只需要产生最高分辨率的表示,就将i确定为0。
- (2.1) 如果j > i,此时的目标是将所有分支上采样到和i分支相同的分辨率并融合,也就是说j所代表的分支分辨率比i分支低,2**(j-i)表示j分支上采样这么多倍才能和i分支分辨率相同。先使用1x1卷积将j分支的通道数变得和i分支一致,进而跟着BN,然后依据上采样因子将j分支分辨率上采样到和i分支分辨率相同,此处使用最近邻插值;
- (2.2) 如果j = i,也就是说自身与自身之间不需要融合,nothing to do;
- (2.3) 如果j < i,转换角色,此时最终目标是将所有分支采样到和i分支相同的分辨率并融合,注意,此时j所代表的分支分辨率比i分支高,正好和(2.1)相反。此时再次内嵌了一个循环,这层循环的作用是当i-j > 1时,也就是说两个分支的分辨率差了不止二倍,此时还是两倍两倍往上采样,例如i-j = 2时,j分支的分辨率比i分支大4倍,就需要上采样两次,循环次数就是2;
- (2.3.1) 当k == i - j - 1时,举个例子,i = 2,j = 1, 此时仅循环一次,并采用当前模块,此时直接将j分支使用3x3的步长为2的卷积下采样(不使用bias),后接BN,不使用ReLU;
- (2.3.2) 当k != i - j - 1时,举个例子,i = 3,j = 1, 此时循环两次,先采用当前模块,将j分支使用3x3的步长为2的卷积下采样(不使用bias)两倍,后接BN和ReLU,紧跟着再使用(2.3.1)中的模块,这是为了保证最后一次二倍下采样的卷积操作不使用ReLU,猜测也是为了保证融合后特征的多样性;
(e) forward: 前向传播函数,利用以上函数的功能搭建一个HighResolutionModule;
- (1) 当仅包含一个分支时,生成该分支,没有融合模块,直接返回;
- (2) 当包含不仅一个分支时,先将对应分支的输入特征输入到对应分支,得到对应分支的输出特征;紧接着执行融合模块;
- (2.1) 循环将对应分支的输入特征输入到对应分支模型中,得到对应分支的输出特征;
- (2.2) 融合模块:对着这张图看,很容易看懂。每次多尺度之间的加法运算都是从最上面的尺度开始往下加,所以y = x[0] if i == 0 else self.fuse_layers[i]0;加到他自己的时候,不需要经过融合函数的处理,直接加,所以if i == j: y = y + x[j];遇到不是最上面的尺度那个特征图或者它本身相同分辨率的那个特征图时,需要经过融合函数处理再加,所以y = y + self.fuse_layers[i]j。最后将ReLU激活后的融合(加法)特征append到x_fuse,x_fuse的长度等于1(单尺度输出)或者num_branches(多尺度输出)。
class HighResolutionModule(nn.Module):
def __init__(self, num_branches, blocks, num_blocks, num_inchannels,
num_channels, fuse_method, multi_scale_output=True):
super(HighResolutionModule, self).__init__()
self._check_branches(
num_branches, blocks, num_blocks, num_inchannels, num_channels)
self.num_inchannels = num_inchannels
self.fuse_method = fuse_method
self.num_branches = num_branches
self.multi_scale_output = multi_scale_output
self.branches = self._make_branches(
num_branches, blocks, num_blocks, num_channels)
self.fuse_layers = self._make_fuse_layers()
self.relu = nn.ReLU(True)
def _check_branches(self, num_branches, blocks, num_blocks,
num_inchannels, num_channels):
if num_branches != len(num_blocks):
error_msg = 'NUM_BRANCHES({}) <> NUM_BLOCKS({})'.format(
num_branches, len(num_blocks))
logger.error(error_msg)
raise ValueError(error_msg)
if num_branches != len(num_channels):
error_msg = 'NUM_BRANCHES({}) <> NUM_CHANNELS({})'.format(
num_branches, len(num_channels))
logger.error(error_msg)
raise ValueError(error_msg)
if num_branches != len(num_inchannels):
error_msg = 'NUM_BRANCHES({}) <> NUM_INCHANNELS({})'.format(
num_branches, len(num_inchannels))
logger.error(error_msg)
raise ValueError(error_msg)
def _make_one_branch(self, branch_index, block, num_blocks, num_channels,
stride=1):
# ---------------------------(1) begin---------------------------- #
downsample = None
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
),
)
# ---------------------------(1) end---------------------------- #
# ---------------------------(2) begin---------------------------- #
layers = []
layers.append(
block(
self.num_inchannels[branch_index],
num_channels[branch_index],
stride,
downsample
)
)
# ---------------------------(2) middle---------------------------- #
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]
)
)
# ---------------------------(2) end---------------------------- #
return nn.Sequential(*layers)
def _make_branches(self, num_branches, block, num_blocks, num_channels):
branches = []
for i in range(num_branches):
branches.append(
self._make_one_branch(i, block, num_blocks, num_channels)
)
return nn.ModuleList(branches)
def _make_fuse_layers(self):
# ---------------------------(1) begin---------------------------- #
if self.num_branches == 1:
return None
# ---------------------------(1) end---------------------------- #
num_branches = self.num_branches
num_inchannels = self.num_inchannels
# ---------------------------(2) begin---------------------------- #
fuse_layers = []
for i in range(num_branches if self.multi_scale_output else 1):
fuse_layer = []
for j in range(num_branches):
# ---------------------------(2.1) begin---------------------------- #
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')
)
)
# ---------------------------(2.1) end---------------------------- #
# ---------------------------(2.2) begin---------------------------- #
elif j == i:
fuse_layer.append(None)
# ---------------------------(2.2) end---------------------------- #
# ---------------------------(2.3) begin---------------------------- #
else:
conv3x3s = []
for k in range(i-j):
# ---------------------------(2.3.1) begin---------------------------- #
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)
)
)
# ---------------------------(2.3.1) end---------------------------- #
# ---------------------------(2.3.1) begin---------------------------- #
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)
)
)
# ---------------------------(2.3.1) end---------------------------- #
# ---------------------------(2.3) end---------------------------- #
fuse_layer.append(nn.Sequential(*conv3x3s))
fuse_layers.append(nn.ModuleList(fuse_layer))
# ---------------------------(2) end---------------------------- #
return nn.ModuleList(fuse_layers)
def get_num_inchannels(self):
return self.num_inchannels
def forward(self, x):
# ---------------------------(1) begin---------------------------- #
if self.num_branches == 1:
return [self.branches[0](x[0])]
# ---------------------------(1) end---------------------------- #
# ---------------------------(2) begin---------------------------- #
# ---------------------------(2.1) begin---------------------------- #
for i in range(self.num_branches):
x[i] = self.branches[i](x[i])
# ---------------------------(2.1) end---------------------------- #
# ---------------------------(2.2) begin---------------------------- #
x_fuse = []
for i in range(len(self.fuse_layers)):
y = x[0] if i == 0 else self.fuse_layers[i][0](x[0])
for j in range(1, self.num_branches):
if i == j:
y = y + x[j]
else:
y = y + self.fuse_layers[i][j](x[j])
x_fuse.append(self.relu(y))
# ---------------------------(2.2) end---------------------------- #
# ---------------------------(2) end---------------------------- #
return x_fuse
3. HRNet 整体网络结构