Lite-HRNet: A Lightweight High-Resolution Network
本文是想制作一个高性能的轻量化HRNet网络,我个人实际使用中会发现,Small HRNet的性能一般会比同量级的UNet要差一些,个人理解是多尺度信息交互不够充分的原因,毕竟原来的交互方式很简单(带步长的卷积进行下采样,双线性插值进行上采样),因此简单对HRNet进行放缩是不能取得较好的trade-off的。
作者首先是在HRNet中引入Shuffle Block,得到了Naive Lite-HRNet,并且在性能和复杂度上取得了不错的tradeoff。通过进一步分析,作者认为Shuffle Block中的1*1 Conv成为了性能瓶颈,因此想解决这个问题。
在HRNet中多个branch独立使用1*1 Conv计算复杂度会比较高,因此,作者想到了首先把多个branch的特征聚合起来,增强后,然后再作为权重分发回原branch,聚合过程中通过Pooling的方法降低feature map的大小,以此来降低整体计算复杂度,分发过程中再重新上采样回原始分辨率。这样一来一方面可以降低计算复杂度,另一方面还能将独立的各个分支的信息聚合起来,引入多尺度交互,以弥补spatial信息的损失。
个人认为,采用类似的思路,在HRNet多尺度特征交互方面再做些文章是可以进一步提升精度。
Human pose estimation一般比较依赖于高分辨率的特征表示以获得较好的性能,但是目前的网络计算量较大,不能称之为一个高效的网络结构,因此,本文想解决的问题就是如何在计算资源受到约束的情况下部署一个高效的高分辨率模型。
通过简单地将ShuffleNet中的Shuffle Block应用于HRNet,即可得到一个轻量级的HRNet,并且可以获得超越MobileNet、ShuffleNet以及Small HRNet的性能,但是Shuffle Blocks中大量使用的1*1 Conv成为了计算瓶颈,因此,如何能替换掉成本较高的1*1 Conv并且保持甚至取得超越其性能是本文要解决的核心问题。
Shuffle Block会将通道首先分为两个部分,其中的一部分会送入一个1*1 Conv 3*3 DepthWise Conv和1*1 Conv中进行增强,处理完后会和另一部分拼接起来,最终会把通道重新shuffle。
通过简单将Stem中的第2个3*3 Conv以及所有的Residual Block替换为Shuffle Block,并且将所有multi-resolution fusion中的Conv替换为Separable Conv,即可得到 Naive Lite-HRNet。
下面是官方代码中Stem的部分的实现,部分需要说明或者注意的地方,已经加上了中文注释:
class Stem(nn.Module):
def __init__(self,
in_channels,
stem_channels,
out_channels,
expand_ratio,
conv_cfg=None,
norm_cfg=dict(type='BN'),
# 是否使用torch.utils.checkpoint用于降低显存使用,与模型实现没有关系,可以忽略
# 可参考博客:https://blog.csdn.net/ONE_SIX_MIX/article/details/93937091
with_cp=False):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.conv_cfg = conv_cfg
self.norm_cfg = norm_cfg
self.with_cp = with_cp
# Stem中的第一个卷积不使用shuffle block
# ConvModule是MMCV中的一个基本卷积模块:conv/norm/activation
self.conv1 = ConvModule(
in_channels=in_channels,
out_channels=stem_channels,
kernel_size=3,
stride=2,
padding=1,
conv_cfg=self.conv_cfg,
norm_cfg=self.norm_cfg,
act_cfg=dict(type='ReLU'))
mid_channels = int(round(stem_channels * expand_ratio))
branch_channels = stem_channels // 2
if stem_channels == self.out_channels:
inc_channels = self.out_channels - branch_channels
else:
inc_channels = self.out_channels - stem_channels
# Shuffle Block中左侧不做增强的分支
self.branch1 = nn.Sequential(
ConvModule(
branch_channels,
branch_channels,
kernel_size=3,
stride=2,
padding=1,
groups=branch_channels,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=None),
ConvModule(
branch_channels,
inc_channels,
kernel_size=1,
stride=1,
padding=0,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=dict(type='ReLU')),
)
# Shuffle Block中右侧增强分支
self.expand_conv = ConvModule(
branch_channels,
mid_channels,
kernel_size=1,
stride=1,
padding=0,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=dict(type='ReLU'))
self.depthwise_conv = ConvModule(
mid_channels,
mid_channels,
kernel_size=3,
stride=2,
padding=1,
groups=mid_channels, # groups=in_channels 深度可分离卷积
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=None)
self.linear_conv = ConvModule(
mid_channels,
branch_channels
if stem_channels == self.out_channels else stem_channels,
kernel_size=1,
stride=1,
padding=0,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=dict(type='ReLU'))
def forward(self, x):
def _inner_forward(x):
x = self.conv1(x)
x1, x2 = x.chunk(2, dim=1)
x2 = self.expand_conv(x2)
x2 = self.depthwise_conv(x2)
x2 = self.linear_conv(x2)
out = torch.cat((self.branch1(x1), x2), dim=1)
out = channel_shuffle(out, 2) # shuffle channel
return out
if self.with_cp and x.requires_grad:
out = cp.checkpoint(_inner_forward, x)
else:
out = _inner_forward(x)
return out
1*1 Conv的计算复杂度为 Θ ( C 2 ) \Theta\left(C^{2}\right) Θ(C2),3*3 DepthWiseConv的计算复杂度为 Θ ( 9 C ) \Theta(9 C) Θ(9C),其中C为通道数目,具体推导参见后文的5. Conclusion部分。
在Shuffle Block中,当 C > 5 C>5 C>5时,两个1*1卷积的计算复杂度就会超过一个depthwise conv的计算复杂度。
为了降低计算复杂度,本文则是提出使用Element-wise weighting operation去代替1*1 Conv。
Y s = W s ⊙ X s \mathrm{Y}_{s}=\mathrm{W}_{s} \odot \mathrm{X}_{s} Ys=Ws⊙Xs
其中 W s W_s Ws是一个3d的tensor,大小为 W s ∗ H s ∗ C s W_s * H_s * C_s Ws∗Hs∗Cs, ⊙ \odot ⊙是点乘符号。
比较不同的一点是,这个权重会从不同分辨率的feature map中计算得到,可以起到一个跨通道、跨分辨率的特征交互的作用。
对于第 s s s个stage来说,其具有 s s s个平行分支,每个分支的分辨率各不相同,相应地其也会有 s s s个weight maps: W 1 , W 2 , … , W s W_{1}, W_{2}, \ldots, W_{s} W1,W2,…,Ws。这 s s s个weight map将由s个分辨率特征图计算而来:
( W 1 , W 2 , … , W s ) = H s ( X 1 , X 2 , … , X s ) \left(\mathrm{W}_{1}, \mathrm{~W}_{2}, \ldots, \mathrm{W}_{s}\right)=\mathcal{H}_{s}\left(\mathrm{X}_{1}, \mathrm{X}_{2}, \ldots, \mathrm{X}_{s}\right) (W1, W2,…,Ws)=Hs(X1,X2,…,Xs)
其中 { X 1 , … , X s } \left\{\mathrm{X}_{1}, \ldots, \mathrm{X}_{s}\right\} {X1,…,Xs}是 s s s个不同resolution的输入, X 1 X_1 X1表示最大的分辨率, X s X_s Xs则是第 s s s个分辨率feature map。
H s \mathcal{H}_{s} Hs操作具体为:
( X 1 ′ , X 2 ′ , … , X s ) → Conv. → R e L U → Conv. → sigmoid → ( W 1 ′ , W 2 ′ , … , W s ′ ) \begin{aligned} \left(\mathrm{X}_{1}^{\prime}, \mathrm{X}_{2}^{\prime}, \ldots, \mathrm{X}_{s}\right) & \rightarrow \text { Conv. } \rightarrow \mathrm{ReLU} \rightarrow \text { Conv. } \rightarrow \text { sigmoid } \ \rightarrow\left(\mathrm{W}_{1}^{\prime}, \mathrm{W}_{2}^{\prime}, \ldots, \mathrm{W}_{s}^{\prime}\right) \end{aligned} (X1′,X2′,…,Xs)→ Conv. →ReLU→ Conv. → sigmoid →(W1′,W2′,…,Ws′)
其中 X 1 ′ = A A P ( X 1 ) \mathrm{X}_{1}^{\prime}=\mathrm{AAP}\left(\mathrm{X}_{1}\right) X1′=AAP(X1),AAP表示Adaptive Average Pooling, X i ′ \mathrm{X}_{i}^{\prime} Xi′维度均为 W s ∗ H s W_s * H_s Ws∗Hs,实际上是对空间域进行了压缩。
这一部分对应的官方代码为:
class CrossResolutionWeighting(nn.Module):
def __init__(self,
channels,
ratio=16,
conv_cfg=None,
norm_cfg=None,
act_cfg=(dict(type='ReLU'), dict(type='Sigmoid'))):
super().__init__()
if isinstance(act_cfg, dict):
act_cfg = (act_cfg, act_cfg)
assert len(act_cfg) == 2
assert mmcv.is_tuple_of(act_cfg, dict)
self.channels = channels
total_channel = sum(channels)
self.conv1 = ConvModule(
in_channels=total_channel,
out_channels=int(total_channel / ratio),
kernel_size=1,
stride=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg[0])
self.conv2 = ConvModule(
in_channels=int(total_channel / ratio),
out_channels=total_channel,
kernel_size=1,
stride=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg[1])
def forward(self, x):
# mini_size即为当前stage中最小分辨率的shape:H_s, W_s
mini_size = x[-1].size()[-2:] # H_s, W_s
# 将所有stage的input均压缩至最小分辨率,由于最小的一个stage的分辨率已经是最小的了
# 因此不需要进行压缩
out = [F.adaptive_avg_pool2d(s, mini_size) for s in x[:-1]] + [x[-1]]
out = torch.cat(out, dim=1)
out = self.conv1(out) # ReLu激活
out = self.conv2(out) # sigmoid激活
out = torch.split(out, self.channels, dim=1)
out = [
# s为原输入
# a为权重,并通过最近邻插值还原回原输入尺度
s * F.interpolate(a, size=s.size()[-2:], mode='nearest')
for s, a in zip(x, out)
]
return out
在引入跨分辨率信息后,本文还引入了一个单分辨率内部空间域的增强操作:
w s = F s ( X s ) \mathbf{w}_{s}=\mathcal{F}_{s}\left(\mathrm{X}_{s}\right) ws=Fs(Xs)
其中 F s ( ⋅ ) \mathcal{F}_{s}(\cdot) Fs(⋅)的具体实现为:
X s → G A P → F C → R e L U → F C → sigmoid → w s \begin{aligned} \mathrm{X}_{s} \rightarrow \mathrm{GAP} \rightarrow\mathrm{FC} \rightarrow \mathrm{ReLU} \rightarrow \mathrm{FC} \rightarrow \text { sigmoid } \rightarrow \mathrm{w}_{s} \end{aligned} Xs→GAP→FC→ReLU→FC→ sigmoid →ws
官方代码中的具体实现为:
class SpatialWeighting(nn.Module):
def __init__(self,
channels,
ratio=16,
conv_cfg=None,
act_cfg=(dict(type='ReLU'), dict(type='Sigmoid'))):
super().__init__()
if isinstance(act_cfg, dict):
act_cfg = (act_cfg, act_cfg)
assert len(act_cfg) == 2
assert mmcv.is_tuple_of(act_cfg, dict)
self.global_avgpool = nn.AdaptiveAvgPool2d(1)
self.conv1 = ConvModule(
in_channels=channels,
out_channels=int(channels / ratio),
kernel_size=1,
stride=1,
conv_cfg=conv_cfg,
act_cfg=act_cfg[0])
self.conv2 = ConvModule(
in_channels=int(channels / ratio),
out_channels=channels,
kernel_size=1,
stride=1,
conv_cfg=conv_cfg,
act_cfg=act_cfg[1])
def forward(self, x):
out = self.global_avgpool(x)
out = self.conv1(out)
out = self.conv2(out)
return x * out
首先作者给出了各个操作的计算复杂度对比:
具体推导可参加下图:
其复杂度的降低主要来源于两个Pooling的操作,将空间尺度压缩了很多。
可以看出,效果还是很不错的,在一众小网络中取得了不错的精度平衡。
本文主要是在做一个高分辨率的轻量化网络,将Shuffle Block迁移进来,并且基于HRNet多尺度信息丰富的特性,加入了多尺度信息交互,并通过pooling的方法,降低了计算复杂度,同时也获得了较好的性能。