推理引擎之模型压缩浅析

目录

    • 前言
    • 1. 模型压缩架构和流程介绍
    • 2. 低比特量化原理
      • 2.1 量化基础介绍
      • 2.2 量化方法
      • 2.3 量化算法原理
      • 2.4 讨论
    • 3. 感知量化训练QAT原理
      • 3.1 QAT原理
      • 3.2 量化算子插入
      • 3.3 QAT训练流程
      • 3.4 QAT衍生研究
      • 3.5 讨论
    • 4. 训练后量化PTQ
      • 4.1 动态PTQ
      • 4.2 静态PTQ
      • 4.3 KL散度实现静态PTQ
      • 4.4 量化推理
    • 5. 模型剪枝核心原理
      • 5.1 量化与剪枝的区别
      • 5.2 剪枝算法分类(结构化、非结构化)
      • 5.3 剪枝流程
      • 5.4 L1-norm剪枝算法
      • 5.5 讨论
    • 6. 知识蒸馏原理
      • 6.1 What、Why and How
      • 6.1 知识蒸馏背景
      • 6.2 蒸馏的知识方式
    • 7. 知识蒸馏算法解读
      • 7.1 Offline蒸馏
      • 7.2 Online蒸馏
      • 7.3 Self蒸馏
      • 7.4 Hinton经典蒸馏算法解读
    • 总结
    • 参考

前言

这篇文章主要分享下博主最近在 B 站上偶然间看到的一个视频,该视频对模型压缩相关知识进行了简单的介绍,包括量化、剪枝、蒸馏、二值化。比较适合博主这种对各种概念模糊的初学者,因此记录下方便下次查看,博主也就把原作者的话复述了一遍,大家可以自行查看原视频。

视频链接:【推理引擎】模型压缩

文档链接:https://github.com/chenzomi12/DeepLearningSystem/tree/main/043INF_Slim

1. 模型压缩架构和流程介绍

这个系列主要是和大家分享推理引擎或者推理系统里面的模型压缩,也可以叫模型小型化或者模型轻量化。

我们主要是和大家分享模型压缩的 4 件套:

  • 低比特量化
  • 二值化网络
  • 模型剪枝
  • 模型蒸馏

下面来看下在整个推理引擎架构图里面,模型压缩所处位置

在这里插入图片描述

首先最上面有个 API 的层,接着有个模型转换,它会把从不同的 AI 框架训练出来的网络模型转换成为推理引擎的自己的 IR,或者自己的 Schema,那转换成为自己的 IR 之后呢就会经过模型压缩这个功能,那可能会做一些量化、蒸馏、剪枝、二值化,可能会把模型压缩的四件套同时用起来,那这个时候就叫做多维混合压缩算法。实现完模型的压缩之后就真正的去把网络模型给到 Runtime 还有 Kenrel 去执行在不同的硬件上面,这就是整体的流程。

那我们最主要的关注点就是对模型进行压缩,把模型变得越小越好,减少网络模型的大小;第二个就是加快整个推理的速度,使得在推理引擎里面跑得越快越好;最后就是要求保持相同的精度,即在精度损失较小的前提下去减少网络模型的大小和推理速度。

下面我们来看看整体的推理流程,如下图所示:

在这里插入图片描述

我们会把很多不同 AI 框架训练出来的网络模型转换成为推理的模型,接着经过一个模型压缩模块后输出执行,通过模型压缩,如果能使得模型又小,速度又快,精度还能无损,那这是最好的。

OK!以上就是关于模型压缩架构和流程的简单介绍,下面我们会来介绍下具体的压缩方法。

2. 低比特量化原理

这小节内容主要分享量化基础、量化三种方法以及量化算法的原理

2.1 量化基础介绍

模型量化是一种将浮点计算转成低比特定点计算的技术,可以有效的降低模型计算强度、参数大小和内存消耗,但往往带来巨大的精度损失。尤其是在极低比特(<4bit)、二值网络(1bit)、甚至将梯度进行量化时,带来的精度挑战更大。

在这里插入图片描述

如上图所示,数值在计算机里面有多种表示方法,如 FP32、FP16、INT32、INT16 以及 INT8 等,其中 FP32 是我们一般用来去做模型训练的一个精度,我们随便打开一个 ONNX 模型,可以看到里面存储的数据的类型是 float32 即 FP32

在这里插入图片描述

很多时候在模型训练时我们会开启混合精度,即 –amp 参数的指定。那所谓的混合精度一般是把 FP32 和 FP16 混合到一起去训练,FP16 占用的内存位置或地址空间相比于 FP32 确实少了很多。一般我们所说的模型量化,除了把 FP32 的精度降成 FP16,更多的是降到 INT16、INT8 甚至 INT4 更低比特的一种表示,把 FP32 的 32 位比特转换成为更低比特的方式或者技术就叫做模型量化

接下来我们看下神经网络有什么特点

在这里插入图片描述

上图展示了不同的深度学习模型在计算复杂性和分类精度的关系,可以看到基本上模型的效果越好其计算复杂度也越高,相对较大的神经网络模型一般有以下几个特点:

  • 1. 数据参数量大
  • 2. 计算量大
  • 3. 内存占用大
  • 4. 模型精度高

但是当我们要把模型部署起来,特别是在一些资源有限的移动端和边缘嵌入式设备,我们就不得不考虑模型的计算复杂度和参数量了,我们当然希望我们的模型又小,精度又高。

特别是目前的大语言模型,动不动就几百亿的参数量,它们更加需要量化压缩这些技术来减少模型的参数量,提高模型的推理速度。因此我们还是有必要去了解模型压缩相关技术的。

那我们先不去谈模型量化到底做了什么,我们先来看看模型量化的一些优点,主要有以下几点:

  • 1. 保持精度:量化会损失精度,因为这相当于给网络引入了噪声,但是神经网络一般对噪声是不太敏感的,只要控制好量化的程度,对高级任务精度影响可以做到更小。博主之前的文章有测试过 YOLOv5 模型的量化,FP32 到 FP16 基本无损,FP32 到 INT8 会掉 3~4 个点左右,这取决于你的量化方式,QAT 量化相比于 PTQ 量化掉点要好些
  • 2. 加速计算:传统的卷积操作都是使用 FP32 浮点数进行的计算,低比特的位数减少计算性能也更高,INT8 相对比 FP32 的加速比可达到 3 倍甚至更高。博主之前的文章有测试过 YOLOv5 模型的量化,FP16 比 FP32 快 2.4 倍,INT8 比 FP32 快 3 倍
  • 3. 节省内存:与 FP32 类型相比,FP16、INT8、INT4 低精度类型所占用空间更小,对应存储空间和传输时间都可以大幅下降
  • 4. 节能和减少芯片面积:每个数值如果使用了更少的位数表示,则做运算时需要搬运的数据量就少了,减少了访存开销(节能),同时所需的乘法器数目也减少了(减少芯片面积)

下面我们来看下模型量化的五个特点:

  • 1. 参数压缩
  • 2. 提升速度
  • 3. 降低内存
  • 4. 功耗降低
  • 5. 提升芯片面积

也就是对上面的优点进行了一个简单的总结。

接着我们来看下量化技术落地的三大挑战

1. 精度挑战

  • 量化方式:现在的量化方法大部分是线性量化,但线性量化对数据分布的描述不精确
  • 低比特:从 16bits → 4bits 比特数越低,精度损失越大
  • 任务:分类、检测、分割中任务越复杂,精度损失越大
  • 大小:模型越小,精度损失越大

2. 硬件支持程度

  • 不同硬件支持的低比特指令不相同,比如 Jetson nano 就不支持 INT8
  • 不同硬件提供不同的低比特指令计算方式不同(PF16、HF32)
  • 不同硬件体系结构 Kernel 优化方式不同

3. 软件算法是否能加速

  • 混合比特量化需要进行量化和反量化,如果硬件指令不支持低比特,但我还是想要做量化,这时需要插入 Cast 算子,但 Cast 算子将影响 kernel 执行性能
  • 降低运行时内存占用,与降低模型参数量的差异
  • 模型参数量小,压缩比高,不代表执行内存占用少

OK!以上就是关于量化基础知识的介绍,下面我们来看量化方法

2.2 量化方法

量化方法现在来看一般分为三大种:

  • 量化训练(Quant Aware Training,QAT)
    • 量化训练让模型感知量化运算对模型精度带来的影响,通过 finetune 训练降低量化误差
    • 具体实现可以参考:YOLOv5-QAT量化部署
  • 动态离线量化(Post Training Quantization Dynamic,PTQ Dynamic)
    • 动态离线量化仅将模型中特定算子的权重从 FP32 类型映射成 INT8/16 类型
  • 静态离线量化(Post Training Quantization Static,PTQ Static)
    • 静态离线量化使用少量无标签校准数据,采用 KL 散度等方法计算量化比例因子
    • 具体实现可以参考:YOLOv5-PTQ量化部署

下面我们来看一个图,更好的去理解这三种具体的算法

在这里插入图片描述

首先就是感知量化 QAT,我们会先准备一个训练好的网络模型,然后对它进行一个转换,具体是插入一些伪量化的算子,也就是我们常说的 Q/DQ 节点,从而得到一个新的网络模型,接着对新的网络模型进行 Finetuning 微调得到真正量化后的模型,最后交给部署端。

静态离线量化 PTQ-static,首先准备一个训练好的网络模型和一堆训练的数据,然后通过训练数据对模型进行校准,校准的方法可能会用 KL 散度或者其它的方式

