YOLOv11改进 | 独家创新篇 | 结合Dual思想利用HetConv创新一种全新轻量化结构CSPHet(轻量化网络结构必备)

一、本文介绍

本文给大家带来的改进机制是我结合 Dual 的思想利用HetConv提出一种全新的结构 CSPHet ,我们将其用于替换我们的C3k2结构, 可以将参数降低越20W,GFLOPs降低至6.2GFLOPs 同时本文结构为我独家创新,全网无第二份,非常适合用于发表论文,该结构非常灵活,利用Dual卷积思想,结合异构内核卷积来 并行 处理图片,结构上的结合非常合理,同时该结构非常适合轻量化的读者。

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

版本1训练信息:YOLO11-C3k2-HetConv-1 summary: 435 layers, 2,562,603 parameters, 2,562,587 gradients, 6.3 GFLOPs
版本2训练信息:YOLO11-C3k2-HetConv-2 summary: 458 layers, 2,372,251 parameters, 2,372,235 gradients, 6.2 GFLOPs
基础未改进版本YOLO11 summary: 319 layers, 2,594,715 parameters, 2,594,699 gradients, 6.5 GFLOPs

目录

一、本文介绍

二、HetConv原理

2.1 HetConv的基本原理

三、CSPHet的核心代码

四、CSPHet的添加方式

4.1 修改一

4.2 修改二

4.3 修改三

4.4 修改四

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

5.1 CSPHet的yaml文

5.2 CSPHet的训练过程截图

五、本文总结


二、HetConv原理

论文地址: 官方论文地址

代码地址: 该结构为我从第三方库获取无官方地址


2.1 HetConv的基本原理

HetConv(异构内核卷积) 是一种新型的 深度学习 架构,它采用了 不同大小的卷积核 来执行卷积操作。

HetConv(异构内核卷积)采用的 基本原理 如下:

1. 异构内核: HetConv结合了不同大小的 卷积核 ,如图所示,部分核为3x3,部分核为1x1。

2. 参数分区: 在HetConv中,卷积核被分为几个部分,参数`P`代表了这些部分的数量。比如,当`P=2`时,意味着有一半的卷积核是3x3大小,另一半是1x1。

3. 计算效率: 通过使用较小的1x1卷积核替代一部分3x3核,HetConv能够减少所需的浮点运算次数(FLOPs),从而提高计算效率。

4. 参数减少: 由于1x1卷积核比3x3卷积核需要更少的参数,HetConv相比标准卷积操作能够减少 模型 的参数数量。

5. 保持表征能力: 即使减少了计算量和参数,HetConv依然能够保持 卷积神经网络 的表征效率,不牺牲模型的准确性。

下面这张图展示了 标准卷积滤波器和HetConv(异构内核卷积)滤波器之间的区别

图中的HetConv使用了不同大小的内核,具体如下:

- 标准卷积滤波器:所有卷积核大小相同。
- HetConv滤波器(P=2):一半的卷积核为3x3,另一半为1x1。
- HetConv滤波器(P=4):四分之一的卷积核为3x3,剩余的为1x1。

其中`M`代表输入通道的数量,`P`代表将卷积核分为多少部分。当我们增加`P`的值时,使用1x1大小的卷积核的比例就会增加,这样可以减少计算量并减少模型参数,但同时也能保持必要的网络性能。通过这种设计,HetConv可以在减少计算复杂度和模型大小的同时,保持或提高模型准确性。

接下来这张图比较了 HetConv(异构内核卷积)滤波器与其他高效的卷积滤波器:

HetConv滤波器的优势在于它的 异构性能消除了延迟 ,而其他类型的卷积滤波器如组卷积加逐点卷积(GWC+PWC)或深度卷积加逐点卷积(DWC+PWC)至少有一个单元的延迟。

图中清晰地展示了不同卷积类型的结构差异,包括深度卷积、组卷积、标准卷积和HetConv提出的卷积滤波器。通过这种比较,我们可以理解HetConv如何在减少计算资源的同时,还能保持或提高处理速度。


