关于人工智能:详解Swin-Transformer核心实现经典模型也能快速调优

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理