动态离线量化 PTQ-dynamic,准备一个网络模型,然后对其进行转换,最后得到转换后或者量化后的网络模型(使用较少

现在我们来看一下这三种方法有什么区别,对它们做一个简单的比较,如下表所示:

量化方式功能经典适用场景使用条件易用性精度损失预期收益
量化训练(QAT)通过 Finetune 训练将模型量化误差降到最小对量化敏感的场景、模型,例如目标检测、分割、OCR 等有大量带标签的数据极小减少存续空间4x,降低计算内存
静态离线量化(PTQ Static)通过少量校准数据得到量化模型对量化不敏感的场景,例如图像分类任务有少量无标签数据较好较少减少存续空间4x,降低计算内存
动态离线量化(PTQ Dynamic)仅量化模型的可学习权重模型体积大、访存开销大的模型,例如 BERT 模型一般一般减少存续空间2/4x,降低计算内存

QAT 量化训练的精度损失确实比较少,但它的缺点是需要大量带标签的数据进行 Finetune;PTQ Static 静态离线量化方式的好处是精度损失也是比较小的,但不能说没有,因为它缺少了 Finetune 训练的步骤,它只需要有一些少量无标签的数据进行校准;PTQ Dynamic 动态离线量化方法的精度损失一般来说不可控,但它没有任何使用的约束,你想咋用就咋用。

OK!以上就是关于量化方法的介绍,下面我们正式进入到量化原理的分析

2.3 量化算法原理

模型量化桥接了定点和浮点,建立了一种有效的数据映射关系(主要是线性映射),使得以较小的精度损失代价获得了较好的收益。

在这里插入图片描述

上面这个图表示了从定点到浮点的映射,先看左图,上面是指浮点数权重参数的数值,它有最小值和最大值,当然也有 0 值,现在我们希望把这一堆数据映射到一个具体的范围 -127-127 之间,其实就是 INT8 所表示的范围。

当然还有另外一种映射方式,就是截断的方式,看右图,我们会去设置一个最小值和最大值,把最小值和最大值范围内的参数去映射到 -127-127 之间,而不在这个范围内的数据直接把它丢弃掉,这个就是简单的量化方法。

量化的类型其实可以分为对称量化和非对称量化,所谓的对称很简单,就是以 0 作为中心轴,量化范围从 -127-127,就是两边的对称,可以用 INT 来表示。另一种非对称量化可能就没有中心轴了,以 0 作为开始,以 255 作为结束,这种表示方式直接可以使用 UINT 去进行一个表示

在这里插入图片描述

下面我们真正的来到了量化的原理。

要弄懂模型量化的原理就是要弄懂定点和浮点之间的数据映射关系,浮点和定点数据的转换公式如下:
Q = R S + Z R = ( Q − Z ) ∗ S \begin{aligned} Q &= \frac{R}{S}+Z \\ R &= (Q-Z)*S \end{aligned} QR=SR+Z=(QZ)S

  • R R R 表示输入的浮点数据,FP32、FP16 都行
  • Q Q Q 表示量化之后的定点数据,INT 类型的数据
  • Z Z Z 表示零点(Zero Point)的数值,偏移值用于决定做对称量化还是非对称量化
  • S S S 表示缩放因子(Scale)的数值

第一条公式叫做量化,第二条公式叫做反量化

这里面最重要的就是找到缩放因子 S S S 和零点 Z Z Z,有了它们就能够求得 Q Q Q。那如何求取 S S S Z Z Z 呢?其实有很多种方法,这里列举其中比较简单一种方式(MinMax)如下:
S = R max − R min Q max − Q min Z = Q max − R max S \begin{aligned} S &= \frac{R_{\text{max}}-R_{\text{min}}}{Q_{\text{max}}-Q_{\text{min}}} \\ Z &= Q_{\text{max}} - \frac{R_{\text{max}}}{S} \end{aligned} SZ=QmaxQminRmaxRmin=QmaxSRmax

  • Rmax 表示输入浮点数据中的最大值
  • Rmin 表示输入浮点数据中的最小值
  • Qmax 表示最大的定点值(127/255)
  • Qmin 表示最小的定点值(-128/0)

下面我们来看下 MinMax 方式的实际使用公式推导

1. 量化算法原始浮点精度数据与量化后 INT8 数据的转换如下:
f l o a t = s c a l e × ( u i n t + o f f s e t ) float = scale\times(uint+offset) float=scale×(uint+offset)

  • f l o a t float float 表示原始模型中的 float32 精度的数据
  • s c a l e scale scale 表示缩放尺度
  • u i n t uint uint 表示量化后模型中的 int8 精度的数据
  • o f f s e t offset offset 表示偏移量 Z Z Z
  • 这是反量化公式

2. 确定后通过原始 float32 高精度数据计算得到 uint8 数据的转换即如下公式所示:
u i n t 8 = r o u n d ( f l o a t / s c a l e ) − o f f s e t uint8 = round(float/scale)-offset uint8=round(float/scale)offset

  • r o u n d round round 表示四舍五入操作
  • 这是量化公式

3. 若待量化数据的取值范围伪 [ X m i n , X m a x ] [X_{min},X_{max}] [Xmin,Xmax],则 s c a l e scale scale 的计算公式如下:
s c a l e = ( x max − x min / Q max − Q min ) scale = (x_{\text{max}}-x_{\text{min}} / Q_{\text{max}}-Q_{\text{min}}) scale=(xmaxxmin/QmaxQmin)
4. o f f s e t offset offset 的计算方式如下:
o f f s e t = Q min − r o u n d ( x min / s c a l e ) offset = Q_\text{min} - round(x_{\text{min}}/scale) offset=Qminround(xmin/scale)
其它求取的 S S S Z Z Z 方式可以参考:TensorRT量化第三课:动态范围的常用计算方法

OK!以上就是关于量化算法原理的知识,在下节中我们将会讨论感知量化训练 QAT 的相关知识。

2.4 讨论

讨论1:在量化基础介绍小节中我们有介绍模型量化的最终目的是得到更小性能更好的小模型,那我们为什么不直接训练一个小模型呢?在小模型上面调参让它性能更好呢?🤔

以下是一些理由和讨论:(from ChatGPT)

1. 大模型的训练容量:大型模型在训练时具有更多的容量,可以捕捉到更多的特征和模式。而小模型由于参数限制可能无法捕捉到这些模式。通过先训练一个大模型,然后进行量化或压缩,可以尝试保留这些重要的模式。

2. 技术挑战:直接训练小模型确实存在一些技术挑战,例如训练不稳定、容易过拟合等。而大模型则更容易训练,并且可以利用现有的预训练技术。

3. 通用性 vs. 特定性:大模型通常被设计为通用模型,可以在多种任务上工作。一旦这些模型被训练好,可以通过量化和压缩来适应特定的任务或应用,而不需要为每个新任务从头开始训练。

讨论2:为什么我们不直接训练低精度(例如 INT8、INT4)的模型呢?

直接在低精度(例如 INT8、INT4)下训练模型是一个有趣的想法,并且确实有研究者在这方面进行了探索。但是,直接在低精度下训练模型面临着一些挑战和问题:(from ChatGPT)

1. 梯度消失和爆炸:使用低精度表示时,数值范围和精度都会受到限制。这可能导致在训练过程中遇到梯度消失或爆炸的问题,这些问题在高精度下可能不会出现。

2. 数值稳定性:低精度可能导致数值不稳定性,特别是在某些需要精确计算的操作(例如 Batch Normalization)中。

3. 收敛速度和最终性能:由于表示的限制,直接在低精度下训练的模型可能需要更多的时间才能收敛,且最终的模型性能可能不如在高精度下训练的模型。

4. 硬件和软件支持:直接在低精度下训练可能需要特定的硬件和软件支持,这可能并不是所有平台都具备的。

5. 训练技巧:许多现代的深度学习训练技巧和方法(例如优化器、正则化方法等)都是在高精度下开发和优化的。在低精度下,这些方法可能需要调整或重新设计。

总的来说,虽然直接在低精度下训练模型面临一些挑战,但它仍然是一个有前景的研究方向。与此同时,高精度训练后再进行量化仍然是一种更为常见和实用的方法。

3. 感知量化训练QAT原理

这个小节内容主要分享感知量化训练 QAT 原理、伪量化算子插入、QAT 训练流程以及 QAT 训练最新研究

3.1 QAT原理

感知量化训练(Aware Quantization Training)模型中插入伪量化节点 fake quant 来模拟量化引入的误差。端测推理的时候折叠 fake quant 节点中的属性到 tensor 中,在端测推理的时候折叠 fake quant 节点中的属性到 tensor 中,在端测推理的过程中直接使用 tensor 中带有的量化属性参数。

简单来说就是在一个正常的网络模型中去插入一些伪量化的算子或者节点,这个节点叫做 Fake Quant,之所以称为 Fake 是因为它不是真正的量化,而是用来模拟量化的时候引入的一些误差。而在真正端侧推理的时候需要把这些 Fake Quant 去进行一个折叠,最后做推理。

那折叠是什么呢?🤔

在感知量化训练 QAT 中,折叠是指在推理过程中,将伪量化节点中的量化属性参数(scale 和 zero-point)直接应用到输入的张量上,而不再使用伪量化节点来模拟量化引入的误差。这个过程实际上是将量化操作与反量化操作进行逆操作,将原始的浮点数张量恢复为量化后的整数张量,以便在推理中高效地进行计算。

在这里插入图片描述

在上面的计算图中,输入中插入了一个伪量化的算子,接着还需要对权重也插入一个伪量化的算子,完成卷积计算之后会给 BN 层进行一个学习,学习完之后进入了 ReLU,在 BN 层后面也会插入一个伪量化的节点在里面,像这种在一个正常的计算图里面去插入各种伪量化节点的量化方式就叫做 QAT 量化。

刚刚大量的去提到一些伪量化的节点 Fake Quant,那 Fake Quant 的节点有什么用呢?以下是它的两个比较大的作用:

  • 1. 找到输入数据的分布,即找到 min 和 max 值
  • 2. 模拟量化到低比特操作的时候的精度损失,把该损失作用到网络模型中,传递给损失函数,让优化器去在训练过程中对该损失值进行优化

我们来看下伪量化节点的正向传播具体是如何计算的。

为了求得网络模型 tensor 数据精确的 Min 和 Max 值,因此在模型训练的时候插入伪量化节点来模拟引入的误差,得到数据的分布。对于每一个算子,量化参数通过下面的方式得到:
clamp ( x , x min , x max ) : = min ( max ( x , x min ) , x max ) \text{clamp}(x,x_\text{min},x_\text{max}):= \text{min}(\text{max}(x,x_\text{min}),x_\text{max}) clamp(x,xmin,xmax):=min(max(x,xmin),xmax)
有了最小值和最大值之后就可以去求量化的 scale,通过 scale 就可以把输入数据直接量化成 INT8。正向传播就会做这个工作,除了记录最大值和最小值,它还要做一个量化模拟的操作,如下所示:

在这里插入图片描述

假设之前的数据是一条平滑的数据,类似于一条线性的直线,经过伪量化算子进行模拟的时候就变成了阶梯型状,它把大部分的数据都直接消掉了,从 FP32 的数据变成了 INT8 的数据,那这个就是伪量化算子的正向传播。

有正向是不是应该有反向,那现在来看看反向传播时伪量化算子具体怎么实现。

按照前面正向传播的公式,如果对其求导数会导致权重为 0,权重为 0 就没有办法去学习了,因此反向传播的时候相当于一个直接估算器:
δ o u t = δ i n , I ( x ∈ S ) ∈ S : x : x min ⁡ ≤ x ≤ x max ⁡ \delta_{out}=\delta_{in},I_{(x\in S)}\in S:x:x_{\min}\leq x\leq x_{\max} δout=δin,I(xS)S:x:xminxxmax
值得注意的是,输入数据 x x x 必须要在量化范围之内,如果不在的话则需要把它进行截断,如下图所示:

在这里插入图片描述

了解完伪量化算子前向和反向传播之后,还有一个很重要的工作,就是更新 Min 和 Max,因为每一个 epoch 都会有不同的数据输入,有不同的 Min 和 Max,更新的方式有点类似于 BN 算子去更新 beta 和 gamma 的这种方式,它主要是通过 running 和 moving 去完成更新的。

3.2 量化算子插入

前面我们讲了 Fake Quant 伪量化算子怎么去实现,正向传播怎么进行伪量化学习,反向传播怎么进行截断,那我们应该在哪些地方插入 Fake Quant 伪量化节点呢?

一般我们会在密集计算算子、激活算子、网络输入输出等地方插入伪量化节点,下面我们来看一个实际操作的图:

在这里插入图片描述

左边是包含 Conv、BN 和 ReLU 三个简单算子的计算图,右边是插入伪量化算子的计算图,一般来说我们会对输入、Conv 的 weights 还有激活后面插入伪量化算子,这个是一般的插入方式。

值得注意的是,如果你研究感知量化算法,你可以提出很多不同的插入方式,也可以自己造一个伪量化算子,上面这个是最原始的一种方式。

OK!我们简单的讲了感知量化训练的一般通用性的算法,还讲了伪量化算子是怎么实现的,包括正向反向,还讲了伪量化算子是怎么插入到计算图里面的,接下来我们来讲下 QAT 训练的流程

3.3 QAT训练流程

QAT 的工作流程图如下所示:

在这里插入图片描述

步骤如下:

1. 预训练模型:开始时,你需要一个预训练的模型。这个模型通常是通过常规方法在大量数据上训练得到的。

2. Fuse 模块:在这一步,相关的层或模块被合并,以便于后续的量化过程。

3. 插入 stubs 和 observers:为了量化模型,需要在关键位置插入 stubs 和 observers(即伪量化算子)。这些工具可以帮助在训练过程中捕捉和模拟量化的效果。

4. 训练/微调:在此阶段,使用标准的训练数据对模型进行训练或微调。但与常规训练不同的是,此时模型已经被修改,可以模拟量化操作的效果。

5. 量化:在训练完成后,模型将经过量化处理,将 32 位的浮点数转化为较低位宽的整数,例如 8 位。

6. QAT 模型:最后,您将得到一个量化感知训练后的模型,通常这个模型在大小和推理速度上都有所优化,同时尽量保持与原始模型相近的准确度。

值得注意的是,最后得到的 QAT 网络模型没有办法去执行推理,它要经过推理系统的一个转换模块然后去掉一些冗余的伪量化的算子才能够正常的推理。

3.4 QAT衍生研究

最后,我们来简单聊一聊 QAT 的衍生研究

Straight Through Estimation Derivative Approximation 这篇文章(博主未找到该篇文章)中作者提出了一种新的伪量化算子,如下所示:

在这里插入图片描述

其中的正向传播和我们之前讲解的一样,但反向传播就不再是一个简单的分段了

另外还有一些科研类的创新文章会对计算图或者对量化的流程进行改进,比如 Quantization and Deployment of Deep Neural Networks on Microcontrollers 这篇文章

在这里插入图片描述

最后量化的方式和种类还有不同的层次,感兴趣的可以看看 Per-channel Quantization Level Allocation for Quantizing Convolutional Neural Networks 这篇文章

在这里插入图片描述

3.5 讨论

讨论1:伪量化节点有参数吗?如果有其参数有哪些属性呢?🤔

伪量化节点的参数通常包括以下属性:(from chatGPT)

1. 量化比特数(bitwidth):表示量化操作的比特数,例如 8 位整数量化表示。通常,8 位整数量化用于表示权重和激活值。这个属性决定了量化的精度。

2. 量化范围(quantization range):表示量化操作的范围,通常是一个最小值和一个最大值,用于确定如何将浮点数映射到整数表示。这个范围是根据训练数据和模型的统计信息动态确定的。

3. 缩放因子(scale factor):通常是量化范围的倒数,用于将浮点数映射到整数表示。缩放因子等于量化范围的宽度除以量化比特数的最大整数值。

4. 零点(zero point):表示整数表示中的零值对应的浮点数值。它通常等于量化范围的最小值。

这些属性使得伪量化节点能够模拟量化操作的效果,从而允许模型在训练期间适应量化的影响。在推理期间,你可以选择将伪量化节点的属性折叠到张量中,这意味着不再使用伪量化节点来模拟量化,而是直接使用张量中包含的量化属性参数来执行量化操作。

在这里插入图片描述

上图中的模型已经插入了 Q/DQ 节点,我们可以看到节点中包含着缩放因子属性 scale,其类型为 float32 以及零点属性 zero-point,其类型为 int8。

讨论2:为什么说伪量化操作在反向传播时求导数的权重为 0 呢?🤔

考虑公式:
δ o u t = δ i n , I ( x ∈ S ) ∈ S : x : x min ⁡ ≤ x ≤ x max ⁡ \delta_{out}=\delta_{in},I_{(x\in S)}\in S:x:x_{\min}\leq x\leq x_{\max} δout=δin,I(xS)S:x:xminxxmax
这是一个分段函数。对于在范围 [ x min , x max ] [x_\text{min},x_\text{max}] [xmin,xmax] 内的 x x x,其导数为 1(因为这部分是线性的)。但是,对于不在这个范围内的 x x x,函数是常数,其导数为 0。这意味着,当 x x x 不在量化范围内时,梯度就是 0,因此权重更新也就是 0。

但是,在实际的伪量化的反向传播中,为了确保模型能够正常训练,我们不直接使用这个 0 梯度。相反,对于在范围内的输入,我们传递原始的梯度(即 1),而对于超出范围的输入,我们则截断它,使其保持在量化范围内。

所以,当说伪量化节点的反向传播对其求导数的权重为 0 时,指的是在量化范围外的那部分输入

讨论3:如何平滑计算伪量化阶段的 Min 和 Max?像 BN 层计算一般会有一个平滑的计算过程,在具体算子或kernel 实现的时候就会有一个 moving mean moving variance 去进行一个平滑,它和 BN 层平滑类似吗?

在 BN 层中,移动均值和移动方差的计算使用了指数移动平均(Exponential Moving Average, EMA)。具体地,给定一个新的均值 μ \mu μ 和方差 σ 2 \sigma^2 σ2,它们可以按以下方式更新:
m o v i n g _ m e a n = β × m o v i n g _ m e a n + ( 1 − β ) × μ m o v i n g _ v a r = β × m o v i n g _ v a r + ( 1 − β ) × σ 2 moving\_mean = \beta\times moving\_mean+ ( 1- \beta) \times \mu \\ moving\_var= \beta\times moving\_var+ ( 1- \beta) \times \sigma^2 moving_mean=β×moving_mean+(1β)×μmoving_var=β×moving_var+(1β)×σ2
其中 β \beta β 是一个介于 0 和 1 之间的系数,通常接近 1,例如 0.9 或 0.99

对于伪量化,我们可以采用类似的方法来平滑地计算 Min 和 Max 值。假设 x m i n _ n e w x_{min\_new} xmin_new 是新的最小值, x m a x _ n e w x_{max\_new} xmax_new 是新的最大值,那么移动最小值和移动最大值可以按照如下方式更新:
m o v i n g _ m i n = β × m o v i n g _ m i n + ( 1 − β ) × x m i n _ n e w m o v i n g _ m a x = β × m o v i n g _ m a x + ( 1 − β ) × x m a x _ n e w moving\_min = \beta\times moving\_min+ ( 1- \beta) \times x_{min\_new} \\ moving\_max= \beta\times moving\_max+ ( 1- \beta) \times x_{max\_new} moving_min=β×moving_min+(1β)×xmin_newmoving_max=β×moving_max+(1β)×xmax_new
这种方法可以确保在整个训练过程中 Min 和 Max 值的计算是稳定的,从而避免因小批次内的数据变化而产生的过大波动。这与 BN 层中移动均值和移动方差的平滑方法是类似的。

讨论4:如果要对 Batch Normal 进行折叠,那么计算公式或者 kernel 会变成什么样?会不会有新的变化呢?

QAT 感知量化训练的过程中 BN 层的计算确实需要对它进行一个特殊插入,如下图所示:

在这里插入图片描述

左边是没有融合之前的,右边是融合之后的算子,具体计算公式的变化如下:

在这里插入图片描述

4. 训练后量化PTQ

之前我们讲解了感知量化训练 QAT,这节我们来看下另外一种量化方式-训练后量化 PTQ

4.1 动态PTQ

PTQ 量化有静态和动态之分,我们先看一个比较简单的,就是动态离线量化(PTQ Dynamic)

它主要有以下特性:

  • 仅将模型中特定算子的权重从 FP32 类型映射成 INT8/16 类型
  • 主要可以减小模型大小,对特定加载权重费时的模型可以起到一定加速效果
  • 但是对于不同输入值,其缩放因子是动态计算,因此动态量化是几种量化方法中性能最差的
  • 权重量化成 INT16 类型,模型精度不受影响,模型大小为原始的 1/2
  • 权重量化成 INT8 类型,模型精度会受到影响,模型大小为原始的 1/4

算法流程如下:

在这里插入图片描述

首先是拿到一个已经训练好的网络模型,接着将这个网络模型的 FP32 的权重直接转换成 INT8 这种量化模型,最后将转换成 INT8 权重的量化模型对外输出即可

值得注意的是,动态离线量化这种方式性能比较差,大家做简单了解即可。

4.2 静态PTQ

接着我们来看静态离线量化(PTQ Static),像华为昇腾里面的推理引擎 ACL 和英伟达的 TensorRT 里面的量化模块都是采用的静态离线量化方式。

现在大部分的推理引擎或推理框架都会采用离线量化的这种方式作为里面集成的一个模块,因此大家需要对它了解。

它具有以下特性:

  • 静态离线量化也称为校正量化或者数据集量化。使用少量无标签校准数据,核心是计算量化比例因子。使用静态量化后的模型进行预测,在此过程中量化模型的缩放因子会根据输入数据的分布进行调整

u i n t 8 = r o u n d ( f l o a t / s c a l e ) − o f f s e t uint8 = round(float/scale)-offset uint8=round(float/scale)offset

  • 静态离线量化的目标是求取量化比例因子,主要通过对称量化、非对称量化方式来求,而找最大值或者阈值的方法又有 MinMax、KLD、ADMM、EQ 等方法

下面看一下 PTQ Static 算法的主要流程:

在这里插入图片描述

步骤如下:

  1. 预训练模型:开始时,你需要一个预训练的模型。这个模型通常是通过常规方法在大量数据上训练得到的。
  2. Fuse 模块:在这一步,相关的层或模块被合并,以便于后续的量化过程。
  3. 插入 stubs 和 observers:为了量化模型,需要在关键位置插入 stubs 和 observers(即伪量化算子)。这些工具可以帮助在训练过程中捕捉和模拟量化的效果。
  4. 校准数据:准备用于校准的数据。这个数据不需要带标签,只要从真实的数据场景里面获取即可,通常是从训练数据中选取一个子集
  5. 校准:使用校准数据进行模型校准。校准的目的是确定量化的范围和参数,这通常是通过分析模型在校准数据上的行为来完成的。
  6. 量化:利用校准算法获得的量化参数将模型的权重和激活从浮点数转换为定点数或整数
  7. PTQ模型:最后,我们得到了一个量化后的模型,这个模型通常有较小的体积并可以更快地进行推理

在静态离线量化里里面为了更好的去得到 scale 或者去计算数据分布,通常会使用到校准算法,而常见的校准算法校准 KL 散度校准法,用 KL 散度去校准数据集,下面我们一起来了解下。

4.3 KL散度实现静态PTQ

KL 散度校准法的原理如下

  • KL 散度校准法也叫做相对熵,其中 P P P 表示真实分布, Q Q Q 表示非真实分布或 P P P 的近似分布,其计算公式如下:

D K L ( P f ∥ Q q ) = ∑ i = 1 N P ( i ) ∗ l o g 2 P f ( i ) Q q ( i ) D_{KL}(P_f\parallel Q_q)=\sum_{i=1}^NP(i)*log_2\frac{P_f(i)}{Q_q(i)} DKL(PfQq)=i=1NP(i)log2Qq(i)Pf(i)

  • 相对熵,用来衡量真实分布与非真实分布的差异大小。目的是改变量化域,实则就是改变真实的分布,并使得修改后的真实分布在量化后与量化前相对熵越小越好。
  • 主要是去对比两个分布之间的差异,其中 P P P 是真实的分布, Q Q Q 是预测的分布,KL 散度就去对比真实的分布跟预测的分布之间的一个近似值,或者它们的差异的大小,我们希望它们的差异越小越好,也就是量化后的分布和量化前的分布越接近越好

KL 散度校准法的流程如下:

  • 选取 validation 数据集中一部分具有代表的数据作为校准数据集 Calibration
  • 对于校准数据进行 FP32 的推理,对于每一层
    • 收集 activation 的分布直方图
    • 使用不同的 threshold 来生成一定数量的量化好的分布
    • 计算量化好的分布与 FP32 分布的 KL divergence,并选取使 KL 最小的 threshold 作为 saturation 的阈值

通俗的理解,算法收集激活 Act 直方图,并生成一组具有不同阈值的 8 位表示法,选择具有最小 KL 散度的表示;此时的 KL 散度在参考分布(FP32 激活)和量化分布(INT8 激活)之间。

这里有几个点需要注意以下,首先就是需要准备一些小批量的数据也就是刚才所说的 Calibration Dataset 校准数据集,这里面一般选取 500 到 1000 张图片左右就够了,不用太大,当然也不能太少。博主之前有做过相关的校准图片数量对模型性能影响的实现,具体可参考:YOLOv5-PTQ量化部署

所以大家不用浪费太多的时间去调多少张不同的数据集,更多的可以去调一调通过什么阈值什么参数去产生不同的数据的分布。

具体的算法流程如下:


  • Run FP32 inference on Calibration Dataset.
  • For each Layer:
    • collect histograms of activations.
    • generate many quantized distributions with different saturation thresholds.
    • pick threshold which minimizes KL divergence(ref_divergence, quant_divergence).

  • 需要准备小批量数据(500~1000 张图片)校准用的数据集
  • 使用校准数据集在 FP32 精度的网络下推理,并收集激活值的直方图
  • 不断调整阈值,并计算相对熵,得到最优解

下面这段是 NVIDIA TensorRT 的一个关于 KL 散度或者静态量化后训练的伪代码,这里就不一一介绍了,感兴趣的看官可以看下:TensorRT量化第三课:动态范围的常用计算方法

在这里插入图片描述

4.4 量化推理

我们在之前讲了很多种的不同的量化方式,但是实际上真正的端侧量化推理部署应该是怎样的呢?

端侧量化推理的结构方式主要有三种,分别是下图(a)FP32 输入 FP32 输出、图(b)FP32 输入 INT8 输出、图(c)INT8 输入 INT32 输出

在这里插入图片描述

第一种就是输入是 FP32,接着通过量化成 INT8 的权重,然后通过一个 Conv2d,这个卷据算子对应的参数也是 INT8,通过卷积计算出来后的数据是 INT32,因为如果输出还是 INT8 的话会造成一个溢出,最后经过反量化回 FP32 再给到下一层的输入。

第二种方式的输入是 FP32 输出是 INT8,FP32 的数据进去通过量化成 INT8,然后经过卷积变成 INT32,最后会做一个重量化,把 INT32 的数据量化成 INT8,如果下一个算子也是卷积的话就可以直接串起来了,所以引申处第三种方式。

第三种方式的输入是 INT8,卷积之后输出是 INT32,然后再做一个重量化变成 INT8 再输出。

所以在整个计算图,整个网络模型的量化过程当中会遇到三种不同的方式。INT8 卷积里面涉及到上面三种不同的模式,因为不同的卷积通过不同的方式进行拼接。使用 INT8 量化进行 inference 时,由于数据是实时的,因此数据需要在线量化,量化的流程如下图所示。数据量化涉及 Quantize、Dequantize 和 Requantize 等 3 种操作。

在这里插入图片描述

值得注意的是量化 Quant 和反量化 DeQuant 这两个算子一定会有,而重量化 ReQuant 会不会存在取决于网络模型的具体的形态。在端侧量化推理部署的时候,会多 Quantize、Dequantize 和 Requantize 这几个不同的算子,这是跟没有量化之前最大的区别。

我们先来看看 Quantize 量化 这个算子,该算子会将 float32 数据量化为 int8。在离线转换工具转换的时候,就要根据之前提到的不论是 QAT 还是 PTQ 找到 x max x_{\text{max}} xmax x min x_{\text{min}} xmin Q max Q_{\text{max}} Qmax Q min Q_{\text{min}} Qmin 也就是数据的分布,接着离线转换工具就会根据它们计算出 scale 和 offset,在真正端侧推理部署也就是 runtime 去运行的时候就会根据 scale 和 offset 把 FP32 的数据转成 INT8,具体的量化公式如下:
s c a l e = ( x max − x min ) / ( Q max − Q min ) o f f s e t = Q min − r o u n d ( x min / s c a l e ) u i n t 8 = r o u n d ( f l o a t / s c a l e ) − o f f s e t f l o a t = s c a l e × ( u i n t + o f f s e t ) \begin{aligned} scale &= (x_{\text{max}}-x_{\text{min}})/(Q_{\text{max}}-Q_{\text{min}}) \\ offset &= Q_{\text{min}} - round(x_{\text{min}}/scale) \\ uint8 &= round(float/scale)-offset \\ float &= scale \times(uint+offset) \end{aligned} scaleoffsetuint8float=(xmaxxmin)/(QmaxQmin)=Qminround(xmin/scale)=round(float/scale)offset=scale×(uint+offset)
接着我们来看看第二个算子 Dequantize 反量化,INT8 相乘、加之后的结果用 INT32 格式存储,如果下一个 Operation 需要 float32 格式数据作为输入,则可以通过 Dequantize 反量化操作将 INT32 数据反量化为 float32。Dequantize 反量化推导过程如下:
y = x ⋅ w = x s c a l e ⋅ ( x i n t + x o f f s e t ) ⋅ w s c a l e ⋅ ( w i n t + w o f f s e t ) = ( x s c a l e ∗ w s c a l e ) ⋅ ( x i n t + x o f f s e t ) ⋅ ( w i n t + w o f f s e t ) = ( x s c a l e ⋅ w s c a l e ) ⋅ ( x i n t ⋅ w i n t + x i n t x o f f s e t + w i n t x o f f s e t + w o f f s e t x o f f s e t ) = ( x s c a l e ⋅ w s c a l e ) ⋅ ( I N T 3 2 r e s u l t + x i n t x o f f s e t + w i n t x o f f s e t + w o f f s e t x o f f s e t ) ≈ ( x s c a l e ⋅ w s c a l e ) ⋅ I N T 3 2 r e s u l t \begin{aligned} y&=x\cdot w \\ &=x_{scale}\cdot(x_{int}+x_{offset})\cdot w_{scale}\cdot(w_{int}+w_{offset}) \\ &=(x_{scale}*w_{scale})\cdot(x_{int}+x_{offset})\cdot(w_{int}+w_{offset}) \\ &=(x_{scale}\cdot w_{scale})\cdot(x_{int}\cdot w_{int}+x_{int}x_{offset}+w_{int}x_{offset}+w_{offset}x_{offset}) \\ &=(x_{scale}\cdot w_{scale})\cdot(INT32_{result}+x_{int}x_{offset}+w_{int}x_{offset}+w_{offset}x_{offset}) \\ &\approx(x_{scale}\cdot w_{scale})\cdot INT32_{result} \end{aligned} y=xw=xscale(xint+xoffset)wscale(wint+woffset)=(xscalewscale)(xint+xoffset)(wint+woffset)=(xscalewscale)(xintwint+xintxoffset+wintxoffset+woffsetxoffset)=(xscalewscale)(INT32result+xintxoffset+wintxoffset+woffsetxoffset)(xscalewscale)INT32result
最后一个就是 Requantize 重量化 算子,INT8 相乘、加之后的结果用 INT32 格式存储,如果下一层需要 INT8 格式数据作为输入,则通过 Requantize 重量化操作将 INT32 数据重量化为 INT8。重量化推导过程如下:
y = x ⋅ w = x s c a l e ⋅ ( x i n t + x o f f s e t ) ⋅ w s c a l e ⋅ ( w i n t + w o f f s e t ) = ( x s c a l e ⋅ w s c a l e ) ⋅ ( x i n t + x o f f s e t ) ⋅ ( w i n t + w o f f s e t ) = ( x s c a l e ⋅ w s c a l e ) ∗ I N T 3 2 r e s u l t \begin{aligned} y&=x\cdot w \\ &=x_{scale}\cdot(x_{int}+x_{offset})\cdot w_{scale}\cdot(w_{int}+w_{offset}) \\ &=(x_{scale}\cdot w_{scale})\cdot(x_{int}+x_{offset})\cdot(w_{int}+w_{offset}) \\ &=(x_{scale}\cdot w_{scale})*INT32_{result} \end{aligned} y=xw=xscale(xint+xoffset)wscale(wint+woffset)=(xscalewscale)(xint+xoffset)(wint+woffset)=(xscalewscale)INT32result
其中 y y y 为下一个节点的输入,即 y = x n e x t y = x_{next} y=xnext
y i n t = y s c a l e ∗ ( y i n t + y o f f s e t ) y_{int} = y_{scale} * (y_{int}+y_{offset}) yint=yscale(yint+yoffset)
则有:
x n e x t i n t = ( x s c a l e ⋅ w s c a l e / x n e x t s c a l e ) ⋅ I N T 3 2 r e s u l t − x n e x t o f f s e t x_{next\ int} = (x_{scale} \cdot w_{scale} / x_{next \ scale})\cdot INT32_{result}-x_{next\ offset} xnext int=(xscalewscale/xnext scale)INT32resultxnext offset
因此重量化不仅需要本层输入 input 和权重的 scale,更加需要下一个 op 算子的输入的 scale 和 offset,所以在运行量化推理的过程当中确实需要到全图的信息,也就是整个计算图的信息。

5. 模型剪枝核心原理

这小节内容主要分享剪枝原理、剪枝流程以及 L1-norm 剪枝算法的具体实现

模型压缩提出了三部分的优化:

  • 1. 减少内存密集的访问量
  • 2. 提高获取模型参数的时间
  • 3. 加速模型推理时间

不管是剪枝还是量化都属于模型压缩的一部分,而它们所做的工作都是围绕上面三个点进行优化的。那量化和剪枝到底有什么区别呢?我们下面来看下。

5.1 量化与剪枝的区别

模型量化是值通过减少权重表示或激活所需的比特数来压缩模型

模型剪枝研究模型权重中的冗余,并尝试删除/修剪冗余和非关键的权重

在这里插入图片描述

To prune, or not to prune: exploring the efficacy of pruning for model compression 这篇论文大家可以作为剪枝的白皮书去看,这篇论文有以下几个观点:

  • 1. 在内存占用相同情况下,大稀疏模型比小密集模型实现了更高的精度
  • 2. 经过剪枝之后稀疏模型要优于同体积非稀疏模型,说明网络模型中很多参数是不一定需要的
  • 3. 资源有限的情况下,剪枝是比较有效的模型压缩策略
  • 4. 优化点还可以往硬件稀疏矩阵储存方向发展

5.2 剪枝算法分类(结构化、非结构化)

剪枝算法主要有两大类别:

  • Unstructured Pruning(非结构化剪枝):随机对独立的权重或者神经元连接进行剪枝
    • 优点:剪枝算法简单,模型压缩比高
    • 缺点:精度不可控,剪枝后权重矩阵稀疏,没有专用硬件难以实现压缩和加速的效果(很少使用
  • Structured Pruning(结构化剪枝):对 filter / channel / layer 进行剪枝
    • 优点:大部分算法在 channel 或者 layer 上进行剪枝,保留原始卷积结构,不需要专用硬件来实现
    • 缺点:剪枝算法相对复杂

具体可参考:剪枝与重参第一课:修剪结构和标准

在这里插入图片描述

5.3 剪枝流程

对模型进行剪枝有三种常见的做法

  • 1. 训练一个模型 → 对模型进行剪枝 → 对剪枝后模型进行微调
  • 2. 在模型训练过程中进行剪枝 → 对剪枝后模型进行微调
  • 3. 进行剪枝 → 从头训练剪枝后模型(使用较少

模型剪枝主要单元有以下三个

  • 训练 Training:训练过参数化模型,得到最佳网络性能,以此为基准;
  • 剪枝 Pruning:根据算法对模型进行剪枝,调整网络结构中通道或层数,得到剪枝后的网络结构
  • 微调 Finetune:在原数据集上进行微调,用于重新弥补因为剪枝后的稀疏模型丢失的精度性能

在这里插入图片描述

模型剪枝的流程如下图所示

在这里插入图片描述

第一种就是最原始的模型剪枝的流程,第二种就是会有多个子网络模型进行采样,然后对这些子模型进行评估,看哪个模型的精度性能比较好,最后选择其中一个对它微调即可,最后一种就是基于 NAS 自动搜索的方式,第三种更多的是学术的前沿,在工业界使用较少,因为基于 NAS 的搜索太消耗资源了

5.4 L1-norm剪枝算法

下面我们来了解一个具体的剪枝算法—L1-norm based Channel Pruning

该剪枝算法是结构化剪枝,专门针对 channel 进行剪枝,剪枝的标准使用 L1-norm 进行约束。

通过 L1-norm 标准来衡量卷积核的重要性,L1-norm 是一个很好的选择卷积核的方法,认为如果一个 filter 的绝对值和比较小,说明该 filter 并不重要。[论文]指出对剪枝后的网络结构从头训练要比对重新训练剪枝后的网络好

基于这个算法原理我们来看下具体的算法步骤


1. 对每个卷积核 F i j F_{ij} Fij 计算它的权重绝对值(L1-norm)之和 S j = ∑ l = 1 j i Σ ∣ K l ∣ S_{j}=\sum_{l=1}^{ji}\Sigma|Kl| Sj=l=1jiΣ∣Kl

2. 根据卷积核的 L1-norm 值 S j S_j Sj 进行排序;

3. 将 m 个权重绝对值之和最小的卷积核以及对应 feature maps 进行剪枝;

4. 下一个卷积层中与剪掉 feature maps 相关的卷积核 F i + 1 , j F_{i+1,j} Fi+1,j 进行剪枝;

5. 对于第 i 层和第 i+1 层的新权重矩阵被创建,剩下权重参数被复制到新模型中。


L1-norm 是一个非常经典的剪枝算法,非常欢迎大家去学习一下

5.5 讨论

讨论1:博主之前在学习剪枝量化课程的时候,有提到要对预训练模型进行约束训练,主要是为模型的 BN 层增加 L1 约束,然后再进行剪枝,我们为什么要进行约束训练?最后微调阶段又为什么去除约束训练,不去除又会发生什么?🤔

约束训练的目的是为了鼓励模型中的某些权重趋向于 0,具体地说,L1 正则化会使得某些权重的值变得非常接近与 0,这为后续的剪枝步骤提供了便利,因为那些接近于 0 的权重或通道可以被视为不重要并被剪掉。加入 L1 约束的方法其实就是我们上面提到的 L1-norm 剪枝算法的一部分,该算法使用 L1 范数来评估网络中每个通道的重要性。那些具有较小 L1 范数的通道被认为是不重要的,并可能在剪枝步骤中被删除。

在剪枝后进行微调的主要目的是为了恢复因剪枝导致的模型性能下降。微调过程中,我们希望模型在较小的学习率下逐渐适应其新的结构,并优化其残留的权重以获得更好的性能。

在微调阶段去除 L1 约束的原因是因为我们不再需要鼓励更多的权重接近于 0,因为剪枝已经完成了。继续应用 L1 约束可能会干扰权重的优化,因为它可能会继续促使一些权重接近于 0,而这并不是微调阶段的目的。

如果在微调时仍然保留 L1 约束,那么这种约束可能会与微调的目的发生冲突。模型可能会在尝试优化其性能的同时,仍然受到使权重接近于 0 的约束。这可能导致微调过程不够有效,甚至可能导致模型的性能下降。

讨论2:为什么我们约束训练后就知道如何去剪枝呢?我们从约束训练中得到了什么?🤔

约束训练的结果: 通过 L1 约束训练,我们得到的是一个模型,其中某些权重或参数的值非常小,接近于 0。这些接近于 0 的权重或参数代表了对模型输出影响最小的部分。在神经网络中,权重的绝对值可以被视为该权重的重要性的一个指示。更大的权重值意味着更强烈的影响,而接近于 0 的权重值意味着很小或没有影响。因此,L1 约束训练为我们提供了一个直观的、基于权重值的方法来评估每个权重或参数的重要性。

如何进行剪枝:在约束训练后,我们可以为模型中的权重或参数设定一个阈值。权重值低于这个阈值的权重或参数可以被认为是不重要的,并被剪掉。例如,对于 BN 层,我们可以根据其对应的 γ \gamma γ(缩放系数)的 L1 范数来决定是否剪掉整个通道。一个常见的方法是选择一个阈值,如最小的 10% 或 20% 的 γ \gamma γ 值,然后将这些通道剪掉。

为什么这样做有效:这种基于 L1 范数的剪枝方法的背后思想是,那些重要性低的权重或参数在整体模型性能中的贡献很小,因此去掉它们不太可能对模型性能产生大的影响。同时,剪枝可以显著减少模型的大小和计算量。

讨论3:为什么对 BN 层增加 L1 约束后能够指导 Conv 的剪枝呢?🤔

我们可以用下面一张图说明:

在这里插入图片描述

我们都知道 BN 层主要执行两个操作:标准化(normalize)和缩放移位(scale and shift)。标准化是为了使特征的分布接近于均值为 0、方差为 1 的正态分布。缩放移位则是通过两个参数 γ \gamma γ(缩放因子)和 β \beta β(偏移因子)来进行的。

上图中展示了大的缩放移位和小的缩放移位对特征分布的影响。可以看到,大的缩放移位会引起特征分布的显著变化,而小的缩放移位则较为微妙。如果 BN 层后某个通道的特征分布没有发生显著变化(即 γ \gamma γ β \beta β 的影响较小),这暗示这个通道可能对模型输出的贡献不大。因此,可以考虑对这些通道进行剪枝。

因此 BN 层能够指导 conv 通道的剪枝的原因主要是:

  • BN 层可以揭示卷积通道的有效性,通过观察 BN 参数(特别是缩放因子 γ \gamma γ)的大小和特征分布的变化,可以评估通道的重要性。
  • 如果 BN 层的参数或特征分布显示某个通道的贡献很小,那么这个通道可以被认为是不重要的,并可以考虑剪枝。

6. 知识蒸馏原理

这小节内容主要分享知识蒸馏的背景、原理以及蒸馏的知识方式

6.1 What、Why and How

问题:什么是知识蒸馏?为什么需要知识蒸馏?如何使用知识蒸馏?

**知识蒸馏(Knowledge Distillation)**是深度学习中的一个技术,用于将一个大型、复杂的模型(称为“教师模型”)的”知识“传递给一个小型、简化的模型(称为”学生模型“)。这个过程涉及到使用教师模型的预测结果来指导学生模型的训练。(from chatGPT)

为什么需要知识蒸馏呢?🤔主要涉及以下几点:

  • 部署限制:大模型往往有巨大的参数量和计算需求,这使得它们难以部署在资源有限的环境中,如移动设备和嵌入式系统。通过知识蒸馏,我们可以得到一个性能相近但参数量远小的模型。
  • 计算效率:小模型通常更快,需要更少的计算资源。
  • 泛化能力:在某些情况下,经过知识蒸馏训练的学生模型可能具有比直接训练的小模型更好的泛化能力

如何使用知识蒸馏呢?🤔主要涉及以下几点:

  • 训练教师模型:首先,你需要有一个已经训练好的大型模型。这个模型可以是任何复杂的深度网络,如 ResNet、BERT等。
  • 生成软标签:使用教师模型对训练数据集进行预测,得到软标签。软标签是指模型输出的概率分布,而不是硬的类标签。
  • 训练学生模型:使用这些软标签来训练学生模型。损失函数通常是学生模型预测的概率分布与教师模型的概率分布之间的距离,例如 KL 散度。
  • 可选的技巧:有许多技巧和变种可以增强知识蒸馏的效果,例如使用温度来调整软标签的“软度”,或者结合硬标签和软标签的损失。

6.1 知识蒸馏背景

  • Knowledge Distillation(KD)最初是 Hinton 在 Distilling the Knowledge in a Neural Network 提出,与 Label smoothing 动机类似,但是 KD 生成 soft label 的方式是通过教师网络得到的
  • KD 可以视为将教师网络学到的知识压缩到学生网络中,另外一些工作 Circumventing outlier of auto augment with knowledge distillation 则将 KD 视为数据增强方法的一种

知识蒸馏主要思想:Student Model 学习模型模仿 Teacher Model 教师模型,二者相互竞争,直到学生模型可以与教师模型持平甚至卓越的表现,有点 GAN 网络的味道🤔,如下图所示

在这里插入图片描述

知识蒸馏的算法,主要由:1)知识 Knowledge、2)蒸馏算法 Distillate、3)师生架构三个关键部分组成

