一、本文介绍
本文给大家带来的改进机制是 MobileNetV2 ,其是专为移动和嵌入式视觉应用设计的 轻量化网络 结构。其在MobilNetV1的基础上采用反转残差结构和线性瓶颈层。这种结构通过轻量级的深度卷积和线性卷积过滤特征,同时去除狭窄层中的非线性,以维持表征能力。 MobileNetV2在性能上和精度上都要比V1版本强很多 ,其在多种应用(如对象检测、细粒度分类、面部属性识别和大规模地理定位)中都展现了一定的有效性 ,(yolov11系列全系轻量化) 。
二、MobileNetV2的框架原理
官方论文地址:
官方论文地址
官方代码地址: 官方代码地址
2.1 MobileNetV2的基本原理
MobileNetV2是在MobileNetV1基础上提出来的,其不光具有V1的全部改进,还提出了采用反转残差结构和线性瓶颈层。这种结构通过轻量级的深度卷积和线性卷积过滤特征,同时去除狭窄层中的非线性,以维持表征能力。MobileNetV2通过这种设计提高了 性能 ,并在多种任务和基准测试上表现出色。此外,它提出了一种新的框架SSDLite,用于移动设备上的目标检测,并展示了如何构建移动语义分割模型Mobile DeepLabv3。这种方法允许输入/输出域与变换的表达力解耦,为进一步分析提供了方便的框架。
MobileNetV2的主要创新点包括:
1. 反转残差结构:
使用轻量级的深度卷积作为扩展层来提高特征过滤的效率。
2. 线性瓶颈层:
在狭窄的层中去除非线性激活函数,以保持网络的表征能力。
3. SSDLite框架:
用于移动设备上的高效目标检测,它是一种简化和优化的SSD框架。
2.1.1 反转残差结构
反转残差结构是MobileNetV2的关键特性 ,它采用轻量级的深度可分离卷积作为扩展层。这种结构首先使用1x1的卷积将输入特征图的通道数扩大,然后应用深度可分离卷积对这些扩展的特征图进行空间特征提取,最后再次通过1x1的卷积将通道数减少,恢复到原来的尺寸。这样的设计有效地提高了网络处理特征的效率,同时减少了参数数量和计算成本。通过这种方式,MobileNetV2能够在保持 模型 轻量的同时,提供足够的模型表现力,适用于移动和嵌入式设备上的高效计算。
上图展示了残差块和反转残差块之间的区别:
(a) 残差:
传统的残差块通过直接连接输入和输出来促进特征的传递,通常包含具有高通道数的层和ReLU激活函数。
(b) 反转残差块:
在反转残差块中,连接是在瓶颈层之间,即通道数较少的层,而且去除了非线性激活函数,以保持特征的表达力。这种设计通常首先用一个扩展层增加通道数,然后应用深度卷积处理特征,并且在最后一个线性层减少通道数。
2.1.2 线性瓶颈层
线性瓶颈层是MobileNetV2架构中的另一个关键特性。在这种结构中,传统的非线性激活函数被有意地从瓶颈层中去除。瓶颈层是指那些通道数较少的卷积层,它们位于扩展层和压缩层之间。这样做的目的是为了减少信息在通过狭窄层时的损失,因为非线性操作可能会破坏特征中的一些信息。通过保持这些层的线性,网络能够维持更丰富的特征表示,这对于提高模型的整体性能至关重要。
总结: 就是在一些卷积层里面把激活函数删除掉了,类似于v8中的Bottleneck模块,将其中的激活函数删除掉。
2.1.3 SSDLite框架
SSDLite是一个轻量级的目标检测框架,专为移动设备优化。它是SSD框架的简化版本,通过使用深度可分离卷积替换SSD中的标准卷积,显著减少了计算量和模型的大小。SSDLite继承了SSD的单次检测机制,使得模型在进行目标检测时既高效又准确。这种设计使SSDLite非常适合在资源受限的设备上进行实时目标检测任务。
上图展示了可分离卷积块的演变。其中:
(a) 展示了常规的卷积。
(b) 展示了可分离卷积块,这种块首先使用深度卷积分别处理每个输入通道,然后用一个1x1的卷积组合这些特征。
(c) 展示了带有线性瓶颈的可分离卷积,它在瓶颈层中移除了非线性激活函数,以保持特征的表达力。
(d) 展示了带有扩展层的瓶颈结构,它使用一个扩展层放大特征空间,然后再用深度卷积和1x1卷积进行处理。
对角线阴影的纹理表示不包含非线性的层,最后的浅色层表示下一个块的开始。请注意,当堆叠时,2d和2c是等效的块。
三、MobileNetV2的核心代码
下面的代码是整个MobileNetV2的核心代码,大家如果想学习可以和上面的框架原理对比着看一看估计会有一定的收获,使用方式看章节四。
- import torch
- from torch import nn
- __all__ = ['MobileNetV2_n', 'MobileNetV2_s', 'MobileNetV2_m']
- class ConvNormReLUBlock(nn.Module):
- def __init__(
- self,
- in_channels: int,
- out_channels: int,
- kernel_size: list,
- stride: int = 1,
- padding: int = 0,
- groups: int = 1,
- bias: bool = False,
- activation: bool = nn.ReLU6,
- ):
- """Constructs a block containing a combination of convolution, batchnorm and relu
- Args:
- in_channels (int): input channels
- out_channels (int): output channels
- kernel_size (list): kernel size parameter for convolution
- stride (int, optional): stride parameter for convolution. Defaults to 1.
- padding (int, optional): padding parameter for convolution. Defaults to 0.
- groups (int, optional): number of blocked connections from input channel to output channel for convolution. Defaults to 1.
- bias (bool, optional): whether to enable bias in convolution. Defaults to False.
- activation (bool, optional): activation function to use. Defaults to nn.ReLU6.
- """
- super().__init__()
- self.conv = nn.Conv2d(
- in_channels,
- out_channels,
- kernel_size,
- stride=stride,
- padding=padding,
- groups=groups,
- bias=bias,
- )
- self.bn = nn.BatchNorm2d(out_channels)
- self.activation = activation()
- def forward(self, x):
- """Perform forward pass."""
- x = self.conv(x)
- x = self.bn(x)
- x = self.activation(x)
- return x
- class InverseResidualBlock(nn.Module):
- def __init__(
- self,
- in_channels: int,
- out_channels: int,
- expansion_factor: int = 6,
- stride: int = 1,
- ):
- """Constructs a inverse residual block with depthwise seperable convolution
- Args:
- in_channels (int): input channels
- out_channels (int): output channels
- expansion_factor (int, optional): Calculating the input & output channel for depthwise convolution by multiplying the expansion factor with input channels. Defaults to 6.
- stride (int, optional): stride paramemeter for depthwise convolution. Defaults to 1.
- CSDN:Snu77
- """
- super().__init__()
- hidden_channels = in_channels * expansion_factor
- self.residual = in_channels == out_channels and stride == 1
- self.conv1 = (
- ConvNormReLUBlock(in_channels, hidden_channels, (1, 1))
- if in_channels != hidden_channels
- else nn.Identity() # If it's not the first layer, then we need to add a 1x1 convolutional layer to expand the number of channels
- )
- self.depthwise_conv = ConvNormReLUBlock(
- hidden_channels,
- hidden_channels,
- (3, 3),
- stride=stride,
- padding=1,
- groups=hidden_channels,
- )
- self.conv2 = ConvNormReLUBlock(
- hidden_channels, out_channels, (1, 1), activation=nn.Identity
- )
- def forward(self, x):
- """Perform forward pass."""
- identity = x
- x = self.conv1(x)
- x = self.depthwise_conv(x)
- x = self.conv2(x)
- if self.residual:
- x = torch.add(x, identity)
- return x
- class MobileNetV2(nn.Module):
- def __init__(
- self,
- input_channel: int = 3,
- depth_multiplier: float = 1,
- ):
- """Constructs MobileNetV2 architecture
- Args:
- n_classes (int, optional): output neuron in last layer. Defaults to 1000.
- input_channel (int, optional): input channels in first conv layer. Defaults to 3.
- dropout (float, optional): dropout in last layer. Defaults to 0.2.
- """
- super().__init__()
- # The configuration of MobileNetV2
- # input channels, expansion factor, output channels, repeat, stride,
- config = (
- (32, 1, 16, 1, 1),
- (16, 6, 24, 2, 2),
- (24, 6, 32, 3, 2),
- (32, 6, 64, 4, 2),
- (64, 6, 96, 3, 1),
- (96, 6, 160, 3, 2),
- (160, 6, 320, 1, 1),
- )
- layers = [
- ConvNormReLUBlock(input_channel, int(32 * depth_multiplier), (3, 3), stride=2, padding=1)
- ]
- # 遍历配置并添加 InverseResidualBlock 层
- for in_channels, expansion_factor, out_channels, repeat, stride in config: # repeat不放缩了已经足够轻量化了
- for _ in range(repeat):
- layers.append(
- InverseResidualBlock(
- in_channels=int(in_channels * depth_multiplier),
- out_channels=int(out_channels * depth_multiplier),
- expansion_factor=expansion_factor,
- stride=stride,
- )
- )
- in_channels = out_channels # 更新输入通道
- stride = 1 # 重复层的 stride 设为 1
- # 将层列表转换为 nn.Sequential
- self.model = nn.Sequential(*layers)
- self.width_list = [i.size(1) for i in self.forward(torch.randn(1, 3, 640, 640))]
- def forward(self, x):
- """Perform forward pass."""
- unique_tensors = {}
- for model in self.model:
- x = model(x)
- width, height = x.shape[2], x.shape[3]
- unique_tensors[(width, height)] = x
- result_list = list(unique_tensors.values())[-4:]
- return result_list
- def MobileNetV2_n(width_mult=0.5):
- model = MobileNetV2(depth_multiplier=0.25)
- return model
- def MobileNetV2_s(width_mult=1.0):
- model = MobileNetV2(depth_multiplier=0.5)
- return model
- def MobileNetV2_m(width_mult=1.5):
- model = MobileNetV2(depth_multiplier=1)
- return model
- if __name__ == "__main__":
- # Generating Sample image
- image_size = (1, 3, 224, 224)
- image = torch.rand(*image_size)
- # Model
- mobilenet_v2 = MobileNetV2()
- out = mobilenet_v2(image)
- for i in range(len(out)):
- print(out[i].size())
四、手把手教你添加 MobileNetV2网络结构
4.1 修改一
第一步还是建立文件,我们找到如下 ultralytics /nn文件夹下建立一个目录名字呢就是'Addmodules'文件夹 ( 用群内的文件的话已经有了无需新建) ! 然后在其内部建立一个新的py文件将核心代码复制粘贴进去即可
4.2 修改二
第二步我们在该目录下创建一个新的py文件名字为'__init__.py'( 用群内的文件的话已经有了无需新建) ,然后在其内部导入我们的检测头如下图所示。
4.3 修改三
第三步我门中到如下文件'ultralytics/nn/tasks.py'进行导入和注册我们的模块( 用群内的文件的话已经有了无需重新导入直接开始第四步即可) !
从今天开始以后的教程就都统一成这个样子了,因为我默认大家用了我群内的文件来进行修改!!
4.4 修改四
添加如下两行代码!!!
4.5 修改五
找到七百多行大概把具体看图片,按照图片来修改就行,添加红框内的部分,注意没有()只是函数名。
- elif m in {自行添加对应的模型即可,下面都是一样的}:
- m = m(*args)
- c2 = m.width_list # 返回通道列表
- backbone = True
4.6 修改六
下面的两个红框内都是需要改动的。
- if isinstance(c2, list):
- m_ = m
- m_.backbone = True
- else:
- m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module
- t = str(m)[8:-2].replace('__main__.', '') # module type
- m.np = sum(x.numel() for x in m_.parameters()) # number params
- m_.i, m_.f, m_.type = i + 4 if backbone else i, f, t # attach index, 'from' index, type
4.7 修改七
修改七和前面的都不太一样,需要修改前向传播中的一个部分, 已经离开了parse_model方法了。
可以在图片中开代码行数,没有离开task.py文件都是同一个文件。 同时这个部分有好几个前向传播都很相似,大家不要看错了, 是70多行左右的!!!,同时我后面提供了代码,大家直接复制粘贴即可,有时间我针对这里会出一个视频。
代码如下->
- def _predict_once(self, x, profile=False, visualize=False, embed=None):
- """
- Perform a forward pass through the network.
- Args:
- x (torch.Tensor): The input tensor to the model.
- profile (bool): Print the computation time of each layer if True, defaults to False.
- visualize (bool): Save the feature maps of the model if True, defaults to False.
- embed (list, optional): A list of feature vectors/embeddings to return.
- Returns:
- (torch.Tensor): The last output of the model.
- """
- y, dt, embeddings = [], [], [] # outputs
- for m in self.model:
- if m.f != -1: # if not from previous layer
- x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
- if profile:
- self._profile_one_layer(m, x, dt)
- if hasattr(m, 'backbone'):
- x = m(x)
- if len(x) != 5: # 0 - 5
- x.insert(0, None)
- for index, i in enumerate(x):
- if index in self.save:
- y.append(i)
- else:
- y.append(None)
- x = x[-1] # 最后一个输出传给下一层
- else:
- x = m(x) # run
- y.append(x if m.i in self.save else None) # save output
- if visualize:
- feature_visualization(x, m.type, m.i, save_dir=visualize)
- if embed and m.i in embed:
- embeddings.append(nn.functional.adaptive_avg_pool2d(x, (1, 1)).squeeze(-1).squeeze(-1)) # flatten
- if m.i == max(embed):
- return torch.unbind(torch.cat(embeddings, 1), dim=0)
- return x
到这里就完成了修改部分,但是这里面细节很多,大家千万要注意不要替换多余的代码,导致报错,也不要拉下任何一部,都会导致运行失败,而且报错很难排查!!!很难排查!!!
注意!!! 额外的修改!
关注我的其实都知道,我大部分的修改都是一样的,这个网络需要额外的修改一步,就是s一个参数,将下面的s改为640!!!即可完美运行!!
打印计算量问题解决方案
我们找到如下文件'ultralytics/utils/torch_utils.py'按照如下的图片进行修改,否则容易打印不出来计算量。
注意事项!!!
如果大家在验证的时候报错形状不匹配的错误可以固定 验证集 的图片尺寸,方法如下 ->
找到下面这个文件ultralytics/ models /yolo/detect/train.py然后其中有一个类是DetectionTrainer class中的build_dataset函数中的一个参数rect=mode == 'val'改为rect=False
五、MobileNetV2的yaml文件
5.1 yaml文件
训练信息:YOLO11-MobileNetV2 summary: 432 layers, 1,630,183 parameters, 1,630,167 gradients, 3.9 GFLOPs
我提供了三个版本分别是对应YOLOv8n v8s v8m。 MobileNetV2_n, MobileNetV2_s, MobileNetV2_m
- # Ultralytics YOLO 🚀, AGPL-3.0 license
- # YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
- # Parameters
- nc: 80 # number of classes
- scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
- # [depth, width, max_channels]
- n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
- s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
- m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
- l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
- x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
- # 我提供了三个版本分别是对应YOLOv8n v8s v8m。 MobileNetV2_n, MobileNetV2_s, MobileNetV2_m
- # YOLO11n backbone
- backbone:
- # [from, repeats, module, args]
- - [-1, 1, MobileNetV2_n, []] # 0-4 P1/2 这里是四层大家不要被yaml文件限制住了思维,不会画图进群看视频.
- - [-1, 1, SPPF, [1024, 5]] # 5
- - [-1, 2, C2PSA, [1024]] # 6
- # YOLO11n head
- head:
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 3], 1, Concat, [1]] # cat backbone P4
- - [-1, 2, C3k2, [512, False]] # 9
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 2], 1, Concat, [1]] # cat backbone P3
- - [-1, 2, C3k2, [256, False]] # 12 (P3/8-small)
- - [-1, 1, Conv, [256, 3, 2]]
- - [[-1, 9], 1, Concat, [1]] # cat head P4
- - [-1, 2, C3k2, [512, False]] # 15 (P4/16-medium)
- - [-1, 1, Conv, [512, 3, 2]]
- - [[-1, 6], 1, Concat, [1]] # cat head P5
- - [-1, 2, C3k2, [1024, True]] # 18 (P5/32-large)
- - [[12, 15, 18], 1, Detect, [nc]] # Detect(P3, P4, P5)
5.2 训练文件的代码
可以复制我的运行文件进行运行。
- import warnings
- warnings.filterwarnings('ignore')
- from ultralytics import YOLO
- if __name__ == '__main__':
- model = YOLO("替换你的yaml文件地址")
- model.load('yolov8n.pt')
- model.train(data=r'你的数据集的地址',
- cache=False,
- imgsz=640,
- epochs=150,
- batch=4,
- close_mosaic=0,
- workers=0,
- device=0,
- optimizer='SGD'
- amp=False,
- )
六、成功运行记录
下面是成功运行的截图,已经完成了有1个epochs的训练,图片太大截不全第2个epochs了。
七、本文总结
到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv11改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充 , 如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~