学习资源站

YOLOv11改进-Conv篇-利用CVPR2024-DynamicConv提出的GhostModule辅助C3k2进行特征融合(全网独家首发)

一、本文介绍

本文给大家带来的改进机制是 CVPR2024 的改进机制 DynamicConv 其是CVPR2024的最新改进机制,这个论文中介绍了一个名为ParameterNet的新型 设计原则 它旨在在大规模视觉预训练模型中增加参数数量,同时尽量不增加浮点运算(FLOPs) ,所以本文的DynamicConv被提出来了,使得网络在保持低 FLOPs 的同时增加参数量,在其提出的时候它也提出了一个新的模块hostModule,我用其魔改C3k2从而达到创新的目的,从而允许这些网络从大规模视觉预训练中获益(本文的机制可以轻量化网络模型)。

欢迎大家订阅我的专栏一起学习YOLO!



二、原理介绍

官方论文地址: 官方论文地址点击此处即可跳转

官方代码地址: 官方代码地址点击此处即可跳转


动态卷积(Dynamic Convolution)是《DynamicConv.pdf》中提出的一种关键技术,旨在增加网络的参数量而几乎不增加额外的浮点运算(FLOPs)。以下是关于动态卷积的主要信息和原理:

主要原理:

1. 动态卷积的定义:
动态卷积通过对每个输入样本动态选择或组合不同的卷积核(称为"experts"),来处理输入数据。这种方法可以视为是对传统卷积操作的扩展,它允许网络根据输入的不同自适应地调整其参数。

2. 参数和计算的动态化:
在动态卷积中,不是为所有输入使用固定的卷积核,而是有多个卷积核(或参数集),并且根据输入的特性动态选择使用哪个核。
这种选择通过一个学习得到的 函数 (例如,使用多层感知机(MLP)和softmax函数)来动态生成控制各个卷积核贡献的权重。

3. 计算过程:
给定输入特征 X ,和一组卷积核 W_1, W_2, ..., W_M ,每个核对应一个专家。
每个专家的贡献由一个动态系数 \alpha_i \alpha_i 控制,这些系数是针对每个输入样本动态生成的。
输出 Y 是所有动态选定的卷积核操作的加权和: Y = \sum_{i=1}^M \alpha_i (X * W_i)
其中 * 表示卷积操作, \alpha_i 是通过一个小型网络(如MLP)动态计算得出的,这个小网络的输入是全局平均池化后的特征。

动态卷积的优点:

  • 参数效率高:通过共享和动态组合卷积核,动态卷积可以在增加极少的计算成本的情况下显著增加模型的参数量。
  • 适应性强:由于卷积核是针对每个输入动态选择的,这种方法可以更好地适应不同的输入特征,理论上可以提高模型的泛化能力。
  • 资源使用优化:动态卷积允许模型在资源有限的环境中(如移动设备)部署更复杂的网络结构,而不会显著增加计算负担。

动态卷积的设计思想突破了传统卷积网络结构的限制,通过动态调整和优化计算资源的使用,实现了在低FLOPs条件下提升网络 性能 的目标,这对于需要在计算资源受限的设备上运行高效 AI 模型的应用场景尤为重要。


三、核心代码