6.2 蒸馏的知识方式

Knowledge 知识的方式可以分为以下几种:

  • 1. response-based knowledge
  • 2. feature-based knowledge
  • 3. relation-based knowledge
  • 4. Architecture-based knowledge(使用较少

下面我们逐一介绍

Response-Based Knowledge

  • 主要指 Teacher Model 教师模型输出层的特征。主要思想是让 Student Model 学生模型直接学习教师模式的预测结果(Knowledge)。
  • 通俗的来讲就是一个知识点,老师充分学习完了,然后把结论告诉学生就好了
  • 假设张量 z t z_t zt 为教师模型输出,张量 z s z_s zs 为学生模型输出,Response-based knowledge 蒸馏形式可以被描述为:

L R e s D ( z t , z s ) = L R ( z t , z s ) L_{ResD}(z_{t},z_{s})={\mathcal L}_{R}(z_{t},z_{s}) LResD(zt,zs)=LR(zt,zs)

学习流程如下图所示:

在这里插入图片描述

红色的代表老师,绿色的代表学生,我们会把 Teacher Model 输出的最后一层的特征给到学生网络模型中去学习,然后通过 Distillation Loss 让学生模型最后一层的特征学习老师模型输出的特征。老师的输出特征是比较固定的,而学生是不太固定需要去学习的,于是通过一个损失函数去模拟去学习使得两个的 Logits 越小越好。

Feature-Based Knowledge

深度神经网络善于学习到不同层级的表征,因此中间层和输出层的都可以被用作知识来训练学生模型,中间层学习知识的 Feature-Based Knowledge 对于 Respone-Based Knowledge 是一个很好的补充,其主要思想是将教师和学习的特征激活进行关联起来。Feature-Based Knowledge 知识转移的蒸馏损失可表示为:
L F e a D ( f t ( x ) , f s ( x ) ) = L F ( ϕ t ( f t ( x ) ) , ϕ t ( f s ( x ) ) ) L_{Fea\ D}(f_{t}(x),f_{s}(x))={\mathcal L}_{F}(\phi_{t}(f_{t}(x)),\phi_{t}(f_{s}(x))) LFea D(ft(x),fs(x))=LF(ϕt(ft(x)),ϕt(fs(x)))
Feature-Based Knowledge 整体工作流程图如下所示:

在这里插入图片描述

这里面的 Distillation Loss 更多是创建在 Teacher Model 和 Student Model 的中间层,通过中间层去创建一个连接关系,这种算法的好处就是教师网络可以给学生网络提供非常大量的有用的参考信息,但如何有效地从教师模型中选择提示层,从学生模型中选择引导层,仍有待进一步研究。另外由于提示层和引导层的大小存在显著差异,如何正确匹配教师和学生的特征表示也需要探讨。

Relation-Based Knowledge

前面基于 Feature-Based Knowledge 和 Response-Based Knowledge 知识都使用了教师模型中特定层中特征的输出。基于关系的知识即 Relation-Based Knowledge 进一步探索了不同层或数据样本之间的关系。一般情况下,基于特征图关系的关系知识的蒸馏损失可以表示为:
L R e l D ( f t ( x ) , f s ( x ) ) = L R ( ψ t ( f ˊ t , f ˋ t ) , ψ t ( f ˊ s , f ˋ s ) ) L_{Rel\ D}(f_{t}(x),f_{s}(x))={\mathcal L}_{R}(\psi_{t}({\acute{f}}_{t},{\grave{f}}_{t}),\psi_{t}({\acute{f}}_{s},{\grave{f}}_{s})) LRel D(ft(x),fs(x))=LR(ψt(fˊt,fˋt),ψt(fˊs,fˋs))
Relation-Based Knowledge 整体工作流程图如下所示:

在这里插入图片描述

这里面的 Distillation Loss 就不仅仅去学习刚才讲到的网络模型中间的特征,还有最后一层的特征,而且它会学习数据样本还有网络模型层之间的一个关系。

OK!本节就和大家分享两个知识,一个是知识蒸馏的背景,为什么要去蒸馏,蒸馏有什么好处。另一个就是蒸馏的知识形式,更多的关注于 Knowledge 怎么提取,从哪里来。

7. 知识蒸馏算法解读

在上节内容中我们已经充分地了解了知识蒸馏算法提出的背景,还有知识蒸馏的知识形态。这节我们主要是去学习三个具体的知识蒸馏的方法,还有 Hinton 经典的知识蒸馏论文解读

知识蒸馏可以划分为:

  • 1.离线蒸馏:offline distillation
  • 2.在线蒸馏:online distillation
  • 3.自蒸馏:self-distillation

在这里插入图片描述

7.1 Offline蒸馏

大多数蒸馏采用 Offline Distillation,蒸馏过程被分为两个阶段:1)蒸馏前教师模型预训练;2)蒸馏算法迁移知识。因此 Offline Distillation 主要侧重于知识迁移部分。

