一、本文介绍
本文给大家带来的改进机制是MobileNetV2,其是专为移动和嵌入式视觉应用设计的轻量化网络结构。其在MobilNetV1的基础上采用反转残差结构和线性瓶颈层。这种结构通过轻量级的深度卷积和线性卷积过滤特征,同时去除狭窄层中的非线性,以维持表征能力。MobileNetV2在性能上和精度上都要比V1版本强很多,其在多种应用(如对象检测、细粒度分类、面部属性识别和大规模地理定位)中都展现了一定的有效性。
适用检测目标:轻量化网络结构适合非常轻量化的读者,同时具有涨点效果
推荐指数:⭐⭐⭐⭐
专栏回顾:YOLOv8改进系列专栏——本专栏持续复习各种顶会内容——科研必备
效果回顾展示->
二、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的核心代码,大家如果想学习可以和上面的框架原理对比着看一看估计会有一定的收获,使用方式看章节四。
"""A from-scratch implementation of MobileNetV2 paper ( for educational purposes ).
Paper
MobileNetV2: Inverted Residuals and Linear Bottlenecks - https://arxiv.org/abs/1801.04381
author : shubham.aiengineer@gmail.com
"""
import torch
from torch import nn
from torchsummary import summary
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.
"""
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,
n_classes: int = 1000,
input_channel: int = 3,
dropout: float = 0.2,
):
"""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),
)
self.model = nn.Sequential(
ConvNormReLUBlock(input_channel, 32, (3, 3), stride=2, padding=1)
)
for in_channels, expansion_factor, out_channels, repeat, stride in config:
for _ in range(repeat):
self.model.append(
InverseResidualBlock(
in_channels=in_channels,
out_channels=out_channels,
expansion_factor=expansion_factor,
stride=stride,
)
)
in_channels = out_channels
stride = 1
self.index = [24, 32, 96, 320]
self.width_list = [i.size(1) for i in self.forward(torch.randn(1, 3, 640, 640))]
def forward(self, x):
"""Perform forward pass."""
results = [None, None, None, None]
for model in self.model:
x = model(x)
if x.size(1) in self.index:
position = self.index.index(x.size(1)) # Find the position in the index list
results[position] = x
results.append(x)
return results
if __name__ == "__main__":
# Generating Sample image
image_size = (1, 3, 224, 224)
image = torch.rand(*image_size)
# Model
mobilenet_v2 = MobileNetV2()
# summary(
# mobilenet_v2,
# input_data=image,
# col_names=["input_size", "output_size", "num_params"],
# device="cpu",
# depth=2,
# )
out = mobilenet_v2(image)
print("Output shape : ", out.shape)
四、手把手教你添加MobileNetV2网络结构
这个主干的网络结构添加起来算是所有的改进机制里最麻烦的了,因为有一些网略结构可以用yaml文件搭建出来,有一些网络结构其中的一些细节根本没有办法用yaml文件去搭建,用yaml文件去搭建会损失一些细节部分(而且一个网络结构设计很多细节的结构修改方式都不一样,一个一个去修改大家难免会出错),所以这里让网络直接返回整个网络,然后修改部分 yolo代码以后就都以这种形式添加了,以后我提出的网络模型基本上都会通过这种方式修改,我也会进行一些模型细节改进。创新出新的网络结构大家直接拿来用就可以的。下面开始添加教程->
(同时每一个后面都有代码,大家拿来复制粘贴替换即可,但是要看好了不要复制粘贴替换多了)
修改一
我们复制网络结构代码到“ultralytics/nn/modules”目录下创建一个py文件复制粘贴进去 ,我这里起的名字是MobileNetV1。
修改二
找到如下的文件"ultralytics/nn/tasks.py" 在开始的部分导入我们的模型如下图。
from .modules.MobileNetV1 import MobileNetV2
修改三
添加如下两行代码!!!
修改四
找到七百多行大概把具体看图片,按照图片来修改就行,添加红框内的部分,注意没有()只是函数名。
elif m in {自行添加对应的模型即可,下面都是一样的}:
m = m()
c2 = m.width_list # 返回通道列表
backbone = True
修改五
下面的两个红框内都是需要改动的。
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
修改六
如下的也需要修改,全部按照我的来。
代码如下把原先的代码替换了即可。
if verbose:
LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m.np:10.0f} {t:<45}{str(args):<30}') # print
save.extend(x % (i + 4 if backbone else i) for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
layers.append(m_)
if i == 0:
ch = []
if isinstance(c2, list):
ch.extend(c2)
if len(c2) != 5:
ch.insert(0, 0)
else:
ch.append(c2)
修改七
修改七和前面的都不太一样,需要修改前向传播中的一个部分, 已经离开了parse_model方法了。
可以在图片中开代码行数,没有离开task.py文件都是同一个文件。 同时这个部分有好几个前向传播都很相似,大家不要看错了,是70多行左右的!!!,同时我后面提供了代码,大家直接复制粘贴即可,有时间我针对这里会出一个视频。
代码如下->
def _predict_once(self, x, profile=False, visualize=False):
"""
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.
Returns:
(torch.Tensor): The last output of the model.
"""
y, dt = [], [] # 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)
return x
到这里就完成了修改部分,但是这里面细节很多,大家千万要注意不要替换多余的代码,导致报错,也不要拉下任何一部,都会导致运行失败,而且报错很难排查!!!很难排查!!!
修改八
我们找到如下文件'ultralytics/utils/torch_utils.py'按照如下的图片进行修改。
五、MobileNetV2的yaml文件
复制如下yaml文件进行运行!!!
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv8 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=yolov8n.yaml' will call yolov8.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs
s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs
m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs
l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs
x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOP
# YOLOv8.0n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, MobileNetV2, []] # 4
- [-1, 1, SPPF, [1024, 5]] # 5
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 6
- [[-1, 3], 1, Concat, [1]] # 7 cat backbone P4
- [-1, 3, C2f, [512]] # 8
- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 9
- [[-1, 2], 1, Concat, [1]] # 10 cat backbone P3
- [-1, 3, C2f, [256]] # 11 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 12
- [[-1, 8], 1, Concat, [1]] # 13 cat head P4
- [-1, 3, C2f, [512]] # 14 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 15
- [[-1, 5], 1, Concat, [1]] # 16 cat head P5
- [-1, 3, C2f, [1024]] # 17 (P5/32-large)
- [[11, 14, 17], 1, Detect, [nc]] # Detect(P3, P4, P5)
六、成功运行记录
下面是成功运行的截图,已经完成了有1个epochs的训练,图片太大截不全第2个epochs了。
七、本文总结
到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv8改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充,目前本专栏免费阅读(暂时,大家尽早关注不迷路~),如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~
专栏回顾:YOLOv8改进系列专栏——本专栏持续复习各种顶会内容——科研必备