核心代码的使用方式看章节四!

  1. """
  2. An implementation of GhostNet Model as defined in:
  3. GhostNet: More Features from Cheap Operations. https://arxiv.org/abs/1911.11907
  4. The train script of the model is similar to that of MobileNetV3
  5. Original model: https://github.com/huawei-noah/CV-backbones/tree/master/ghostnet_pytorch
  6. """
  7. import math
  8. from functools import partial
  9. import torch
  10. import torch.nn as nn
  11. import torch.nn.functional as F
  12. from timm.layers import drop_path, SqueezeExcite
  13. from timm.models.layers import CondConv2d, hard_sigmoid, DropPath
  14. __all__ = ['C3k2_GhostModule']
  15. _SE_LAYER = partial(SqueezeExcite, gate_fn=hard_sigmoid, divisor=4)
  16. class DynamicConv(nn.Module):
  17. """ Dynamic Conv layer
  18. """
  19. def __init__(self, in_features, out_features, kernel_size=1, stride=1, padding='', dilation=1,
  20. groups=1, bias=False, num_experts=4):
  21. super().__init__()
  22. self.routing = nn.Linear(in_features, num_experts)
  23. self.cond_conv = CondConv2d(in_features, out_features, kernel_size, stride, padding, dilation,
  24. groups, bias, num_experts)
  25. def forward(self, x):
  26. pooled_inputs = F.adaptive_avg_pool2d(x, 1).flatten(1) # CondConv routing
  27. routing_weights = torch.sigmoid(self.routing(pooled_inputs))
  28. x = self.cond_conv(x, routing_weights)
  29. return x
  30. class ConvBnAct(nn.Module):
  31. """ Conv + Norm Layer + Activation w/ optional skip connection
  32. """
  33. def __init__(
  34. self, in_chs, out_chs, kernel_size, stride=1, dilation=1, pad_type='',
  35. skip=False, act_layer=nn.ReLU, norm_layer=nn.BatchNorm2d, drop_path_rate=0., num_experts=4):
  36. super(ConvBnAct, self).__init__()
  37. self.has_residual = skip and stride == 1 and in_chs == out_chs
  38. self.drop_path_rate = drop_path_rate
  39. # self.conv = create_conv2d(in_chs, out_chs, kernel_size, stride=stride, dilation=dilation, padding=pad_type)
  40. self.conv = DynamicConv(in_chs, out_chs, kernel_size, stride, dilation=dilation, padding=pad_type,
  41. num_experts=num_experts)
  42. self.bn1 = norm_layer(out_chs)
  43. self.act1 = act_layer()
  44. def feature_info(self, location):
  45. if location == 'expansion': # output of conv after act, same as block coutput
  46. info = dict(module='act1', hook_type='forward', num_chs=self.conv.out_channels)
  47. else: # location == 'bottleneck', block output
  48. info = dict(module='', hook_type='', num_chs=self.conv.out_channels)
  49. return info
  50. def forward(self, x):
  51. shortcut = x
  52. x = self.conv(x)
  53. x = self.bn1(x)
  54. x = self.act1(x)
  55. if self.has_residual:
  56. if self.drop_path_rate > 0.:
  57. x = drop_path(x, self.drop_path_rate, self.training)
  58. x += shortcut
  59. return x
  60. class GhostModule(nn.Module):
  61. def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, act_layer=nn.ReLU, num_experts=4):
  62. super(GhostModule, self).__init__()
  63. self.oup = oup
  64. init_channels = math.ceil(oup / ratio)
  65. new_channels = init_channels * (ratio - 1)
  66. self.primary_conv = nn.Sequential(
  67. DynamicConv(inp, init_channels, kernel_size, stride, kernel_size // 2, bias=False, num_experts=num_experts),
  68. nn.BatchNorm2d(init_channels),
  69. act_layer() if act_layer is not None else nn.Sequential(),
  70. )
  71. self.cheap_operation = nn.Sequential(
  72. DynamicConv(init_channels, new_channels, dw_size, 1, dw_size // 2, groups=init_channels, bias=False,
  73. num_experts=num_experts),
  74. nn.BatchNorm2d(new_channels),
  75. act_layer() if act_layer is not None else nn.Sequential(),
  76. )
  77. def forward(self, x):
  78. x1 = self.primary_conv(x)
  79. x2 = self.cheap_operation(x1)
  80. out = torch.cat([x1, x2], dim=1)
  81. return out[:, :self.oup, :, :]
  82. class GhostBottleneck(nn.Module):
  83. """ Ghost bottleneck w/ optional SE"""
  84. def __init__(self, in_chs, out_chs, dw_kernel_size=3,
  85. stride=1, act_layer=nn.ReLU, se_ratio=0., drop_path=0., num_experts=4):
  86. super(GhostBottleneck, self).__init__()
  87. has_se = se_ratio is not None and se_ratio > 0.
  88. self.stride = stride
  89. mid_chs = in_chs * 2
  90. # Point-wise expansion
  91. self.ghost1 = GhostModule(in_chs, mid_chs, act_layer=act_layer, num_experts=num_experts)
  92. # Depth-wise convolution
  93. if self.stride > 1:
  94. self.conv_dw = nn.Conv2d(
  95. mid_chs, mid_chs, dw_kernel_size, stride=stride,
  96. padding=(dw_kernel_size - 1) // 2, groups=mid_chs, bias=False)
  97. self.bn_dw = nn.BatchNorm2d(mid_chs)
  98. else:
  99. self.conv_dw = None
  100. self.bn_dw = None
  101. # Squeeze-and-excitation
  102. self.se = _SE_LAYER(mid_chs, se_ratio=se_ratio,
  103. act_layer=act_layer if act_layer is not nn.GELU else nn.ReLU) if has_se else None
  104. # Point-wise linear projection
  105. self.ghost2 = GhostModule(mid_chs, out_chs, act_layer=None, num_experts=num_experts)
  106. # shortcut
  107. if in_chs == out_chs and self.stride == 1:
  108. self.shortcut = nn.Sequential()
  109. else:
  110. self.shortcut = nn.Sequential(
  111. DynamicConv(
  112. in_chs, in_chs, dw_kernel_size, stride=stride,
  113. padding=(dw_kernel_size - 1) // 2, groups=in_chs, bias=False, num_experts=num_experts),
  114. nn.BatchNorm2d(in_chs),
  115. DynamicConv(in_chs, out_chs, 1, stride=1, padding=0, bias=False, num_experts=num_experts),
  116. nn.BatchNorm2d(out_chs),
  117. )
  118. self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
  119. def forward(self, x):
  120. shortcut = x
  121. # 1st ghost bottleneck
  122. x = self.ghost1(x)
  123. # Depth-wise convolution
  124. if self.conv_dw is not None:
  125. x = self.conv_dw(x)
  126. x = self.bn_dw(x)
  127. # Squeeze-and-excitation
  128. if self.se is not None:
  129. x = self.se(x)
  130. # 2nd ghost bottleneck
  131. x = self.ghost2(x)
  132. x = self.shortcut(shortcut) + self.drop_path(x)
  133. return x
  134. class Bottleneck(nn.Module):
  135. """Standard bottleneck."""
  136. def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
  137. """Initializes a standard bottleneck module with optional shortcut connection and configurable parameters."""
  138. super().__init__()
  139. c_ = int(c2 * e) # hidden channels
  140. self.cv1 = Conv(c1, c_, k[0], 1)
  141. self.cv2 = Conv(c_, c2, k[1], 1, g=g)
  142. self.add = shortcut and c1 == c2
  143. def forward(self, x):
  144. """Applies the YOLO FPN to input data."""
  145. return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
  146. def autopad(k, p=None, d=1): # kernel, padding, dilation
  147. """Pad to 'same' shape outputs."""
  148. if d > 1:
  149. k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
  150. if p is None:
  151. p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
  152. return p
  153. class Conv(nn.Module):
  154. """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
  155. default_act = nn.SiLU() # default activation
  156. def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
  157. """Initialize Conv layer with given arguments including activation."""
  158. super().__init__()
  159. self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
  160. self.bn = nn.BatchNorm2d(c2)
  161. self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
  162. def forward(self, x):
  163. """Apply convolution, batch normalization and activation to input tensor."""
  164. return self.act(self.bn(self.conv(x)))
  165. def forward_fuse(self, x):
  166. """Perform transposed convolution of 2D data."""
  167. return self.act(self.conv(x))
  168. class C2f(nn.Module):
  169. """Faster Implementation of CSP Bottleneck with 2 convolutions."""
  170. def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
  171. """Initializes a CSP bottleneck with 2 convolutions and n Bottleneck blocks for faster processing."""
  172. super().__init__()
  173. self.c = int(c2 * e) # hidden channels
  174. self.cv1 = Conv(c1, 2 * self.c, 1, 1)
  175. self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
  176. self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
  177. def forward(self, x):
  178. """Forward pass through C2f layer."""
  179. y = list(self.cv1(x).chunk(2, 1))
  180. y.extend(m(y[-1]) for m in self.m)
  181. return self.cv2(torch.cat(y, 1))
  182. def forward_split(self, x):
  183. """Forward pass using split() instead of chunk()."""
  184. y = list(self.cv1(x).split((self.c, self.c), 1))
  185. y.extend(m(y[-1]) for m in self.m)
  186. return self.cv2(torch.cat(y, 1))
  187. class C3(nn.Module):
  188. """CSP Bottleneck with 3 convolutions."""
  189. def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
  190. """Initialize the CSP Bottleneck with given channels, number, shortcut, groups, and expansion values."""
  191. super().__init__()
  192. c_ = int(c2 * e) # hidden channels
  193. self.cv1 = Conv(c1, c_, 1, 1)
  194. self.cv2 = Conv(c1, c_, 1, 1)
  195. self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
  196. self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=((1, 1), (3, 3)), e=1.0) for _ in range(n)))
  197. def forward(self, x):
  198. """Forward pass through the CSP bottleneck with 2 convolutions."""
  199. return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))
  200. class C3k(C3):
  201. """C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks."""
  202. def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
  203. """Initializes the C3k module with specified channels, number of layers, and configurations."""
  204. super().__init__(c1, c2, n, shortcut, g, e)
  205. c_ = int(c2 * e) # hidden channels
  206. # self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
  207. self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
  208. class C3k2_GhostModule(C2f):
  209. """Faster Implementation of CSP Bottleneck with 2 convolutions."""
  210. def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
  211. """Initializes the C3k2 module, a faster CSP Bottleneck with 2 convolutions and optional C3k blocks."""
  212. super().__init__(c1, c2, n, shortcut, g, e)
  213. self.m = nn.ModuleList(
  214. C3k(self.c, self.c, 2, shortcut, g) if c3k else GhostBottleneck(self.c, self.c) for _ in range(n)
  215. ) # c3k = True用C3k提取特征, False 时用GhostBottleneck融合特征
  216. if __name__ == "__main__":
  217. # Generating Sample image
  218. image_size = (1, 64, 224, 224)
  219. image = torch.rand(*image_size)
  220. # Model
  221. model = C3k2_GhostModule(64, 64)
  222. out = model(image)
  223. print(out.size())


四、手把手教你添加GhostModule机制

4.1 修改一

第一还是建立文件,我们找到如下 ultralytics /nn文件夹下建立一个目录名字呢就是'Addmodules'文件夹( 用群内的文件的话已经有了无需新建) !然后在其内部建立一个新的py文件将核心代码复制粘贴进去即可。


4.2 修改二

第二步我们在该目录下创建一个新的py文件名字为'__init__.py'( 用群内的文件的话已经有了无需新建) ,然后在其内部导入我们的检测头如下图所示。


4.3 修改三

第三步我门中到如下文件'ultralytics/nn/tasks.py'进行导入和注册我们的模块( 用群内的文件的话已经有了无需重新导入直接开始第四步即可)

从今天开始以后的教程就都统一成这个样子了,因为我默认大家用了我群内的文件来进行修改!!


4.4 修改四

按照我的添加在parse_model里添加即可。


到此就修改完成了,大家可以复制下面的yaml文件运行。


五、GhostModule的yaml文件和运行记录

5.1 GhostModule的yaml文件

此版本的训练信息:YOLO11-C3k2-GhostModule summary: 430 layers, 2,599,899 parameters, 2,599,883 gradients, 5.9 GFLOPs

版本信息:

# c3k = True用C3k提取特征, False 时用GhostBottleneck融合特征
  1. # Ultralytics YOLO 🚀, AGPL-3.0 license
  2. # YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
  3. # Parameters
  4. nc: 80 # number of classes
  5. scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
  6. # [depth, width, max_channels]
  7. n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
  8. s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
  9. m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
  10. l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
  11. x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
  12. # YOLO11n backbone
  13. backbone:
  14. # [from, repeats, module, args]
  15. - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  16. - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
  17. - [-1, 2, C3k2_GhostModule, [256, False, 0.25]]
  18. - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  19. - [-1, 2, C3k2_GhostModule, [512, False, 0.25]]
  20. - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  21. - [-1, 2, C3k2_GhostModule, [512, True]]
  22. - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  23. - [-1, 2, C3k2_GhostModule, [1024, True]]
  24. - [-1, 1, SPPF, [1024, 5]] # 9
  25. - [-1, 2, C2PSA, [1024]] # 10
  26. # YOLO11n head
  27. head:
  28. - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  29. - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  30. - [-1, 2, C3k2_GhostModule, [512, False]] # 13
  31. - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  32. - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  33. - [-1, 2, C3k2_GhostModule, [256, False]] # 16 (P3/8-small)
  34. - [-1, 1, Conv, [256, 3, 2]]
  35. - [[-1, 13], 1, Concat, [1]] # cat head P4
  36. - [-1, 2, C3k2_GhostModule, [512, False]] # 19 (P4/16-medium)
  37. - [-1, 1, Conv, [512, 3, 2]]
  38. - [[-1, 10], 1, Concat, [1]] # cat head P5
  39. - [-1, 2, C3k2_GhostModule, [1024, True]] # 22 (P5/32-large)
  40. - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)


5.2 训练代码

大家可以创建一个py文件将我给的代码复制粘贴进去,配置好自己的文件路径即可运行。

  1. import warnings
  2. warnings.filterwarnings('ignore')
  3. from ultralytics import YOLO
  4. if __name__ == '__main__':
  5. model = YOLO('ultralytics/cfg/models/v8/yolov8-C2f-FasterBlock.yaml')
  6. # model.load('yolov8n.pt') # loading pretrain weights
  7. model.train(data=r'替换数据集yaml文件地址',
  8. # 如果大家任务是其它的'ultralytics/cfg/default.yaml'找到这里修改task可以改成detect, segment, classify, pose
  9. cache=False,
  10. imgsz=640,
  11. epochs=150,
  12. single_cls=False, # 是否是单类别检测
  13. batch=4,
  14. close_mosaic=10,
  15. workers=0,
  16. device='0',
  17. optimizer='SGD', # using SGD
  18. # resume='', # 如过想续训就设置last.pt的地址
  19. amp=False, # 如果出现训练损失为Nan可以关闭amp
  20. project='runs/train',
  21. name='exp',
  22. )


5.3 GhostModule的训练过程截图


五、本文总结

到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv11改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充,如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~