论文: https://arxiv.org/abs/2109.00699v1
代码: https://github.com/IVIPLab/FBSNet
FBSNet 的网络结构可分为三部分:
initial block
dual-branch backbone
feature aggregation module
Initial Block
Initial Block 包括三个 的卷积层,并在每一个卷积层之后添加了 Batch Normalization 和 PReLU 激活函数,在三层卷积层结束之后,又进行了一次 BN 和 PReLU
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 class Conv (nn.Module ): def __init__ (self, nIn, nOut, kSize, stride, padding, dilation=(1 , 1 ), groups=1 , bn_acti=False , bias=False ): super ().__init__() self.bn_acti = bn_acti self.conv = nn.Conv2d(nIn, nOut, kernel_size=kSize, stride=stride, padding=padding, dilation=dilation, groups=groups, bias=bias) if self.bn_acti: self.bn_prelu = BNPReLU(nOut) def forward (self, input ): output = self.conv(input ) if self.bn_acti: output = self.bn_prelu(output) return output class BNPReLU (nn.Module ): def __init__ (self, nIn ): super ().__init__() self.bn = nn.BatchNorm2d(nIn, eps=1e-3 ) self.acti = nn.PReLU(nIn) def forward (self, input ): output = self.bn(input ) output = self.acti(output) return output self.init_conv = nn.Sequential( Conv(3 , 16 , 3 , 2 , padding=1 , bn_acti=True ), Conv(16 , 16 , 3 , 1 , padding=1 , bn_acti=True ), Conv(16 , 16 , 3 , 1 , padding=1 , bn_acti=True ), ) self.bn_prelu_1 = BNPReLU(16 )
Conv 1
1 Conv(3 , 16 , 3 , 2 , padding=1 , bn_acti=True )
第一层的输入通道数为 3,输出通道数为 16,卷积核大小为 3,步长为 2,padding 为 1
由于步长为 2,这一层会对图像进行下采样 ,将输入的宽高减半
并通过 16 个 的卷积核对图像特征进行初步提取
在卷积操作后,进行批归一化和 PReLU 激活,提高模型的训练稳定性和非线性表达能力
Conv 2 and Conv 3
1 2 Conv(16 , 16 , 3 , 1 , padding=1 , bn_acti=True ) Conv(16 , 16 , 3 , 1 , padding=1 , bn_acti=True )
第二层和第三层具有相同的结构,通过 16 个 的卷积核提取更高级的图像特征
但是这两层的步长均为 1,不会改变特征图的尺寸
Batch Normalization
BN 是深度学习中一种用于加速神经网络训练,稳定网络训练过程的方法
在训练过程中,随着网络参数的更新,前一层的参数变化会导致后一层的输入分布发生变化,这种现象称为内部协变量偏移(Internal Covariate Shift)
内部协变量偏移会导致训练过程不稳定,收敛速度变慢,需要更小的学习率和更仔细的参数初始化
BN 通过对每个小批次的数据进行归一化 ,使得每一层的输入保持稳定的分布
原理
首先计算批次的均值 和方差
然后使用均值和方差对输入进行归一化,得到零均值 ,单位方差 的输入
这里的 是一个很小的常数, 防止除以零
最后引入**可训练的参数 和 **对输入进行缩放和平移,恢复数据的表达能力
注意事项
BN 通常放在全连接层或卷积层之后,激活函数之前
对批大小敏感,批量太小可能导致估计的均值和方差不准确,影响模型性能
在训练阶段,使用当前批次的数据计算均值和方差,在测试阶段,使用在训练过程中累积的全局均值和方差
PReLU 激活函数
PReLU (Parametric Rectified Linear Unit)是 ReLU(Rectified Linear Unit)激活函数的改进版本,它在 ReLU 的基础上增加了一个可学习的参数 ,用于调整负半轴的斜率。
ReLU
ReLU 激活函数的定义是 ,其简单高效,计算速度快,但是存在以下问题:
PReLU
PReLU 定义如下:
当 当
其中 是一个可学习的参数,初始值通常为一个小的正数
PReLU 由于负半轴的斜率 可学习,且通常不为零,避免了神经元输出恒为零的情况
同时允许负值通过激活函数,保留了输入为负值时的有用信息
dual-branch backbone
dual-branch backbone 采用双分支结构,由 Semantic Information Branch (SIB) 和 Spatial Detail Branch (SDB) 组成
SIB
SIB 整体采用编码器–解码器 结构,通过两个下采样块将特征图尺寸下采样到原始图像尺寸的 1/8,然后通过两个上采样块恢复原始图像尺寸
在下采样和上采样操作中间穿插了 Channel Attention Module (CAM) 和 Bottleneck Residual Unit (BRU)
Channel Attention Module (CAM)
由于通道中包含丰富的特征信息和干扰噪声,SIB 使用 CAM 来强调需要突出的特征,在实现时采用 [ECA-Net](1910.03151 (arxiv.org) ) 中提出的 Efficient Channel Attention Module(ECA) 模块,其过程如下:
其中, 为通道注意力图, 为输入特征, 表示卷积核大小为 的卷积运算
表示平均池化, 表示压缩和重新加权, 为 Sigmoid 激活函数
其 pytorch
实现如下:
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 class eca_layer (nn.Module ): """Constructs a ECA module. Args: channel: Number of channels of the input feature map k_size: Adaptive selection of kernel size """ def __init__ (self, channel, k_size=3 ): super (eca_layer, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1 ) self.conv = nn.Conv1d(1 , 1 , kernel_size=k_size, padding=(k_size - 1 ) // 2 , bias=False ) self.sigmoid = nn.Sigmoid() def forward (self, x ): b, c, h, w = x.size() y = self.avg_pool(x) y = self.conv(y.squeeze(-1 ).transpose(-1 , -2 )).transpose(-1 , -2 ).unsqueeze(-1 ) y = self.sigmoid(y) return x * y.expand_as(x)
首先通过一个自适应的2D平均池化层 ,对输入特征图在空间维度上进行全局平均池化,得到每个通道的全局平均值,将输入特征图的尺寸变为 ,每个通道通过池化被压缩成一个值,输出维度为 [batch_size, channels, 1, 1]
然后通过 y.squeeze(-1).transpose(-1, -2)
去除最后一个维度并交换最后两个维度,得到 [batch_size, 1, channels]
,从而将 1D 卷积操作应用到通道维度
然后再通过 transpose(-1, -2).unsqueeze(-1)
恢复原有形状 [batch_size, channels, 1, 1]
使用Sigmoid
激活函数将输出值归一化到 0 到 1 之间,得到每个通道的权重
最后将得到的权重 y
与输入的特征图 x
相乘完成对通道的加权 ,调整各通道的贡献程度
Bottleneck Residual Unit (BRU)
BRU 是一个三分支模块,左分支负责提取局部信息和短距离信息 ,右分支用于扩大感受野以获取长距离特征信息 ,中间分支专门用于保存输入信息 ,示意图如下:
图中使用的 和 卷积是采用卷积因子化策略 ,将标准的 卷积分解成两个一维卷积核,可以在保证模型性能的同时显著减少模型参数
其代码如下:
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 44 45 46 47 48 49 50 51 52 53 class BRUModule (nn.Module ): def __init__ (self, nIn, d=1 , kSize=3 , dkSize=3 ): super ().__init__() self.bn_relu_1 = BNPReLU(nIn) self.conv1x1_init = Conv(nIn, nIn // 2 , 1 , 1 , padding=0 , bn_acti=True ) self.dconv3x1 = Conv(nIn // 2 , nIn // 2 , (dkSize, 1 ), 1 , padding=(1 , 0 ), groups=nIn // 2 , bn_acti=True ) self.dconv1x3 = Conv(nIn // 2 , nIn // 2 , (1 , dkSize), 1 , padding=(0 , 1 ), groups=nIn // 2 , bn_acti=True ) self.ca11 = eca_layer(nIn // 2 ) self.dconv1x3_l = Conv(nIn // 2 , nIn // 2 , (1 , dkSize), 1 , padding=(0 , 1 ), groups=nIn // 2 , bn_acti=True ) self.dconv3x1_l = Conv(nIn // 2 , nIn // 2 , (dkSize, 1 ), 1 , padding=(1 , 0 ), groups=nIn // 2 , bn_acti=True ) self.ddconv3x1 = Conv(nIn // 2 , nIn // 2 , (dkSize, 1 ), 1 , padding=(1 * d, 0 ), dilation=(d, 1 ), groups=nIn // 2 , bn_acti=True ) self.ddconv1x3 = Conv(nIn // 2 , nIn // 2 , (1 , dkSize), 1 , padding=(0 , 1 * d), dilation=(1 , d), groups=nIn // 2 , bn_acti=True ) self.ca22 = eca_layer(nIn // 2 ) self.ddconv1x3_r = Conv(nIn // 2 , nIn // 2 , (1 , dkSize), 1 , padding=(0 , 1 * d), dilation=(1 , d), groups=nIn // 2 , bn_acti=True ) self.ddconv3x1_r = Conv(nIn // 2 , nIn // 2 , (dkSize, 1 ), 1 , padding=(1 * d, 0 ), dilation=(d, 1 ), groups=nIn // 2 , bn_acti=True ) self.bn_relu_2 = BNPReLU(nIn // 2 ) self.ca0 = eca_layer(nIn // 2 ) self.ca = eca_layer(nIn // 2 ) self.conv1x1 = Conv(nIn // 2 , nIn, 1 , 1 , padding=0 , bn_acti=False ) self.shuffle_end = ShuffleBlock(groups=nIn // 2 ) def forward (self, input ): output = self.bn_relu_1(input ) output = self.conv1x1_init(output) br1 = self.dconv3x1(output) br1 = self.dconv1x3(br1) b1 = self.ca11(br1) br1 = self.dconv1x3_l(b1) br1 = self.dconv3x1_l(br1) br2 = self.ddconv3x1(output) br2 = self.ddconv1x3(br2) b2 = self.ca22(br2) br2 = self.ddconv1x3_r(b2) br2 = self.ddconv3x1_r(br2) output = br1 + br2 + self.ca0(output )+ b1 + b2 output = self.bn_relu_2(output) output = self.conv1x1(output) output = self.ca(output) out = self.shuffle_end(output + input ) return out
首先对输入进行批归一化和 PReLU 激活,经过 卷积减半通道数
左分支
经过 (3,1)
和 (1,3)
的卷积操作,再经过 ECA 注意力层,并重复一次 (1,3)
和 (3,1)
卷积,提取局部信息和短距离信息
右分支
使用膨胀卷积扩展感受野,再经过 ECA 注意力层并重复膨胀卷积,获取长距离特征信息
中间分支
使用 ECA 层增强特征,然后再与左右分支 ECA 层的输出,最后的输出三者进行加和
最后,输出重复开头的过程,进行批归一化和 PReLU 激活并经过 卷积,再通过一个 ECA 层增强特征,然后通过 ShuffleBlock
进行通道混洗
1 2 3 4 5 6 7 8 9 10 11 12 class ShuffleBlock (nn.Module ): def __init__ (self, groups ): super (ShuffleBlock, self).__init__() self.groups = groups def forward (self, x ): '''Channel shuffle: [N,C,H,W] -> [N,g,C/g,H,W] -> [N,C/g,g,H,w] -> [N,C,H,W]''' N, C, H, W = x.size() g = self.groups return x.view(N, g, int (C / g), H, W).permute(0 , 2 , 1 , 3 , 4 ).contiguous().view(N, C, H, W)
ShuffleBlock
将通道维度分为 g 组,每组的通道数为 c/g,然后使用 permute()
方法将通道和组的维度交换,最后恢复原始形状
从而将不同组的通道顺序打乱,使得下一层的卷积能够处理不同组之间混合后的通道,增加特征的表达能力和相互依赖性。
SDB
在 SDB 分支中,使用了 Detail Residual Module (DRM) 和 Spatial Attention Module (SAM)
DRM
DRM 是专门为补充语义分支中丢失的空间细节而设计的,由 3 个 的卷积层和一个 的卷积层构成
为了获得更多特征,将第二和第三个卷积层的通道数增加到原始输入的 4 倍(4C),最后使用一个 的卷积层将通道数再次减少到 C
但是在 FBSNet 开源的代码中,只使用了 3 个 的卷积层
1 2 3 self.conv_sipath1 = Conv(16 , 32 , 3 , 1 , 1 , bn_acti=True ) self.conv_sipath2 = Conv(32 , 128 , 3 , 1 , 1 , bn_acti=True ) self.conv_sipath3 = Conv(128 , 32 , 3 , 1 , 1 , bn_acti=True )
SAM
空间注意力模块 SAM 是沿通道轴应用最大池化和平均池化,然后通过标准卷积对其进行串联 ,从而生成有效的特征描述
其过程可描述如下:
其中, 为所需的空间注意力图, 为输入特征, 表示卷积核大小为 的卷积层, 表示连接操作, 表示平均池化操作, 表示最大池化操作, 为 Sigmoid 函数
其代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class SpatialAttention (nn.Module ): def __init__ (self, kernel_size=7 ): super (SpatialAttention, self).__init__() assert kernel_size in (3 , 7 ), 'kernel size must be 3 or 7' padding = 3 if kernel_size == 7 else 1 self.conv1 = nn.Conv2d(2 , 1 , kernel_size, padding=padding, bias=False ) self.sigmoid = nn.Sigmoid() def forward (self, x ): avg_out = torch.mean(x, dim=1 , keepdim=True ) max_out, _ = torch.max (x, dim=1 , keepdim=True ) x = torch.cat([avg_out, max_out], dim=1 ) x = self.conv1(x) return self.sigmoid(x)
首先沿着通道维度计算每个像素的通道均值 ,得到一个均值特征图,再沿着通道维度计算每个像素的通道最大值 ,得到一个最大值特征图
然后将均值特征图和最大值特征图沿着通道维度进行拼接,生成一个具有两个通道的特征图
对拼接后的特征图应用卷积操作,降维到单个通道,进一步提取空间特征
对卷积后的输出通过 Sigmoid 激活函数,将结果限制在 [0, 1]
之间,形成空间注意力权重矩阵
feature aggregation module
首先将语义分支和空间分支的输出加和,并进行进行批归一化和 PReLU 激活
1 output = self.bn_prelu_8(output + output_sipath)
然后使用 CoordAttention 对特征进行融合,其代码如下
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 44 class CoordAtt (nn.Module ): def __init__ (self, inp, oup, reduction=4 ): super (CoordAtt, self).__init__() self.pool_h = nn.AdaptiveAvgPool2d((None , 1 )) self.pool_w = nn.AdaptiveAvgPool2d((1 , None )) mip = max (8 , inp // reduction) self.conv1 = nn.Conv2d(inp, mip, kernel_size=1 , stride=1 , padding=0 ) self.bn1 = nn.BatchNorm2d(mip) self.act = h_swish() self.conv_h = nn.Conv2d(mip, oup, kernel_size=1 , stride=1 , padding=0 ) self.conv_w = nn.Conv2d(mip, oup, kernel_size=1 , stride=1 , padding=0 ) def forward (self, x ): identity = x n, c, h, w = x.size() x_h = self.pool_h(x) x_w = self.pool_w(x).permute(0 , 1 , 3 , 2 ) y = torch.cat([x_h, x_w], dim=2 ) y = self.conv1(y) y = self.bn1(y) y = self.act(y) x_h, x_w = torch.split(y, [h, w], dim=2 ) x_w = x_w.permute(0 , 1 , 3 , 2 ) a_h = self.conv_h(x_h).sigmoid() a_w = self.conv_w(x_w).sigmoid() out = identity * a_w * a_h return out class h_swish (nn.Module ): def __init__ (self, inplace=True ): super (h_swish, self).__init__() self.sigmoid = h_sigmoid(inplace=inplace) def forward (self, x ): return x * self.sigmoid(x)
首先对于输入特征,分别在 H 和 W 两个方向进行平均池化,得到两个大小分别为 [B, C, H, 1]
和 [B, C, W, 1]
(x_w 使用 permute()
做了转置)的特征图
然后将 x_h
和 x_w
沿着高度和宽度的方向拼接在一起,得到大小为 [B, C, H+W, 1]
的特征图
随后对其进行 卷积、批归一化和激活函数处理,融合特征信息
然后通过 torch.split()
将 y
在拼接的维度上分割,得到分别对应原始的高度和宽度方向的特征图
再分别通过 卷积得到高度和宽度方向的注意力权重,并通过 sigmoid()
激活函数将其压缩到 [0, 1]
范围,表示每个通道在高度和宽度方向的权重
最终的输出是输入乘以高度和宽度的权重,根据权重调整特征图的不同部分