学习资源站

30-添加DilateFormer(MSDA)注意力机制(中科院一区顶刊_即插即用的多尺度全局注意力机制)

YOLOv5改进系列(29)——添加DilateFormer(MSDA)注意力机制(中科院一区顶刊|即插即用的多尺度全局注意力机制)

 

🚀 一、DilateFormer介绍

学习资料:

1.1 DilateFormer简介 

背景

ViT 模型计算复杂性感受野大小之间的权衡上存在矛盾:

  • 具体来说,ViT 模型使用全局注意力机制,能够在任意图像块之间建立长远距离上下文依赖关系,但是全局感受野带来的是平方级别的计算代价。
  • 同时,在浅层特征上,直接进行全局依赖性建模可能存在冗余,因此是没必要的。

本文方法 

(1)多尺度空洞注意力(MSDA)            

  • MSDA 能够模拟小范围内的局部和稀疏的图像块交互
  • 在浅层次上,注意力矩阵具有局部性稀疏性两个关键属性
  • 这表明在浅层次的语义建模中,远离查询块的块大部分无关
  • 因此,全局注意力模块中存在大量的冗余

(2)DilateFormer 

  • 底层阶段堆叠 MSDA 块
  • 高层阶段使用全局多头自注意力块
  • 这样的设计使得模型能够在处理低级信息时,充分利用局部性和稀疏性,而在处理高级信息时,又能模拟远距离依赖

实现效果

  • 论文通过在不同的视觉任务上进行实验,发现 DilateFormer 模型取得了很好的效果。
  • 在 ImageNet-1K 分类任务上,与现有最优秀的模型相比,DilateFormer 模型的性能相当,但所需的FLOPs(浮点运算次数)减少了70%。
  • 在其它的视觉任务,如 COCO 对象检测/实例分割任务和 ADE20K 语义分割任务,DilateFormer 模型同样取得了优秀的表现。 

1.2  DilateFormer网络结构

① DilateFormer

如上图所示,DilateFormer的整体架构主要由四个阶段构成:

  • 第一阶段第二阶段,使用 MSDA
  • 第三阶段第四阶段,使用普通的多头自注意力(MHSA)

对于图像输入:DilateFormer 首先使用重叠的分词器进行 patch 嵌入,然后通过交替控制卷积核的步长大小(1或2)来调整输出特征图的分辨率

对于前一阶段的 patches,采用了一个重叠的下采样器,具有重叠的内核大小为 3,步长为 2

整个模型的每一部分都使用了条件位置嵌入(CPE)来使位置编码适应不同分辨率的输入。

代码:

  1. class DilateBlock(nn.Module):
  2. "Implementation of Dilate-attention block"
  3. def __init__(self, dim, num_heads, mlp_ratio=4., qkv_bias=False,qk_scale=None, drop=0., attn_drop=0.,
  4. drop_path=0.,act_layer=nn.GELU, norm_layer=nn.LayerNorm, kernel_size=3, dilation=[1, 2, 3],
  5. cpe_per_block=False):
  6. super().__init__()
  7. self.dim = dim # 表示输入特征的维度
  8. self.num_heads = num_heads # 表示多头注意力机制中的注意力头数
  9. self.mlp_ratio = mlp_ratio # MLP(多层感知机)隐藏层维度与输入维度之比
  10. self.kernel_size = kernel_size
  11. self.dilation = dilation # 卷积核的膨胀率,指定每一层的膨胀率
  12. self.cpe_per_block = cpe_per_block # 是否在每个块中使用位置嵌入
  13. if self.cpe_per_block:
  14. self.pos_embed = nn.Conv2d(dim, dim, 3, padding=1, groups=dim)
  15. self.norm1 = norm_layer(dim)
  16. self.attn = MultiDilatelocalAttention(dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale,
  17. attn_drop=attn_drop, kernel_size=kernel_size, dilation=dilation)
  18. '''
  19. : qkv_bias:是否允许注意力机制中的查询、键、值的偏置。
  20. : qk_scale:注意力机制中查询和键的缩放因子。
  21. : drop:MLP中的dropout概率。
  22. : attn_drop:注意力机制中的dropout概率。
  23. '''
  24. self.drop_path = DropPath(
  25. drop_path) if drop_path > 0. else nn.Identity()
  26. self.norm2 = norm_layer(dim)
  27. mlp_hidden_dim = int(dim * mlp_ratio)
  28. self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim,
  29. act_layer=act_layer, drop=drop)
  30. def forward(self, x):
  31. if self.cpe_per_block:
  32. x = x + self.pos_embed(x)
  33. x = x.permute(0, 2, 3, 1)
  34. x = x + self.drop_path(self.attn(self.norm1(x)))
  35. x = x + self.drop_path(self.mlp(self.norm2(x)))
  36. x = x.permute(0, 3, 1, 2)
  37. #B, C, H, W
  38. return x