三、CSPHet的核心代码

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

  1. import torch
  2. import torch.nn as nn
  3. __all__ = ['C3k2_HetConv1', 'C3k2_HetConv2']
  4. def autopad(k, p=None, d=1): # kernel, padding, dilation
  5. """Pad to 'same' shape outputs."""
  6. if d > 1:
  7. k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
  8. if p is None:
  9. p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
  10. return p
  11. class Conv(nn.Module):
  12. """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
  13. default_act = nn.SiLU() # default activation
  14. def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
  15. """Initialize Conv layer with given arguments including activation."""
  16. super().__init__()
  17. self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
  18. self.bn = nn.BatchNorm2d(c2)
  19. self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
  20. def forward(self, x):
  21. """Apply convolution, batch normalization and activation to input tensor."""
  22. return self.act(self.bn(self.conv(x)))
  23. def forward_fuse(self, x):
  24. """Perform transposed convolution of 2D data."""
  25. return self.act(self.conv(x))
  26. class HetConv(nn.Module):
  27. def __init__(self, input_channels, output_channels, stride=1, p=4):
  28. """
  29. Initialize the HetConv class.
  30. :param input_channels: the number of input channels
  31. :param output_channels: the number of output channels
  32. :param stride: convolution stride
  33. :param p: the value of P used in HetConv
  34. """
  35. super(HetConv, self).__init__()
  36. self.p = p
  37. self.input_channels = input_channels
  38. self.output_channels = output_channels
  39. self.filters = nn.ModuleList()
  40. self.convolution_1x1_index = []
  41. # Compute the indices of input channels fed to 1x1 convolutional kernels in all filters.
  42. # These indices of input channels are also the indices of the 1x1 convolutional kernels in the filters.
  43. # This is only executed when the HetConv class is created,
  44. # and the execution time is not included during inference.
  45. for i in range(self.p):
  46. self.convolution_1x1_index.append(self.compute_convolution_1x1_index(i))
  47. # Build HetConv filters.
  48. for i in range(self.p):
  49. self.filters.append(self.build_HetConv_filters(stride, p))
  50. def compute_convolution_1x1_index(self, i):
  51. """
  52. Compute the indices of input channels fed to 1x1 convolutional kernels in the i-th branch of filters (i=0, 1, 2,…, P-1). The i-th branch of filters consists of the {i, i+P, i+2P,…, i+N-P}-th filters.
  53. :param i: the i-th branch of filters in HetConv
  54. :return: return the required indices of input channels
  55. """
  56. index = [j for j in range(0, self.input_channels)]
  57. # Remove the indices of input channels fed to 3x3 convolutional kernels in the i-th branch of filters.
  58. while i < self.input_channels:
  59. index.remove(i)
  60. i += self.p
  61. return index
  62. def build_HetConv_filters(self, stride, p):
  63. """
  64. Build N/P filters in HetConv.
  65. :param stride: convolution stride
  66. :param p: the value of P used in HetConv
  67. :return: return N/P HetConv filters
  68. """
  69. temp_filters = nn.ModuleList()
  70. # nn.Conv2d arguments: nn.Conv2d(input_channels, output_channels, kernel_size, stride, padding)
  71. temp_filters.append(nn.Conv2d(self.input_channels//p, self.output_channels//p, 3, stride, 1, bias=False))
  72. temp_filters.append(nn.Conv2d(self.input_channels-self.input_channels//p, self.output_channels//p, 1, stride, 0, bias=False))
  73. return temp_filters
  74. def forward(self, input_data):
  75. """
  76. Define how HetConv processes the input images or input feature maps.
  77. :param input_data: input images or input feature maps
  78. :return: return output feature maps
  79. """
  80. output_feature_maps = []
  81. # Loop P times to get output feature maps. The number of output feature maps = the batch size.
  82. for i in range(0, self.p):
  83. # M/P HetConv filter kernels perform the 3x3 convolution and output to N/P output channels.
  84. output_feature_3x3 = self.filters[i][0](input_data[:, i::self.p, :, :])
  85. # (M-M/P) HetConv filter kernels perform the 1x1 convolution and output to N/P output channels.
  86. output_feature_1x1 = self.filters[i][1](input_data[:, self.convolution_1x1_index[i], :, :])
  87. # Obtain N/P output feature map channels.
  88. output_feature_map = output_feature_1x1 + output_feature_3x3
  89. # Append N/P output feature map channels.
  90. output_feature_maps.append(output_feature_map)
  91. # Get the batch size, number of output channels (N/P), height and width of output feature map.
  92. N, C, H, W = output_feature_maps[0].size()
  93. # Change the value of C to the number of output feature map channels (N).
  94. C = self.p * C
  95. # Arrange the output feature map channels to make them fit into the shifted manner.
  96. return torch.cat(output_feature_maps, 1).view(N, self.p, C//self.p, H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
  97. class CSPHet_Bottleneck(nn.Module):
  98. def __init__(self, dim):
  99. super().__init__()
  100. self.DualPConv = nn.Sequential(HetConv(dim, dim), HetConv(dim, dim))
  101. def forward(self, x):
  102. return self.DualPConv(x)
  103. class Bottleneck(nn.Module):
  104. """Standard bottleneck."""
  105. def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
  106. """Initializes a standard bottleneck module with optional shortcut connection and configurable parameters."""
  107. super().__init__()
  108. c_ = int(c2 * e) # hidden channels
  109. self.cv1 = Conv(c1, c_, k[0], 1)
  110. self.cv2 = Conv(c_, c2, k[1], 1, g=g)
  111. self.add = shortcut and c1 == c2
  112. def forward(self, x):
  113. """Applies the YOLO FPN to input data."""
  114. return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
  115. class C2f(nn.Module):
  116. """Faster Implementation of CSP Bottleneck with 2 convolutions."""
  117. def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
  118. """Initializes a CSP bottleneck with 2 convolutions and n Bottleneck blocks for faster processing."""
  119. super().__init__()
  120. self.c = int(c2 * e) # hidden channels
  121. self.cv1 = Conv(c1, 2 * self.c, 1, 1)
  122. self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
  123. self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
  124. def forward(self, x):
  125. """Forward pass through C2f layer."""
  126. y = list(self.cv1(x).chunk(2, 1))
  127. y.extend(m(y[-1]) for m in self.m)
  128. return self.cv2(torch.cat(y, 1))
  129. def forward_split(self, x):
  130. """Forward pass using split() instead of chunk()."""
  131. y = list(self.cv1(x).split((self.c, self.c), 1))
  132. y.extend(m(y[-1]) for m in self.m)
  133. return self.cv2(torch.cat(y, 1))
  134. class C3(nn.Module):
  135. """CSP Bottleneck with 3 convolutions."""
  136. def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
  137. """Initialize the CSP Bottleneck with given channels, number, shortcut, groups, and expansion values."""
  138. super().__init__()
  139. c_ = int(c2 * e) # hidden channels
  140. self.cv1 = Conv(c1, c_, 1, 1)
  141. self.cv2 = Conv(c1, c_, 1, 1)
  142. self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
  143. self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=((1, 1), (3, 3)), e=1.0) for _ in range(n)))
  144. def forward(self, x):
  145. """Forward pass through the CSP bottleneck with 2 convolutions."""
  146. return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))
  147. class C3k(C3):
  148. """C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks."""
  149. def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
  150. """Initializes the C3k module with specified channels, number of layers, and configurations."""
  151. super().__init__(c1, c2, n, shortcut, g, e)
  152. c_ = int(c2 * e) # hidden channels
  153. # self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
  154. self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
  155. class C3kPConv(C3):
  156. """C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks."""
  157. def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
  158. """Initializes the C3k module with specified channels, number of layers, and configurations."""
  159. super().__init__(c1, c2, n, shortcut, g, e)
  160. c_ = int(c2 * e) # hidden channels
  161. # self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
  162. self.m = nn.Sequential(*(CSPHet_Bottleneck(c_) for _ in range(n)))
  163. class C3k2_HetConv1(C2f):
  164. """Faster Implementation of CSP Bottleneck with 2 convolutions."""
  165. def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
  166. """Initializes the C3k2 module, a faster CSP Bottleneck with 2 convolutions and optional C3k blocks."""
  167. super().__init__(c1, c2, n, shortcut, g, e)
  168. self.m = nn.ModuleList(
  169. C3k(self.c, self.c, 2, shortcut, g) if c3k else CSPHet_Bottleneck(self.c)for _ in range(n)
  170. )
  171. # 解析利用MLLABlock替换Bottneck
  172. class C3k2_HetConv2(C2f):
  173. """Faster Implementation of CSP Bottleneck with 2 convolutions."""
  174. def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
  175. """Initializes the C3k2 module, a faster CSP Bottleneck with 2 convolutions and optional C3k blocks."""
  176. super().__init__(c1, c2, n, shortcut, g, e)
  177. self.m = nn.ModuleList(
  178. C3kPConv(self.c, self.c, 2, shortcut, g) if c3k else Bottleneck(self.c, self.c, shortcut, g) for _ in
  179. range(n)
  180. )
  181. # 解析利用MLLABlock替换C3k中的Bottneck
  182. if __name__ == "__main__":
  183. # Generating Sample image
  184. image_size = (1, 64, 224, 224)
  185. image = torch.rand(*image_size)
  186. # Model
  187. model = C3k2_HetConv1(64, 128)
  188. out = model(image)
  189. print(out.size())


四、CSPHet的添加方式


4.1 修改一

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


4.2 修改二

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


4.3 修改三

第三步我门中到如下文件'ultralytics/nn/tasks.py'进行导入和注册我们的模块( !


4.4 修改四

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


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


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

5.1 CSPHet的yaml文件1

下面的添加 CSPHet 是我实验结果的版本,大家需要注意的是轻量化的结构往往模型收敛速度都会变慢因为模型变简单了,学习特征的能力变弱了,一般需要加大epochs训练的次数。

此版本的训练信息:YOLO11-C3k2-HetConv-1 summary: 435 layers, 2,562,603 parameters, 2,562,587 gradients, 6.3 GFLOPs

  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_HetConv1, [256, False, 0.25]]
  18. - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  19. - [-1, 2, C3k2_HetConv1, [512, False, 0.25]]
  20. - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  21. - [-1, 2, C3k2_HetConv1, [512, True]]
  22. - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  23. - [-1, 2, C3k2_HetConv1, [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_HetConv1, [512, False]] # 13
  31. - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  32. - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  33. - [-1, 2, C3k2_HetConv1, [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_HetConv1, [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_HetConv1, [1024, True]] # 22 (P5/32-large)
  40. - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)


5.2 CSPHet的yaml文件2

此版本的训练信息:YOLO11-C3k2-HetConv-2 summary: 458 layers, 2,372,251 parameters, 2,372,235 gradients, 6.2 GFLOPs

  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_HetConv2, [256, False, 0.25]]
  18. - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  19. - [-1, 2, C3k2_HetConv2, [512, False, 0.25]]
  20. - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  21. - [-1, 2, C3k2_HetConv2, [512, True]]
  22. - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  23. - [-1, 2, C3k2_HetConv2, [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_HetConv2, [512, False]] # 13
  31. - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  32. - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  33. - [-1, 2, C3k2_HetConv2, [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_HetConv2, [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_HetConv2, [1024, True]] # 22 (P5/32-large)
  40. - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)


5.3 CSPHet的训练过程截图


五、本文总结

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