当前位置:   article > 正文

2024年3月16日~2024年3月22日周报

2024年3月16日~2024年3月22日周报

一、前言

  上周阅读了D2UNet。

  本周完成了 D2UNet 网络的复现任务。在复现过程中,学习了一些新的概念,比如:可变形卷积、亚像素卷积、残差连接等,对网络的设计与调整有了新的认识。

二、 D 2 U N e t D^{2} UNet D2UNet 网络

在这里插入图片描述

图1. D2UNet 网络结构

2.1 可变形卷积

  可变形卷积的概念最先出自2017年的论文:Deformable Convolutional Networks
  学习参考:可变形卷积(Deformable Conv)原理解析与torch代码实现

2.1.1 普通卷积原理

  传统的卷积操作是将输入的特征图分为一个个与卷积核大小相同的部分,然后进行卷积操作,每部分在特征图上的位置都是固定的。

  例如:图2为普通卷积在输入特征图上进行卷积计算的过程,其中卷积核的大小为33,输入特征图的尺寸在77,将卷积核权重与输入特征图对应位置元素相乘并求和得到输出特征图的元素,按照一定方式滑动窗口即可计算得到整张输出特征图。
在这里插入图片描述

图2. 普通卷积的过程

  对于输入特征图上的任意一点 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)=pnRw(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)
在这里插入图片描述

图3. 3*3卷积核点相对偏移示例图

   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 位置的元素值,由卷积核与输入特征图进行卷积得到。

2.2.1 可变形卷积原理

  传统卷积的卷积核为固定的大小与形状,对于形状规则的物体可能会有更好的效果,如果形变比较复杂的物体呢?

  一般来讲,可采用的做法有:丰富数据集、引入更多复杂形变的样本、使用数据增强等。这里思考一个问题:是否可以采用更加灵活的卷积核呢?
在这里插入图片描述

图4. 标准卷积与可变形卷积的卷积示例

  由上图可知,可变形卷积的采样位置更加符合物体本身的形状和尺寸,而标准卷积的形式却不能做到这一点。可变形卷积顶层特征图中最终的特征点学习了物体的整体特征,这个特征只针对于物体本身,相比于卷积的卷积它更能排除背景噪声的干扰,得到更有用的信息。
在这里插入图片描述

图5. 可变形卷积的不同采样点

  图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)=pnRw(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)

