一、本文介绍
本文给大家带来的改进机制是FasterNet网络,将其用来替换我们的 特征提取网络 ,其旨在 提高计算速度而不牺牲准确性 ,特别是在视觉任务中。它通过一种称为 部分卷积(PConv) 的新技术来减少冗余计算和内存访问。这种方法使得FasterNet在多种设备上运行速度比其他网络快得多,同时在各种视觉任务中保持高准确率。经过我的实验该主干网络确实能够涨点在大中小三种物体检测上,大家可以在 源代码 中进行修改版本的使用。 本文通过介绍其主要框架原理,然后教大家如何添加该网络结构到网络模型中。
(本文内容可根据yolov11的N、S、M、L、X进行二次缩放,轻量化更上一层)。
二、FasterNet原理
论文地址:
官方论文地址
代码地址: 官方代码地址
2.1 FasterNet的基本原理
FasterNet 是一种高效的 神经网络 架构,旨在 提高计算速度而不牺牲准确性 ,特别是在视觉任务中。它通过一种称为 部分卷积(PConv) 的新技术来减少冗余计算和内存访问。这种方法使得FasterNet在多种设备上运行速度比其他网络快得多,同时在各种视觉任务中保持高准确率。例如,FasterNet在ImageNet-1k数据集上的表现超过了其他模型,如 MobileViT-XXS ,展现了其在速度和准确度方面的优势。
FasterNet的基本原理可以总结为以下几点:
1. 部分卷积(PConv): FasterNet引入了部分卷积(PConv),这是一种新型的卷积方法,它通过只处理输入通道的一部分来减少计算量和内存访问。
2. 加速神经网络 : FasterNet利用PConv的优势,实现了在多种设备上比其他现有神经网络更快的运行速度,同时保持了较高的准确度。
下面为大家展示的是 FasterNet的整体架构 。
它包括四个层次化的阶段,每个阶段由一系列FasterNet块组成,并由嵌入或合并层开头。最后三层用于特征分类。在每个FasterNet块中,PConv层之后是两个点状卷积(PWConv)层。为了保持特征多样性并实现更低的延迟,仅在中间层之后放置了 归一化和激活层 。
2.2 部分卷积
部分卷积(PConv) 是一种 卷积神经网络中的操作 ,旨在提高计算效率。它通过 只在输入特征图的一部分上执行卷积操作 ,而非传统卷积操作中的全面应用。这样,PConv可以减少不必要的计算和内存访问,因为它忽略了输入中认为是冗余的部分。这种方法特别适合在资源有限的设备上运行 深度学习 模型,因为它可以在不牺牲太多性能的情况下,显著降低计算需求。
下面我为大家展示了FasterNet中的 部分卷积(PConv)与传统卷积和深度卷积/分组卷积的比较 :
PConv通过仅对输入通道的一小部分应用滤波器,同时保持其余通道不变,实现了快速和高效的特性提取。PConv的计算复杂度 (FLOPs) 低于常规卷积,但高于深度卷积/分组卷积,这样在减少计算资源的同时提高了运算性能。
2.3 加速神经网络
加速神经网络 主要通过优化计算路径、减少模型大小和复杂性、提高操作效率,以及使用高效的 硬件 实现等方式来降低模型的推理时间。这些方法包括 简化网络层 、 使用更快的激活函数 、 采用量化技术 将 浮点运算转换为整数运算 ,以及使用特殊的算法来减少内存访问次数等。通过这些策略,可以在不损害模型准确性的前提下,使神经网络能够更快地处理数据和做出预测。
三、FasterNet的核心代码
- # Copyright (c) Microsoft Corporation.
- # Licensed under the MIT License.
- import torch
- import torch.nn as nn
- from timm.models.layers import DropPath, trunc_normal_
- from functools import partial
- from typing import List
- from torch import Tensor
- import copy
- import os
- class Partial_conv3(nn.Module):
- def __init__(self, dim, n_div, forward):
- super().__init__()
- self.dim_conv3 = dim // n_div
- self.dim_untouched = dim - self.dim_conv3
- self.partial_conv3 = nn.Conv2d(self.dim_conv3, self.dim_conv3, 3, 1, 1, bias=False)
- if forward == 'slicing':
- self.forward = self.forward_slicing
- elif forward == 'split_cat':
- self.forward = self.forward_split_cat
- else:
- raise NotImplementedError
- def forward_slicing(self, x: Tensor) -> Tensor:
- # only for inference
- x = x.clone() # !!! Keep the original input intact for the residual connection later
- x[:, :self.dim_conv3, :, :] = self.partial_conv3(x[:, :self.dim_conv3, :, :])
- return x
- def forward_split_cat(self, x: Tensor) -> Tensor:
- # for training/inference
- x1, x2 = torch.split(x, [self.dim_conv3, self.dim_untouched], dim=1)
- x1 = self.partial_conv3(x1)
- x = torch.cat((x1, x2), 1)
- return x
- class MLPBlock(nn.Module):
- def __init__(self,
- dim,
- n_div,
- mlp_ratio,
- drop_path,
- layer_scale_init_value,
- act_layer,
- norm_layer,
- pconv_fw_type
- ):
- super().__init__()
- self.dim = dim
- self.mlp_ratio = mlp_ratio
- self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
- self.n_div = n_div
- mlp_hidden_dim = int(dim * mlp_ratio)
- mlp_layer: List[nn.Module] = [
- nn.Conv2d(dim, mlp_hidden_dim, 1, bias=False),
- norm_layer(mlp_hidden_dim),
- act_layer(),
- nn.Conv2d(mlp_hidden_dim, dim, 1, bias=False)
- ]
- self.mlp = nn.Sequential(*mlp_layer)
- self.spatial_mixing = Partial_conv3(
- dim,
- n_div,
- pconv_fw_type
- )
- if layer_scale_init_value > 0:
- self.layer_scale = nn.Parameter(layer_scale_init_value * torch.ones((dim)), requires_grad=True)
- self.forward = self.forward_layer_scale
- else:
- self.forward = self.forward
- def forward(self, x: Tensor) -> Tensor:
- shortcut = x
- x = self.spatial_mixing(x)
- x = shortcut + self.drop_path(self.mlp(x))
- return x
- def forward_layer_scale(self, x: Tensor) -> Tensor:
- shortcut = x
- x = self.spatial_mixing(x)
- x = shortcut + self.drop_path(
- self.layer_scale.unsqueeze(-1).unsqueeze(-1) * self.mlp(x))
- return x
- class BasicStage(nn.Module):
- def __init__(self,
- dim,
- depth,
- n_div,
- mlp_ratio,
- drop_path,
- layer_scale_init_value,
- norm_layer,
- act_layer,
- pconv_fw_type
- ):
- super().__init__()
- blocks_list = [
- MLPBlock(
- dim=dim,
- n_div=n_div,
- mlp_ratio=mlp_ratio,
- drop_path=drop_path[i],
- layer_scale_init_value=layer_scale_init_value,
- norm_layer=norm_layer,
- act_layer=act_layer,
- pconv_fw_type=pconv_fw_type
- )
- for i in range(depth)
- ]
- self.blocks = nn.Sequential(*blocks_list)
- def forward(self, x: Tensor) -> Tensor:
- x = self.blocks(x)
- return x
- class PatchEmbed(nn.Module):
- def __init__(self, patch_size, patch_stride, in_chans, embed_dim, norm_layer):
- super().__init__()
- self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_stride, bias=False)
- if norm_layer is not None:
- self.norm = norm_layer(embed_dim)
- else:
- self.norm = nn.Identity()
- def forward(self, x: Tensor) -> Tensor:
- x = self.norm(self.proj(x))
- return x
- class PatchMerging(nn.Module):
- def __init__(self, patch_size2, patch_stride2, dim, norm_layer):
- super().__init__()
- self.reduction = nn.Conv2d(dim, 2 * dim, kernel_size=patch_size2, stride=patch_stride2, bias=False)
- if norm_layer is not None:
- self.norm = norm_layer(2 * dim)
- else:
- self.norm = nn.Identity()
- def forward(self, x: Tensor) -> Tensor:
- x = self.norm(self.reduction(x))
- return x
- class FasterNet(nn.Module):
- def __init__(self,
- in_chans=3,
- num_classes=1000,
- embed_dim=96,
- depths=(1, 2, 8, 2),
- mlp_ratio=2.,
- n_div=4,
- patch_size=4,
- patch_stride=4,
- patch_size2=2, # for subsequent layers
- patch_stride2=2,
- patch_norm=True,
- feature_dim=1280,
- drop_path_rate=0.1,
- layer_scale_init_value=0,
- norm_layer='BN',
- act_layer='RELU',
- fork_feat=True,
- init_cfg=None,
- pretrained=None,
- pconv_fw_type='split_cat',
- **kwargs):
- super().__init__()
- if norm_layer == 'BN':
- norm_layer = nn.BatchNorm2d
- else:
- raise NotImplementedError
- if act_layer == 'GELU':
- act_layer = nn.GELU
- elif act_layer == 'RELU':
- act_layer = partial(nn.ReLU, inplace=True)
- else:
- raise NotImplementedError
- if not fork_feat:
- self.num_classes = num_classes
- self.num_stages = len(depths)
- self.embed_dim = embed_dim
- self.patch_norm = patch_norm
- self.num_features = int(embed_dim * 2 ** (self.num_stages - 1))
- self.mlp_ratio = mlp_ratio
- self.depths = depths
- # split image into non-overlapping patches
- self.patch_embed = PatchEmbed(
- patch_size=patch_size,
- patch_stride=patch_stride,
- in_chans=in_chans,
- embed_dim=embed_dim,
- norm_layer=norm_layer if self.patch_norm else None
- )
- # stochastic depth decay rule
- dpr = [x.item()
- for x in torch.linspace(0, drop_path_rate, sum(depths))]
- # build layers
- stages_list = []
- for i_stage in range(self.num_stages):
- stage = BasicStage(dim=int(embed_dim * 2 ** i_stage),
- n_div=n_div,
- depth=depths[i_stage],
- mlp_ratio=self.mlp_ratio,
- drop_path=dpr[sum(depths[:i_stage]):sum(depths[:i_stage + 1])],
- layer_scale_init_value=layer_scale_init_value,
- norm_layer=norm_layer,
- act_layer=act_layer,
- pconv_fw_type=pconv_fw_type
- )
- stages_list.append(stage)
- # patch merging layer
- if i_stage < self.num_stages - 1:
- stages_list.append(
- PatchMerging(patch_size2=patch_size2,
- patch_stride2=patch_stride2,
- dim=int(embed_dim * 2 ** i_stage),
- norm_layer=norm_layer)
- )
- self.stages = nn.Sequential(*stages_list)
- self.fork_feat = fork_feat
- self.forward = self.forward_det
- # add a norm layer for each output
- self.out_indices = [0, 2, 4, 6]
- for i_emb, i_layer in enumerate(self.out_indices):
- if i_emb == 0 and os.environ.get('FORK_LAST3', None):
- raise NotImplementedError
- else:
- layer = norm_layer(int(embed_dim * 2 ** i_emb))
- layer_name = f'norm{i_layer}'
- self.add_module(layer_name, layer)
- self.apply(self.cls_init_weights)
- self.init_cfg = copy.deepcopy(init_cfg)
- if self.fork_feat and (self.init_cfg is not None or pretrained is not None):
- self.init_weights()
- self.width_list = [i.size(1) for i in self.forward(torch.randn(1, 3, 640, 640))]
- def cls_init_weights(self, m):
- if isinstance(m, nn.Linear):
- trunc_normal_(m.weight, std=.02)
- if isinstance(m, nn.Linear) and m.bias is not None:
- nn.init.constant_(m.bias, 0)
- elif isinstance(m, (nn.Conv1d, nn.Conv2d)):
- trunc_normal_(m.weight, std=.02)
- if m.bias is not None:
- nn.init.constant_(m.bias, 0)
- elif isinstance(m, (nn.LayerNorm, nn.GroupNorm)):
- nn.init.constant_(m.bias, 0)
- nn.init.constant_(m.weight, 1.0)
- def forward_det(self, x: Tensor) -> Tensor:
- # output the features of four stages for dense prediction
- x = self.patch_embed(x)
- outs = []
- for idx, stage in enumerate(self.stages):
- x = stage(x)
- if self.fork_feat and idx in self.out_indices:
- norm_layer = getattr(self, f'norm{idx}')
- x_out = norm_layer(x)
- outs.append(x_out)
- return outs
- if __name__ == "__main__":
- # Generating Sample image
- image_size = (1, 3, 640, 640)
- image = torch.rand(*image_size)
- # Model
- model = FasterNet()
- out = model(image)
- print(len(out))
四、手把手教你添加FasterNet机制
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 修改七
如下的也需要修改,全部按照我的来。
代码如下把原先的代码替换了即可。
- 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)
4.8 修改八
修改八和前面的都不太一样,需要修改前向传播中的一个部分, 已经离开了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
五、FasterNet的yaml文件
5.1 FasterNet的yaml文件
训练信息:YOLO11-FasterNet summary: 351 layers, 2,384,163 parameters, 2,384,147 gradients, 5.6 GFLOPs
使用说明: # 下面 [-1, 1, FasterNet, [0.25]] 参数位置的0.25是通道放缩的系数, YOLOv11N是0.25 YOLOv11S是0.5 YOLOv11M是1. YOLOv11l是1 YOLOv11是1.5大家根据自己训练的YOLO版本设定即可.
- # 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
- # 下面 [-1, 1, FasterNet, [0.25]] 参数位置的0.25是通道放缩的系数, YOLOv11N是0.25 YOLOv11S是0.5 YOLOv11M是1. YOLOv11l是1 YOLOv11是1.5大家根据自己训练的YOLO版本设定即可.
- # YOLO11n backbone
- backbone:
- # [from, repeats, module, args]
- - [-1, 1, FasterNet, [0.25]] # 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('yolov8-MLLA.yaml')
- # 如何切换模型版本, 上面的ymal文件可以改为 yolov8s.yaml就是使用的v8s,
- # 类似某个改进的yaml文件名称为yolov8-XXX.yaml那么如果想使用其它版本就把上面的名称改为yolov8l-XXX.yaml即可(改的是上面YOLO中间的名字不是配置文件的)!
- # model.load('yolov8n.pt') # 是否加载预训练权重,科研不建议大家加载否则很难提升精度
- model.train(data=r"C:\Users\Administrator\PycharmProjects\yolov5-master\yolov5-master\Construction Site Safety.v30-raw-images_latestversion.yolov8\data.yaml",
- # 如果大家任务是其它的'ultralytics/cfg/default.yaml'找到这里修改task可以改成detect, segment, classify, pose
- cache=False,
- imgsz=640,
- epochs=150,
- single_cls=False, # 是否是单类别检测
- batch=16,
- close_mosaic=0,
- workers=0,
- device='0',
- optimizer='SGD', # using SGD
- # resume='runs/train/exp21/weights/last.pt', # 如过想续训就设置last.pt的地址
- amp=True, # 如果出现训练损失为Nan可以关闭amp
- project='runs/train',
- name='exp',
- )
六、成功运行记录
下面是成功运行的截图,已经完成了有1个epochs的训练,图片太大截不全第2个epochs了。
七、本文总结
到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv11改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充 , 如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~