神经网络加速之量化模型(附带代码)

神经网络加速之量化模型(附带代码)

作者:Xjtu @郝泽宇 鸣谢:Xjtu @魏亚东 I.C. @董豪

量化模型(Quantized Model)是一种模型加速(Model Acceleration)方法的总称,包括二值化网络(Binary Network)、三值化网络(Ternary Network),深度压缩(Deep Compression)等。鉴于网上关于量化模型的不多,而且比较零散,本文将结合TensorLayer来讲解各类量化模型,并讨论一下我们过去遇到的各种坑。。文章最后会介绍一些关于人工智能芯片的技术。

TensorLayer是一个基于TensorFlow的高级开发工具,提供大量数据处理和建模API,具备灵活性高、运行速度快等优点。今年3月,TensorLayer提供了一套搭建量化网络的试验版本API,不过目前这套API依然用矩阵乘法而不是加减或bitcount运算来加速(我们等会会提到)。因此,这套API并不能加速,关于产品部署,目前可以用TensorLayer训练模型,然后用自定义的C/C++实现的二值化计算(TensorLayer有可能会提供一套额外的专门运行二值化网络的框架,并支持可以从TensorLayer那读取模型)。注意,作为试验版本,这套API有可能会被修改。更多关于模型加速的技术,可关注:github.com/tensorlayer/

Keywords: 模型压缩(Model Compression),模型加速(Model Acceleration),二值化网络(Binary Network),量化模型(Quantized Model)

随着神经网络深度增加,网络节点变得越来越多,规模随之变得非常大,这是对移动硬件设备非常不友好的,所以想要在有限资源的硬件设备上布置性能良好的网络,就需要对网络模型进行压缩和加速,其中量化模型由于在硬件上移植会非常方便,在理论上来讲,是非常有发展潜力的。比较有名气的量化模型有Deepcompression,Binary-Net,Tenary-Net,Dorefa-Net,下面对这几种量化模型进行介绍:

DeepCompression

SongHan这篇文章可以说是神经网络压缩领域开山之作,怎么说呢这篇文章很早就注意到了,也复现了,做了很多实验。也一直想用到硬件参数压缩以及模型加速当中,在这个过程中遇到了很多问题,现在提出来跟大家一起探讨。

算法整体框架如图:



Deepcompression主要分为三个主要的部分:剪枝,量化,哈夫曼编码,下面分别探讨这几种方法并且分析他们在硬件前向配置的加速潜力。

剪枝(purning):其实这个思路的核心非常简单,就是当网络收敛到一定程度的时候,作者认为阈值小于一定权重的权重对网络作用很小,那么这些权重就被无情的抛弃了。注意,是抛弃,彻底抛弃,在复现的时候这个地方是一个大坑,被剪掉的权重不会再接收任何梯度。然后下面的套路简单了,就是很简单的将网络reload,然后重新训练至收敛。重复这个过程,直到网络参数变成一个高度稀疏的矩阵。这个过程最难受的就是调参了,由于小的参数会不断被剪枝,为了持续增大压缩率,阈值必须不断增大,那么剩下的就看你的调参大法6不6了。当初为了解决这个问题还专门设计了一个基于准确率损失和压缩率上升的公式,用于压缩。算是效果还可以,自己调参真的很难受。

最后参数会变成一个稀疏的矩阵,作者自己提出了一种编码方式:



当压缩率低于一定的值时,编码解码开销其实是非常大的,甚至到一定范围,编码后的存储量甚至大于不压缩。

第二个就是量化了,将接近的值变成一个数。大概的思路如下:



需要注意的是,量化其实是一种权值共享的策略。量化后的权值张量是一个高度稀疏的有很多共享权值的矩阵,对非零参数,我们还可以进行定点压缩,以获得更高的压缩率。

论文的最后一步是使用哈夫曼编码进行权值的压缩,其实如果将权值使用哈夫曼编码进行编码,解码的代价其实是非常大的,尤其是时间代价。还需要注意的是,deepcompression中对于输出没有压缩。所以这种方案对于硬件加速的主要作用体现在遇到0即可zero skip,即使用判断语句替代乘法器。

Binary-Net

  • Quantized Neural Networks: Training Neural Networks with Low Precision Weights and Activations
  • Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1
  • XNOR-Net: ImageNet Classification Using Binary Convolutional Neural Networks