x(p)=qG(q,p)·x(q)=qg(qx,px)·g(qy,py)·x(q)=qmax(0,1|qx,px|)·max(0,1|qypy|)·x(q)
\tag{4} x(p)=qG(q,p)x(q)=qg(qx,px)g(qy,py)x(q)=qmax(0,1qx,px)max(0,1qypy)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)=w1p1+w2p2+w3p3+w4p4(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的偏移量。
在这里插入图片描述

图6. 可变形卷积示意图

  各部分维度表示如下:

  • 输入特征图(input feature map):(batch,H,W,C)
  • offset field:(Batch,H,W,2N)
  • 输出特征图(output feature map):(Batch,H,W,N)

可变形卷积的一些细节:

  • 首先在输入特征图上进行标准卷积得到N个2维的偏移量 ( Δ x , Δ y ) (\Delta x,\Delta y) (Δx,Δy),然后分别对输入特征图上的各个点的值进行修正,假设feature map为P,则 P ( x , y ) = P ( x + Δ x , y + Δ y ) P(x,y) = P(x+\Delta x,y+\Delta y) P(x,y)=P(x+Δx,y+Δy),当 x + Δ x x+\Delta x x+Δx 为小数的时候,需要使用双线性插值计算 P ( x + Δ x , y + Δ y ) P(x+\Delta x,y+\Delta y) P(x+Δx,y+Δy),形成N个特征图,然后再使用N个卷积核进行对应卷积得到输出;
  • 假设输入特征图的维度为(batch,H,W,C),一个batch内的特征图(一个C个)共用一个offset field,一个batch内的每张特征图使用到的偏移量是一样的;
  • 可变形卷积不改变输入特征图的尺寸;

2.1.3 可变形卷积代码实现

  执行操作:

  • 调整inpfeat的大小,使其高度和宽度是原始大小的1/cond_downscale_rate;
  • 将inpfeat和x_main在通道维度上拼接,并通过offset_conv1生成初始偏移量;
  • 如果提供了previous_offset,则将其与初始偏移量在通道维度上拼接,并通过offset_conv2生成最终的偏移量。如果没有提供previous_offset,则直接将初始偏移量传递给offset_conv2;
  • 使用生成的偏移量和可变形卷积dcn处理x_main;
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • nn.GroupNorm(num_groups=32, num_channels=channel, eps=1e-6, affine=True) 解释:
    • GroupNorm是组归一化层,用于对特征图进行归一化;
    • num_groups=32表示将通道数分为32组进行归一化;
    • num_channels=channel是特征图的通道数;
    • eps=1e-6是为了避免除以零而加入的小量;
    • affine=True表示使用可学习的仿射变换(缩放和平移);
    • nn.SiLU(inplace=True):SiLU(或称为Swish)是一种非线性激活函数;inplace=True意味着操作会在输入数据上进行,节省内存
  • nn.Conv2d(channel, channel, groups=channel, kernel_size=7, padding=3) 解释:
    • 这是一个深度可分离卷积,其中groups=channel意味着每个输入通道都有自己独立的卷积核;
    • padding=3是为了保持特征图的空间尺寸不变;

2.2 亚像素卷积

  亚像素卷积的概念出自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比插值、反卷积、反池化这些上采样方法计算量要更少,因此网络的运行速度会有很大提升,如下图所示:
在这里插入图片描述

图7. 在单个2.0 GHz主频的CPU核心上运行的Set14图像集的平均峰值信噪比(PSNR)和运行时间

  在正常情况下,卷积操作回事feature map 的高和宽变小,当 s t r i d e = 1 r < 1 stride = \frac{1}{r} < 1 stride=r1<1 时,可以让卷积后的feature map的高和宽变大,就实现了分辨率的提升也就是超分辨重建,这个操作叫做sub-pixel convolution。
在这里插入图片描述

图8. 高效亚像素卷积神经网络(ESPCN)包含两个用于特征图提取的卷积层,以及一个亚像素卷积层,该层将来自低分辨率(LR)空间的特征图聚合起来,并在单个步骤中构建超分辨率(SR)图像。

   对于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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  PixelShuffleBlock 类: 这个类是一个包含卷积层和亚像素重排层的模块,用于实现亚像素卷积操作。

  • init(self, in_channel, out_channel, upscale_factor, kernel=3, stride=1, padding=1): 初始化函数,接收输入通道数in_channel、输出通道数out_channel、上采样因子upscale_factor以及卷积层的参数(如卷积核大小kernel、步长stride和填充padding);
    • 定义一个卷积层self.conv,该卷积层的输出通道数是out_channel * upscale_factor ** 2,这是为了后续进行亚像素重排操作;
    • 定义一个亚像素重排层self.ps;
  • forward(self, x): 前向传播函数,接收一个输入张量x。
    • 首先,将输入张量x通过卷积层self.conv进行处理。
    • 然后,将卷积层的输出通过亚像素重排层self.ps进行上采样。
    • 返回处理后的张量。

2.3 残差网络

  网络越深,可获取的信息越多,特征也更加丰富。但是由于网络加深会造成梯度爆炸和梯度消失的问题。ResNet是一种残差网络,可以将它理解为一个子网络,该子网络经过堆叠可以变成一个很深的网络。

  目前针对这种现象已经有了解决的方法:对输入数据和中间层的数据进行归一化操作,这种方法可以保证网络在反向传播中采用随机梯度下降(SGD),从而让网络达到收敛。但是,这个方法仅对几十层的网络有用,当网络再往深处走的时候,这种方法就没什么效果了。

  残差模块是ResNet的基本单元,它包含两个或多个卷积层,以及一个跳跃连接(Skip Connection)。跳跃连接将输入直接与卷积层的输出相加,形成残差连接。这样的设计使得模型可以学习到残差,即剩余的映射,而不仅仅是对输入的变换。

  残差模块结构图如下,Residual Block有两种,一种两层结构,一种三层结构。
在这里插入图片描述

图9. 残差模块结构图

  若把网络层看为是映射函数,在传统的前馈网络中,网络中堆叠的层可以将输入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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 前向传播函数 forward :
    • y = F.relu(self.bn1(self.conv1(x))): 输入张量x首先通过self.conv1卷积层,然后经过批量归一化self.bn1,最后应用ReLU激活函数;
    • y = self.bn2(self.conv2(y)): 激活后的张量y再次通过self.conv2卷积层和批量归一化self.bn2;
    • if self.conv3: x = self.conv3(x): 如果定义了1x1卷积层(即use_1x1conv为True),则输入张量x通过self.conv3进行通道数和空间尺寸的调整;
    • y += x: 这是残差连接的关键部分,原始输入x(可能经过1x1卷积调整)与经过两个卷积层的输出y相加,形成残差;
    • return F.relu(y): 对相加后的残差结果再次应用ReLU激活函数,并返回输出;

三、梯度消失与梯度爆炸

   本周单独运行了DDNet网络,其运行时间相比之前缩减了约40%,效率显著提升。

   另外,在调试InversionNet代码的过程中,感觉出现了梯度消失问题,在网络中加入了亚像素卷积、残差连接进行尝试。

  • 梯度消失的表现:
    • 模型无法从训练数据中获得更新,损失几乎保持不变;
  • 梯度消失出现的可能原因:
    • 在深层网络中,如果激活函数的导数小于1,根据链式求导法则,靠近输入层的参数的梯度因为乘了很多的小于1的数而越来越小,最终就会趋近于0;
    • 网络层次过深、激活函数选择不当等,比如sigmoid函数;
  • 梯度爆炸的表现:
    • 模型不稳定,在更新过程中的损失出现显著变化;
    • 训练过程中,模型损失变成 NaN(表示在训练过程中遇到了某种是数学上的不稳定性,使得模型无法正常更新权重);
  • 梯度爆炸出现的可能原因:
    • 求解损失函数对参数的偏导数时,在梯度的连续乘法中总是遇上很大的绝对值,部分参数的梯度因为乘了很多较大的数而变得非常大,导致模型无法收敛;
    • 网络层次过深,或者权重初始值太大;
  • 梯度消失与梯度爆炸的解决办法:
    • 重新设计网络结构,减少网络层数,调整学习率(梯度消失增大、梯度爆炸减小);
    • 激活函数采用ReLU、Leaky等;
    • 在输入数据和中间层的数据进行Batch Normalization归一化操作;
    • 使用残差模块,DesNet或者LSTM 等结构(避免梯度消失);
    • l1、l2正则化(避免梯度爆炸);
    • 减小学习率、减小batch size(避免梯度爆炸);

   学习参考:梯度消失、梯度爆炸及其表现和解决方法

   但是目前大多数网络使用ReLU激活函数等,也同样会出现梯度消失的问题。因此,还是要具体问题具体分析。

四、小结

  • 下周安排: 参考DDNet网络结构,尝试修改网络结构。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/空白诗007/article/detail/879569
推荐阅读
相关标签
  

闽ICP备14008679号