2020 年,基于自注意力机制的 Vision Transformer 将用于 NLP 畛域的 Transformer 模型胜利地利用到了 CV 畛域的图像分类上,并在 ImageNet 数据集上失去 88.55% 的精度。
然而想要真正地将 Transformer 模型利用到整个 CV 畛域,有两点问题须要解决。1、超高分辨率的图像所带来的计算量问题;2、CV 畛域工作繁多,如语义宰割,指标检测,实力宰割等密集预测型工作。而最后的 Vision Transformer 是不具备多尺度预测的,因而仅在分类一个工作能够很好地工作。
针对第一个问题,通过参考卷积网络的工作形式,以及窗口自注意力模型,Swin Transformer 提出了一种带挪动窗口的自注意力模型。通过串联窗口自注意力运算 (W-MSA) 以及滑动窗口自注意力运算(SW-MSA),使得 Swin Transformer 在取得近乎全局注意力能力的同时,又将计算量从图像大小的平方关系降为线性关系,大大地缩小了运算量,进步了模型推理速度。
针对第二个问题,在每一个模块 (Swin Transformer Block) 中,Swin Transformer 通过特色交融的形式 (PatchMerging,可参考卷积网络里的池化操作) 每次特色抽取之后都进行一次下采样,减少了下一次窗口注意力运算在原始图像上的感触野,从而对输出图像进行了多尺度的特征提取,使得在 CV 畛域的其余密集预测型工作上的体现也是 SOTA。
下图为 paperwithcode 上的截图,截止 2022/1/22 号,Swin Transformer 在各个 CV 工作上仍然出现霸榜状态。在 CV 畛域,个别在某个工作上能够进步 1% 就曾经很了不起了,而 Swin Transformer 则是在各个工作上进步了 2%~3% 的精度。
将 Swin Transformer 外围
制成 SwinT 模块的价值
如下图所示,Swin Transformer 的外围模块就是黄色局部,咱们须要将这个局部制成一个通用的 SwinT 接口,使得更多相熟 CNN 的开发者将 Swin Transformer 利用到 CV 畛域的不同工作中。
这么做的价值有两点:1、Swin Transformer 本身的能力弱小,这个接口将不会过期。 ①实现超大尺寸整张图片的全局注意力运算所须要的超级计算单元短时间内不会呈现(集体开发者也很难领有这种算力),也就是说,窗口注意力仍然能继续应用一到两年;②当初个别认为,简略无效的才是最好的,而 Swin Transformer 的实现则非常简单,很容易让人看懂并记住其工作原理;③实际上,Swin Transformer 也失去了 SOTA,并且胜利地取得了马尔奖,简略与弱小两者加在一起才是能拿马尔奖的起因。
2、实现方便快捷的编程,例如咱们要将 Unet 变成 Swin-Unet,咱们将只须要间接将 Conv2D 模块替换成 SwinT 模块即可。 咱们通常须要在同一个网络中,不仅应用 Swin Transformer 中的块,也会应用到 Conv2D 模块(例如 Swin Transformer 用在下层抽取全局特色,Conv2D 用在上层抽取部分特色),因而咱们要对原 Swin Transformer 模型进行架构上的更改。
挪动窗口为什么能有全局特色抽取的能力
Swin Transformer 中注意力机制是如何运行的,如下图。首先,咱们对每个色彩内的窗口进行自注意力运算,如 [1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16] 每个列表内的元素做自注意力运算。
而后,滑动窗口,能够看作背景黑框在图像上滑动对图像进行的从新切分。
最初,将图像补回原来的大小,这一步是不便代码的编写,并且对窗口中本来不相邻的区域不做注意力运算。留神,窗口是由黑框决定的。也就是说,因为原图像中 [4,7,10,13] 相邻,因而左上角 [4,7,10,13] 一起做注意力运算;而 [16,11,6,1] 本来不相邻,因而右下角 [16],[11],[6],[1] 独自做注意力运算,而 [16],[11] 之间不做注意力运算。左下角 [12,15],[2,5] 各自相邻,因而 [12,15] 做注意力运算,[2,5]做注意力运算 [12,15] 和[2,5]之间不做注意力运算。
通过这两步,美好的事件产生了,咱们首先在第一步建设了 [1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16] 各自窗口之间的分割,而后在第二步建设了 [4,7,10,13] 之间的分割。能够察看到,通过这二步,咱们得以建设 [1,2,3,4,5,6,7,8,9,10,11,12] 之间的分割,滑动窗口 + 原始窗口就如同一个高速通道在图像的左上角和右下角之间建设起了自注意力的分割,从而取得了全局感触野。
咱们能够发现,滑窗和不滑窗两步是缺一不可的。只有两者同时存在,咱们才可能建设全局的注意力。因而,W-MSA 和 SW-MSA 必须作为一个整体一起应用。后续在咱们的 SwinT 模块的源代码中,将应用 W -MSA、SW-MSA 和 PatchMerging 下采样,并将这三局部整合成一个模块。本文章的后续咱们将演示这个接口如何应用,利用这个接口实在地搭建一个 SwinResnet 网络并对其进行性能测试!
SwinT 接口的应用形式
SwinT 接口的源代码能够参考:
https://aistudio.baidu.com/aistudio/projectdetail/3288357
# 导入包,miziha 中含有 SwinT 模块
import paddle
import paddle.nn as nn
import miziha
#创立测试数据
test_data = paddle.ones([2, 96, 224, 224]) #[N, C, H, W]
print(f'输出尺寸:{test_data.shape}')
#创立 SwinT 层
'''
参数:in_channels: 输出通道数,同卷积
out_channels: 输入通道数,同卷积
以下为 SwinT 独有的,相似于卷积中的核大小,步幅,填充等
input_resolution: 输出图像的尺寸大小
num_heads: 多头注意力的头数,应该设置为能被输出通道数整除的值
window_size: 做注意力运算的窗口的大小,窗口越大,运算就会越慢
qkv_bias: qkv 的偏置,默认 None
qk_scale: qkv 的尺度,注意力大小的一个归一化,默认 None #Swin-V1 版本
dropout: 默认 None
attention_dropout: 默认 None
droppath: 默认 None
downsample: 下采样,默认 False,设置为 True 时,输入的图片大小会变为输出的一半
'''
swint1 = miziha.SwinT(in_channels=96, out_channels=256, input_resolution=(224,224), num_heads=8, window_size=7, downsample=False)
swint2 = miziha.SwinT(in_channels=96, out_channels=256, input_resolution=(224,224), num_heads=8, window_size=7, downsample=True)
conv1 = nn.Conv2D(in_channels=96, out_channels=256, kernel_size=3, stride=1, padding=1)
#前向流传,打印输出形态
output1 = swint1(test_data)
output2 = swint2(test_data)
output3 = conv1(test_data)
print(f'SwinT 的输入尺寸:{output1.shape}')
print(f'下采样的 SwinT 的输入尺寸:{output2.shape}') #下采样
print(f'Conv2D 的输入尺寸:{output3.shape}')
运行上述代码,模型将会输入:
输出尺寸:[2, 96, 224, 224]
SwinT 的输入尺寸:[2, 256, 224, 224]
下采样的 SwinT 的输入尺寸:[2, 256, 112, 112]
Conv2D 的输入尺寸:[2, 256, 224, 224]
应用 SwinT 替换 Resnet 中 Conv2D 模型
创立 Swin Resnet 并进行测试!
这部分,咱们理论展现了如何应用 SwinT 来替换掉现有模型中相应的 Conv2D 模块,整个过程对源码改变小。
源码链接:
https://www.paddlepaddle.org.cn/tutorials/projectdetail/3106582#anchor-10
为了展现理论的成果,咱们应用 Cifar10 数据集 (这是一个工作较简略且数据较少的数据集) 对模型精度,速度两方面给出了后果,证实了 SwinT 模块在成果上至多是不差于 Conv2D 的,因为运行整个流程须要 6 个小时,因而没有过多调节超参数避免过拟合。尽管一般的 resnet50 能够调高 batch 来进步速度,然而 batch 大小是与模型正则化无关的一个参数,因而将 batch 都管制在了一个大小进行比照测试。
首先创立卷积批归一化块,在 resnet50 中应用的是 batchnorm,而在 SwinT 模块中曾经自带了 layernorm,因而这块代码不须要做改变。
# ResNet 模型代码
# ResNet 中应用了 BatchNorm 层,在卷积层的前面加上 BatchNorm 以晋升数值稳定性
# 定义卷积批归一化块
class ConvBNLayer(paddle.nn.Layer):
def __init__(self,
num_channels,
num_filters,
filter_size,
stride=1,
groups=1,
act=None):
# num_channels, 卷积层的输出通道数
# num_filters, 卷积层的输入通道数
# stride, 卷积层的步幅
# groups, 分组卷积的组数,默认 groups= 1 不应用分组卷积
super(ConvBNLayer, self).__init__()
# 创立卷积层
self._conv = nn.Conv2D(
in_channels=num_channels,
out_channels=num_filters,
kernel_size=filter_size,
stride=stride,
padding=(filter_size - 1) // 2,
groups=groups,
bias_attr=False)
# 创立 BatchNorm 层
self._batch_norm = paddle.nn.BatchNorm2D(num_filters)
self.act = act
def forward(self, inputs):
y = self._conv(inputs)
y = self._batch_norm(y)
if self.act == 'leaky':
y = F.leaky_relu(x=y, negative_slope=0.1)
elif self.act == 'relu':
y = F.relu(x=y)
return y
这部分咱们定义残差块,残差块是 Resnet 最外围的单元,咱们须要将其中 Conv2D 替换为 SwinT。
# 定义残差块
# 每个残差块会对输出图片做三次卷积,而后跟输出图片进行短接
# 如果残差块中第三次卷积输入特色图的形态与输出不统一,则对输出图片做 1x1 卷积,将其输入形态调整成统一
class BottleneckBlock(paddle.nn.Layer):
def __init__(self,
num_channels,
num_filters,
stride,
resolution,
num_heads=8,
window_size=8,
downsample=False,
shortcut=True):
super(BottleneckBlock, self).__init__()
# 创立第一个卷积层 1x1
self.conv0 = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters,
filter_size=1,
act='relu')
# 创立第二个卷积层 3x3
# self.conv1 = ConvBNLayer(
# num_channels=num_filters,
# num_filters=num_filters,
# filter_size=3,
# stride=stride,
# act='relu')
#如果尺寸为 7x7,启动 cnn,因为这个大小不容易划分等大小窗口了
# 应用 SwinT 进行替换,如下
if resolution == (7,7):
self.swin = ConvBNLayer(num_channels=num_filters,
num_filters=num_filters,
filter_size=3,
stride=1,
act='relu')
else:
self.swin = miziha.SwinT(in_channels=num_filters,
out_channels=num_filters,
input_resolution=resolution,
num_heads=num_heads,
window_size=window_size,
downsample=downsample)
# 创立第三个卷积 1x1,但输入通道数乘以 4
self.conv2 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters * 4,
filter_size=1,
act=None)
# 如果 conv2 的输入跟此残差块的输出数据形态统一,则 shortcut=True
# 否则 shortcut = False,增加 1 个 1x1 的卷积作用在输出数据上,使其形态变成跟 conv2 统一
if not shortcut:
self.short = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters * 4,
filter_size=1,
stride=stride)
self.shortcut = shortcut
self._num_channels_out = num_filters * 4
def forward(self, inputs):
y = self.conv0(inputs)
swin = self.swin(y)
conv2 = self.conv2(swin)
# 如果 shortcut=True,间接将 inputs 跟 conv2 的输入相加
# 否则须要对 inputs 进行一次卷积,将形态调整成跟 conv2 输入统一
if self.shortcut:
short = inputs
else:
short = self.short(inputs)
y = paddle.add(x=short, y=conv2)
y = F.relu(y)
return y
最初,咱们搭建残缺的 SwinResnet。
# 搭建 SwinResnet
class SwinResnet(paddle.nn.Layer):
def __init__(self, num_classes=12):
super().__init__()
depth = [3, 4, 6, 3]
# 残差块中应用到的卷积的输入通道数,图片的尺寸信息,多头注意力参数
num_filters = [64, 128, 256, 512]
resolution_list = [[(56,56),(56,56)],[(56,56),(28,28)],[(28,28),[14,14]],[(14,14),(7,7)]]
num_head_list = [4, 8, 16, 32]
# SwinResnet 的第一个模块,蕴含 1 个 7x7 卷积,前面跟着 1 个最大池化层
#[3, 224, 224]
self.conv = ConvBNLayer(
num_channels=3,
num_filters=64,
filter_size=7,
stride=2,
act='relu')
#[64, 112, 112]
self.pool2d_max = nn.MaxPool2D(
kernel_size=3,
stride=2,
padding=1)
#[64, 56, 56]
# SwinResnet 的第二到第五个模块 c2、c3、c4、c5
self.bottleneck_block_list = []
num_channels = 64
for block in range(len(depth)):
shortcut = False
for i in range(depth[block]):
# c3、c4、c5 将会在第一个残差块应用 downsample=True;其余所有残差块 downsample=False
bottleneck_block = self.add_sublayer('bb_%d_%d' % (block, i),
BottleneckBlock(
num_channels=num_channels,
num_filters=num_filters[block],
stride=2 if i == 0 and block != 0 else 1,
downsample=True if i == 0 and block != 0 else False,
num_heads=num_head_list[block],
resolution=resolution_list[block][0] if i == 0 and block != 0 else resolution_list[block][1],
window_size=7,
shortcut=shortcut))
num_channels = bottleneck_block._num_channels_out
self.bottleneck_block_list.append(bottleneck_block)
shortcut = True
# 在 c5 的输入特色图上应用全局池化
self.pool2d_avg = paddle.nn.AdaptiveAvgPool2D(output_size=1)
# stdv 用来作为全连贯层随机初始化参数的方差
import math
stdv1 = 1.0 / math.sqrt(2048 * 1.0)
stdv2 = 1.0 / math.sqrt(256 * 1.0)
# 创立全连贯层,输入大小为类别数目,通过残差网络的卷积和全局池化后,# 卷积特色的维度是[B,2048,1,1],故最初一层全连贯的输出维度是 2048
self.out = nn.Sequential(nn.Dropout(0.2),
nn.Linear(in_features=2048, out_features=256,
weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Uniform(-stdv1, stdv1))),
nn.LayerNorm(256),
nn.Dropout(0.2),
nn.LeakyReLU(),
nn.Linear(in_features=256,out_features=num_classes,
weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Uniform(-stdv2, stdv2)))
)
def forward(self, inputs):
y = self.conv(inputs)
y = self.pool2d_max(y)
for bottleneck_block in self.bottleneck_block_list:
y = bottleneck_block(y)
y = self.pool2d_avg(y)
y = paddle.reshape(y, [y.shape[0], -1])
y = self.out(y)
return y
应用搭建的网络进行模型的训练
Mode = 0 #批改此处即可训练三个不同的模型
import paddle
import paddle.nn as nn
from paddle.vision.models import resnet50, vgg16, LeNet
from paddle.vision.datasets import Cifar10
from paddle.optimizer import Momentum
from paddle.regularizer import L2Decay
from paddle.nn import CrossEntropyLoss
from paddle.metric import Accuracy
from paddle.vision.transforms import Transpose, Resize, Compose
from model import SwinResnet
# 确保从 paddle.vision.datasets.Cifar10 中加载的图像数据是 np.ndarray 类型
paddle.vision.set_image_backend('cv2')
# 加载模型
resnet = resnet50(pretrained=False, num_classes=10)
import math
stdv1 = 1.0 / math.sqrt(2048 * 1.0)
stdv2 = 1.0 / math.sqrt(256 * 1.0)
#批改 resnet 最初一层,增强模型拟合能力
resnet.fc = nn.Sequential(nn.Dropout(0.2),
nn.Linear(in_features=2048, out_features=256,
weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Uniform(-stdv1, stdv1))),
nn.LayerNorm(256),
nn.Dropout(0.2),
nn.LeakyReLU(),
nn.Linear(in_features=256,out_features=10,
weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Uniform(-stdv2, stdv2)))
)
model = SwinResnet(num_classes=10) if Mode == 0 else resnet
#打包模型
model = paddle.Model(model)
# 创立图像变换
transforms = Compose([Resize((224,224)), Transpose()]) if Mode != 2 else Compose([Resize((32, 32)), Transpose()])
# 应用 Cifar10 数据集
train_dataset = Cifar10(mode='train', transform=transforms)
valid_dadaset = Cifar10(mode='test', transform=transforms)
# 定义优化器
optimizer = Momentum(learning_rate=0.01,
momentum=0.9,
weight_decay=L2Decay(1e-4),
parameters=model.parameters())
# 进行训练前筹备
model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5)))
# 启动训练
model.fit(train_dataset,
valid_dadaset,
epochs=40,
batch_size=80,
save_dir="./output",
num_workers=8)
测试后果剖析
以下 res224 指 Resnet50 输出图像尺寸为 224×224,res32 指 Resnet50 输出图像尺寸为 32×32。
咱们察看到在训练损失和训练集精度上,三个模型 (SwinResnet、res224、res32) 达到的成果靠近;而在测试精度上,SwinResnet 精度达到 80.3%,res224 精度达到 82.9%,res32 精度达到 71.6%。① 在精度上,SwinResnet 与 res224 差距不大,因为这是一个小数据集,所以实际上 SwinResnet 的能力是受限的,而且 SwinResnet 整体精度是简直线性的一个晋升。② 在速度上,SwinResnet 为 950ms 一个 batch,而 res224 是 250ms 一个 batch, 因而运算速度是四倍的差距实际上是能够承受的。
另一方面,咱们也发现,因为 Cifar10 数据集图片大小实际上是 32×32 的,然而将其插值到 224 之后再接 Resnet 比间接接 Resnet 的精度进步了 11.3%。这是一个微小的晋升,只管咱们没有引入任何额定的信息量。一个解释是:因为 Resnet 是用来做 Imagenet 图片分类的,而图像大小为 224×224,因而不适用于 32×32 图片作为模型的输出,只管两张图片的信息量齐全没有差异。这揭示了卷积核查尺寸大小变动的一个不适应性,难以捕获不同尺寸物体的信息,这是因为卷积核固定的大小所造成的。
SwinT 的利用场景
1、应用 SwinT 模块搭建残缺的 Swin Transformer 模型复现论文。
2、能够将现有的骨干为 Conv2D 的模型替换为 SwinT 从而搭建性能更好的网络,如 Swin-Unet,以及在平时各种场景中须要叠加很多层 CNN 能力抽取深度特色的中央,能够将几个 Conv2D 层替换为一个 SwinT。> 3、因为 SwinT 输入输出齐全同 Conv2D,因而也能够用在语义宰割、指标检测等简单工作上。> 4、能够同时应用 SwinT 和 Conv2D 进行模型搭建,在须要提取高级全局特色的时候应用 SwinT 在须要部分信息时应用 Conv2D,非常灵活。
总结
咱们将 Swin Transformer 最外围的模块制作成了 SwinT 接口,应用模式相似 Conv2D。首先,这极大的不便了开发者们进行网络模型的编写,尤其是要自定义模型架构时,并混合应用 Conv2D 和 SwinT;而后,咱们认为 SwinT 接口的内容非常简单并且高效,因而这个接口短期内将不会过期,能够领有时效性上的保障;最初,咱们实在地对该接口进行了测试,证实了该接口的易用性以及精度性能。
我的项目链接:https://aistudio.baidu.com/aist