通常我们在构建神经网络模型中使用的精度都是32位单精度浮点数,在网络模型规模较大的时候,需要的内存资源就会非常巨大,而浮点数是由一位符号位,八位指数位和尾数位三个部分构成的。完成浮点加减运算的操作过程大体分为四步:

1. 0 操作数的检查,即若至少有一个参与运算的数为零直接可得到结果。

2. 比较阶码大小并完成对阶

3. 尾数进行加或减运算;

4. 结果规格化并进行舍入处理。

带来的问题是网络在运行过程中不仅需要大量的内存还需要大量的计算资源,那么quantization的优越性就体现出来了,在2016年发表在NIPS的文章《Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1》中,提出了利用降低权重和输出的精度的方法来加速模型,因为这样会大幅的降低网络的内存大小和访问次数,并用bit-wise operator代替arithmetic operator。下面具体介绍一下这种方法的原理,在训练BNN时,将权重和输出置为1或-1,下面是两种二值化的方法:

第一种直接将大于等于零的参数置为1,小于0的置为-1;

x^b=sign(x)=\begin{cases} +1& \text{if x>0}\\ -1& \text{otherwise} \end{cases}\\

第二种将绝对值大于1的参数置为1,将绝对值小于1的参数根据距离±1的远近按概率随机置为±1。

x^b=sign(x)=\begin{cases} +1& \text{with probability}\ \sigma(x)\\ -1& \text{with probability}\ 1-p \end{cases}\\

公式中是一个clip函数:


\sigma(x)=clip(\frac{x+1}{2},0,1)=max(0,min(\frac{x+1}{2}))\\

第二种二值化方式看起来更为合理,但是由于引入了按概率分布的随机一比特数,所以硬件实现会消耗很多时间,我们通常使用第一种量化方法来对权重和输出进行量化。

虽然BNN的参数和各层的输出是二值化的,但梯度不得不用较高精度的实数而不是二值进行存储。因为梯度很小,所以使用无法使用低精度来正确表达梯度,同时梯度是有高斯白噪声的,累加梯度才能抵消噪声。

另一方面,二值化相当于给权重和输出值添加了噪声,而这样的噪声具有正则化作用,可以防止模型过拟合。所以,二值化也可以被看做是Dropout的一种变形,Dropout是将输出按概率置0,从而造成一定的稀疏性,而二值化将权重也进行了稀疏,所以更加能够防止过拟合。

由于sign函数的导数在非零处都是0,所以,在梯度回传时使用tanh来代替sign进行求导。假设loss function是C,input是r,对r做二值化有:


\\ q = sign(r)\\


C对q的的导数使用gq表示,那么q对r的导数就变成了:


g_r=g_q1_{|r|\leq1}\\


这样就可以进行梯度回传,给出一种包含bn的二值化网络的梯度算法:



BN最大的作用就是加速学习,减少权重尺度影响,带来一定量的正则化,可以提高网络性能,但是,BN涉及很多矩阵运算(matrix multiplication),会降低运算速度,因此,提出了一种shift-based Batch Normalization。

使用SBN来替换传统的BN,SBN最大的优势就是几乎不需要进行矩阵运算,而且还不会对性能带来损失。基于SBN,又提出 Shift based AdaMax:



网络除了输入以外,全部都是二值化的,所以需要对第一层进行处理:



作者还对二值化网络扩展到n-bit quantized,


\textbf{LinearQuant}\left( x,bitwidt \right)=Clip\left( round\left( \frac{x}{bitwidth} \right)\times bitwitdth,minV,maxV \right)\\ \textbf{LogQuant}\left( x,bitwidt \right)=Clip\left( AP2\left( x \right),minV,maxV \right)\\


二值化的论文对mnist、cifar-10、SVHN进行了测试,最后得到的test error如下:



完了作者为了挑战高难度,又用了alexnet和googlenet在imagenet上做了测试,看出来结果也是一般,所以较复杂的网络较大的数据集采用bnn看来影响还是蛮大的,作者不服气又提出了一些小技巧,比如什么放宽tanh的边界啊,用2-bit的activitions,也提升了一些准确率,作者也在rnn做language task上进行了二值化,结果也贴出来,分析了那么多模型,应该可以说在牺牲那么多运算和储存资源的情况下准确率差强人意。




