秋招面试专栏推荐 :深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转
💡💡💡本专栏所有程序均经过测试,可成功执行💡💡💡
专栏目录 :《YOLOv8改进有效涨点》专栏介绍 & 专栏目录 | 目前已有70+篇内容,内含各种Head检测头、损失函数Loss、Backbone、Neck、NMS等创新点改进——点击即可跳转
DSConv(分布移位卷积)的卷积层变体,它可以很容易地替换到标准神经网络架构中,并实现更低的内存使用和更高的计算速度。 DSConv 将传统的卷积核分解为两个组件:可变量化核 (VQK) 和分布偏移。通过在 VQK 中仅存储整数值来实现更低的内存使用和更高的速度,同时通过应用基于内核和通道的分布偏移来保留与原始卷积相同的输出。我们在 ResNet50 和 ResNet34 以及 AlexNet 和 MobileNet 上测试 ImageNet 中的 DSConv。通过将浮点运算替换为整数运算,将卷积核中的内存使用量减少了 14 倍,并将运算速度提高了 10 倍。文章在介绍主要的原理后,将手把手教学如何进行模块的代码添加和修改,并将修改后的完整代码放在文章的最后,方便大家一键运行,小白也可轻松上手实践。以帮助您更好地学习深度学习目标检测YOLO系列的挑战。
目录
1. 原理
论文地址:DSConv: Efficient Convolution Operator——点击即可跳转
官方代码:官方代码仓库——点击即可跳转
DSConv 是分布移位卷积 (Distribution Shift Convolution) 的缩写,是一种卷积运算符,旨在实现卷积神经网络 (CNN) 的有效量化,同时保持准确性。下面详细解释其主要原理和组件:
1. 背景和动机
量化是一种用于将神经网络中权重和激活的精度从浮点表示降低到较低位宽整数表示的技术。这减少了内存使用量并提高了计算效率,这对于在资源受限的设备上部署神经网络至关重要。但是,量化通常需要使用标记数据重新训练网络以保持准确性,这并不总是可行的。DSConv 旨在通过实现有效量化来解决此问题,而无需使用标记数据重新训练。
2. DSConv 的关键概念
-
概率分布维护:DSConv 背后的主要见解是,保持量化模型和原始模型之间的权重和激活的概率分布可以保持准确性。这是通过将卷积权重分解为低精度分量和高精度分布偏移分量来实现的。
-
块浮点 (BFP) 方法:DSConv 使用块浮点方法来量化激活。激活张量被分成块,每个块共享一个指数。这减少了精度损失并保持了激活值的分布。
3. 权重量化
-
权重分解:权重张量沿深度维度分成块。每个块都有一个低位整数分量(可变量化核,VQK)和一个高精度浮点缩放因子(核分布偏移,KDS)。
-
最小化分布偏移:可以使用两种方法来确定 KDS 值:最小化 Kullback-Leibler (KL) 散度或最小化原始权重和量化权重之间的 L2 范数。由于 L2 范数方法具有闭式解,因此更受青睐。
4. 激活量化
-
共享指数:使用块浮点方法量化激活,其中激活块共享单个指数,允许进行低位整数运算。这可以保持激活的分布并减少精度损失。
5. 推理优化
-
整数运算:在推理过程中,VQK 和尾数张量的低位整数值可实现高效的整数运算,与浮点运算相比,计算速度显著加快。
6. 灵活性和超参数调整
-
超参数 B:块大小超参数 ( B ) 决定了准确性和内存使用/速度之间的权衡。较大的 ( B ) 会减少浮点运算的数量,但可能会增加量化误差。
7. 实验结果
-
最新性能:DSConv 已在 ResNet、DenseNet、GoogLeNet、AlexNet 和 VGG-Net 等流行神经网络架构上进行了测试。它仅使用 4 位量化,无需重新训练,即可实现最新结果,准确度损失不到 1%。
结论
DSConv 是一种新颖的量化技术,通过保留原始模型的概率分布,可有效降低 CNN 权重和激活的精度,同时保持准确度。它利用低位整数运算和灵活的块浮点方法的组合来实现显着的内存和计算节省。这使得它适合在计算资源有限的硬件上部署,而无需进行大量重新训练。
2. 代码实现
2.1 添加C2f_DSConv到YOLOv8代码中
关键步骤一:将下面代码粘贴到在/ultralytics/ultralytics/nn/modules/block.py中,并在该文件的__all__中添加“C2f_DSConv”
from torch.nn.modules.conv import _ConvNd
from torch.nn.modules.utils import _pair
import math
import torch.nn.functional as F
class DSConv(_ConvNd):
def __init__(self, in_channels, out_channels, kernel_size, stride=1,
padding=None, dilation=1, groups=1, padding_mode='zeros', bias=False, block_size=32, KDSBias=False,
CDS=False):
padding = _pair(autopad(kernel_size, padding, dilation))
kernel_size = _pair(kernel_size)
stride = _pair(stride)
dilation = _pair(dilation)
blck_numb = math.ceil((in_channels / (block_size * groups)))
super(DSConv, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
False, _pair(0), groups, bias, padding_mode)
# KDS weight From Paper
self.intweight = torch.Tensor(out_channels, in_channels, *kernel_size)
self.alpha = torch.Tensor(out_channels, blck_numb, *kernel_size)
# KDS bias From Paper
self.KDSBias = KDSBias
self.CDS = CDS
if KDSBias:
self.KDSb = torch.Tensor(out_channels, blck_numb, *kernel_size)
if CDS:
self.CDSw = torch.Tensor(out_channels)
self.CDSb = torch.Tensor(out_channels)
self.reset_parameters()
def get_weight_res(self):
# Include expansion of alpha and multiplication with weights to include in the convolution layer here
alpha_res = torch.zeros(self.weight.shape).to(self.alpha.device)
# Include KDSBias
if self.KDSBias:
KDSBias_res = torch.zeros(self.weight.shape).to(self.alpha.device)
# Handy definitions:
nmb_blocks = self.alpha.shape[1]
total_depth = self.weight.shape[1]
bs = total_depth // nmb_blocks
llb = total_depth - (nmb_blocks - 1) * bs
# Casting the Alpha values as same tensor shape as weight
for i in range(nmb_blocks):
length_blk = llb if i == nmb_blocks - 1 else bs
shp = self.alpha.shape # Notice this is the same shape for the bias as well
to_repeat = self.alpha[:, i, ...].view(shp[0], 1, shp[2], shp[3]).clone()
repeated = to_repeat.expand(shp[0], length_blk, shp[2], shp[3]).clone()
alpha_res[:, i * bs:(i * bs + length_blk), ...] = repeated.clone()
if self.KDSBias:
to_repeat = self.KDSb[:, i, ...].view(shp[0], 1, shp[2], shp[3]).clone()
repeated = to_repeat.expand(shp[0], length_blk, shp[2], shp[3]).clone()
KDSBias_res[:, i * bs:(i * bs + length_blk), ...] = repeated.clone()
if self.CDS:
to_repeat = self.CDSw.view(-1, 1, 1, 1)
repeated = to_repeat.expand_as(self.weight)
print(repeated.shape)
# Element-wise multiplication of alpha and weight
weight_res = torch.mul(alpha_res, self.weight)
if self.KDSBias:
weight_res = torch.add(weight_res, KDSBias_res)
return weight_res
def forward(self, input):
# Get resulting weight
# weight_res = self.get_weight_res()
# Returning convolution
return F.conv2d(input, self.weight, self.bias,
self.stride, self.padding, self.dilation,
self.groups)
class DSConv2D(Conv):
def __init__(self, inc, ouc, k=1, s=1, p=None, g=1, d=1, act=True):
super().__init__(inc, ouc, k, s, p, g, d, act)
self.conv = DSConv(inc, ouc, k, s, p, g, d)
class Bottleneck_DSconv(nn.Module):
"""Standard bottleneck."""
def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
"""Initializes a bottleneck module with given input/output channels, shortcut option, group, kernels, and
expansion.
"""
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, k[0], 1)
self.cv2 = DSConv2D(c_, c2, k[1], 1, g=g)
self.add = shortcut and c1 == c2
def forward(self, x):
"""'forward()' applies the YOLO FPN to input data."""
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class C2f_DSConv(nn.Module):
"""Faster Implementation of CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
"""Initialize CSP bottleneck layer with two convolutions with arguments ch_in, ch_out, number, shortcut, groups,
expansion.
"""
super().__init__()
self.c = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, 2 * self.c, 1, 1)
self.cv2 = DSConv2D((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
self.m = nn.ModuleList(Bottleneck_DSconv(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
def forward(self, x):
"""Forward pass through C2f layer."""
y = list(self.cv1(x).chunk(2, 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
def forward_split(self, x):
"""Forward pass using split() instead of chunk()."""
y = list(self.cv1(x).split((self.c, self.c), 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
DSConv(分布式卷积)是一种高效的卷积操作符,其主要流程包括以下几个步骤:
预训练网络中的权重张量分块:
从预训练网络中,将权重张量按深度维度划分为可变长度的块(block),每个块的长度由超参数B决定。
激活量化:
使用块浮点格式(BFP)对激活值进行量化。这里的块大小与权重张量的块大小相同。
整数乘法:
将激活值和权重张量的整数值相乘,以最大化推理速度。这一步骤通过使用更低成本的整数操作代替传统的浮点操作来加速计算。
尺度调整:
将最终的乘积值乘以相应的尺度因子,以将各个块的分布调整到正确的范围。
这些步骤的具体实现如下:
-
权重量化:
-
每个块中的权重共享一个浮点值。权重张量被划分为低位整数值组成的张量和浮点尺度因子组成的张量。尺度因子的大小调整为捕捉值的范围。
-
-
激活量化:
-
使用相同的块大小对激活值进行量化。
-
-
计算加速:
-
利用整数操作(例如,乘法、加法)代替传统的浮点操作,从而显著提高计算速度。
-
-
尺度调整:
-
通过乘以预先计算好的尺度因子,调整量化后的激活值和权重值的分布。
-
通过这些步骤,DSConv能够在保持高精度的同时显著减少计算量和内存占用,并且在无需重新训练数据的情况下实现高效推理。
2.2 更改init.py文件
关键步骤二:修改modules文件夹下的__init__.py文件,先导入函数
然后在下面的__all__中声明函数
2.3 新增yaml文件
关键步骤三:在 \ultralytics\ultralytics\cfg\models\v8下新建文件 yolov8_C2f_DSConv.yaml并将下面代码复制进去
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv10 object detection model. 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]
l: [1.00, 1.00, 512]
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, SCDown, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]]
- [-1, 1, SCDown, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2fCIB, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 1, PSA, [1024]] # 10
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f_DSConv, [512, True]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 13], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f_DSConv, [512, True]] # 19 (P4/16-medium)
- [-1, 1, SCDown, [512, 3, 2]]
- [[-1, 10], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f_DSConv, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, v10Detect, [nc]] # Detect(P3, P4, P5)
温馨提示:本文只是对yolov10基础上添加模块,如果要对yolov10n/l/m/x进行添加则只需要指定对应的depth_multiple 和 width_multiple。
# YOLOv10n
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.25 # layer channel multiple
max_channels: 1024 # max_channels
# YOLOv10s
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
max_channels: 1024 # max_channels
# YOLOv10l
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
max_channels: 512 # max_channels
# YOLOv10m
depth_multiple: 0.67 # model depth multiple
width_multiple: 0.75 # layer channel multiple
max_channels: 768 # max_channels
# YOLOv10x
depth_multiple: 1.33 # model depth multiple
width_multiple: 1.25 # layer channel multiple
max_channels: 512 # max_channels
2.4 注册模块
关键步骤四:在task.py的parse_model函数中进行注册,添加C2f_DSConv,
还是parse_model函数中
2.5 执行程序
在train.py中,将model的参数路径设置为yolov10_C2f_DSConv.yaml的路径
建议大家写绝对路径,确保一定能找到
from ultralytics import YOLO
# Load a model
# model = YOLO('yolov8n.yaml') # build a new model from YAML
# model = YOLO('yolov8n.pt') # load a pretrained model (recommended for training)
model = YOLO(r'/projects/ultralytics/ultralytics/cfg/models/v8/yolov8_C2f_DSConv.yaml') # build from YAML and transfer weights
# Train the model
model.train(batch=16)
🚀运行程序,如果出现下面的内容则说明添加成功🚀
from n params module arguments
0 -1 1 1856 ultralytics.nn.modules.conv.Conv [3, 64, 3, 2]
1 -1 1 73984 ultralytics.nn.modules.conv.Conv [64, 128, 3, 2]
2 -1 3 279808 ultralytics.nn.modules.block.C2f [128, 128, 3, True]
3 -1 1 295424 ultralytics.nn.modules.conv.Conv [128, 256, 3, 2]
4 -1 6 2101248 ultralytics.nn.modules.block.C2f [256, 256, 6, True]
5 -1 1 137728 ultralytics.nn.modules.block.SCDown [256, 512, 3, 2]
6 -1 6 8396800 ultralytics.nn.modules.block.C2f [512, 512, 6, True]
7 -1 1 537600 ultralytics.nn.modules.block.SCDown [512, 1024, 3, 2]
8 -1 3 6896640 ultralytics.nn.modules.block.C2fCIB [1024, 1024, 3, True]
9 -1 1 2624512 ultralytics.nn.modules.block.SPPF [1024, 1024, 5]
10 -1 1 3948032 ultralytics.nn.modules.block.PSA [1024, 1024]
11 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
12 [-1, 6] 1 0 ultralytics.nn.modules.conv.Concat [1]
13 -1 3 8025708 ultralytics.nn.modules.block.C2f_DSConv [1536, 512, 3, True]
14 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
15 [-1, 4] 1 0 ultralytics.nn.modules.conv.Concat [1]
16 -1 3 1247744 ultralytics.nn.modules.block.C2f [768, 256, 3]
17 -1 1 590336 ultralytics.nn.modules.conv.Conv [256, 256, 3, 2]
18 [-1, 13] 1 0 ultralytics.nn.modules.conv.Concat [1]
19 -1 3 7632492 ultralytics.nn.modules.block.C2f_DSConv [768, 512, 3, True]
20 -1 1 268800 ultralytics.nn.modules.block.SCDown [512, 512, 3, 2]
21 [-1, 10] 1 0 ultralytics.nn.modules.conv.Concat [1]
22 -1 3 30338156 ultralytics.nn.modules.block.C2f_DSConv [1536, 1024, 3, True]
23 [16, 19, 22] 1 3686806 ultralytics.nn.modules.head.v10Detect [1, [256, 512, 1024]]
YOLOv10_test summary: 683 layers, 77,083,674 parameters, 77,083,658 gradients, 185.8 GFLOPs
3. 完整代码分享
https://pan.baidu.com/s/1_KtTtIPgemxlIBNMcgUbsw?pwd=hyxi
提取码: hyxi
4. GFLOPs
关于GFLOPs的计算方式可以查看:百面算法工程师 | 卷积基础知识——Convolution
未改进的YOLOv10l GFLOPs
改进后的GFLOPs
5. 进阶
可以与其他的注意力机制或者损失函数等结合,进一步提升检测效果
6. 总结
DSConv(Distribution Shift Convolution,分布偏移卷积)是一种优化卷积神经网络量化的方法,旨在在保持高精度的同时减少计算复杂度和内存占用。其主要原理是通过将权重张量分块,并在每个块内使用低位整数表示和浮点尺度因子组合的方式来量化权重,保持原始权重分布的同时最小化量化误差。激活值同样通过块浮点格式(BFP)进行量化,每个块共享一个指数值以保持分布。推理过程中,利用低位整数操作替代浮点操作以加速计算,并通过预先计算好的尺度因子调整量化值的分布,从而实现高效且准确的推理,无需重新训练网络。这种方法在多种经典网络架构上实现了近乎无损的量化效果,同时显著提升了运行效率。