通常采用单向知识转移和两阶段训练过程。在步骤1)中需要教师模型参数量比较大,训练时间比较长,这种方式对学生模型的蒸馏比较高效。

在这里插入图片描述

:这种训练模式下的学生模型往往过度依赖于教师模型。

7.2 Online蒸馏

Online Distillation 主要针对参数量大、精度性能好的教师模型不可获得的情况。教师模型和学生模型同时更新,整个知识蒸馏算法是一种有效的端到端可训练方案。

在这里插入图片描述

:现有的 Online Distillation 往往难以获得在线环境下参数量大、精度性能好的教师模型。

7.3 Self蒸馏

Self-Distillation 的教师模型和学生模型使用相同的网络结构,同样采用端到端可训练方案,属于 Online Distillation 的一种特例。

在这里插入图片描述

现在总结下上面提到的三种知识蒸馏方法,offline distillation、online distillation 和 self-distillation 三种蒸馏方法可以看做是学习过程:

  • Offline Distillation:指知识渊博教师向学生传授知识
  • Online Distillation:指教师和学生共同学习
  • Self-Distillation:指学生自己学习知识

7.4 Hinton经典蒸馏算法解读

在 16 年到 17 年的时候,Hinton 在 Distilling the knowledge in a neural network 这篇文章中第一次提出知识蒸馏的概念,在正式了解这篇文章之前,我们先要去看看两个概念:Hard-target 和 Soft-target