x = tf.placeholder(tf.float32, shape=[batch_size, 28, 28, 1])
net = tl.layers.InputLayer(x, name='input')
net = tl.layers.BinaryConv2d(net, 32, (5, 5), (1, 1), padding='SAME', b_init=None, name='bcnn1')
net = tl.layers.MaxPool2d(net, (2, 2), (2, 2), padding='SAME', name='pool1')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn1')

net = tl.layers.SignLayer(net)
net = tl.layers.BinaryConv2d(net, 64, (5, 5), (1, 1), padding='SAME', b_init=None, name='bcnn2')
net = tl.layers.MaxPool2d(net, (2, 2), (2, 2), padding='SAME', name='pool2')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn2')

net = tl.layers.FlattenLayer(net)

net = tl.layers.SignLayer(net)
net = tl.layers.BinaryDenseLayer(net, 256, b_init=None, name='dense')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn3')

net = tl.layers.SignLayer(net)
net = tl.layers.BinaryDenseLayer(net, 10, b_init=None, name='bout')
net = tl.layers.BatchNormLayer(net, is_train=is_train, name='bno')

上面是给MNIST设计的一个BinaryNet,。。。

作者最后又分析了一下时间复杂度和功率效率,毕竟bnn的主要任务就是压缩和加速,说了时间复杂度可以降低60%,原理是说可以卷积核复用,举个例子,因为一个3x3的卷积核做了二值以后,只有2的9次方个独一的卷积核,相比于没有二值化的卷积核,在文章中的cifar-10网络中独一的卷积核数量只有42%那么多;内存资源减少了31/32(原本每个参数32bit,压缩后每个参数1bit),运算资源,硬件层面上看32bits损耗200个位,1bit只损耗一个位(bit-wise operation);最后在gpu上还可以进行SWAR(single instruction,multiple data within register)的处理,对xnor进行优化,SWAR的基本思想是将32个二进制变量组连接成32位寄存器,从而在按位操作(例如XNOR)上获得32倍的加速。 使用SWAR,可以仅用3条指令评估32个连接:

a_1+=popcount\left( xnor\left( a_0^32b,w_1^32b \right) \right)\\


就可以用1(加和)+4(popcount,四个8位)+1(xnor)个time cycle来进行运算,原来的 a_1\leftarrow a_1+2^{n-1}\times XnorDotProduct\left( a^n_0,w^b_1 \right) ,则是32个time cycle,提高了32/6倍的速度。



Xnor-Net在BNN的基础上引入了比例因子,让二值化之后的参数和原始的参数的L2范数最小,提高了模型的精度,


\textbf{I}\ast\textbf{W}=\left(\textbf{I}\oplus\textbf{B}\right)\alpha\\ \alpha^*,\textbf{B}^*=\mathop{argmin}_{\alpha,\textbf{B}}J\left( \textbf{B},\alpha \right)\\


对卷积操作的比例因子进行简化,降低了其运算复杂度,



由于在一般网络下,一层卷积的kernel规格是固定的,kernel和input在进行卷积的时候,input会有重叠的地方,所以在进行量化因子的运算时,先对input全部在channel维求平均,得到的矩阵A,再和一个w x h的卷积核k进行卷积得到比例因子矩阵K,其中


k_{i,j}=\frac{1}{w\times h}\\


在imagenet上结果也比bnn要好很多




Ternary-Net

  • Ternary Weight Networks paper

权值三值化的核心:

首先,认为多权值相对比于二值化具有更好的网络泛化能力。其次,认为权值的分布接近于一个正态分布和一个均匀分布的组合。最后,使用一个scale参数去最小化三值化前的权值和三值化之后的权值的L2距离。

基本原理阐述如下:

参数三值化的方式如下:


W^t_i=f_t\left( W_i|\Delta \right)=\begin{cases} +1,&if\ \ \ W_i\ >\Delta\\ \ \ \ 0,&if\ \left| W_i \right|\leq\Delta\\ -1,&if\ W_i<-\Delta \end{cases}\\


其实就是简单的选取一个阈值(Δ),大于这个阈值的权值变成1,小于-阈值的权值变成-1,其他变成0。当然这个阈值其实是根据权值的分布的先验知识算出来的。本文最核心的部分其实就是阈值和scale参数alpha的推导过程。

在参数三值化之后,作者使用了一个scale参数去让三值化之后的参数更接近于三值化之前的参数。具体的描述如下:


