推理加速

大家都知道,深度神经网络有着精度高,灵活性强,适用面广等一系列优点,但相比传统机器学习(如逻辑回归,决策树等),深度神经网络往往有着训练速度慢,推理速度慢等缺点,这些缺点在很多实际场景中是不可忽视的。

例如语音识别,我们的数据都是上传到云端,再从云端传送到对方设备。倘若数据在模型中就要跑上个0.5秒,那么对方收到信息很可能就是1秒多之后了。这看起来也可以接受,但若是放在自动驾驶中,这0.5秒足以决定一次事故是否会发生,这个场景中模型推理时间将是十分重要的。

那么,当我们感觉模型推理速度无法达到我们的需求时,就需要对模型进行加速。个人总结了一些常用的方法,在这里分享一下。(之前的博客中有大量的公式推导,导致看起来很累,因此这次只是简单介绍一下方法,对原理感兴趣的朋友可以去查相关内容。)

SVD分解

在这里插入图片描述
SVD分解就是将一个m✖️n的矩阵分解为三个矩阵的乘积,具体分解过程如下:
在这里插入图片描述
这里我们将原矩阵A分解成三个矩阵的乘积,其中对角元素lambda是按照降序排列的,例如我们有100个lambda,但由于是降序排列,后面的影响度会越来越小,因此可以选择只保留前面几个(详细原因不做介绍,可参考关于SVD分解的文章)。我们不妨选择保留10个:
在这里插入图片描述
那么原来A这个200✖️100的矩阵就被分解成一个200✖️10的矩阵,一个10✖️10的矩阵和一个10✖️100的矩阵相乘。

因为只保留了10个lambda,因此相比原矩阵,我们分解出来的三个矩阵将涵盖较少的信息。大家可能会问,既然分解后的表达能力不如分解前,那费这么大劲分解的好处是什么呢?

回到我们的目的:加速运算。我们来看分解前后的运算次数。我们对分解前的A乘上一个100✖️1的向量,那么它的计算次数是200✖️100=20000次乘法;而对分解后的结果乘上100✖️1的矩阵,它需要做10✖️100+10✖️200=3000次乘法,大大加快了运算的效率。

考虑到这一点,我们可以对神经网络中的权重矩阵进行SVD分解。例如:两个隐层都是4096维的,那么连接两个隐层的权重矩阵就是4096✖️4096的,那么我们将它分解成4096✖️1024+1024✖️1024+1024✖️4096三个矩阵,那么模型的推理速度将提升一倍。

但同时,矩阵分解也会带来一定的精度损失。比如直接训练4096✖️4096的网络,精度达到90%,那么做完矩阵分解后的网络可能精度变为85%,**所以我们在分解之后,还需要用一个较小的学习率再训练一下,这个过程叫做fine tuning。**通常在fine tuning之后,新网络的精度会和原网络近似,甚至达到超越原网络的精度。

那么就会有同学问了,既然最后精度不会差,那么为什么不一开始就将网络设计成SVD分解后的结构呢?答案是,如果这么做,那么原90%的精度,这么做之后将可能连80%都达不到。原因是,参数越多,模型表达能力越强。在训练好大网络后,在进行矩阵分解产生新网络,那我们得到的新网络将有一组非常好的初始化参数,即新网络继承了原网络绝大部分表达能力;而直接将网络设计成分解后的样子进行训练,那么初始化参数是随机的,将很难优化到SVD分解那么好。

Hidden Node prune

这种方法是通过隐层裁剪的方式来减少隐层参数,从而达到运算加速的目的。其基本思想是:

  1. 对隐层节点重要度排序
  2. 定义每个节点的删除率,删除一定比例的“不重要”的节点

至于1中节点的重要度排序,我们可以认为,从上一个隐层传到每个节点的权重绝对值之和越大的节点,其对于这个节点来说越重要。因此我们要先计算每个节点的=接收上一个隐层的权重绝对值之和,再对这个和进行排序,去掉一定比例的节点。