传统的神经网络训练方法是定义一个损失函数,目标是使预测值尽可能接近真实值(Hard-target),损失函数就是使神经网络的损失值和尽可能小。这种训练过程是对 ground truth 求极大似然。在知识蒸馏中,是使用大模型的类别概率作为 Soft-target 的训练过程。

在这里插入图片描述

假设现在是识别 MNIST 数据集中的 10 个数字,我现在输入一张写着 2 数字的图片到神经网络中去,当然希望网络预测是 2 的概率越高越好,如左图所示。那这种方式在数学或者统计方面就是对 ground truth 真实的数据求极大的似然值。

在知识蒸馏中,就像右边的图所示,希望网络能够学习到更多的其它额外像的知识。假设现在还是输入一张写着 2 数字的图片到神经网络中,我们希望网络能学到更多冗余的信息,比如 2 手写数字的字体长得像 3,网络把这个冗余的信息也学习出来是最好的。

因此上面左边的图就叫做 Hard Target,右边的图就叫做 Soft Target。

下面再了解另外一个概念叫做 SoftMax with Temperature,在 SoftMax 函数里面增加一个温度系数。

传统的 softmax 函数定义如下:
q i = e x p ( z i ) ∑ j e x p ( z j ) q_{i}=\frac{exp(z_{i})}{\sum_{j}exp(z_{j})} qi=jexp(zj)exp(zi)
使用软标签就是修改了 softmax 函数,增加温度系数 T:
q i = e x p ( z i / T ) ∑ j e x p ( z j / T ) q_{i}=\frac{exp(z_{i}/T)}{\sum_{j}exp(z_{j}/T)} qi=jexp(zj/T)exp(zi/T)
其中 q i q_i qi 是每个类别输出的概率, z i z_i zi 是每个类别输出的 logits,T 是温度。温度 T=1 时,为标准 Softmax。T 越高,softmax 的输出概率分布越趋平滑,其分布的熵越大,负标签携带的信息会被相对地放大,模型训练将更加关注负标签。

