源码:github.com/DingXiaoH/A…
论文:arxiv.org/pdf/1908.03…
前言
在推理阶段我们往往关心的是训练阶段得到的权重结构是否能够“优秀”,我们在训练阶段往往是不那么在乎训练耗时。如果能有一个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的任意一个变量,对于每个分支,我们首先等效地将批量归一化的参数融合为卷积核和一个偏置项,然后将融合的核和偏置项相加,得到一层。
代码实现
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融合进去,进行实验验证。更改的网络如下:
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。本人能力有限,路过的各位大神若发现纰漏的地方还望指教一二!感谢!希望本文能够帮助到大家。