\begin{cases}\alpha^*,&W^{t*}=&\mathop{argmin}_{\alpha,W^t}J\left( \alpha,W^t \right)=\|W-\alpha W\left( x \right)^t \|^2_2\\ &s.t.&\alpha\ge 0,W^t_i\in\left\{ -1,0,1 \right\},i=1,2,…,n \end{cases}\\


利用此公式推导出alpha的值如下:


\alpha^*_\Delta=\frac1{\left| I_\Delta \right|}\sum_{i\in I_\Delta}\left| W_i\right|\\


由此推得阈值的计算公式如下:


\Delta^*=\mathop{arg\ max}_{\Delta>0}\frac1{\left| I_\Delta \right|}\left( \sum_{i\in I_\Delta}\left| W_i \right| \right)^2\\

由于这个式子需要迭代才能得到解,会造成训练速度过慢的问题,所以如果可以提前预测权值的分布,就可以通过权值分布大大减少阈值计算的计算量。文中推导了正态分布和平均分布两种情况,并按照权值分布是正态分布和平均分布组合的先验知识提出了计算阈值的经验公式。


\Delta^*\approx 0.7\cdot \left(|\textbf{W}\right|)\approx\frac{0.7}n\sum_{i=1}^n\left| W_i\right|\\

三值化论文的最终结果如下:



反正就是抓住BNN一顿diss呗,谁让人家准确率高呢。

当然,这种方法有进化版本,我们完全可以将权值组合变成(-2,-1,0,1,2)的组合,以期获得更高的准确率。正好我之前也推过相关的公式,现在贴出来供大家参考,这个时候权值的离散化公式变成了:


W^t_i=f_t\left( W_i|\Delta \right)=\begin{cases} +2,&if\ \ \ \ \ \ \ \ \ \ \ W_i\ >\Delta_2\\ +1,&if\ \ \ \ \ \Delta_2>\ W_i>\Delta_1\\ \ \ \ 0,&if\ \ \ \ \ \ \ \ \ \left| W_i \right|\leq\Delta_1\\ -1,&if\ -\Delta_2<W_i<-\Delta_1\\ -2,&if\ \ \ \ \ \ \ \ \ W_i<-\Delta_2 \end{cases}\\

Scale参数的计算公式变成了:


\alpha^*_\Delta=\frac1{\left( I_{\Delta_2}+4I_{\Delta_2}\right)}\left( \sum_{i\in I_{\Delta_1}}{W_i}+2\sum_{i\in I_{\Delta21}}{W_i} \right)\\


此时阈值的计算公式变成了:


\Delta_1^*=scale_1E\left(|W \right|)\approx \frac{0.5}n\sum_{i=1}^{n}{\left| W_i \right|}\\ \Delta_2^*=scale_2E\left(|W \right|)\approx \frac{1.4}n\sum_{i=1}^{n}{\left| W_i \right|}\\

需要声明的是,这个算法我只在一个非常不知名的matlab的一个纯cpu版本慢到爆炸反正就是难以忍受那种框架上面实际实现过,取得了比三值化更高的准确率,但是!对于这个算法在tensorflow上面的实现我真是一筹莫展,因为tensorflow某些机制。。。,算法的具体实现方式如下:

net = tl.layers.InputLayer(x, name='input')
net = tl.layers.TernaryConv2d(net, 32, (5, 5), (1, 1), padding='SAME', b_init=None, name='bcnn1')
net = tl.layers.MaxPool2d(net, (2, 2), (2, 2), padding='SAME', name='pool1')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn1')
net = tl.layers.TernaryConv2d(net, 64, (5, 5), (1, 1), padding='SAME', b_init=None, name='bcnn2')
net = tl.layers.MaxPool2d(net, (2, 2), (2, 2), padding='SAME', name='pool2')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn2')

net = tl.layers.FlattenLayer(net)
net = tl.layers.TernaryDenseLayer(net, 256, b_init=None, name='dense')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn3')
net = tl.layers.TernaryDenseLayer(net, 10, b_init=None, name='bout')
net = tl.layers.BatchNormLayer(net, is_train=is_train, name='bno')
return net

上面是tensorlayer提供的三值化的MNIST测试代码。

权值三值化并没有完全消除乘法器,在实际前向运算的时候,它需要给每一个输出乘以一个scale参数,然后这个时候的权值是(-1,0,1),以此来减少了乘法器的数目,至于为什么减少跟BNN是一样的道理。