过程如下: 

  1. 首先,如果启用了位置嵌入(cpe_per_block为true),则将输入张量与通过nn.Conv2d实现的位置嵌入相加。
  2. 接着,将输入张量维度进行调整,将通道维度放到最后。
  3. 然后,通过多头膨胀局部注意力机制处理输入,再通过MLP处理上一步的输出。
  4. 最后,再次调整张量维度,并返回处理后的结果。

② MSDA

MSAD 模块是DilateFormer 的核心部分。

如上图所示,MSDA 模块同样采用多头的设计:将特征图的通道分为 n 个不同的头部,并在不同的头部使用不同的空洞率执行滑动窗口膨胀注意力(SWDA)

目的:这样可以在被关注的感受野内的各个尺度上聚合语义信息,并有效地减少自注意力机制的冗余,无需复杂的操作和额外的计算成本。

具体的操作如下:

  1. 对于每个头部,都会有一个独立的膨胀率r_{i}
  2. 从特征图中获取切片Q_{i}K_{i}V_{i},执行 SWDA,得到输出h_{i}
  3. ​将所有头部的输出连接在一起,然后通过一个线性层进行特征聚合

代码:

  1. class DilateAttention(nn.Module):
  2. "Implementation of Dilate-attention"
  3. def __init__(self, head_dim, qk_scale=None, attn_drop=0, kernel_size=3, dilation=1):
  4. super().__init__()
  5. self.head_dim = head_dim # 注意力头的维度
  6. self.scale = qk_scale or head_dim ** -0.5 # 查询和键的缩放因子,默认为 head_dim ** -0.5。
  7. self.kernel_size=kernel_size
  8. self.unfold = nn.Unfold(kernel_size, dilation, dilation*(kernel_size-1)//2, 1)
  9. self.attn_drop = nn.Dropout(attn_drop) # 注意力机制中的dropout概率
  10. def forward(self,q,k,v):
  11. #B, C//3, H, W
  12. B,d,H,W = q.shape
  13. q = q.reshape([B, d//self.head_dim, self.head_dim, 1 ,H*W]).permute(0, 1, 4, 3, 2) # B,h,N,1,d
  14. k = self.unfold(k).reshape([B, d//self.head_dim, self.head_dim, self.kernel_size*self.kernel_size, H*W]).permute(0, 1, 4, 2, 3) #B,h,N,d,k*k
  15. attn = (q @ k) * self.scale # B,h,N,1,k*k
  16. attn = attn.softmax(dim=-1)
  17. attn = self.attn_drop(attn)
  18. v = self.unfold(v).reshape([B, d//self.head_dim, self.head_dim, self.kernel_size*self.kernel_size, H*W]).permute(0, 1, 4, 3, 2) # B,h,N,k*k,d
  19. x = (attn @ v).transpose(1, 2).reshape(B, H, W, d)
  20. return x
  21. class MultiDilatelocalAttention(nn.Module):
  22. "Implementation of Dilate-attention"
  23. def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None,
  24. attn_drop=0.,proj_drop=0., kernel_size=3, dilation=[1, 2, 3]):
  25. super().__init__()
  26. self.dim = dim
  27. self.num_heads = num_heads
  28. head_dim = dim // num_heads
  29. self.dilation = dilation
  30. self.kernel_size = kernel_size
  31. self.scale = qk_scale or head_dim ** -0.5
  32. self.num_dilation = len(dilation)
  33. assert num_heads % self.num_dilation == 0, f"num_heads{num_heads} must be the times of num_dilation{self.num_dilation}!!"
  34. self.qkv = nn.Conv2d(dim, dim * 3, 1, bias=qkv_bias)
  35. self.dilate_attention = nn.ModuleList(
  36. [DilateAttention(head_dim, qk_scale, attn_drop, kernel_size, dilation[i])
  37. for i in range(self.num_dilation)])
  38. self.proj = nn.Linear(dim, dim)
  39. self.proj_drop = nn.Dropout(proj_drop)
  40. def forward(self, x):
  41. B, H, W, C = x.shape
  42. x = x.permute(0, 3, 1, 2)# B, C, H, W
  43. qkv = self.qkv(x).reshape(B, 3, self.num_dilation, C//self.num_dilation, H, W).permute(2, 1, 0, 3, 4, 5)
  44. #num_dilation,3,B,C//num_dilation,H,W
  45. x = x.reshape(B, self.num_dilation, C//self.num_dilation, H, W).permute(1, 0, 3, 4, 2 )
  46. # num_dilation, B, H, W, C//num_dilation
  47. for i in range(self.num_dilation):
  48. x[i] = self.dilate_attention[i](qkv[i][0], qkv[i][1], qkv[i][2])# B, H, W,C//num_dilation
  49. x = x.permute(1, 2, 3, 0, 4).reshape(B, H, W, C)
  50. x = self.proj(x)
  51. x = self.proj_drop(x)
  52. return x

 class DilateAttention

  1. 首先,对输入的qkv进行一些预处理。
  2. 然后,将qk进行矩阵乘法,并使用缩放因子进行缩放,得到的矩阵表示注意力分布。
  3. 接着,对注意力分布进行 softmax、dropout 操作,将注意力分布与v进行矩阵乘法。
  4. 最后,通过一系列的维度调整得到输出张量 x

class MultiDilatelocalAttention 

  1. 首先,对输入张量进行维度调整,将通道维度放到第二个位置。
  2. 然后,使用一个卷积层 self.qkv 对输入张量进行处理,得到包含查询、键、值的张量,再进行维度调整。
  3. 接着,使用一个 nn.ModuleList 包含多个DilateAttention模块,每个模块对输入进行多头膨胀局部注意力机制的处理。
  4. 最后,将处理后的张量经过全连接层和dropout层,得到最终的输出。

🚀二、具体添加方法

2.1 添加顺序 

(1)models/common.py    -->  加入新增的网络结构

(2)     models/yolo.py       -->  设定网络结构的传参细节,将DilateFormer类名加入其中。(当新的自定义模块中存在输入输出维度时,要使用qw调整输出维度)
(3) models/yolov5*.yaml  -->  新建一个文件夹,如yolov5s_DilateFormer.yaml,修改现有模型结构配置文件。(当引入新的层时,要修改后续的结构中的from参数)
(4)         train.py                -->  修改‘--cfg’默认参数,训练时指定模型结构配置文件 


2.2 具体添加步骤  

第①步:在common.py中添加DilateFormer模块 

将下面的DilateFormer代码复制粘贴到common.py文件的末尾。

  1. import torch
  2. import torch.nn as nn
  3. from functools import partial
  4. from timm.models.layers import DropPath, to_2tuple, trunc_normal_
  5. class Mlp(nn.Module):
  6. def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
  7. super().__init__()
  8. out_features = out_features or in_features
  9. hidden_features = hidden_features or in_features
  10. self.fc1 = nn.Linear(in_features, hidden_features)
  11. self.act = act_layer()
  12. self.fc2 = nn.Linear(hidden_features, out_features)
  13. self.drop = nn.Dropout(drop)
  14. def forward(self, x):
  15. x = self.fc1(x)
  16. x = self.act(x)
  17. x = self.drop(x)
  18. x = self.fc2(x)
  19. x = self.drop(x)
  20. return x
  21. class DilateAttention(nn.Module):
  22. "Implementation of Dilate-attention"
  23. def __init__(self, head_dim, qk_scale=None, attn_drop=0, kernel_size=3, dilation=1):
  24. super().__init__()
  25. self.head_dim = head_dim
  26. self.scale = qk_scale or head_dim ** -0.5
  27. self.kernel_size=kernel_size
  28. self.unfold = nn.Unfold(kernel_size, dilation, dilation*(kernel_size-1)//2, 1)
  29. self.attn_drop = nn.Dropout(attn_drop)
  30. def forward(self,q,k,v):
  31. #B, C//3, H, W
  32. B,d,H,W = q.shape
  33. q = q.reshape([B, d//self.head_dim, self.head_dim, 1 ,H*W]).permute(0, 1, 4, 3, 2) # B,h,N,1,d
  34. k = self.unfold(k).reshape([B, d//self.head_dim, self.head_dim, self.kernel_size*self.kernel_size, H*W]).permute(0, 1, 4, 2, 3) #B,h,N,d,k*k
  35. attn = (q @ k) * self.scale # B,h,N,1,k*k
  36. attn = attn.softmax(dim=-1)
  37. attn = self.attn_drop(attn)
  38. v = self.unfold(v).reshape([B, d//self.head_dim, self.head_dim, self.kernel_size*self.kernel_size, H*W]).permute(0, 1, 4, 3, 2) # B,h,N,k*k,d
  39. x = (attn @ v).transpose(1, 2).reshape(B, H, W, d)
  40. return x
  41. class MultiDilatelocalAttention(nn.Module):
  42. "Implementation of Dilate-attention"
  43. def __init__(self, dim, num_heads=4, qkv_bias=False, qk_scale=None,
  44. attn_drop=0.,proj_drop=0., kernel_size=3, dilation=[1, 2]):
  45. super().__init__()
  46. self.dim = dim
  47. self.num_heads = num_heads
  48. head_dim = dim // num_heads
  49. self.dilation = dilation
  50. self.kernel_size = kernel_size
  51. self.scale = qk_scale or head_dim ** -0.5
  52. self.num_dilation = len(dilation)
  53. assert num_heads % self.num_dilation == 0, f"num_heads{num_heads} must be the times of num_dilation{self.num_dilation}!!"
  54. self.qkv = nn.Conv2d(dim, dim * 3, 1, bias=qkv_bias)
  55. self.dilate_attention = nn.ModuleList(
  56. [DilateAttention(head_dim, qk_scale, attn_drop, kernel_size, dilation[i])
  57. for i in range(self.num_dilation)])
  58. self.proj = nn.Linear(dim, dim)
  59. self.proj_drop = nn.Dropout(proj_drop)
  60. def forward(self, x):
  61. x = x.permute(0, 3, 1, 2) # B, C, H, W
  62. B, C, H, W = x.shape
  63. qkv = self.qkv(x).reshape(B, 3, self.num_dilation, C //self.num_dilation, H, W).permute(2, 1, 0, 3, 4, 5)
  64. #num_dilation,3,B,C//num_dilation,H,W
  65. x = x.reshape(B, self.num_dilation, C//self.num_dilation, H, W).permute(1, 0, 3, 4, 2 )
  66. # num_dilation, B, H, W, C//num_dilation
  67. for i in range(self.num_dilation):
  68. x[i] = self.dilate_attention[i](qkv[i][0], qkv[i][1], qkv[i][2])# B, H, W,C//num_dilation
  69. x = x.permute(1, 2, 3, 0, 4).reshape(B, H, W, C)
  70. x = self.proj(x)
  71. x = self.proj_drop(x)
  72. return x
  73. class DilateBlock(nn.Module):
  74. "Implementation of Dilate-attention block"
  75. def __init__(self, dim, num_heads=4, mlp_ratio=4., qkv_bias=False,qk_scale=None, drop=0., attn_drop=0.,
  76. drop_path=0.,act_layer=nn.GELU, norm_layer=nn.LayerNorm, kernel_size=3, dilation=[1, 2],
  77. cpe_per_block=False):
  78. super().__init__()
  79. self.dim = dim
  80. self.num_heads = num_heads
  81. self.mlp_ratio = mlp_ratio
  82. self.kernel_size = kernel_size
  83. self.dilation = dilation
  84. self.cpe_per_block = cpe_per_block
  85. if self.cpe_per_block:
  86. self.pos_embed = nn.Conv2d(dim, dim, 3, padding=1, groups=dim)
  87. self.norm1 = norm_layer(dim)
  88. self.attn = MultiDilatelocalAttention(dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale,
  89. attn_drop=attn_drop, kernel_size=kernel_size, dilation=dilation)
  90. self.drop_path = DropPath(
  91. drop_path) if drop_path > 0. else nn.Identity()
  92. def forward(self, x):
  93. x = x.permute(0, 3, 2, 1)
  94. x = x + self.drop_path(self.attn(self.norm1(x)))
  95. x = x.permute(0, 3, 2, 1)
  96. #B, C, H, W
  97. return x
  98. def autopad(k, p=None, d=1): # kernel, padding, dilation
  99. # Pad to 'same' shape outputs
  100. if d > 1:
  101. k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
  102. if p is None:
  103. p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
  104. return p
  105. class Conv(nn.Module):
  106. # Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)
  107. default_act = nn.SiLU() # default activation
  108. def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
  109. super().__init__()
  110. self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
  111. self.bn = nn.BatchNorm2d(c2)
  112. self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
  113. def forward(self, x):
  114. return self.act(self.bn(self.conv(x)))
  115. def forward_fuse(self, x):
  116. return self.act(self.conv(x))
  117. class C3_DilateBlock(nn.Module):
  118. # CSP Bottleneck with 3 convolutions
  119. def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
  120. super().__init__()
  121. c_ = int(c2 * e) # hidden channels
  122. self.cv1 = Conv(c1, c_, 1, 1)
  123. self.cv2 = Conv(c1, c_, 1, 1)
  124. self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
  125. self.m = nn.Sequential(*(DilateBlock(c_) for _ in range(n)))
  126. def forward(self, x):
  127. return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))

第②步:修改yolo.py文件 

首先找到yolo.py里面parse_model函数的这一行

加入 DilateBlockC3_DilateBlock 这两个模块 


第③步:创建自定义的yaml文件    

第1种,在SPPF前单独加一层 

  1. # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
  2. # Parameters
  3. nc: 80 # number of classes
  4. depth_multiple: 0.33 # model depth multiple
  5. width_multiple: 0.50 # layer channel multiple
  6. anchors:
  7. - [10,13, 16,30, 33,23] # P3/8
  8. - [30,61, 62,45, 59,119] # P4/16
  9. - [116,90, 156,198, 373,326] # P5/32
  10. # YOLOv5 v6.0 backbone
  11. backbone:
  12. # [from, number, module, args]
  13. [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
  14. [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
  15. [-1, 3, C3, [128]],
  16. [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
  17. [-1, 6, C3, [256]],
  18. [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
  19. [-1, 9, C3, [512]],
  20. [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
  21. [-1, 3, C3, [1024]],
  22. [-1, 3, DilateBlock, [1024]],
  23. [-1, 1, SPPF, [1024, 5]], # 9
  24. ]
  25. # YOLOv5 v6.0 head
  26. head:
  27. [[-1, 1, Conv, [512, 1, 1]],
  28. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  29. [[-1, 6], 1, Concat, [1]], # cat backbone P4
  30. [-1, 3, C3, [512, False]], # 13
  31. [-1, 1, Conv, [256, 1, 1]],
  32. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  33. [[-1, 4], 1, Concat, [1]], # cat backbone P3
  34. [-1, 3, C3, [256, False]], # 17 (P3/8-small)
  35. [-1, 1, Conv, [256, 3, 2]],
  36. [[-1, 14], 1, Concat, [1]], # cat head P4
  37. [-1, 3, C3, [512, False]], # 20 (P4/16-medium)
  38. [-1, 1, Conv, [512, 3, 2]],
  39. [[-1, 10], 1, Concat, [1]], # cat head P5
  40. [-1, 3, C3, [1024, False]], # 23 (P5/32-large)
  41. [[18, 21, 24], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
  42. ]

第2种,替换C3模块 

  1. # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
  2. # Parameters
  3. nc: 80 # number of classes
  4. depth_multiple: 0.33 # model depth multiple
  5. width_multiple: 0.50 # layer channel multiple
  6. anchors:
  7. - [10,13, 16,30, 33,23] # P3/8
  8. - [30,61, 62,45, 59,119] # P4/16
  9. - [116,90, 156,198, 373,326] # P5/32
  10. # YOLOv5 v6.0 backbone
  11. backbone:
  12. # [from, number, module, args]
  13. [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
  14. [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
  15. [-1, 3, C3, [128]],
  16. [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
  17. [-1, 6, C3, [256]],
  18. [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
  19. [-1, 9, C3, [512]],
  20. [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
  21. [-1, 3, C3, [1024]],
  22. [-1, 1, SPPF, [1024, 5]], # 9
  23. ]
  24. # YOLOv5 v6.0 head
  25. head:
  26. [[-1, 1, Conv, [512, 1, 1]],
  27. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  28. [[-1, 6], 1, Concat, [1]], # cat backbone P4
  29. [-1, 3, C3_DilateBlock, [512, False]], # 13
  30. [-1, 1, Conv, [256, 1, 1]],
  31. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  32. [[-1, 4], 1, Concat, [1]], # cat backbone P3
  33. [-1, 3, C3_DilateBlock, [256, False]], # 17 (P3/8-small)
  34. [-1, 1, Conv, [256, 3, 2]],
  35. [[-1, 14], 1, Concat, [1]], # cat head P4
  36. [-1, 3, C3_DilateBlock, [512, False]], # 20 (P4/16-medium)
  37. [-1, 1, Conv, [512, 3, 2]],
  38. [[-1, 10], 1, Concat, [1]], # cat head P5
  39. [-1, 3, C3_DilateBlock, [1024, False]], # 23 (P5/32-large)
  40. [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
  41. ]

第④步:验证是否加入成功

运行yolo.py

第1种

第2种 

 这样就OK啦!