这个时候网络模型训练时就会更关注于一些负标签的冗余的信息,就是上面提到的 Soft Target,它会将一些冗余的信息额外的信息记录下来,通过简单的设置一个温度系数来控制。

下面看一下比较明确的图:

在这里插入图片描述

从图中可以看出随着 T 的增加可以看到 softmax 输出的分布就会越平滑,信息熵也就会越大,信息的差异也会越少。如何找打一个合适的 T 让网络学习到一些冗余的信息,但又不至于影响模型最终的性能呢?🤔

下面我们就来探讨下如何选择这个 T

负标签中包含一定的信息,尤其是那些负标签概率值显著高于平均值的标签。但由于 Teacher 模型的训练过程决定了负标签部分概率值都比较小,并且负标签的值越低,其信息就越不可靠。因此温度的选取需要进行实际实验的比较,本质上就是在下面两种情况之中取舍:

  • 当想从负标签中学到一些信息量的时候,温度 T 应该调高一些
  • 当想减少负标签的干扰的时候,温度 T 应该调低一些

接下来我们就正式的回到知识蒸馏这个算法里面,首先需要了解下知识蒸馏算法的训练流程的差异:

  • 传统训练过程 Hard Targets:对 ground truth 求取极大似然 softmax 值
  • KD 训练过程 Soft Targets:用 Teacher 模型的 calss probabilities 作为 soft targets