DoReFa-Net

  • DoReFa-Net: Training Low Bitwidth Convolutional Neural Networks with Low Bitwidth Gradients

Face++团队在16年6月提出的Dorefa-Net和上面两种量化方法思路也是比较接近,但DoReLa-Net 对比例因子的设计更为简单,这里并没有针对卷积层输出的每一个过滤映射计算比例因子,而是对卷积层的整体输出计算一个均值常量作为比例因子。这样的做法可以简化反向运算,因为在他们反向计算时也要实现量化。

文章首先概述如何利用DoReFa-Net中的比特卷积内核,然后详细说明量化权值,激活和梯度以低比特数的方法。

和之前bnn的点积方法一样,dorefa也采用了这种简化的点积方式,


x\cdot y=N-2\times bitcount\left( xnor\left(x,y\right) \right),x_i,y_i\in \left\{ -1,1 \right\}\forall i\\


对于定点数x和y,可以得到下面的公式


\\x\cdot y = \sum_{m=0}^{M-1}\sum_{k=0}^{K-1}2^{m+k}bitcount\left[and\left(c_m\left(x\right),c_k\left(y\right)\right)\right],\\c_m\left(x\right)_i,c_k\left(y\right)_i\ \in \left\{ x \right\}\forall i,m,k.\\


同样为了规避0梯度的问题,采用了直通估计(STE),


\textbf{forward:}r_o=\frac{1}{2^k-1}round\left(\left(2^k-1\right)r_i\right)\\ \textbf{Backward:}\frac{\partial c}{\partial r_i}=\frac{\partial c}{\partial r_o}\\


对于权重二值化的梯度回传,采用下面的方法,即二值化乘比例因子,回传时直接跳过二值化,


\textbf{Forward:}r_o=sign(r_i)\times\textbf{E}\left(\left|r_i\right|\right) \\ \textbf{Backward:}\frac{\partial c}{\partial r_i} = \frac{\partial c}{\partial r_o} \\


比特数k大于1的梯度回传,需要先对参数clip到[0,1]之间,


\textbf{Forward:}r_o=f_w^k\left(r_i\right)=2quantize_k\left(\frac{tanh\left(r_i\right)}{2max\left(|tanh\left(r_i\right)\right|)}+\frac{1}{2}\right)-1 \\ \textbf{Backward:}\frac{\partial c}{\partial r_i} = \frac{\partial r_o}{\partial r_i}\frac{\partial c}{\partial r_o} \\


由于二值化输出会降准确率,所以采用k-bit量化(k>1),这里的r也要经过clip,


f_\alpha^k\left(r\right)=quantize_k\left(r\right)\\


Dorefa的梯度量化方法比较复杂,因为梯度是无界的,并且可能具有比隐层输出更大的值范围。我们可以通过使可微分非线性函数传递值来将隐层输出范围映射到[0,1]。 但是,这种构造不适用于渐变。 文章设计了以下用于梯度k位量化的函数,这里dr是r对损失函数C的偏导,


\widetilde f_\gamma^k \left( dr\right)=2max_0\left( \left| dr \right| \right)\left[ quantize_k\left( \frac{dr}{2max_0\left( \left| dr \right| \right)} +\frac{1}{2}\right)-\frac{1}{2} \right]\\


为了补偿量化梯度带来的潜在偏差,在clip后的结果增加了一个高斯噪声,


\widetilde f_\gamma^k \left( dr\right)=2max_0\left( \left| dr \right| \right)\left[ quantize_k\left[ \frac{dr}{2max_0\left( \left| dr \right| \right)} +\frac{1}{2}+N\left( k \right)\right]-\frac{1}{2} \right]\\


梯度的量化仅在回程中完成,因此文章在每个卷积层的输出上应用以下STE:


\textbf{Forward:}r_o=r_i \\ \textbf{Backward:}\frac{\partial c}{\partial r_i} =\widetilde f_\gamma^k\left(\frac{\partial c}{\partial r_o}\right) \\


最终得到了Dorefa-net的算法,这里对第一层和最后一层不做量化,因为输入层就图像任务来说通常是8-bit的数据,做低比特量化会对精度造成很大的影响,输出层一般是一些one-hot向量,所以一般对输出层也保持原样,除非做特殊的声明。

Dorefa-net为了进一步节省资源将3,4,6步放在一起做,将11,12步融合在一起,节省了中间步骤的全精度数储存消耗的资源。



