【AI系统】MobileNet 系列

MobileNet 系列

在本文会介绍 MobileNet 系列,重点在于其模型结构的轻量化设计,主要介绍详细的轻量化设计原则,基于这原则,MobileNetV1 是如何设计成一个小型,低延迟,低功耗的参数化模型,可以满足各种用例的资源约束。可以更方便的实现分类,检测,嵌入和分割等功能。会结合目前较流行的深度学习技术,在 V1 的基础会分别讲解 V2,V3,V4 做出的改进升级,让读者们更深入了解轻量级网络结构的设计思路与过程。

MobileNet V1 模型

MobileNet V1 是一种体积较小、计算量较少、适用于移动设备的卷积神经网络。mobileNet V1 的主要创新点是用深度可分离卷积(depthwise separable convolution)代替普通的卷积,并使用宽度乘数(width multiply)减少参数量,在 ImageNet 图像分类、Stanford Dog 细粒度图像分类、目标检测、人脸属性识别、人脸编码、以图搜地等计算机视觉任务上,结合知识蒸馏进行评估,MobileNet 表现出极致的轻量化和速度性能。

逐通道卷积

逐通道卷积(Depthwise Convolution)的一个卷积核只有一个通道,输入信息的一个通道只被一个卷积核卷积,这个过程产生的 feature map 通道数和输入的通道数完全一样,如下图所示:

在这里插入图片描述

深度分离卷积把输入特征图的所有通道进行分离,每个通道对应的一个卷积核对该通道的特征图进行单独的卷积操作(也就是说,第 m 个深度卷积核作用在输入的第 m 个通道上,得到输出结果的第 m 个通道)。在深度分离卷积中,每个卷积核的深度固定为 1。

参数量: D k × D k × M D_{k}\times D_{k}\times M Dk×Dk×M

计算量 : D k × D k × M × D f × D f D_{k}\times D_{k}\times M\times D_{f}\times D_{f} Dk×Dk×M×Df×Df

其中 D k D_{k} Dk 为卷积核尺寸, D f D_{f} Df 为特征图尺寸,M 为输入通道数,输出通道数为 1。

逐点卷积

逐点卷积(Pointwise Convolution)的本质就是 1 × 1 1\times 1 1×1 的卷积,它的卷积核的尺寸为 1 × 1 × M 1\times 1\times M 1×1×M,M 为上一层输出信息的通道数。所以这里 Pointwise Convolution 的每个卷积核会将上一步的特征图在通道方向上进行加权组合,生成新的特征图,如下图所示:
在这里插入图片描述

参数量: 1 × 1 × M × N 1\times 1\times M\times N 1×1×M×N

计算量: 1 × 1 × M × N × D f × D f 1\times 1\times M\times N\times D_{f}\times D_{f} 1×1×M×N×Df×Df

其中卷积核尺寸是 1 × 1 1\times1 1×1 D f D_{f} Df 为特征图尺寸, M M M 为输入通道数, N N N 为输出通道数。

因此计算一次深度可分离卷积的总体计算量为:

D k ⋅ D k ⋅ M ⋅ D F ⋅ D F + M ⋅ N ⋅ D F ⋅ D F D_{k}\cdot D_{k}\cdot M\cdot D_{F}\cdot D_{F} + M\cdot N\cdot D_{F}\cdot D_{F} DkDkMDFDF+MNDFDF

它们减少计算量的比例(参数量比例同计算量)为:

D k ⋅ D k ⋅ M ⋅ D F ⋅ D F + M ⋅ N ⋅ D F ⋅ D F D k ⋅ D k ⋅ M ⋅ N ⋅ D F ⋅ D F = 1 N + 1 D k 2 \frac{D_{k}\cdot D_{k}\cdot M\cdot D_{F}\cdot D_{F} + M\cdot N\cdot D_{F}\cdot D_{F}}{D_{k}\cdot D_{k}\cdot M\cdot N\cdot D_{F}\cdot D_{F}}=\frac{1}{N}+\frac{1}{D_{k}^{2}} DkDkMNDFDFDkDkMDFDF+MNDFDF=N1+Dk21