知识蒸馏(teacher student)

我们可以这样理解知识蒸馏:有一个知识量很大但也因此反应很慢的“老师”网络,还有一个知识量较少但反应很快的“学生”网络。现在我们希望“老师”能够将他的知识传授给“学生”,同时“学生”的推理速度还保持不变。

举个例子,有一个分类任务,可以区分很多生物。现在有一张图片是美人鱼,“老师”的知识显示:这张图有20%像人,20%像鱼,60%像美人鱼;而我们若直接把标签传达给“学生”,那么学生只能学到这个图片是“美人鱼”的信息,对于这张图和人或鱼的相似度则完全学习不到,这是我们不愿看到的。

因此,我们的想法是,用数据训练一个大规模的网络,这个网络性能很好但也因此推理速度较慢。用它预测得到的标签(每张图变成一个概率分布而非固定的0,1)去训练一个小网络,小网络的目的就是尽量输出和该概率分布近似的新的概率分布。

统计学中,衡量两个概率分布相似程度的指标是“KL散度”,又叫“相对熵”,其表达式如下:
在这里插入图片描述
衡量的是概率分布P和概率分布Q之间的不相似程度。最小为0,表示两个分布完全相同。其中P(x)表示大网络“老师”得到的概率分布,Q(x)是小网络“学生”得到的概率分布。我们要让两个分布尽可能近似,就要去最小化上面这个函数。其实写开求导后大家会发现,其形式和传统分类任务的损失函数,“交叉熵损失”是一样的,只不过将固定的只有0和1的向量替换成了一个概率分布向量。

参数共享

这里就不细说了,熟悉CNN,RNN以及它们的变体如lstm,gru等的同学应该能明白。
(CNN中的卷积核,gru中输入及上个隐层信息传入三个门的权重矩阵都是做参数共享的,这将大大减少模型的参数量)

神经网络的量化

写这个的原因是,计算机处理浮点数据会比处理整数慢上许多,而权重矩阵大多都是数值较小的浮点数据,直接运行可能会比较慢。因此我们考虑将浮点运算转化成整数值运算。我们可以选择32bit,16bit,或者8bit的运算。具体方法是(32bit举例):

对于一个参数a,我们计算(a✖️2^ 32)取整再除以2^32 。
这样我们信息的损失最多为1/2^32 ,这样一来就将浮点运算变成了整数运算,加速了推理运算的速度,且精度不会有很大损失。

(至于为什么选择32,16,8,这是由GPU性能决定的,这里就不做赘述了。)

Binary Net

这种方法比上面那种还要简单粗暴,它将所有参数都变成-1或1,直接进行位运算,速度几十倍的提升。但信息损失过大,所以其精度相比其他网络会明显下降。

基础较好的同学可能会问,参数都是整数这怎么做反向传播?其实,深度学习三巨头之一的Bengio提出了一种STE的方法,大家可以去找二值化网络最经典的那篇论文,有详细的介绍。

但这种方法精度损失较大,一般实际应用比较少。

基于fft的循环矩阵加速

这种方法是基于傅立叶变换的,先上两张公式:
Ω

在这里插入图片描述

我们将权重矩阵W看成是C生成的循环矩阵(图1中的形式),那么此时
WX 就可以看成是C和X做卷积。而卷积的傅立叶变换就等于傅立叶变换的乘积,因此先对C和X分别作傅立叶变换,相乘,再对结果去做傅立叶变换的逆变换,就可以直接得到计算结果。

而其实傅立叶变换的速度是很快的,要快于直接做WX的矩阵乘法,因此可以达到运算加速的目的。

以上就是几种推理加速的方法,其中最常用的就是SVD分解、知识蒸馏以及神经网络的量化。在实际工作中,若模型推理速度达不到商业标准,那么可以尝试这些方法来减少模型参数或加快运算速度,从而使模型达到预期的效率。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