DorefaNet分别对SVHN和ImageNet进行了实验,准确率如下:





net = tl.layers.InputLayer(x, name='input')
net = tl.layers.DorefaConv2d(net, 1, 3, 32, (5, 5), (1, 1), padding='SAME', b_init=None, name='bcnn1')  #pylint: disable=bare-except
net = tl.layers.MaxPool2d(net, (2, 2), (2, 2), padding='SAME', name='pool1')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn1')
net = tl.layers.DorefaConv2d(net, 1, 3, 64, (5, 5), (1, 1), padding='SAME', b_init=None, name='bcnn2')  #pylint: disable=bare-except
net = tl.layers.MaxPool2d(net, (2, 2), (2, 2), padding='SAME', name='pool2')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn2')

net = tl.layers.FlattenLayer(net)
net = tl.layers.DorefaDenseLayer(net, 1, 3, 256, b_init=None, name='dense')
net = tl.layers.BatchNormLayer(net, act=tl.act.htanh, is_train=is_train, name='bn3')
net = tl.layers.DenseLayer(net, 10, b_init=None, name='bout')
net = tl.layers.BatchNormLayer(net, is_train=is_train, name='bno')

上面是tensorlayer提供的DorefaNet的MNIST测试代码,需要注意的是不同于DorefaNet,我们的实现默认梯度为32bits来尽量获得更高的训练准确率,而且在实际的硬件前向配置中其实是不需要梯度信息的。

压缩算法局限性

目前的压缩算法是存在一些局限性的,最主要的问题还是准确率,论文中为了数据好看往往是选择传统的神经网络结构比如AlexNet,VGG作为测试对象,而这种网络一般是比较冗余的,如果想把参数压缩方案和其他一些方案结合,比如说下面讲到的一些SqueezeNet,MobileNets,ShuffleNet结合起来,会对准确率造成比较大的影响。原因可以归为参数压缩算法其实是一个找次优解的问题,当网络冗余度越小,解越不好找。所以,目前的高精度压缩算法只适合于传统的有很多冗余的网络。

更多加速方法

理论上来讲,量化模型是通往高速神经网络最佳的方法,不过由于种种问题,如实现难度大、准确性不稳定,使用门槛非常大,所以除了量化模型外,目前有很多更加常用的模型加速方法:

  • A Survey of Model Compression and Acceleration for Deep Neural Networks (end of 2017)

这是2017年底的一篇survey

有基于Pruning的

  • Channel Pruning for Accelerating Very Deep Neural Networks

也有基于改变卷积方式的,这是目前最常用的方法

  • SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size
  • MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
  • ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices

传送门:jiqizhixin.com/articles

值得注意的是,当TensorLayer和Keras使用完全相同的MobileNet时,TensorLayer的速度是后者的3倍(Titan XP上测试),大家可以试试。


关于AI芯片

关于硬件实现,这里要推荐一片非常好的survey:rle.mit.edu/eems/wp-con

大家看完这篇文章会对目前最先进的神经网络硬件加速架构有所了解。

由于目前基于PC平台的神经网络加速一定程度上不能满足需要,开发基于硬件例如FPGA的硬件加速平台显得很有必要。其实硬件加速神经网络前向运算的最主要的任务就是完成卷积优化,减少卷积运算的资源和能源消耗非常核心。

卷积优化的主要思路:

内存换取时间:

如果深度学习中每一层的卷积都是针对同一张图片,那么所有的卷积核可以一起对这张图片进行卷积运算,然后再分别存储到不同的位置,这就可以增加内存的使用率,一次加载图片,产生多次的数据,而不需要多次访问图片,这就是用内存来换时间。

乘法优化

以下图为例,上面是两张图片,右边是卷积核。我们可以把卷积核心展开成一条行,然后多个卷积核就可以排列成多行,再把图像也用类似的方法展开,就可以把一个卷积问题转换成乘法问题。这样就是一行乘以一列,就是一个结果了。这样虽然多做了一些展开的操作,但是对于计算来讲,速度会提升很多。


卷积乘法优化

GPU优化

1、了解IO访问的情况以及IO的性能;

2、多线程的并行计算特性;

3、IO和并行计算间的计算时间重叠;