# 定义 DW、PW 卷积模块
def conv_dw(inp, oup, stride):return nn.Sequential(# DW  DW 卷积的卷积核输入与输出的数量一致,且等于分组数nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),  nn.BatchNorm2d(inp),nn.ReLU(inplace=True),# PWnn.Conv2d(inp, oup, 1, 1, 0, bias=False),nn.BatchNorm2d(oup),nn.ReLU(inplace=True),)

宽度&分辨率乘子

宽度和分辨率调整系数用于调整模型的大小和计算复杂性。

  • 宽度系数(α):宽度系数是一个介于 0 和 1 之间的比例因子。通过降低每个卷积层的通道数,可以减少模型中的参数数量和计算量,从而使模型更轻量化。

  • 分辨率系数(ρ):分辨率系数是一个介于 0 和 1 之间的比例因子。通过降低输入图像的分辨率,可以减少卷积操作的计算量和内存消耗。

计算量:

D k ⋅ D k ⋅ α M ⋅ ρ D F ⋅ ρ D F + α M ⋅ α N ⋅ ρ D F ⋅ ρ D F D_{k}\cdot D_{k} \cdot αM\cdotρD_{F}\cdotρD_{F}+ αM\cdot αN\cdotρD_{F}\cdotρD_{F} DkDkαMρDFρDF+αMαNρDFρDF

计算量减少了:

D k ⋅ D k ⋅ α M ⋅ ρ D F ⋅ ρ D F + α M ⋅ α N ⋅ ρ D F ⋅ ρ D F D k ⋅ D k ⋅ M ⋅ N ⋅ D F ⋅ D F = α ρ N + α 2 ρ 2 D k 2 \frac{D_{k}\cdot D_{k} \cdot αM\cdotρD_{F}\cdotρD_{F}+ αM\cdot αN\cdotρD_{F}\cdotρD_{F}}{D_{k}\cdot D_{k} \cdot M\cdot N\cdot D_{F}\cdot D_{F}} =\frac{αρ}{N}+\frac{α^{2}ρ^{2}}{D_{k}^{2}} DkDkMNDFDFDkDkαMρDFρDF+αMαNρDFρDF=Nαρ+Dk2α2ρ2

网络结构实现

在 V1 结构中会加入 BN,并使用 ReLU 激活函数,所以 depthwise separable convolution 的基本结构如下图右面所示, 左面是正常的 conv:

在这里插入图片描述

import torch.nn as nn
import torch
# 定义普通卷积、BN、激活模块
def conv_bn(inp, oup, stride):return nn.Sequential(nn.Conv2d(inp, oup, 3, stride, 1, bias=False),nn.BatchNorm2d(oup),nn.ReLU(inplace=True))
# 定义 DW、PW 卷积模块
def conv_dw(inp, oup, stride):return nn.Sequential(# dwnn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),   # DW 卷积的卷积核输入与输出的数量一致,且等于分组数nn.BatchNorm2d(inp),nn.ReLU(inplace=True),# pwnn.Conv2d(inp, oup, 1, 1, 0, bias=False),nn.BatchNorm2d(oup),nn.ReLU(inplace=True),)

整体网络就是通过不断堆叠 MBconv 组件组成的,首先是一个 3x3 的标准卷积,然后后面就是堆积 depthwise separable convolution,并且可以看到其中的部分 depthwise convolution 会通过 strides=2 进行 down sampling。经过 卷积提取特征后再采用 average pooling 将 feature 变成 1x1,根据预测类别大小加上全连接层,最后是一个 softmax 层。

import torch.nn as nn
import torchclass MobileNetV1(nn.Module):def __init__(self, ch_in, n_classes):super(MobileNetV1, self).__init__()self.model = nn.Sequential(conv_bn(ch_in, 32, 2), #普通卷积conv_dw(32, 64, 1),    #DW 卷积conv_dw(64, 128, 2),conv_dw(128, 128, 1),conv_dw(128, 256, 2),conv_dw(256, 256, 1),conv_dw(256, 512, 2),conv_dw(512, 512, 1),conv_dw(512, 512, 1),conv_dw(512, 512, 1),conv_dw(512, 512, 1),conv_dw(512, 512, 1),conv_dw(512, 1024, 2),conv_dw(1024, 1024, 1),nn.AdaptiveAvgPool2d(1)  #自适应平均池化)self.fc = nn.Linear(1024, n_classes) #线性层def forward(self, x):x = self.model(x)x = x.view(-1, 1024)x = self.fc(x)return x

MobileNet V2 模型

MobileNet-V2 的主要思想就是在 v1 的基础上引入了线性瓶颈 (Linear Bottleneck)和逆残差 (Inverted Residual)来提高网络的表征能力,同样也是一种轻量级的卷积神经网络。

Linear Bottlenecks

MobileNetV1 中引入α参数来做模型通道的缩减,相当于给模型“瘦身”,这样特征信息就能更集中在缩减后的通道中。但研究人员发现深度可分离卷积中有大量卷积核为 0,即有很多卷积核没有参与实际计算。研究后发现是 ReLU 激活函数的问题,认为 ReLU 这个激活函数,在低维空间运算中会损失很多信息,而在高维空间中会保留较多有用信息 。ReLU 会对维度较低的张量造成较大的信息损耗。

维度越低,损失信息越多。(如下图 2 和 3 已经没有螺旋的样子了);维度越高,损失信息越少(当原始输入维度数增加到 15 以后再加 ReLU,基本不会丢失太多的信息,接近输入)。

在这里插入图片描述

如果"manifold of interest"(兴趣流形)都为非零值,则经过 ReLU 相当于只做了一个线性变换,没有信息丢失,维度足够多时,ReLU 能够保留"manifold of interest"(兴趣流形)的完整信息。

论文针对这个问题在 Bottleneck 末尾使用 Linear Bottleneck(即不使用 ReLU 激活,做了线性变换)来代替原本的非线性激活变换。具体到 V2 网络中就是将最后的 Point-Wise 卷积的 ReLU6 都换成线性函数。

实验证据表明,使用线性层至关重要,因为它可以防止非线性破坏太多的信息。

可分离卷积演化

可分离卷积块的演化如下图所示:

  1. 标准卷积(一个大方块);
  2. 深度可分离卷积(=Depthwise convolution+Pointwise Convolution=薄片片+方块块);
  3. linear bottleneck,(高维后)relu6-dw-relu6-pw,降维-升维-;
  4. 和图©等效,(线性激活后)pw 升维-relu6-dw-relu6-pw,降维-线性激活;

在这里插入图片描述

反向残差引入

反向残差(Inverted residuals )如下图所示:

在这里插入图片描述

  • Original residual block:reduce – transfer – expand (中间窄两头宽)

Residual block 先用 1 × 1 1 \times 1 1×1 卷积降通道过 ReLU,再 3 × 3 3 \times 3 3×3 卷积过 ReLU,最后再用 1 × 1 1 \times 1 1×1 卷积过 ReLU 恢复通道,并和输入相加。之所以要 1 × 1 1 \times 1 1×1 卷积降通道,是为了减少计算量,不然中间的 $ 3 \times 3$ 卷积计算量太大。所以 Residual block 是中间窄两头宽。

  • Inverted residual block:expand – transfer – reduce (中间宽两头窄)

在 Inverted Residual block 中,$ 3 \times 3$ 卷积变成 Depthwise 了,计算量很少了,所以通道数可以多一点,效果更好,所以通过 1 × 1 1 \times 1 1×1 卷积先提升通道数,再 Depthwise $ 3 \times 3$ 卷积,最后用 1 × 1 1 \times 1 1×1 卷积降低通道数。两端的通道数都很小,所以 1 × 1 1 \times 1 1×1 卷积升通道和降通道计算量都并不大,而中间的通道数虽然多,但是 Depthwise 的卷积计算量也不。

# 定义 mobile 网络基本结构--即到残差结构
class InvertedResidual(nn.Module):def __init__(self, in_channel, out_channel, stride, expand_ratio):super(InvertedResidual, self).__init__()hidden_channel = in_channel * expand_ratioself.use_shortcut = stride == 1 and in_channel == out_channel  # stride == 1 and in_channel == out_channel:保证输入矩阵与输出矩阵的 shape 一致,且通道数也一致,这样才可以进行 shurtcutlayers = []if expand_ratio != 1:  # 表示如果扩展因子不为 1 时,则使用 1x1 的卷积层(即对输入特征矩阵的深度进行扩充)# 1x1 pointwise convlayers.append(ConvBNReLU(in_channel, hidden_channel, kernel_size=1))layers.extend([# 3x3 depthwise conv# 在 pytorch 中,如果设置的 group=1 的话,就为普通卷积;如果设置的值为输入特征矩阵的深度的话(即 in_channel),则为深度卷积(deptwise conv),并且 Dw 卷积的输出特征矩阵的深度等于输入特征矩阵的深度ConvBNReLU(hidden_channel, hidden_channel, stride=stride, groups=hidden_channel),# 1x1 pointwise conv(linear)  因为其后跟随的是线性激活函数,即 y=x,所以其后面不在跟随激活函数nn.Conv2d(hidden_channel, out_channel, kernel_size=1, bias=False),nn.BatchNorm2d(out_channel),])self.conv = nn.Sequential(*layers)def forward(self, x):if self.use_shortcut:return x + self.conv(x)else:return self.conv(x)

ReLU6 激活

卷积之后通常会接一个 ReLU 非线性激活,在 MobileNet 中使用 ReLU6。ReLU6 在普通的 ReLU 基础上限制最大输出为 6,这是为了在移动端设备 float16/int8 的低精度的时候也能有很好的数值分辨率。

如果对 ReLU 的激活范围不加限制,输出范围为 0 到正无穷,如果激活值非常大,分布在一个很大的范围内,则低精度的 float16/int8 无法很好地精确描述如此大范围的数值,带来精度损失。

# 定义普通卷积、BN 结构
class ConvBNReLU(nn.Sequential):def __init__(self, in_channel, out_channel, kernel_size=3, stride=1, groups=1):padding = (kernel_size - 1) // 2  # padding 的设置根据 kernel_size 来定,如果 kernel_size 为 3,则 padding 设置为 1;如果 kernel_size 为 1,为 padding 为 0super(ConvBNReLU, self).__init__(# 在 pytorch 中,如果设置的 group=1 的话,就为普通卷积;如果设置的值为输入特征矩阵的深度的话(即 in_channel),则为深度卷积(deptwise conv),并且 Dw 卷积的输出特征矩阵的深度等于输入特征矩阵的深度nn.Conv2d(in_channel, out_channel, kernel_size, stride, padding, groups=groups, bias=False),  # groups=1,示普通的卷积;因为接下来要使用的是 BN 层,此处的偏置不起任何作用,所以设置为 1nn.BatchNorm2d(out_channel),nn.ReLU6(inplace=True)    # 此处使用的是 Relu6 激活函数)

网络结构实现

V2 的加入了 1 × 1 1 \times 1 1×1 升维,引入 Shortcut 并且去掉了最后的 ReLU,改为 Linear。步长为 1 时,先进行 1 × 1 1 \times 1 1×1 卷积升维,再进行深度卷积提取特征,再通过 Linear 的逐点卷积降维。

将 input 与 output 相加,形成残差结构。步长为 2 时,因为 input 与 output 的尺寸不符,因此不添加 shortcut 结构。整个结构由 V2 block 堆叠而成。

在这里插入图片描述

from torch import nn
import torchdef _make_divisible(ch, divisor=8, min_ch=None):"""将输入的通道数(ch)调整到 divisor 的整数倍,方便硬件加速This function is taken from the original tf repo.It ensures that all layers have a channel number that is divisible by 8It can be seen here:"""if min_ch is None:min_ch = divisornew_ch = max(min_ch, int(ch + divisor / 2) // divisor * divisor)# Make sure that round down does not go down by more than 10%.if new_ch < 0.9 * ch:new_ch += divisorreturn new_ch# 定义 mobileNetV2 网络
class MobileNetV2(nn.Module):def __init__(self, num_classes=1000, alpha=1.0, round_nearest=8):super(MobileNetV2, self).__init__()block = InvertedResidualinput_channel = _make_divisible(32 * alpha, round_nearest)  # 将卷积核的个数调整为 8 的整数倍last_channel = _make_divisible(1280 * alpha, round_nearest)inverted_residual_setting = [# t, c, n, s[1, 16, 1, 1],[6, 24, 2, 2],[6, 32, 3, 2],[6, 64, 4, 2],[6, 96, 3, 1],[6, 160, 3, 2],[6, 320, 1, 1],]features = []# conv1 layerfeatures.append(ConvBNReLU(3, input_channel, stride=2))   # 添加第一层普通卷积层# building inverted residual residual blockesfor t, c, n, s in inverted_residual_setting:output_channel = _make_divisible(c * alpha, round_nearest)   # 根据 alpha 因子调整卷积核的个数for i in range(n):   # 循环添加倒残差模块stride = s if i == 0 else 1  # s 表示的是倒残差模块结构中第一层卷积对应的步距,剩余层都是 1features.append(block(input_channel, output_channel, stride, expand_ratio=t))  # 添加一系列倒残差结构input_channel = output_channel# building last several layersfeatures.append(ConvBNReLU(input_channel, last_channel, 1))  # 构建最后一层卷积层# combine feature layersself.features = nn.Sequential(*features)# building classifierself.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # 采用自适应平均采样层self.classifier = nn.Sequential(nn.Dropout(0.2),nn.Linear(last_channel, num_classes))# weight initialization  初始化全只能怪for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, mode='fan_out')if m.bias is not None:nn.init.zeros_(m.bias)elif isinstance(m, nn.BatchNorm2d):nn.init.ones_(m.weight)nn.init.zeros_(m.bias)elif isinstance(m, nn.Linear):nn.init.normal_(m.weight, 0, 0.01)   # 初始化为正态分布的函数,均值为 0,方差为 0.01nn.init.zeros_(m.bias)def forward(self, x):x = self.features(x)x = self.avgpool(x)x = torch.flatten(x, 1)x = self.classifier(x)return x

MobileNet V3 模型

MobileNetV3 是由谷歌团队在 2019 年提出的轻量化网络模型,传统的卷积神经网络,内容需求大,运算量大,无法再移动设备以及嵌入式设备上运行,为了解决这一问题,MobileNet V3 网络应运而生。在移动端图像分类、目标检测、语义分割等任务上均取得了优秀的表现。MobileNetV3 采用了很多新的技术,包括针对通道注意力的 Squeeze-and-Excitation 模块、NAS 搜索方法等,这些方法都有利于进一步提升网络的性能。

重新设计耗时层结构

首先,减少网络第一个卷积层的卷积核个数,从 32 减到 16,然后精简了最后的 Stage,将原来搜索到的最后阶段的人工精简,删除了多余的卷积层,将延迟较少了 7 毫秒,将近全部运行时间的 11%,并减少了 3000 万的乘加操作次数,几乎没有损失准确性。

在这里插入图片描述

重新设计激活函数

引入新的非线性激活函数:h-swish。swish 公式:

S w i s h x = x ∗ α ( x ) Swish x = x*α(x) Swishx=xα(x)

这里σ(x)是 sigmoid 函数。swish 虽然提高了精度,但 sigmoid 函数计算是极为昂贵的,在嵌入式移动端不适合它的存在,因此,MobileNet_V3 提出了计算更为简便的 h-swish 函数,其定义如下:

h − s w i s h [ x ] = x R e L U 6 ( x + 3 ) 6 h-swish[x] = x\frac{ReLU6(x+3)}{6} hswish[x]=x6ReLU6(x+3)

替换前后能够对 swish 进行一个很好的近似,如下图所示:

在这里插入图片描述

在网络结构搜索中,作者结合两种技术:资源受限的 NAS(platform-aware NAS)与 NetAdapt,前者用于在计算和参数量受限的前提下搜索网络的各个模块,所以称之为模块级的搜索(Block-wise Search),后者用于对各个模块确定之后网络层的微调。

NAS 搜索全局结构

采用 NSA 方法来搜寻全局网络结构,另外需要针对轻量模型进行优化,用一个多目标奖励。

A C C ( m ) x [ L A T ( m ) / T A R ] w {ACC(m)x[LAT(m)/TAR]}^{w} ACC(m)x[LAT(m)/TAR]w

来近似 pareto 最优解,根据目标延迟 TAR 为每个模型 m 平衡模型精度 ACC(m)和延迟 LAT(m)。用较小的权重因子 w =-0.15 来弥补不同的延迟的更大精度变化。从头训练了一个新的架构搜索,找到了初始的 seed 模型,然后应用 NetAdapt 和其他优化来获得最终的 MobilenetV3-Small 模型。

NetAdapt 搜索层结构

Δ A C C Δ L a t e n c y \frac{ΔACC}{ΔLatency} ΔLatencyΔACC

给定一个 K conv and FC layers 的网络 Net,在每一步的结构更改中,需要减少一个给定个值 deltaR,然后调整每层的卷积核数,生成一个 Net_simp 集合,从中找到目标延时的网络。保持循环,直到满足给定条件后 finetune 网络。V3 用了两种减少延迟的方法来产生网络:

  • 减少 expansion layer 的 size。

  • 减少所有共享相同 bottleneck size 模块的瓶颈。

SE 结构

首先使用一个全局池化层将每个通道变成一个具体的数值,然后接两个全连接层,最后通过一个 H-Sigmoid 函数获取最终的权重,赋值给最初的特征图。

在这里插入图片描述

# 注意力机制模块(SE 模块,即两个全连接层)
# 该模块的基本流程是:
# 先进行自适应平均池化(1x1)———>1x1 的卷积层———>relu 激活层———>1x1 的卷积池化———>hardsigmoid()激活函数激活
class SqueezeExcitation(nn.Module):def __init__(self, input_c: int, squeeze_factor: int = 4):super(SqueezeExcitation, self).__init__()squeeze_c = _make_divisible(input_c // squeeze_factor, 8)    # 获得距离该数最近的 8 的整数倍的数字self.fc1 = nn.Conv2d(input_c, squeeze_c, 1)    # 该卷积的输出的 squeeze_c 是输入 input_c 的 1/4  其作用与全连接层一样self.fc2 = nn.Conv2d(squeeze_c, input_c, 1)def forward(self, x: Tensor) -> Tensor:scale = F.adaptive_avg_pool2d(x, output_size=(1, 1))   # 将特征矩阵每一个 channel 上的数据给平均池化到 1x1 的大小scale = self.fc1(scale)scale = F.relu(scale, inplace=True)scale = self.fc2(scale)scale = F.hardsigmoid(scale, inplace=True)   # 激活函数return scale * x   # 将得到的数据与传入的对应 channel 数据进行相乘

反向残差结构

相对于 MobileNets_V2,MobileNets_V3 的反向残差结构发生改变,在 MobileNets_V2 的反向残差结构基础上加入了 SE 模块。

# 定义 block 的配置类
class InvertedResidualConfig:def __init__(self,input_c: int,  # block 模块中的第一个 1x1 卷积层的输入 channel 数kernel: int,   # depthwise 卷积的卷积核大小expanded_c: int,   # block 模块中的第一个 1x1 卷积层的输出 channel 数out_c: int,  # 经过 block 模块中第二个 1x1 卷积层处理过后得到的 channel 数use_se: bool,  # 是否使用注意力机制模块activation: str,   # 激活方式stride: int,       # 步长width_multi: float):  # width_multi:调节每个卷积层所使用 channel 的倍率因子self.input_c = self.adjust_channels(input_c, width_multi)self.kernel = kernelself.expanded_c = self.adjust_channels(expanded_c, width_multi)self.out_c = self.adjust_channels(out_c, width_multi)self.use_se = use_seself.use_hs = activation == "HS"  # whether using h-swish activationself.stride = stride@staticmethoddef adjust_channels(channels: int, width_multi: float):return _make_divisible(channels * width_multi, 8)# 此为 block 模块,其包含第一个 1x1 卷积层、DeptWis 卷积层、SE 注意力机制层(判断是否需求)、第二个 1x1 卷积层、激活函数(需要判断是否是非线性激活),ConvBNActivation 为普通卷积块卷积层
class InvertedResidual(nn.Module):def __init__(self,cnf: InvertedResidualConfig,   # cnf:配置类参数norm_layer: Callable[..., nn.Module]):      # norm_layer:# BN 层super(InvertedResidual, self).__init__()if cnf.stride not in [1, 2]:  # 判断某一层的配置文件,其步长是否满足条件raise ValueError("illegal stride value.")# 判断是否进行短连接self.use_res_connect = (cnf.stride == 1 and cnf.input_c == cnf.out_c)  # 只有当步长为 1,并且输入通道等于输出通道数layers: List[nn.Module] = []activation_layer = nn.Hardswish if cnf.use_hs else nn.ReLU    # 判断当前的激活函数类型# expand# 判断是否相等,如果相等,则不适用 1x1 的卷积层增加 channel 维度;不相等的话,才使用该层进行升维度if cnf.expanded_c != cnf.input_c:layers.append(ConvBNActivation(cnf.input_c,cnf.expanded_c,kernel_size=1,norm_layer=norm_layer,activation_layer=activation_layer))# depthwise  layers.append(ConvBNActivation(cnf.expanded_c,cnf.expanded_c,kernel_size=cnf.kernel,   # depthwise 卷积的卷积核大小stride=cnf.stride,groups=cnf.expanded_c,    # 深度 DW 卷积norm_layer=norm_layer,   # BN 层activation_layer=activation_layer))# 判断是否需要添加 SE 模块if cnf.use_se:layers.append(SqueezeExcitation(cnf.expanded_c))# projectlayers.append(ConvBNActivation(cnf.expanded_c,cnf.out_c,kernel_size=1,norm_layer=norm_layer,  # BN 层activation_layer=nn.Identity))   # 此层的 activation_layer 就是进行里普通的线性激活,没有做任何的处理self.block = nn.Sequential(*layers)self.out_channels = cnf.out_cself.is_strided = cnf.stride > 1def forward(self, x: Tensor) -> Tensor:result = self.block(x)if self.use_res_connect:result += x   # 进行 shortcut 连接return result

模型结构与实现

核心模块,也是网络的基本模块。主要实现了通道可分离卷积+SE 通道注意力机制+残差连接。结构图如下:

在这里插入图片描述

from typing import Callable, List, Optionalimport torch
from torch import nn, Tensor
from torch.nn import functional as F
from functools import partial# MobileNetV3 网络结构基础框架:其包括:模型的第一层卷积层———>nx【bneckBlock 模块】———>1x1 的卷积层———>自适应平均池化层———>全连接层———>全连接层
class MobileNetV3(nn.Module):def __init__(self,inverted_residual_setting: List[InvertedResidualConfig],           # beneckBlock 结构一系列参数列表last_channel: int,   # 对应的是倒数第二个全连接层输出节点数  1280num_classes: int = 1000,  # 类别个数block: Optional[Callable[..., nn.Module]] = None,   # InvertedResidual 核心模块norm_layer: Optional[Callable[..., nn.Module]] = None):super(MobileNetV3, self).__init__()if not inverted_residual_setting:raise ValueError("The inverted_residual_setting should not be empty.")elif not (isinstance(inverted_residual_setting, List) andall([isinstance(s, InvertedResidualConfig) for s in inverted_residual_setting])):raise TypeError("The inverted_residual_setting should be List[InvertedResidualConfig]")if block is None:block = InvertedResidual   # block 类if norm_layer is None:norm_layer = partial(nn.BatchNorm2d, eps=0.001, momentum=0.01)  # partial()为 python 方法,即为 nn.BatchNorm2d 传入默认的两个参数layers: List[nn.Module] = []# building first layer# 构建第一层卷积结构firstconv_output_c = inverted_residual_setting[0].input_c   # 表示第一个卷积层输出的 channel 数layers.append(ConvBNActivation(3,   # 输入图像数据的 channel 数firstconv_output_c,    # 输出 channelkernel_size=3,stride=2,norm_layer=norm_layer,activation_layer=nn.Hardswish))# building inverted residual blocks# 利用循环的方式添加 block 模块,将每层的配置文件传给 blockfor cnf in inverted_residual_setting:layers.append(block(cnf, norm_layer))# building last several layerslastconv_input_c = inverted_residual_setting[-1].out_c  # 最后的 bneckblock 的输出 channellastconv_output_c = 6 * lastconv_input_c    # lastconv_output_c 与最后的 bneckblock 的输出 channel 数是六倍的关系# 定义最后一层的卷积层layers.append(ConvBNActivation(lastconv_input_c,   # 最后的 bneckblock 的输出 channel 数lastconv_output_c,   # lastconv_output_c 与最后的 bneckblock 的输出 channel 数是六倍的关系kernel_size=1,norm_layer=norm_layer,activation_layer=nn.Hardswish))self.features = nn.Sequential(*layers)self.avgpool = nn.AdaptiveAvgPool2d(1)self.classifier = nn.Sequential(nn.Linear(lastconv_output_c, last_channel),nn.Hardswish(inplace=True),nn.Dropout(p=0.2, inplace=True),nn.Linear(last_channel, num_classes))# initial weightsfor m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, mode="fan_out")if m.bias is not None:nn.init.zeros_(m.bias)elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):nn.init.ones_(m.weight)nn.init.zeros_(m.bias)elif isinstance(m, nn.Linear):nn.init.normal_(m.weight, 0, 0.01)nn.init.zeros_(m.bias)def _forward_impl(self, x: Tensor) -> Tensor:x = self.features(x)x = self.avgpool(x)x = torch.flatten(x, 1)x = self.classifier(x)return xdef forward(self, x: Tensor) -> Tensor:return self._forward_impl(x)

MobileNet V4 模型

MobileNetV4:主要是针对移动设备设计的通用高效架构。在其核心部分,引入了通用倒瓶颈(UIB)搜索块,Mobile MQA,带来了 39%速度提升。同时还带来了一种优化的神经架构搜索(NAS)方法,它提高了 MobileNetV4 搜索的有效性。同时 UIB、Mobile MQA 以及精细化的 NAS 方法的整合,使得 MNv4 模型系列在移动 CPU、DSP、GPU 以及像苹果神经引擎和谷歌 Pixel EdgeTPU 这样的专用加速器上几乎达到了帕累托最优——这是其他测试模型所不具备的特性。最后,为了进一步提升准确度,引入了一种新颖的蒸馏技术。借助这项技术,MNv4-Hybrid-Large 模型在 ImageNet-1K 上的准确度达到了 87%,在 Pixel 8 EdgeTPU 上的运行时间仅为 3.8ms。

设计原则

MobileNetV4 在平衡 MACs 和内存带宽方面进行了投资,旨在以最低的成本获得最大的回报,特别关注网络的起始和结束部分。

在网络的开头,MobileNetV4 使用大而昂贵的前几层来显著提高模型的容量和下游准确性。这些初始层主要由大量的 MACs 组成,因此它们仅在低 Ridge Point 硬件上成本较高。在网络的末端,所有 MobileNetV4 变体使用相同大小的最终全连接(FC)层以最大化准确性,尽管这导致较小尺寸的 MNV4 变体在高 Ridge Point 硬件上遭受更高的 FC 延迟。

由于大的初始卷积层在低 RP 硬件上成本高,但在高 RP 硬件上并不昂贵,而最终的全连接层在高 Ridge Point 硬件上成本高,在低 Ridge Point 硬件上却不贵,MobileNetV4 模型不会同时遭受这两种减速。

换句话说,MNV4 模型能够使用提高准确性的昂贵层,但不会同时承受这些层的组合成本,从而在所有脊点上都实现了几乎是最优的 Pareto 性能。经过一系列的研究,MobileNet V4 提出了以下的设计原则 :

  • 多路径效率问题:组卷积和类似的多路径设计,尽管具有更低的浮点运算次数(FLOPcounts),但由于内存访问复杂度,可能效率较低。

  • 硬件支持很重要:像 SqueezeandExcite(SE)这样的高级模块,

  • GELU、LayerNorm 在 DSPs 上的支持并不好,LayerNorm 的速度也比 BatchNorm 慢,而 SE 在加速器上的表现也比较慢。

  • 简洁的力量:深度卷积和逐点卷积、ReLU、批量归一化以及简单的注意力机制(例如,MHSA)——展示了卓越的效率和硬件兼容性。

MobileNet V4 具有以下原则:

  • 标准组件:优先考虑广泛支持的元素,以实现无缝部署和硬件效率。

  • 灵活的 UIB 模块:作者新颖的可搜索 UIB 构建块支持可适应的空间和通道混合,接收场调整,以及最大化的计算利用率,通过网络架构搜索(NAS),促进了效率和准确性之间的平衡妥协。

  • 采用直接注意力机制:作者的 MobileMQA 机制为了最佳性能而优先考虑简单性。

这些原则使得 MobileNetV4 在所有评估的硬件上大多数情况下都是帕累托最优的。

硬件无关 Pareto 效率

屋顶线模型:为了使一个模型在普遍情况下都高效,它必须能够在其理论的计算复杂度与实际硬件性能之间找到平衡。

在硬件目标上表现出色,这些硬件目标的瓶颈极大地限制了模型性能,这些瓶颈主要由硬件的最高计算吞吐量和最高内存带宽决定。

为此,作者使用了屋顶线模型,该模型估计给定工作负载的性能,并预测它是受内存瓶颈还是计算瓶颈的限制。简而言之,它忽略了特定的硬件细节,只考虑工作负载的操作强度 LayerMACs/(WeightBytes + ActivationBytes) 与硬件处理器和内存系统的理论极限之间的关系。

内存和计算操作大致是并行发生的,因此两个中较慢的那个大约决定了延迟瓶颈。为了将屋顶线模型应用于以为索引的神经网络层,作者可以以下述方式计算模型推理延迟:

M A C T i m e i = L a y e r M A C s i P e a k M A s MACTime_{i}=\frac{LayerMAC_{s_{i}}}{PeakMA_{s}} MACTimei=PeakMAsLayerMACsi

M o d e l T i m e = ∑ i m a x ( M A C T i m e i , M e m T i m e i ) ModelTime=\sum_{i}max(MACTime_{i},MemTime_{i}) ModelTime=imax(MACTimei,MemTimei)

M e m T i m e i = W e i g h t B y t e s i + A t i v a t i o n B y t e s i P e a k M e m m B W MemTime_{i}=\frac{WeightBytes_{i}+AtivationBytes_{i}}{PeakMemmBW} MemTimei=PeakMemmBWWeightBytesi+AtivationBytesi

在屋顶线模型中,硬件行为由_脊点_(RP)来总结,即硬件的峰值 MACs 与峰值 MemBW 之比。也就是说,这是实现最大性能所需的最小操作强度。

如下图 2,3 所示,作者从 RP 预期的最低值(0MAC/字节)扫描到最高值(500MACs/字节)。屋顶线模型仅依赖于数据传输与计算的比率,因此具有相同 RP 的所有硬件通过延迟对工作负载的排名是相同的。3 这意味着,如果新目标的 RP 包含在扫描范围内,那么扫描-RP 的屋顶线分析(见下一段)同样适用于未来的硬件和软件。

在这里插入图片描述

脊点扫描分析:如图 2 和图 3 所示,屋顶线模型揭示了 MobileNetV4 模型如何与其他卷积 MobileNets 相比,实现硬件独立的几乎帕累托最优性能。在低脊点硬件(例如 CPU)上,模型更可能受计算限制而非内存限制。

因此,为了提高延迟,即使以增加内存复杂度为代价(如 MobileNetV3Large-1.5x),也要最小化总的 MAC 数量。

在高脊点硬件上,数据移动是瓶颈,所以 MAC 不会显著减慢模型速度,但可以增加模型容量(如 MobileNetV1-1.5x)。因此,为低脊点优化的模型在高脊点上运行缓慢,因为内存密集和低 MAC 的全连接(FC)层受到内存带宽的限制,不能利用高可用的峰值 MAC。

通用反向 Bottlenecks

作者提出了通用逆瓶颈(UniversalInvertedBottleneck, UIB)模块,这是一个适用于高效网络设计的可适应构建块,它具有灵活地适应各种优化目标的能力,而不会使搜索复杂度爆炸性增长。

倒瓶颈(IB)模块,由 MobileNetV2 提出,已成为高效网络的标准化构建模块。

基于最成功的 MobileNet 要素——可分离的深度卷积(DW)和点式(PW)扩展及倒瓶颈结构,本文引入了一种新的构建块——通用倒瓶颈(UIB)块,如下图所示。其结构相当简单。

作者在倒瓶颈块中引入了两个可选的 DW,一个在扩展层之前,另一个在扩展层和投影层之间。这些 DW 的存在与否是神经网络架构搜索(NAS)优化过程的一部分,从而产生新的架构。尽管这种修改很简单,但作者的新构建块很好地统一了几个重要现有块,包括原始的 IB 块、ConvNext 块以及 ViT 中的 FFN 块。此外,UIB 还引入了一种新的变体:额外的深度卷积 IB(ExtraDW)块。

在这里插入图片描述

除了在神经网络架构搜索(NAS)过程中允许灵活的中间层(IB)结构外,作者还避免了任何人为设计的缩放规则,比如在 EfficientNet 中使用的那种,而是为每个模型大小单独优化结构。

为了防止 NAS 超级网络的大小爆炸性增长,作者共享了通用组件(逐点扩展和投影)并简单地将深度可分离卷积(DWs)作为额外的搜索选项添加进去。结合基于超级网络的网络架构搜索算法,这种方法使得大多数参数(大于 95%)可以在不同的实例之间共享,使得 NAS 变得极其高效。

倒瓶颈(IB)- 在扩展的特征激活上进行空间混合,以增加成本为代价提供更大的模型容量。

ConvNext 通过在扩展之前执行空间混合,实现了使用更大核尺寸进行更廉价的空间混合。

ExtraDW 是本文提出的一种新变体,它允许廉价地增加网络的深度和感受野。它提供了以下几点优势:结合 ConvNext 与 IB.4 的优势。

FFN 是由两个 1x1 的点状卷积(PW)堆叠而成,并在它们之间加入激活和标准化层。PW 是最受加速器友好的操作之一,但最好与其他模块一起使用。

#倒瓶颈层实现        
class UniversalInvertedBottleneckBlock(nn.Module):def __init__(self, inp, oup, start_dw_kernel_size, middle_dw_kernel_size, middle_dw_downsample,stride,expand_ratio):super().__init__()# Starting depthwise conv.self.start_dw_kernel_size = start_dw_kernel_sizeif self.start_dw_kernel_size:            stride_ = stride if not middle_dw_downsample else 1self._start_dw_ = conv_2d(inp, inp, kernel_size=start_dw_kernel_size, stride=stride_, groups=inp, act=False)# Expansion with 1x1 convs.expand_filters = make_divisible(inp * expand_ratio, 8)self._expand_conv = conv_2d(inp, expand_filters, kernel_size=1)# Middle depthwise conv.self.middle_dw_kernel_size = middle_dw_kernel_sizeif self.middle_dw_kernel_size:stride_ = stride if middle_dw_downsample else 1self._middle_dw = conv_2d(expand_filters, expand_filters, kernel_size=middle_dw_kernel_size, stride=stride_, groups=expand_filters)# Projection with 1x1 convs.self._proj_conv = conv_2d(expand_filters, oup, kernel_size=1, stride=1, act=False)def forward(self, x):if self.start_dw_kernel_size:x = self._start_dw_(x)x = self._expand_conv(x)if self.middle_dw_kernel_size:x = self._middle_dw(x)x = self._proj_conv(x)return x

MobileMQA

在本节中,作者介绍了 MobileMQA,这是一个专门为加速器优化的新型注意力块,它能提供超过 39%的推理速度提升。

操作强度的重要性:近期在视觉模型的研究中,人们大多致力于减少算术运算(MACs)以提高效率。然而,在移动加速器上性能的真正瓶颈往往不是计算而是内存访问。这是因为加速器提供的计算能力远大于内存带宽。因此,仅仅最小化 MACs 可能并不会导致更好的性能。相反,作者必须考虑操作强度,即算术运算与内存访问的比率。

MQA 在混合模型中是高效的:MHSA 将 Query、键和值投影到多个空间以捕捉信息的不同方面。多 Query 注意力(MQA)通过在所有头之间使用共享的键和值简化了这一点。尽管多个 Query 头是必要的,但大型语言模型可以有效共享单个键和值的头,而不会牺牲准确度。对于键和值使用一个共享的头,当批处理的 Token 数量相对于特征维度较小时,可以大大减少内存访问需求,从而显著提高操作强度。

这对于面向移动应用的混合视觉模型通常是这种情况,在这种情况下,仅在具有高特征维度的低分辨率后期阶段使用注意力,并且批大小通常为 1。作者的实验证实了 MQA 在混合模型中的优势。如表 1 所示,与 MHSA 相比,MQA 在 EdgeTPUs 和三星 S23GPU 上实现了超过 39%的加速,而质量损失可以忽略不计(-0.03%)。MQA 还将 MACs 和模型参数减少了 25%以上。据作者所知,作者是第一个在移动视觉中使用 MQA 的。

采用非对称空间下采样:受到 MQA 的启发,它在 Query、键和值之间使用非对称计算,作者将空间缩减注意力(SRA)融合到作者优化的 MQA 模块中,以降低键和值的分辨率,同时保持高分辨率 Query。这一策略是由混合模型中空间相邻标记之间的观察到的相关性所启发的,这归因于早期层中的空间混合卷积滤波器。

通过非对称空间下采样,作者在输入和输出之间保持了相同的标记数量,保持了注意力的高分辨率并显著提高了效率。与不同,作者的方法用步长为 2 的 3x3 深度卷积替换了 AvgPooling,为提高模型容量提供了一种成本效益高的方式。

移动 MQA 这里作者提出了作者的移动 MQA 模块:

M o b i l e M Q A ( X ) = C o n c a t ( a t t e n t i o n 1 , . . . , a t t e n t i o n n ) W o Mobile_MQA(X)=Concat(attention_{1},...,attention_{n})W^{o} MobileMQA(X)=Concat(attention1,...,attentionn)Wo

w h e r e a t t e n t i o n j = s o f t m a x ( ( X W Q j ) ( S R ( X ) W K ) T d k ) ( S R ( X ) W V ) where attention_{j}=softmax(\frac{(XW^{Q_{j}})(SR(X)W^K)^T}{\sqrt{d_{k}}})(SR(X)W^{V}) whereattentionj=softmax(dk (XWQj)(SR(X)WK)T)(SR(X)WV)

其中 SR 表示空间缩减,在作者设计中是指步长为 2 的深度可分离卷积(DW),或者在未使用空间缩减的情况下的恒等函数。结合非对称空间下采样可以在极小的精度损失(-0.06%)情况下,带来超过 20%的效率提升。

NAS 优化与结构增强

为了有效地实例化 UIB 块,作者采用了针对性能改进定制的 TuNAS。

增强搜索策略:作者的方法通过实施两阶段搜索,减轻了 TuNAS 因参数共享而偏向于较小滤波器和扩展因子的偏见。这种策略解决了 UIB 的深度层与其他搜索选项之间参数数量方差的问题。

粗粒度搜索:起初,作者专注于确定最优的滤波器大小,同时保持参数固定:一个默认扩展因子为 4 的反向瓶颈块和一个 3x3 的深度可分核。

细粒度搜索:在初步搜索结果的基础上,作者搜索 UIB 的两个深度可分层的配置(包括它们的存在以及 3x3 或 5x5 的核大小),同时保持扩展因子恒定为 4。

增强 TuNAS 的鲁棒训练 TuNAS 的成功取决于对架构质量的准确评估,这对于奖励计算和政策学习至关重要。最初,TuNAS 利用 ImageNet-1k 来训练超级网络,以便进行架构评估。然而,这种做法忽略了实际应用中网络可能遇到的噪声和扰动。为了解决这个问题,作者建议在训练过程中加入鲁棒性训练。具体来说,作者在训练集中引入了多种数据增强和对抗样本,以此来增强模型的鲁棒性。

通过这种方式,TuNAS 可以更好地评估架构在嘈杂环境下的性能,从而提高最终学到的网络架构的质量。然而模型在 ImageNet 上的性能显著受到数据增强、正则化以及超参数选择的影响。鉴于 TuNAS 的架构样本在不断发展变化,找到一个稳定的超参数集合是具有挑战性的。

作者通过一个离线的蒸馏数据集来解决这一问题,这样就不需要额外的增强方法,并减少了对正则化和优化设置的敏感性。最后作者将 TuNAS 的训练扩展到 750 个周期,产生了更深、质量更高的模型。

关于 NAS,作者方法如下:

搜索空间构建

  • 固定初始层:首先在第一阶段使用了一个 Conv2D 层(3x3 核,步长 2)以便快速降低分辨率,随后在第二阶段采用了 NAS 优化的融合 IB 块(步长 2),以平衡效率和准确性。

  • NAS 驱动的优化:NAS 过程精确地确定了在剩余四个阶段中 UIB 块的数量和参数实例化,确保了性能的最优结构。

  • 固定 Head 层:作者使用了与 MobileNetV3 相同的 Head 层配置。

观察到在 UIB 块内的点式卷积在高分辨率下往往表现出较低的运算强度,作者优先在初始层中使用计算密度更高的操作,以平衡效率和准确度。

优化目标

  • MNv4-Conv-S:双重目标——285MMACs 和 0.2ms 延迟(Pixel6EdgeTPU,224px 输入)。

  • MNv4-Conv-M:0.6 毫秒延迟(Pixel6EdgeTPU,256 像素输入)。

  • MNv4-Conv-L:针对 384px 输入,双重延迟目标为 2.3ms(Pixel6EdgeTPU)和 2.0ms(Pixel7EdgeTPU)。

需要注意的是,通过将作者的搜索空间限制在跨设备具有良好相关成本模型的组件上,作者发现 EdgeTPU 延迟优化可以直接产生普遍高效的模型。

from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Unionimport torch
import torch.nn as nnfrom mobilenet.model_config import MODEL_SPECS #通过配置参数构建 Mobile v4 block
def build_blocks(layer_spec):if not layer_spec.get('block_name'):return nn.Sequential()block_names = layer_spec['block_name']layers = nn.Sequential()if block_names == "convbn":schema_ = ['inp', 'oup', 'kernel_size', 'stride']args = {}for i in range(layer_spec['num_blocks']):args = dict(zip(schema_, layer_spec['block_specs'][i]))layers.add_module(f"convbn_{i}", conv_2d(**args))elif block_names == "uib":schema_ =  ['inp', 'oup', 'start_dw_kernel_size', 'middle_dw_kernel_size', 'middle_dw_downsample', 'stride', 'expand_ratio']args = {}for i in range(layer_spec['num_blocks']):args = dict(zip(schema_, layer_spec['block_specs'][i]))layers.add_module(f"uib_{i}", UniversalInvertedBottleneckBlock(**args))elif block_names == "fused_ib":schema_ = ['inp', 'oup', 'stride', 'expand_ratio', 'act']args = {}for i in range(layer_spec['num_blocks']):args = dict(zip(schema_, layer_spec['block_specs'][i]))layers.add_module(f"fused_ib_{i}", InvertedResidual(**args))else:raise NotImplementedErrorreturn layers#构建 Mobilenet v4
class MobileNetV4(nn.Module):def __init__(self, model):# MobileNetV4ConvSmall  MobileNetV4ConvMedium  MobileNetV4ConvLarge# MobileNetV4HybridMedium  MobileNetV4HybridLarge"""Params to initiate MobilenNetV4Args:model : support 5 types of models as indicated in """super().__init__()assert model in MODEL_SPECS.keys()self.model = modelself.spec = MODEL_SPECS[self.model]# conv0self.conv0 = build_blocks(self.spec['conv0'])# layer1self.layer1 = build_blocks(self.spec['layer1'])# layer2self.layer2 = build_blocks(self.spec['layer2'])# layer3self.layer3 = build_blocks(self.spec['layer3'])# layer4self.layer4 = build_blocks(self.spec['layer4'])# layer5   self.layer5 = build_blocks(self.spec['layer5'])       def forward(self, x):x0 = self.conv0(x)x1 = self.layer1(x0)x2 = self.layer2(x1)x3 = self.layer3(x2)x4 = self.layer4(x3)x5 = self.layer5(x4)x5 = nn.functional.adaptive_avg_pool2d(x5, 1 )return [x1, x2, x3, x4, x5]

如果您想了解更多AI知识,与AI专业人士交流,请立即访问昇腾社区官方网站https://www.hiascend.com/或者深入研读《AI系统:原理与架构》一书,这里汇聚了海量的AI学习资源和实践课程,为您的AI技术成长提供强劲动力。不仅如此,您还有机会投身于全国昇腾AI创新大赛和昇腾AI开发者创享日等盛事,发现AI世界的无限奥秘~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/484955.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

25.k个一组翻转链表 python

k个一组翻转链表 题目题目描述示例 1&#xff1a;示例 2&#xff1a;提示&#xff1a;题目链接 题解解题思路python实现代码分析提交结果 题目 题目描述 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&…

开源即时通讯与闭源即时通讯该怎么选择,其优势是什么?

在选择即时通讯软件时&#xff0c;应根据企业的经营领域来选择适合自身需求的开源或闭源方案。不同领域对开源和闭源即时通讯的理念存在差异&#xff0c;因此总结两个点简要分析这两种选择&#xff0c;有助于做出更明智的决策。 一、开源与闭源的根本区别在于软件的源代码是否…

etcd分布式存储系统快速入门指南

在分布式系统的复杂世界中&#xff0c;确保有效的数据管理至关重要。分布式可靠的键值存储在维护跨分布式环境的数据一致性和可伸缩性方面起着关键作用。 在这个全面的教程中&#xff0c;我们将深入研究etcd&#xff0c;这是一个开源的分布式键值存储。我们将探索其基本概念、特…

大语言模型微调与 XTuner 微调实战

1 大语言模型微调 1.1 什么是微调 大语言模型微调&#xff08;Fine-tuning of Large Language Models&#xff09;是指在预训练的大型语言模型基础上&#xff0c;使用特定任务的数据进一步训练模型&#xff0c;以使其更好地适应和执行特定任务的过程&#xff0c;用于使LLM&am…

计算机网络复习5——运输层

运输层解决的是进程之间的逻辑通信问题 两个主机进行通信归根结底是两个主机中的应用程序互相通信&#xff0c;又称为“端到端的通信” 端口 运行在计算机中的进程是用进程标识符来标志的。但不同的操作系统标识进程的方法不统一&#xff0c;因特网重新以统一的方法对TCP/IP…

秒懂:使用js验证hash, content hash , chunk hash的区别

一、使用js验证hash, content hash , chunk hash的区别 1、计算一般的 Hash&#xff08;以简单字符串为例&#xff09; 使用crypto-js库来进行哈希计算&#xff0c;需提前引入npm install crypto-js库。 crypto-js&#xff1a; 是一个JavaScript加密算法库&#xff0c;用于实…

从零开始配置 Docker 网络:快速掌握各类型网络的设置与使用场景

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 Docker 网络类型概述🎯 Bridge 驱动🎯 Host 驱动🎯 None 驱动🎯 Overlay 驱动🎯 Macvlan 驱动🔖 获取网络接口📝 总结:选择合适的网络类型⚓️ 相关链接 ⚓️📖 介绍 📖 如果你曾经在搭建…

PHP语法学习(第六天)

&#x1f4a1;依照惯例&#xff0c;回顾一下昨天讲的内容 PHP语法学习(第五天)主要讲了PHP中的常量和运算符的运用。 &#x1f525; 想要学习更多PHP语法相关内容点击“PHP专栏” 今天给大家讲课的角色是&#x1f34d;菠萝吹雪&#xff0c;“我菠萝吹雪吹的不是雪&#xff0c;而…

一键部署 Poste.io 邮件/邮局/完整教程

在使用 Nginx 或宝塔面板的基础上部署 Poste.io 时&#xff0c;经常会遇到证书申请失败或无法访问等问题。本教程将为您提供一个完整的解决方案。 特别说明&#xff1a;如果您的服务器 IP 已被 Outlook 列入黑名单&#xff0c;发送到 Outlook 邮箱的邮件将会失败。其他邮箱服务…

如何搭建Python的本地Pypi源

前言 在实际生产环境中工作中&#xff0c;为了安全&#xff0c;内网主机是无法连接外网的&#xff0c;开发同事在写Python相关程序时&#xff0c;需要安装大量开发所需的模块&#xff0c;如果单独安装模块的话&#xff0c;有可能会存在大量的依赖&#xff0c;需要一个一个查找…

iOS与Windows间传文件

想用数据线从 windows 手提电脑传文件入 iPhone&#xff0c;有点迂回。 参考 [1]&#xff0c;要在 windows 装 Apple Devices。装完、打开、插线之后会检测到手机&#xff0c;界面&#xff1a; 点左侧栏「文件」&#xff0c;不是就直接可以传&#xff0c;而是要通过某个应用传…

如何高效地架构一个Java项目

引言 Java是企业级应用开发的主流语言之一&#xff0c;而我们作为使用Java语言的程序员&#xff0c;职称有初级、中级、高级、资深、经理、架构&#xff0c;但我们往往只是慢慢通过经验的积累迭代了自己的等级&#xff0c;如果没有保持学习的习惯&#xff0c;大多数程序员会停留…

pytest(一)csv数据驱动

一、csv数据驱动 csv文件内容 1,1,2 3,6,9 100,200,3000csv数据驱动使用方法 import csv import pytestdef get_csv():with open("data.csv") as file:raw csv.reader(file)data []for line in raw:data.append(line)# print(data) #[[1, 1, 2], [3, 6, 9],…

Linux C/C++编程之静态库

【图书推荐】《Linux C与C一线开发实践&#xff08;第2版&#xff09;》_linux c与c一线开发实践pdf-CSDN博客《Linux C与C一线开发实践&#xff08;第2版&#xff09;&#xff08;Linux技术丛书&#xff09;》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 (jd.com…

001集—— 创建一个WPF项目 ——WPF应用程序入门 C#

本例为一个WPF应用&#xff08;.NET FrameWork&#xff09;。 首先创建一个项目 双击xaml文件 双击xaml文件进入如下界面&#xff0c;开始编写代码。 效果如下&#xff1a; 付代码&#xff1a; <Window x:Class"WpfDemoFW.MainWindow"xmlns"http://schema…

优傲协作机器人 Remote TCP Toolpath URCap(操作记录)

目录 一、新机设置项 1、设置管理员密码 2、设置安全密码 3、设置负载 二、激活 Remote TCP & Toolpath URCap 1、插入U盘 2、打开激活面板 3、导入许可证 4、查看是否激活成功 5、启用功能 三、使用流程&#xff08;官方&#xff09; 步骤一 步骤二 步骤三 …

使用springboot-3.4.1搭建一个netty服务并且WebSocket消息通知(适用于设备直连操作,以及回复操作)

引入最新版本 <!--websocket--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>启动类加入 //netty 协议服务端口启动 NettyTcpHandler.start()…

从仪表盘探索 MongoDB 关键指标

这是 MongoDB 监控系列文章的第七篇&#xff0c;前面几篇文章的链接如下&#xff1a; MongoDB 监控&#xff08;一&#xff09;MongoDB 监控&#xff08;二&#xff09;MongoDB 监控&#xff08;三&#xff09;MongoDB 监控&#xff08;四&#xff09;MongoDB 监控&#xff08…

手机LCD分区刷新技术介绍

分区刷新也称为分区变频&#xff0c;LCD分区刷新功能的目的是将屏幕分为上下半区&#xff0c;分区显示不同帧率&#xff0c;上方区块High Frame Rate&#xff0c;下方区块Low Frame Rate。使用者可以动态自定义上方高刷显示区的结尾位置。 当前的智能手机屏幕上&#xff0c;显示…

php基础:文件处理

​​​​​​1.PHP 操作文件 读取文件并写到输出流的 PHP 代码如下&#xff08;如读取成功则 readfile() 函数返回字节数&#xff09;&#xff1a; <?php echo readfile("webdictionary.txt"); ?> 2.PHP 文件打开/读取/关闭 打开使用fopen&#xff08;&…