在论文中知识蒸馏使用 offline distillation 的方式,采用经典的 Teacher-Student 结构,其中 Teacher 是“知识”的输出者,Student 是“知识”的接受者。知识蒸馏的过程分为 2 个阶段:

1. 教师模型训练:训练 Teacher 模型,简称为 Net-T,特点是模型相对复杂,精度较高。对 Teacher 模型不作任何关于模型架构、参数量等方面限制,像 transformer 这类的大模型更加适合。唯一的要求就是,对于输入 X,其都能输出 Y,其中 Y 经过 softmax 映射,输出值对应相应类比的概率值。

2. 学生模型蒸馏:训练 Student 模型,简称为 Net-S,它是参数量较小、模型结构相对简单的模型。同样对于输入 X,其都能输出 Y,Y 经过 softmax 映射后能输出与 Net-T 对应类别的概率值。

论文中,Hinton 将问题限定在分类问题下,或者属于分类的问题,共同点是模型最后有 softmax 层,其输出值对应类别的概率值。知识蒸馏时,由于已经有一个泛化能力较强的 Net-T,在利用 Net-T 来蒸馏 Net-S 时,可以直接让 Net-S 去学习 Net-T 的泛化能力。

下面来看下算法流程:

  • 1. 训练 Teacher Model
  • 2. 利用高温 T h i g h T_{high} Thigh 产生 soft target
  • 3. 使用 {soft target, T h i g h T_{high} Thigh} 与 {hard target, T h i g h T_{high} Thigh} 同时训练 Student Model
  • 4. 设置温度 T = 1,Student Model 用于线上推理

