赞
踩
上周阅读了D2UNet。
本周完成了 D2UNet 网络的复现任务。在复现过程中,学习了一些新的概念,比如:可变形卷积、亚像素卷积、残差连接等,对网络的设计与调整有了新的认识。
可变形卷积的概念最先出自2017年的论文:Deformable Convolutional Networks
学习参考:可变形卷积(Deformable Conv)原理解析与torch代码实现
传统的卷积操作是将输入的特征图分为一个个与卷积核大小相同的部分,然后进行卷积操作,每部分在特征图上的位置都是固定的。
例如:图2为普通卷积在输入特征图上进行卷积计算的过程,其中卷积核的大小为33,输入特征图的尺寸在77,将卷积核权重与输入特征图对应位置元素相乘并求和得到输出特征图的元素,按照一定方式滑动窗口即可计算得到整张输出特征图。
对于输入特征图上的任意一点
p
0
p_0
p0,卷积操作可表示为:
y
(
p
0
)
=
∑
p
n
∈
R
w
(
p
n
)
∗
x
(
p
0
+
p
n
)
(1)
y(p_0) = \sum_{p_{n} \in R} w(p_n) * x(p_0 + p_n) \tag{1}
y(p0)=pn∈R∑w(pn)∗x(p0+pn)(1)
其中,
p
n
p_n
pn 表示卷积核中每一个点相对于中心点的偏移量,可用如下公式表示(3*3的卷积核为例):
R
=
{
(
−
1
,
−
1
)
,
(
−
1
,
0
)
,
…
,
(
0
,
0
)
,
…
,
(
1
,
0
)
,
(
1
,
1
)
}
(2)
R = \{(-1,-1),(-1,0),\dots,(0,0),\dots,(1,0),(1,1)\} \tag{2}
R={(−1,−1),(−1,0),…,(0,0),…,(1,0),(1,1)}(2)
w ( p n ) w(p_n) w(pn) 表示卷积核相对位置的权重, x ( p 0 + p n ) x(p_0 + p_n) x(p0+pn) 表示输入特征图上 p 0 + p n p_0+p_n p0+pn 位置处的元素值, y ( p 0 ) y(p_0) y(p0) 表示输出特征图上 p 0 p_0 p0 位置的元素值,由卷积核与输入特征图进行卷积得到。
传统卷积的卷积核为固定的大小与形状,对于形状规则的物体可能会有更好的效果,如果形变比较复杂的物体呢?
一般来讲,可采用的做法有:丰富数据集、引入更多复杂形变的样本、使用数据增强等。这里思考一个问题:是否可以采用更加灵活的卷积核呢?
由上图可知,可变形卷积的采样位置更加符合物体本身的形状和尺寸,而标准卷积的形式却不能做到这一点。可变形卷积顶层特征图中最终的特征点学习了物体的整体特征,这个特征只针对于物体本身,相比于卷积的卷积它更能排除背景噪声的干扰,得到更有用的信息。
图5中(a)是常见的3*3卷积核的采样方式,(b)是采样可变形卷积,加上偏移量后采样点的变化,(c)和(d)是可变形卷积的特殊形式。
因此,可变形卷积的原理是基于一个网络学习offset(偏移),使得卷积核在输入特征图上的采样点发生偏移,集中于感兴趣的区域或者目标。
可变形卷积在公式1的基础上为每个点引入了一个偏移量,偏移量是由输入特征图与另一个卷积生成的,通常是小数,可变形卷积的操作公式如下:
y
(
p
0
)
=
∑
p
n
∈
R
w
(
p
n
)
∗
x
(
p
0
+
p
n
+
Δ
p
n
)
(3)
y(p_0) = \sum_{p_n \in R} w(p_n) * x(p_0 + p_n + \Delta p_n) \tag{3}
y(p0)=pn∈R∑w(pn)∗x(p0+pn+Δpn)(3)
其中, Δ p n ) \Delta p_n) Δpn) 表示偏移量。
由于加入偏移量后的位置一般为小数,并不对应输入特征图上实际的像素点,因此需要使用插值来得到偏移后的像素值,通常可采用双线性插值,用公式表示如下:
x
(
p
)
=
∑
q
G
(
q
,
p
)
⋅
x
(
q
)
=
∑
q
g
(
q
x
,
p
x
)
⋅
g
(
q
y
,
p
y
)
⋅
x
(
q
)
=
∑
q
max
(
0
,
1
−
∣
q
x
,
p
x
∣
)
⋅
max
(
0
,
1
−
∣
q
y
−
p
y
∣
)
⋅
x
(
q
)
(4)
其中,公式中最后一行的 max ( 0 , 1 − . . . ) \max(0, 1-...) max(0,1−...)限制了插值点与邻域点不超过1个像素的距离。双线性插值是指对图像进行缩放,假设原始图像的大小为 m × n m \times n m×n,目标图像的大小为 a × b a \times b a×b,那么两幅图像的边长之比为 m / a , n / b m / a , n / b m/a,n/b。
这里需要注意的是,这个比例通常不是整数,而非整数的坐标无法在像图这类离散数据上使用。双线性插值通过寻找距离对应坐标最贱的四个像素点,来计算该点的值。也可以理解为将插值点位置的像素值设置为其四个邻域像素点的加权和。比如,对应的坐标为(2.5,4.5),那么最近的像素为(2,4)(2,5)(3,4)(3,5)。如果图像为灰度图像,那么
(
i
,
j
)
(i,j)
(i,j) 的灰度值可以通过以下公式计算:
f
(
i
,
j
)
=
w
1
∗
p
1
+
w
2
∗
p
2
+
w
3
∗
p
3
+
w
4
∗
p
4
(5)
f(i,j) = w_1*p_1+w_2*p_2+w_3*p_3+w_4*p_4 \tag{5}
f(i,j)=w1∗p1+w2∗p2+w3∗p3+w4∗p4(5)
其中,
p
i
,
(
i
=
1
,
2
,
3
,
4
)
p_i,(i=1,2,3,4)
pi,(i=1,2,3,4) 是最近的四个像素点,
w
i
=
1
,
2
,
3
,
4
w_i = 1,2,3,4
wi=1,2,3,4 为各点的相应权重,每个点的加权权重需要根据它与插值点横、纵坐标的距离来设置,最终得到插值点的像素值。
需要注意的是,可变形卷积的offset(偏移)是额外使用一个卷积来生成的,与最终要完成可变形卷积操作的卷积不是同一个。下图所示为可变形卷积的示意图,其中: N N N 表示卷积核区域的大小(卷积核的个数),若卷积核大小为3*3,则 N = 9 N=9 N=9。图中绿色过程为卷积学习偏移的过程,offset field 的通道大小为 2 N 2N 2N,表示卷积核分别学习x方向与y方向的偏移量。
2N的解释:对于每一个点来说需要n个周围点作为输入,每一个点都有x,y两个坐标,均包含偏移。所以代表一个点的n个输入点的每一个x和y的偏移量。
各部分维度表示如下:
可变形卷积的一些细节:
执行操作:
class TextureWarpingModule(nn.Module): def __init__(self, channel, cond_channels, cond_downscale_rate, deformable_groups, previous_offset_channel=0): """ :param channel: 主要特征图和偏移量的通道数 :param cond_channels: 条件特征图的通道数 :param cond_downscale_rate: 条件特征图的下采样比率 :param deformable_groups: 可变形卷积的分组数 :param previous_offset_channel: 先前偏移量的通道数(默认为0) """ super(TextureWarpingModule, self).__init__() self.cond_downscale_rate = cond_downscale_rate self.offset_conv1 = nn.Sequential( nn.Conv2d(channel + cond_channels, channel, kernel_size=1), nn.GroupNorm(num_groups=32, num_channels=channel, eps=1e-6, affine=True), nn.SiLU(inplace=True), nn.Conv2d(channel, channel, groups=channel, kernel_size=7, padding=3), nn.GroupNorm(num_groups=32, num_channels=channel, eps=1e-6, affine=True), nn.SiLU(inplace=True), nn.Conv2d(channel, channel, kernel_size=1)) self.offset_conv2 = nn.Sequential( nn.Conv2d(channel + previous_offset_channel, channel, 3, 1, 1), nn.GroupNorm(num_groups=32, num_channels=channel, eps=1e-6, affine=True), nn.SiLU(inplace=True)) self.dcn = DCNv2Pack(channel, channel, 3, padding=1, deformable_groups=deformable_groups) def forward(self, x_main, inpfeat, previous_offset=None): """ :param x_main: 主要特征图 :param inpfeat: 条件特征图 :param previous_offset: 可选的先前偏移量(默认为0) :return: """ _, _, h, w = inpfeat.shape inpfeat = F.interpolate( inpfeat, size=(h // self.cond_downscale_rate, w // self.cond_downscale_rate), mode='bilinear', align_corners=False) offset = self.offset_conv1(torch.cat([inpfeat, x_main], dim=1)) if previous_offset is None: offset = self.offset_conv2(offset) else: offset = self.offset_conv2(torch.cat([offset, previous_offset], dim=1)) warp_feat = self.dcn(x_main, offset) return warp_feat, offset
亚像素卷积的概念出自2016年的论文:Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network
ESPCN是2016提出的,是经典的超分辨率重建算法文章,可以应用于图像压缩解码端重构图像。它的效果和现在的文章相比不算好,但是所提出的Efficient Sub-pixel Convolution,也叫亚像素卷积/子像素卷积为后面网络PSNR的提升做出了很大贡献,关键这个Sub-pixel Convolution比插值、反卷积、反池化这些上采样方法计算量要更少,因此网络的运行速度会有很大提升,如下图所示:
在正常情况下,卷积操作回事feature map 的高和宽变小,当
s
t
r
i
d
e
=
1
r
<
1
stride = \frac{1}{r} < 1
stride=r1<1 时,可以让卷积后的feature map的高和宽变大,就实现了分辨率的提升也就是超分辨重建,这个操作叫做sub-pixel convolution。
对于sub-pixel convolution,作者将一个 H × W H × W H×W的低分辨率输入图像(Low Resolution)作为输入,低分辨率图像特征提取完毕后,生成 n 1 n_1 n1个特征图,然后经过中间一堆操作,到该上采样的时候,在最后一个卷积调整层可以通过Sub-pixel操作将其变为 r H × r W rH \times rW rH×rW的高分辨率图像(High Resolution)。但是其实现过程不是直接通过插值等方式产生这个高分辨率图像,而是通过卷积先得到个通道的特征图(特征图大小和输入低分辨率图像一致),然后通过周期筛选(periodic shuffing)的方法得到这个高分辨率的图像,其中r为上采样因子(upscaling factor),也就是图像的扩大倍率。
代码实现:
class PixelShuffleBlock(nn.Module):
def __init__(self, in_channel, out_channel, upscale_factor, kernel=3, stride=1, padding=1):
super(PixelShuffleBlock, self).__init__()
self.conv = nn.Conv2d(in_channel, out_channel * upscale_factor ** 2, kernel, stride, padding)
self.ps = nn.PixelShuffle(upscale_factor)
def forward(self, x):
x = self.ps(self.conv(x))
return x
PixelShuffleBlock 类: 这个类是一个包含卷积层和亚像素重排层的模块,用于实现亚像素卷积操作。
网络越深,可获取的信息越多,特征也更加丰富。但是由于网络加深会造成梯度爆炸和梯度消失的问题。ResNet是一种残差网络,可以将它理解为一个子网络,该子网络经过堆叠可以变成一个很深的网络。
目前针对这种现象已经有了解决的方法:对输入数据和中间层的数据进行归一化操作,这种方法可以保证网络在反向传播中采用随机梯度下降(SGD),从而让网络达到收敛。但是,这个方法仅对几十层的网络有用,当网络再往深处走的时候,这种方法就没什么效果了。
残差模块是ResNet的基本单元,它包含两个或多个卷积层,以及一个跳跃连接(Skip Connection)。跳跃连接将输入直接与卷积层的输出相加,形成残差连接。这样的设计使得模型可以学习到残差,即剩余的映射,而不仅仅是对输入的变换。
残差模块结构图如下,Residual Block有两种,一种两层结构,一种三层结构。
若把网络层看为是映射函数,在传统的前馈网络中,网络中堆叠的层可以将输入x映射为 F ( x ) F(x) F(x),则整体网络的输出为 H ( x ) H(x) H(x),其中 F ( x ) = H ( x ) F(x)=H(x) F(x)=H(x)。但是对于恒等映射函数 f ( x ) = x f(x)=x f(x)=x,即网络的输入与输出相等,直接让这样的层去拟合这样的恒等映射函数会很困难。 f ( x ) = 0 f(x) =0 f(x)=0比较容易训练拟合。因此可以让输出 H ( x ) = F ( x ) + x H(x)=F(x)+x H(x)=F(x)+x,若整体网络 H ( x ) H(x) H(x)需要是恒等映射,只需要把堆叠层拟合为 F ( x ) = 0 F(x) = 0 F(x)=0即可。
假设x为估计值(上一层ResNet输出的特征映射), H ( x ) H(x) H(x) 表示需要求解的映射,也就是观测值。现将该问题转换为求解网络的残差映射函数,也就是 F ( x ) F(x) F(x),其中 F ( x ) = H ( x ) − x F(x) = H(x) - x F(x)=H(x)−x。残差是指观测值与估计值之间的差。
那么求解的问题变为: H ( x ) = F ( x ) + x H(x) = F(x) + x H(x)=F(x)+x。
代码实现:
class ResidualBlock(nn.Module): def __init__(self, in_channels, num_channels, use_1x1conv=False, strides=1): super(ResidualBlock, self).__init__() self.conv1 = nn.Conv2d(in_channels, num_channels, kernel_size=3, stride=strides, padding=1, ) self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2d( in_channels, num_channels, kernel_size=1, stride=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm2d(num_channels) self.bn2 = nn.BatchNorm2d(num_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): y = F.relu(self.bn1(self.conv1(x))) y = self.bn2(self.conv2(y)) if self.conv3: x = self.conv3(x) y += x return F.relu(y)
本周单独运行了DDNet网络,其运行时间相比之前缩减了约40%,效率显著提升。
另外,在调试InversionNet代码的过程中,感觉出现了梯度消失问题,在网络中加入了亚像素卷积、残差连接进行尝试。
学习参考:梯度消失、梯度爆炸及其表现和解决方法。
但是目前大多数网络使用ReLU激活函数等,也同样会出现梯度消失的问题。因此,还是要具体问题具体分析。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。