前言
在推理阶段我们往往关心的是训练阶段得到的权重结构是否能够“优秀”,我们在训练阶段往往是不那么在乎训练耗时。如果能有一个trick能够有效的增加ACC,只要在现实可接受 的范围内,那么这个trick可以认定为一个“好trick”。
在训练阶段使用非对称卷积(以下简称ACBlock)块可以做到(替代非1 x 1 的卷积核的卷积层)提高权重的ACC值,在推理阶段还原被替换的卷积层。 ACBlock的使用可以融合到其他网络中,做到即插即用。
原理简介
ACNET精髓部分
如下图所示:我们将每3× 3层替换为由3× 3层、1×3和3×1内核组成的ACB,并对其输出进行求和。训练完成后,我们将每个ACB中的不对称核加入到骨架上,即正方形核的交叉部分,如图所示,将模型转换回与原来相同的结构。在实践中,这种转换是通过使用原始结构构建一个新的模型,并使用转换后的ACNet学习参数对其进行初始化来实现的。
ACNET过程
我们使用滑动窗口来提供具有不同核大小的2D卷积模块。这里我们有三个卷积层,它们的核大小分别为3 × 3,1 × 3和3 × 1,它们的输入相同。
例如,我们只描述左上角和右下角的滑动窗口。可以观察到,保持可加性的关键是三个层可以共享同一个滑动窗口。因此,如果我们将conv2和conv3的核加到conv1的相应位置上,使用得到的核对原始输入进行运算将产生相同的结果,仅使用乘法的分配律(Eq. 5)就可以很容易地验证这一点。
BN和分支融合。假设我是输入特征图M的任意一个变量,对于每个分支,我们首先等效地将批量归一化的参数融合为卷积核和一个偏置项,然后将融合的核和偏置项相加,得到一层。
代码实现
ini
复制代码
import torch from torch import nn class CropLayer(nn.Module): # (- 1,0)表示该层应该裁剪特征映射的第一行和最后一行。(0, -1)对第一列和最后一列进行裁剪 def __init__(self, crop_set): super(CropLayer, self).__init__() self.rows_to_crop = - crop_set[0] self.cols_to_crop = - crop_set[1] assert self.rows_to_crop >= 0 assert self.cols_to_crop >= 0 def forward(self, input): return input[:, :, self.rows_to_crop:-self.rows_to_crop, self.cols_to_crop:-self.cols_to_crop] # 3x3 + 1x3 + 3x1 class ACBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, padding_mode='zeros'): super(ACBlock, self).__init__() # 训练 self.square_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(kernel_size, kernel_size), stride=stride, padding=padding, dilation=dilation, groups=groups, bias=False, padding_mode=padding_mode) self.square_bn = nn.BatchNorm2d(num_features=out_channels) center_offset_from_origin_border = padding - kernel_size // 2 ver_pad_or_crop = (center_offset_from_origin_border + 1, center_offset_from_origin_border) hor_pad_or_crop = (center_offset_from_origin_border, center_offset_from_origin_border + 1) if center_offset_from_origin_border >= 0: self.ver_conv_crop_layer = nn.Identity() ver_conv_padding = ver_pad_or_crop self.hor_conv_crop_layer = nn.Identity() hor_conv_padding = hor_pad_or_crop else: self.ver_conv_crop_layer = CropLayer(crop_set=ver_pad_or_crop) ver_conv_padding = (0, 0) self.hor_conv_crop_layer = CropLayer(crop_set=hor_pad_or_crop) hor_conv_padding = (0, 0) self.ver_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(kernel_size, 1), stride=stride, padding=ver_conv_padding, dilation=dilation, groups=groups, bias=False, padding_mode=padding_mode) self.hor_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(1, kernel_size), stride=stride, padding=hor_conv_padding, dilation=dilation, groups=groups, bias=False, padding_mode=padding_mode) self.ver_bn = nn.BatchNorm2d(num_features=out_channels) self.hor_bn = nn.BatchNorm2d(num_features=out_channels) # forward函数 def forward(self, input): square_outputs = self.square_conv(input) square_outputs = self.square_bn(square_outputs) vertical_outputs = self.ver_conv_crop_layer(input) vertical_outputs = self.ver_conv(vertical_outputs) vertical_outputs = self.ver_bn(vertical_outputs) horizontal_outputs = self.hor_conv_crop_layer(input) horizontal_outputs = self.hor_conv(horizontal_outputs) horizontal_outputs = self.hor_bn(horizontal_outputs) return square_outputs + vertical_outputs + horizontal_outputs if __name__ == "__main__": x = torch.ones(1, 3, 224, 224) ACN_Conv2d = ACBlock(in_channels=3, out_channels=64, kernel_size=3, padding=1) y = ACN_Conv2d(x) print(y.shape)
实验验证
我们使用Alexnet作为模板将ACNET融合进去,进行实验验证。更改的网络如下:
ini
复制代码
import torch import torch.nn as nn from nets import ACBlock class AlexNetAC(nn.Module): def __init__(self, num_classes: int = 1000) -> None: super(AlexNetAC, self).__init__() self.features = nn.Sequential( # nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2), ACBlock(in_channels=3, out_channels=64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # nn.Conv2d(64, 192, kernel_size=5, padding=2), ACBlock(in_channels=64, out_channels=192, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # nn.Conv2d(192, 384, kernel_size=3, padding=1), ACBlock(in_channels=192, out_channels=384, kernel_size=3, padding=1), nn.ReLU(inplace=True), # nn.Conv2d(384, 256, kernel_size=3, padding=1), ACBlock(in_channels=384, out_channels=256, kernel_size=3, padding=1), nn.ReLU(inplace=True), # nn.Conv2d(256, 256, kernel_size=3, padding=1), ACBlock(in_channels=256, out_channels=256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), ) self.avgpool = nn.AdaptiveAvgPool2d((6, 6)) self.classifier = nn.Sequential( nn.Dropout(), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Linear(4096, num_classes), ) def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.features(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.classifier(x) return x
结语
在原论文中讲解到了卷积融合的地方不限于相加,大家可以尝试其他的方式进行融合square_outputs 、 vertical_outputs 和 horizontal_outputs。本人能力有限,路过的各位大神若发现纰漏的地方还望指教一二!感谢!希望本文能够帮助到大家。