对于NVIDIA的GPU来讲,内存访问是有一些特性的,连续合并访问可以很好地利用硬件的带宽。你可以看到,NVIDIA最新架构的GPU,其核心数目可能并没有明显增加,架构似乎也没有太大变化,但在几个计算流处理器中间增加缓存,就提高了很大的性能,为IO访问这块儿带来了很大优化。

Strassen算法

分析CNN的线性代数特性,增加加法减少乘法,这样降低了卷积运算的计算的复杂度 o(n^3)\rightarrow o(n^{2.81}) ,但是这种方法不适合在硬件里面使用,这里就不做详细的介绍了。

卷积中的数据重用

在软件中的卷积运算,其实我们是在不断的读取数据,进行数据计算。也就是说卷积操作中数据的存取其实是一个很大的浪费,卷积操作中数据的重用如下图所示:



卷积中的数据重用

那么想办法减少数据的重用,减少数据的存取成为解决卷积计算问题的一个很重要的方面。

目前这样的方法有很多种,最主要的方法包括以下几种:

权重固定:最小化权重读取的消耗,最大化卷积和卷积核权重的重复使用;

输出固定:最小化部分和R/W能量消耗,最大化本地积累;

NLR(No Local Reuse):使用大型全局缓冲区共享存储,减少DRAM访问能耗;

RS:在内部的寄存器中最大化重用和累加,针对整体能源效率进行优化,而不是只针对某种数据类型。

下表是在45NM CMOS的基础上对于不同的操作的能耗进行的统计。对32位的各种操作的能耗进行统计,可以看到从DRAM里面存取数据的能量消耗是最大的。是32位整型数据进行加法的能量消耗的6400倍。那么,从数据存取角度考虑卷积的优化就显得尤为必要了。

32位数不同操作能耗对比

可行性分析

在进行设计之前先对设计的可行性进行分析,分析过程包括卷积运算可实现性分析、卷积运算并行性分析,卷积的计算公式可以表示成下面的形式:


O[x][y]=\sum_{i=0}^{R-1}\sum_{j=0}^{S-1}I[c][Ux+i][Uy+j]\times w[c][i][j]\\

K=1\ \ \ \ Psum=\sum_{b=0}^{R-1}\sum_{c=0}^{S-1}image[b][c]\times w[b][c]\\


各个参数的意义在表 内详细表示:

卷积公式参数含义

在GPU中加速时,主要通过将数据最大程度的并行运算,增加了GPU的使用率从而加快了速度。但是这种方法在硬件实现的时候是不可行的,因为这种方法本质上没有降低能耗,而DNN模型的高能耗和大量的数据是其在可穿戴设备上面进行部署所需要面对的困难。

下面对一个卷积部分和运算进行分析,如下图 :

对第一组的PE整列,输入的是从Image的第0行到第R-1行的S列的数据,同样的对于第二列的PE阵列输入的是第2行到第R的S列的数据。每一列的PE计算得到一个最终的Psum的结果,那么如果设置PE阵列的列数为N的话,每次我们就可以计算得到连续的N个部分和的结果。

不断更新PE(process element,即处理单元)中Image缓冲区的数据,就可以模拟卷积在水平方向上面的滑动,不断更新整个PE阵列的数据输入,就可以模拟卷积窗在垂直方向上面的滑动,最终完成整个卷积运算的实现。对应的卷积运算公式的细节在图 中已经给出了,每一组PE产生一个部分和的结果的话,那么增加PE阵列的组数,就可以一次性产生多个部分和计算结果,这里的组数就是并行度。

上面的内容简单论证用数据重用的方式实现卷积运算的可行性,至于实现的具体数据流,还有相对用的系统的架构。



卷积运算PE阵列(并行度N)

压缩算法在实际硬件芯片的应用

其实压缩算法应用硬件芯片非常简单,就是简单的将硬件芯片原来使用的乘法器进行替换,如果是BNN,参数只有两种情形,那么如果参数为1的时候,直接通过,不计算,如果参数为-1的时候,翻转最高位即可。

同理三值化中增加了一个0参数,这个可以直接跳过不进行计算。至于参数为(-2,-1,0,1,2)的情形,参数为2时就增加了一个移位运算,参数为-2的时候增加了一个最高位的翻转。

如果是DorefaNet,权值和输出都固定在一定的种类内部,那么他们的乘积情形也只有一定的种类,这个时候相当于把乘法器变成了一个寻址操作,每次乘法只需要在LUT(look-up table,查找表)里面寻找到正确的结果读出即可。

编辑于 2018-08-07