在这里插入图片描述

Student Model 的训练或者蒸馏过程相对来说会复杂一点,它有两个损失,在训练 Net-T 的过程中,高温蒸馏过程的目标函数由 distill loss(对应 soft target)和 student loss(对应 hard target)加权得到,计算公式如下:
L = α L s o f t + β L h a r d L s o f t = − ∑ j N p j T log ⁡ ( q j T ) L h a r d = − ∑ j N c j l o g ( q j ) \begin{aligned} L&=\alpha L_{soft}+\beta L_{hard} \\ L_{soft}& =-\sum_j^Np_j^T\log(q_j^T) \\ L_{hard}& =-\sum_j^Nc_j\mathrm{log}(q_j) \end{aligned} LLsoftLhard=αLsoft+βLhard=jNpjTlog(qjT)=jNcjlog(qj)
soft label 的损失函数来自于 Teacher Model,hard label 的损失函数来自于真实的数据标签,通过结合两个损失函数去学习。

OK!以上就是关于知识蒸馏的内容

总结

本系列视频主要讲解了模型压缩的几种常见方法,包括量化、剪枝和蒸馏,其中量化和剪枝博主之前有简单了解过,这次再理清一些基础概念并加深印象,蒸馏方面也初步了解了其原理,感谢 ZOMI 老师。

总的来说,该系列视频作为导读了解一些基本概念还是不错的,真正要深入研究还行需要大家多看看论文和相关代码,动手实践才行😄

参考

  • 【推理引擎】模型压缩
  • YOLOv5-QAT量化部署
  • YOLOv5-PTQ量化部署
  • 剪枝与重参第一课:修剪结构和标准
  • TensorRT量化第三课:动态范围的常用计算方法
  • https://github.com/chenzomi12/DeepLearningSystem/tree/main/043INF_Slim
  • To prune, or not to prune: exploring the efficacy of pruning for model compression
  • Distilling the Knowledge in a Neural Network

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

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

相关文章

SystemVerilog Assertions应用指南 Chapter 1.14蕴含操作符

1.14蕴含操作符 属性p7有下列特别之处 (1)属性在每一个时钟上升沿寻找序列的有效开始。在这种情况下,它在每个时钟上升沿检查信号“a”是否为高。 (2)如果信号“a”在给定的任何时钟上升沿不为高,检验器将产生一个错误信息。这并不是一个有效的错误信息,因为我…

Leetcode 454 四数相加II(哈希表 + getOrDefault方法用于获取Map中指定键的值,如果键不存在,则返回一个默认值)

Leetcode 454 四数相加II&#xff08;哈希表&#xff09; 解法1 HashMap getOrDefault方法 解法1 HashMap getOrDefault方法 【HashMap】 【⭐️HashMap常用操作】 创建HashMap&#xff1a;HashMap<Integer, Integer> hash new HashMap<>(); 向HashMap添加元素…

【类和对象+this引用】

文章目录 面向对象与面向过程面向对象关注的是对象&#xff0c;用类描述这个对象如何定义类如何更改类名 类的实例化this引用总结 面向对象与面向过程 面向对象就是解决问题的一种思想&#xff0c;主要依靠对象之间的交互完成一件事情。 面向过程好比传统的洗衣服方式&#x…

17 Transformer 的解码器(Decoders)——我要生成一个又一个单词

Transformer 编码器 编码器在干吗&#xff1a;词向量、图片向量&#xff0c;总而言之&#xff0c;编码器就是让计算机能够更合理地&#xff08;不确定性的&#xff09;认识人类世界客观存在的一些东西 Transformer 解码器 解码器会接收编码器生成的词向量&#xff0c;然后通…

STM32,我想看单片机上的外设时钟,我怎么看?

一&#xff1a;在工程中加入rcc文件 首先需要加载我们的时钟函数的文件 stm32f10x_rcc.h 和 stm32f10x_rcc.c 文件 二&#xff1a;查看文件 在h头文件 尾部&#xff0c;有我们这个总线的函数 在函数体内&#xff0c;有我们这个宏定义的 外设时钟&#xff0c;我们拿就行了 APB2_…

App分发的策略和注意事项

当今的数字化时代中&#xff0c;移动应用程序已经成为了人们生活中不可或缺的一部分。随着智能手机的普及和移动互联网的快速发展&#xff0c;应用程序的分发方式也变得越来越多样化。 App分发是指将移动应用程序通过特定的渠道传递给终端用户的过程。在应用程序开发完成后&am…

解决matlab报错“输入参数的数目不足”

报错语句&#xff1a;tanh((peakNums-parameter)/2) 报错提示&#xff1a;输入参数的数目不足 运行环境&#xff1a;matlab2021b 分析原因&#xff1a; 当执行peakNums - parameter时&#xff0c;如果peakNums和parameter都是向量&#xff0c;那么这并不一定意味着会得到对应…

2.卷积神经网络(CNN)

一句话引入&#xff1a; 如果我们要做图像识别&#xff0c;用的是一个200x200的图片&#xff0c;那么BP神经网络的输入层就需要40000个神经元&#xff0c;因为是全连接&#xff0c;所以整个BP神经网络的参数量就是160亿个&#xff0c;显然不能这样来训练网络&#xff0c;所以我…

HBuilder插件推荐

整理一下我觉得好用的插件&#xff0c;后期可能会有更改 eslint-js eslint-plugin-vue Prettier scss/sass编译 右键复制vue页面路径&#xff0c;主要用于快速复制vue页面的路径到浏览器

订单30分钟自动关闭的五种解决方案

1 前言 在开发中&#xff0c;往往会遇到一些关于延时任务的需求。例如 生成订单30分钟未支付&#xff0c;则自动取消生成订单60秒后,给用户发短信 对上述的任务&#xff0c;我们给一个专业的名字来形容&#xff0c;那就是延时任务 。那么这里就会产生一个问题&#xff0c;这…

基于Java的汽车维修预约管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

Java学习_day03_变量数据类型运算符

文章目录 变量定义声明赋值使用简化 数据类型基本数据类型整型浮点型布尔型字符型空型 引用数据类型数据类型转换自动类型转换强制类型转换 运算符算术运算符赋值运算符比较运算符逻辑运算符位运算符条件运算符一元运算符二元运算符三元运算符运算符优先级 变量 变量类似于数学…

云服务器搭建Hadoop分布式

文章目录 1.服务器配置2.Java环境3. 安装Hadoop4. 集群配置5. 编写集群的启动脚本 1.服务器配置 服务器主机名配置115.157.197.82s110核115.157.197.84s210核115.157.197.109s310核115.157.197.31s410核115.157.197.60gracal10核 所有的软件安装在/opt/module下&#xff0c;软…

光学知识整理-偏振光

偏振光 目录基础概念基础概念的补充平面偏振光&#xff08;线偏振光&#xff09;部分偏振光圆偏振光椭圆偏振光菲涅耳公式相位关系 反射折射所引起的偏振态的改变斯托克斯倒逆关系重要参数 目录 基础概念 光是横波&#xff1a;光是电磁波,其电场分量(电场强度)E、磁场分量(磁…

biquad滤波器的设计

1.介绍 Biquad滤波器是一种常用的数字滤波器结构&#xff0c;它使用二阶差分方程&#xff08;difference equation&#xff09;来实现滤波功能。它得名于其包含两个极点&#xff08;poles&#xff09;和一个零点&#xff08;zero&#xff09;。 双二阶滤波器(biquad)是最常用…

当我让文心一言写个代码来庆祝1024程序员节,它写的代码是……

先让它写个自我介绍吧~ 大家好&#xff0c;我是一个人工智能语言模型&#xff0c;我的中文名是文心一言&#xff0c;英文名是ERNIE Bot。我可以协助您完成范围广泛的任务并提供有关各种主题的信息&#xff0c;比如回答问题&#xff0c;提供定义和解释及建议。如果您有任何问题…

改进YOLOv5 | 头部解耦 | 将YOLOX解耦头添加到YOLOv5 | 涨点杀器

改进YOLOv5 | 头部解耦 | 将YOLOX解耦头添加到YOLOv5 论文地址:https://arxiv.org/abs/2107.08430 文章目录 改进YOLOv5 | 头部解耦 | 将YOLOX解耦头添加到YOLOv51. 解耦头原理2. 解耦头对收敛速度的影响3. 解耦头对精度的影响4. 代码改进方式第一步第二步第三步第四步第五步本…

Kotlin笔记(六):泛型的高级特性

前面学习了Kotlin中的泛型的基本用法,跟Java中的泛型大致相同&#xff0c;Kotlin在泛型方面还提供了不少特有的功能&#xff0c;掌握了这些功能&#xff0c;你将可以更好玩转Kotlin&#xff0c;同时还能实现一些不可思议的语法特性&#xff0c;那么我们自然不能错过这部分内容了…

Wordpress - Xydown独立下载页面插件

Wordpress - Xydown独立下载页面插件&#xff1b; 1.使用ftp将demo.php和download.php上传到网站根目录&#xff08;两个文件中设计网站信息的代码可根据实际情况修改为自己的信息&#xff09; 使用ftp将demo.php和download.php上传到网站根目录&#xff08;两个文件中设计…