Latency)、高吞吐(Throughpout)、高效率(Efficiency)挑战的。而模型压缩算法可以将一个庞大而复杂的预训练模型转化为一个精简的小模型,从而减少对硬件的存储、带宽和计算需求,以达到加速模型推理和落地的目的。
近年来主流的模型压缩方法包括:数值量化(Data Quantization,也叫模型量化),模型稀疏化(Model sparsification,也叫模型剪枝 Model Pruning),知识蒸馏(Knowledge Distillation), **轻量化网络设计(Lightweight Network Design)和 张量分解(Tensor Decomposition)**。
其中模型剪枝是一种应用非常广的模型压缩方法,其可以直接减少模型中的参数量。本文会对模型剪枝的定义、发展历程、分类以及算法原理进行详细的介绍。
Pruning)也叫模型稀疏化,不同于模型量化对每一个权重参数进行压缩,稀疏化方法是尝试直接“删除”部分权重参数。模型剪枝的原理是通过剔除模型中 “不重要” 的权重,使得模型减少参数量和计算量,同时尽量保证模型的精度不受影响。
当然,权重剪枝(Weight Pruning)虽然影响较小但不等于没有影响,且不同类型、不同顺序的网络层,在权重剪枝后影响也各不相同。论文[1]在 AlexNet 的 CONV 层和 FC 层的做了剪枝敏感性实验,结果如下图所示。
从图中实验结果可以看出,卷积层的剪枝敏感性大于全连接层,且第一层卷积层最为敏感。论文作者推测这是因为全连接层本身参数冗余性更大,第一层卷积层的输入只有 3 个通道所以比起他卷积层冗余性更少。
即使是移除绝对值接近于 0 的权重也会带来推理精度的损失,因此为了恢复模型精度,通常在剪枝之后需要再训练模型。典型的模型剪枝三段式工作 pipeline 流程[1]和剪枝前后网络连接变化如下图所示。
剪枝算法常用的是三段式工作 pipeline: 训练、剪枝、微调。上述算法步骤中,其中重点问题有两个,一个是如何评估连接权重的重要性,另一个是如何在重训练中恢复模型精度。对于评估连接权重的重要性,有两个典型的方法,一是基于神经元连接权重数值幅度的方法[1],这种方法原理简单;二是使用目标函数对参数求二阶导数表示参数的贡献度[10]。
基于神经元连接权重幅度的厕率好像在20世纪90年代就被提出来了,知识在韩松论文中[1]又被应用了。剪枝
Three-Step Training Pipeline 中三个阶段权重数值分布如下图所示。微调之后的模型权重分布将部分地恢复正态分布的特性。
深度网络中存在权重稀疏性:(a)剪枝前的权重分布;(b)剪除0值附近权值后的权重分布;(c)网络微调后的权重分布从单峰变成了双峰。值得注意的是,韩松提出的权重稀疏方法是细粒度稀疏,去只能在专用硬件上-EIE实现加速效果,是对硬件不友好的稀疏方法,因为其稀疏后得到的权重矩阵是高度非规则的矩阵,如下图所示。
ReLU 激活函数,并在 AlexNet模型[3]中第一次得到了实践。
后续伴随着 BN 层算子的提出,“2D卷积-BN层-ReLU激活函数”三个算子串联而成的基本单元就构成了后来 CNN 模型的基础组件,如下述 Pytorch 代码片段所示:
早期是 “2D卷积-ReLU激活函数-池化” 这样串接的组件。
# cbr 组件示例代码
def convbn_relu(in_planes, out_planes, kernel_size, stride, pad, dilation):
return nn.Sequential(
nn.Conv2d(in_planes, out_planes,
kernel_size=kernel_size, stride=stride,
padding=dilation if dilation > 1 else pad,
dilation=dilation, bias=False),
nn.BatchNorm2d(out_planes),
nn.ReLU(inplace=True)
)
ReLU 激活函数的定义为:
$$ReLU(x) = max(0, x)$$
该函数使得负半轴的输入都产生 0 值的输出,这可以认为激活函数给网络带了另一种类型的稀疏性;另外 max_pooling 池化操作也会产生类似稀疏的效果。即无论网络接收到什么输入,大型网络中很大一部分神经元的输出大多为零。激活和池化稀疏效果如下图所示。
即 ReLU 激活层和池化层输出特征图矩阵是稀疏的。受以上现象启发,论文[4]经过了一些简单的统计,发现无论输入什么样图像数据,CNN 中的许多神经元都具有非常低的激活。作者认为零神经元很可能是冗余的(
redundant),可以在不影响网络整体精度的情况下将其移除。 因为它们的存在只会增加过度拟合的机会和优化难度,这两者都对网络有害。
由于神经网络具有乘法-加法-激活计算过程,因此其输出大部分为零的神经元对后续层的输出以及最终结果的贡献很小。由此,提出了一种神经元剪枝算法。首先,定义了
APoZ (Average Percentage of Zeros)指标来衡量经过 ReLU 映射后神经元零激活的百分比。假设 $O_c^{(i)}$表示网络第 $i$ 层中第 $c$ 个通道(特征图),那么第 $i$ 层中第 $c$ 个的滤波器(论文中用神经元 neuron)的 APoZ 定义如下:
$$
APoZ^{(i)}c = APoZ(O_c^{(i)}) = \frac{\sum_k^N \sum_j^M f(O^{(i)}{c,j}(k=0))}{N \times M}
$$
这里,$f\left( \cdot \right)$ 对真的表达式输出 1,反之输出 0,$M$ 表示 $O_c^{(i)}$ 输出特征图的大小,$N$ 表示用于验证的图像样本个数。由于每个特征图均来自一个滤波器(神经元)的卷积及激活映射结果,因此上式衡量了每个神经元的重要性。
下图给出了在 VGG-16 网络中,利用 50,000 张 ImageNet 图像样本计算得到的 CONV5-3 层的 512 个和 FC6 层的 4096 个 APoZ 指标分布图。
这里更高是指更接近于模型输出侧的网络层。
可以看出 CONV5-3 层的大多数神经元的该项指标都分布在 93%附近。实际上,VGG-16 模型中共有 631 个神经元的 APoZ 值超过90%。激活函数的引入反映出 VGG 网络存在着大量的稀疏与冗余性,且大部分冗余都发生在更高的卷积层和全连接层。
激活稀疏的工作流程和稀疏前后网络连接变化如下图所示。
工作流程沿用韩松论文[1]提出的 Three-Step Training Pipeline,算法步骤如下所示:
SGD 算法中,99.9% 的梯度交换都是冗余的。例如下图显示了在 AlexNet 的训练早期,各层参数梯度的幅值还是较高的。但随着训练周期的增加,参数梯度的稀疏度显著增大。
AlexNet 模型的训练是采用分布式训练。深度神经网络训练中的各层梯度值存在高度稀疏特性。
因为梯度交换成本很高,由此导致了网络带宽成为了分布式训练的瓶颈,为了克服分布式训练中的通信瓶颈,梯度稀疏(Gradient Sparsification)得到了广泛的研究,其实现的途径包括:
pipeline 流程并不一定是准确的,最新的研究表明,对于随机初始化网络先进行剪枝操作再进行训练,有可能会比剪枝预训练网络获得更高的稀疏度和精度。此需要更多研究。
GPU硬件!
论文[1]作者提出了专用加速器硬件 EIE 去支持他的细粒度权重剪枝算法。
因为,“非结构化稀疏”(Unstructured Sparsity)主要通过对权重矩阵中的单个或整行、整列的权重值进行修剪。修剪后的新权重矩阵会变成稀疏矩阵(被修剪的值会设置为 0)。因而除非硬件平台和计算库能够支持高效的稀疏矩阵计算,否则剪枝后的模型是无法获得真正的性能提升的!
由此,许多研究开始探索通过给神经网络剪枝添加一个“规则”的约束-结构化剪枝(Structured pruning),使得剪枝后的稀疏模式更加适合并行硬件计算。 “结构化剪枝”的基本修剪单元是滤波器或权重矩阵的一个或多个Channel。由于结构化剪枝没有改变权重矩阵本身的稀疏程度,现有的计算平台和框架都可以实现很好的支持。
这种引入了“规则”的结构化约束的稀疏模式通常被称为结构化稀疏(Structured Sparsity),在很多文献中也被称之为粗粒度稀疏(Coarse-grained Sparsity)或块稀疏(Block Sparsity),结构化和非结构化稀疏针对的都是权重参数。
过滤器 filter,也叫滤波器,相当于 3 维对卷积核。输出的 feature map 的数量/通道数等于滤波器数量。
CNN 模型中通道剪枝的核心在于如何减少中间特征的数量,其中一个经典思路是基于重要性因子,即评估一个通道的有效性,再配合约束一些通道使得模型结构本身具有稀疏性,从而基于此进行剪枝。
基于重要性因子的方法进行通道剪枝,和前面非结构化剪枝中的基于权重幅度的方法来进行连接剪枝类似,都有点主观性太强。论文Learning Efficient Convolutional Networks through Network Slimming[7] 认为 conv-layer 的每个channel 的重要程度可以和 bn 层关联起来,如果某个 channel 后的
bn 层中对应的 scaling factor 足够小,就说明该 channel 的重要程度低,可以被忽略。如下图中橙色的两个通道被剪枝。
BN 层的计算公式如下:
$$
\begin{aligned}
\mu_B &= \frac{1}{m}\sum_1^m x_i \
\sigma^2_B &= \frac{1}{m} \sum_1^m (x_i-\mu_B)^2 \
n_i &= \frac{x_i-\mu_B}{\sqrt{\sigma^2_B + \epsilon}} \
z_i &= \gamma n_i + \beta = \frac{\gamma}{\sqrt{\sigma^2_B + \epsilon}}x_i + (\beta - \frac{\gamma\mu_{B}}{\sqrt{\sigma^2_B + \epsilon}})
\end{aligned}
$$
其中,bn 层中的 $\gamma$ 参数被作为 channel-level 剪枝 所需的缩放因子(scaling factor)。
当一个阶段的输出特征发生变化时(一些特征被抛弃),其对应的每个残差结构的输入特征和输出特征都要发生相应的变化,所以整个阶段中,每个残差结构的第一个卷积层的输入通道数,以及最后一个卷积层的输出通道数都要发生相同的变化。由于这样的影响只限定在当前的阶段,不会影响之前和之后的阶段,因此我们称这个剪枝过程为阶段级别的剪枝(stage-level pruning)。
阶段级别的剪枝加上滤波器级别的剪枝能够使网络的形状更均匀,而避免出现沙漏状的网络结构。此外,阶段级别的剪枝能够剪除更多的网络参数,这给网络进一步压缩提供了支持。
综上所述,深度神经网络的权值稀疏应该在模型有效性和计算高效性之间做权衡。
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah
我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
我有一些非常大的模型,我必须将它们迁移到最新版本的Rails。这些模型有相当多的验证(User有大约50个验证)。是否可以将所有这些验证移动到另一个文件中?说app/models/validations/user_validations.rb。如果可以,有人可以提供示例吗? 最佳答案 您可以为此使用关注点:#app/models/validations/user_validations.rbrequire'active_support/concern'moduleUserValidationsextendActiveSupport:
对于Rails模型,是否可以/建议让一个类的成员不持久保存到数据库中?我想将用户最后选择的类型存储在session变量中。由于我无法从我的模型中设置session变量,我想将值存储在一个“虚拟”类成员中,该成员只是将值传递回Controller。你能有这样的类(class)成员吗? 最佳答案 将非持久属性添加到Rails模型就像任何其他Ruby类一样:classUser扩展解释:在Ruby中,所有实例变量都是私有(private)的,不需要在赋值前定义。attr_accessor创建一个setter和getter方法:classUs
我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案
ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序