人们对神经网络的研究已经超过半个世纪。最初时,人们研究的神经网络通常只有几层的网络,随着研究的深入,特别是深度学习的兴起,深度网络通常有更多的层数,今天的神经网络一般在五层以上,甚至多达一千多层。我们用的神经网络,通常是中间有一个隐层或者有两个隐层。今天,我们所说的深度神经网络指的是什么?直白的说就是用的神经网络隐藏层有很多层,例如,2012年ImageNet 竞赛的冠军模型AlexNet用了8层,2014年冠军模型GooleNet用了22层,2015年冠军模型RestNet用了152 层,2016年多达1207层。
我们在与学习研究神经网络对比,增加了深度求解遇到三个主问题:
一是神经网络的原生问题,求解过程中会遇到梯度消失或者梯度爆炸问题;
二是神经网络性能,训练过程太慢,耗费资源;
三是神经网络过拟合问题。
2006年,深度学习之父 Geoffrey Hinton等人利用预训练方法缓解了局部最优解问题,基于深信度网(DBN)提出非监督贪心逐层训练算法,解决深层结构训练等优化难题。将隐含层推动到了7层,至此,神经网络真正意义上有了“深度”,不过,仍没有给出“深度”等明确定义,例如在语音识别中4层网络就被认为是“较深的”,而对于图像识别模型,20多层也比较常见。为了克服梯度消失问题,用ReLU等激活函数代替了 sigmoid,形成了深度神经网络( DNN)的基本形式。单从结构上来说,全连接的DNN和多层感知机是没有任何区别的。
我们以ImageNet竞赛为例,从各年最佳参赛者的表现来看。可以看出算法的准确情况由最初25%以上的错误率25%,到2015年,ImageNet竞赛冠军模型ResNet已经超过人类水平的错误率(5%),将错误率降到3%以下。
虽然根据不同应用场景,深度神经网络的结构和深度各异,但基本符合神经网络的输入层、隐藏层、输出层结构,而且,目前重点是快速演化以提升模型准确性和效率。所有深度神经网络的输入值,是一套与神经网络表征能力密切相关的信息值。这些值可以是一张图片的像素,也可以是一段音频样本的频率和振幅的数字化表示。
神经网络的层与层之间一般是全连接(FC,也指多层感知器),即第n层的任意一个神经元一定与第n+1层的任意一个神经元相连。虽然DNN从整体上看起来很庞大、复杂,但是从微观的局部模型来说,与感知机是一样,即一个线性关系加上一个激活函数。
输入诺干个x 经过线运算得z,激活函数的运算得到一个新的值,这个值相当于开始输入的x,用a代指。
我们谈论的深度学习实际指的是基于深度神经网络(deep neural networks,DNN)的学习,也就是深度人工神经网络所进行的学习过程,或称作Deep Learning。这里Deep所指的神经网络的深度就是指神经网络层数多。不过,AI业界没有清晰地、具体地给出多少层的深度网络算是深层神经网络。所以在这里我们也就不强调多深算深的概念了,我们就权且管超过2隐藏层的深度都叫深度神经网络。
实际上,DNN这个深度神经网络概念广义上一般来说CNN、RNN、GAN等都属于其范畴之内。DNN与CNN(卷积神经网络)的区别是DNN特指全连接的神经元结构,并不包含卷积单元或是时间上的关联。DNN是指包含多个隐层的神经网络,根据神经元的特点,可以分为MLP、CNNs、RNNs等,从神经元的角度来讲解,MLP是最朴素的DNN,CNNs是encode了空间相关性的DNN,RNNs是encode进了时间相关性的DNN。
在DNN中,用于处理输入的神经网络一般有两种主要结构:前馈神经网络以及循环神经网络。在前馈神经网络中,所有输入计算处理都是在前一层输出基础上进行的,直到最后一层就是深度神经网络的输出;在循环神经网络(例如LSTM )中,网络中间环节是有内在记忆的,数据输入计算处理顺序依存上一次输出为相关影响,循环反馈作用于神经网络后输出,在这样深度神经网络中,一些中间值会被存储于网络中,也被用作与后续输入影响神经计算。
用于网络链接的深度神经网络,一般可以使用全连接(FC,也指多层感知器)的形式,如图左两层间部分所示。在一个全连接层中,所有上一层的输出与下一层的所有输入都是相连接的。这样很耗费资源,占用大量的存储和计算空间。不过,在许多实际应用中,我们可以采用移除激活之间的一些连接方式,也就是将参数权重设置为0,形成一个稀疏连接层,而且这类结构化的稀疏性也不影响准确性,如果这套权重被用于每一个输入计算,就会进一步提高模型效率,显著降低权重的存储要求。
通用近似定理(Universal approximation theorem,也译万能逼近定理)是指:如果一个前馈神经网络具有线性输出层和至少一层隐藏层,只要给予网络足够数量的神经元,便可以实现以足够高精度来逼近任意一个在 ℝn 的紧子集 (Compact subset) 上的连续函数。
如图11-5中所示神经网络通用近似模拟函数,(a)为任意函数,(b)为任意函数被神经网络以任意精度模拟。
即使函数是连续的,有关神经网络能不能解决所有问题,也是很有争议的。原因很简单,我们的理想很美好,现实问题比较多。比如,深度学习的生成对抗网络(GAN)的提出者伊恩·古德费洛(IanGoodfellow)就曾说过:“仅含有一层的前馈网络的确足以有效地表示任何函数,但是,这样的网络结构可能会格外庞大,进而无法正确地学习和泛化。
人工神经网络最神奇的地方可能就在于,它可以在理论上证明:“一个包含足够多隐含层神经元的多层前馈网络,能以任意精度逼近任意预定的连续函数”。通用近似定理告诉我们,不管函数 f ( x ) f(x) f(x)在形式上有多复杂,我们总能确保找到一个神经网络,对任何可能的输入,以任意高的精度近似输出 f ( x ) f(x) f(x),即使函数有多个输入和输出,即 f ( x 1 , x 2 , x 3 ⋯ x n ) f(x_1,x_2,x_3⋯x_n ) f(x1,x2,x3⋯xn)。
第一层:
h 1 = f ( ∑ i = 1 m w i x i + b 1 ) h_1=f(∑_{i=1}^{m}w_i x_i+b_1 ) h1=f(∑i=1mwixi+b1)
第二层:
h 2 = f ( ∑ j = 1 n w j x j + b 2 ) h_2=f(∑_{j=1}^{n}w_j x_j+b_2 ) h2=f(∑j=1nwjxj+b2)
通用近似定理的结论告诉我们,可能存在能够正确地学习和泛化的某种结构的神经网络。也就是说一个前馈神经网络如果具有线性输出层和至少一层具有任何一种激活函数(例如sigmoid激活函数)的隐藏层,只要给予网络足够数量的隐藏单元,它可以以任意的精度来近似任何从一个有限维空间到另一个有限维空间的Borel 可测函数。
基于通用近似定理的理想,我们以什么样的方法,如何构建复杂的神经网络来解决问题呢?我们主要从神经网络的层数、神经元的数量两个方面入手,也就是宽度和深度是深度学习模型的两个基本维度
Goodfellow等著《深度学习》中的一张图片(图11-6),表明对某个特定问题而言,隐藏层越多,精确度越高。
我们知道理论上一个浅度神经网络可以做得和深度网络一样好,但是事实往往并非如此。为什么呢?Goodfellow等著《深度学习》为上面的问题的解答提供了一些理由。
(1)浅度网络的神经元数量将随着任务复杂度的提升进行几何级数的增长,因此浅度网络要发挥作用,会变得很大,很可能比深度网络更大。这个理由的依据是很多论文都证明了在某些案例中,浅度网络的神经元数量将随着任务复杂度的提升进行几何级数的增长,但是我们并不清楚这一结论是否适用于诸如MNIST分类和围棋这样的任务。
(2)选择深度模型编码了一个非常通用的信念,我们想要学习的函数应该涉及若干较简单的函数的组合。从表征学习的视角来说,我们相信正学习的问题包括发现一组差异的底层因素,这些因素可以进一步用其他更简单的差异的底层因素来描述。
我们要解决比较复杂的问题,要么增加深度,要么增加宽度,而增加宽度的代价往往远高于深度。
2015年,ImageNet上提出的152层的残差网络赢得了多项图像辨识竞赛的冠军。这是一个巨大的成功,看起来是一个令人难以抗拒的越深越好的论据。
在参数一样的前提下,模型深的比浅的效果更好,加大深度”就相当于函数中的模块化,也就是“并不急于解决问题,而是把问题切成比较小的问题,再解决”。而且这样的好处是:“数据集要求低,并不需要太多的数据”。
浅层神经网络可以表示的特征抽象程度不高,而层次越深,特征的抽象程度越高,也就是在某些特定任务上所谓的“效果越好”,这也是为什么深度神经网络可以做出很多只有人类才能做到的需要高度抽象理解能力的事情。
随着深度的增加,逼近函数的效果好,已经有论文证明神经网络训练的过程就是调整参数的过程,可以调整的参数(weights and bias)越多,意味着调整的自由度越大,从而逼近效果越好。
然而,预测效果却不一定好。针对同一个问题,层数少的时候效果差,这时候逐渐增加层数可以提高效果,但是如果盲目不停地增加层数,则会容易引起过拟合,从而导致预测效果不好,所以并不是层数越多,预测效果就一定会越好的。此外,添加更多层会导致更高的训练误差。
因此,神经网络随着深度的增加,实际的效果是先变好,然后再变差。例如在CIFAR-10项目上使用56层的网络其错误率,不仅在训练集上,而且在测试集上,都高于20层网络。
这种现象的本质问题是由于出现了信息丢失而产生的过拟合问题。随着神经网络层数的加深,我们将面临三个重大问题:
(1)非凸优化问题,即优化函数越来越容易陷入局部最优解;
神经网络核心神经元是线性回归,在线性回归当中,从任意一个点出发搜索,最终必然是下降到全局最小值附近的。而在多层神经网络中,从不同点出发,可能最终困在局部最小值。局部最小值是神经网络结构带来的挥之不去的阴影,随着隐层层数的增加,非凸的目标函数越来越复杂,局部最小值点成倍增长,利用有限数据训练的深层网络,性能还不如较浅层网络。所以,从本质上来看,深度结构带来的非凸优化仍然不能解决(包括现在的各类深度学习算法和其他非凸优化问题都是如此),这限制着深度结构的发展。
(2)梯度消失问题(Gradient Vanish)
随着网络层数增加,“梯度消失”(或者说是梯度发散diverge)现象更加严重。比如我们常常在多层神经网络中使用sigmoid作为神经元的激活函数。对于幅度为1的信号,在BP反向传播梯度时,每传递一层,梯度衰减为原来的1/4。这样随着层数增多,梯度指数衰减后,后面的隐层基本上接受不到有效的训练信号。那么在深度学习中是如何解决局部极值及梯度消失问题的呢?
这个问题实际上是由神经元中使用激活函数所引起的,比如多层使用Sigmoid系函数,会使得输入信号从输出层开始呈指数衰减。
在数学上,激活函数的作用就是将输入数据映射到0到1上(其中,tanh是映射-1到+1上)。至于映射的原因,除了对数据进行正则化外,大概是控制数据,使其只在一定的范围内。当然也有另外细节作用,例如Sigmoid(tanh)中,能在激活的时候,更关注数据在零(或中心点)前后的细小变化,而忽略数据在极端时的变化,例如ReLU还有避免梯度消失的作用。通常,Sigmoid(tanh)多用于全连接层,而ReLU多用于卷积层。
对于梯度消失问题, Hinton为了解决深层神经网络的训练问题,于2006年提出逐层预训练方法,比如深度信念网的训练;以及最近提出CNN中的ReLu激活函数,则从根本上提出了度消失问题的解决方案;对于深受度消失问题困扰的RNN,其变种LSTM也克服了这个问题。
(3)过拟合问题。
真实的应用中想要的并不是让模型尽量模拟训练数据的行为,而是希望通过训练出来的模型对未知的数据给予判断。模型在训练数据上的表现并不一定代表了它在未知数据上的表现。模型过拟合是神经网络庞大复杂结构和参数,以及数据集所带来的,这样结构和参数、数据集尽管使训练错误率降的很低,但是测试错误率却高的离谱,模型达不到泛化要求。
对于深度神经网络模型,与其他机器学习模型相比,一般不易过拟合。而神经网络过拟合问题,往往是在训练数据不够多,网络结构很复杂,或者过度训练时,可能会产生过拟合问题。
那么过拟合的通俗解释就是,随着对于给定的模型的训练过程的进行,在训练集上的错误率渐渐减小,但是在测试集上的错误率却反而渐渐增大。这是因为训练出来的网络过拟合了训练集,对测试集外的数据效果不好。也就是如过产生了过拟合问题,那么用训练集得到的准确率同测试集得到的准确率相差非常大。
那么为了防止过拟合问题,我们可用的方法有:增大数据集(例如增加噪声)、采用正则化方法、在网络层采用dropout方法,以及损失函数是用于优化训练数据。
为了解决神经网络变深、结构变复杂所带来难以训练的问题,我们总结出一系列训练解决方案。
我们开发人员往往习惯把原始数据直接扔给DNN,在这种情况下,DNN仍然能够给出不错的结果。但是,有句老话说“给定恰当的数据类型,一个简单的模型能比复杂 DNN 提供更好、更快的结果”。我们不讨论特例,在今天,这句话仍然没有过时。因此,不管你是在计算机视觉( CV),自然语言处理(NLP)还是统计建模(Statistical Modelling)等领域,想要对原始数据预处理,有几个方法可以得到更好的训练数据:
激励函数是把非线性(non-linearity)加入了神经网络模型中,是神经网络的重要核心部分。
激励函数包括Sigmoid 函数、Tanh函数、ReLu函数等等激励函数。比如多年来,很多人一直倾向的选择Sigmoid 函数,但是,Sigmoid 函数将进一步导致梯度消失,近年来,我们更多是使用ReLu函数。实际工作中,需要根据具体情况,选择适合的激励函数。
我们通常采用保留超出最优数量的隐藏单元方案,尽量避免出现模型欠拟合(underfitting)几率,而且任何正则化方法( regularization method)都会处理超出的单元。
另外,当采用无监督预训练的表示时(unsupervised pre-trained representations),隐藏单元的最优数目一般会变得更大。因此,通过增加隐藏单元的数目,模型会得到所需的灵活性,以在预训练表示中过滤出最合适的信息。
对于隐层的数量,我们采用直接选择最优数目方案。正如 Yoshua Bengio 在 Quora中提到的:“你只需不停增加层,直到测试误差不再减少。”
我们常见几种权重初始化方法:全部初始化为零、初始化为随机数、Xavier/Glorot Initialization初始化、MSRA/He initialization初始化等等,常用的神经网络权重初始化方法有Xavier和MSRA。
这种方式最简单,我们在线性回归、logistics回归的时候,经常把参数初始化为0。
W = np.zeros(input_layer_neurons, hidden_layer_neurons)
如果是将权重W全部初始化为零,这样它们的梯度一样,那么每一层所学到的参数都是一样的,在反向传播的过程中,每一层的神经元也是相同的。因此会导致代价函数在开始的时候明显下降,但是一段时间以后,停止继续下降。
用小的随机数字初始化权重,以打破不同单元间的对称性,权重参数随机初始化为服从均值为零和方差为1的高斯分布函数。
W = np.random.randn(input_layer_neurons, hidden_layer_neurons)
开始模型可以很好的运行一段时间,但是随着时间增加,前向传递时,方差开始减少,梯度也开始向零靠近,会导致梯度消失。特别地,当激活函数为sigmoid时,梯度接近0.5;当激活函数为时tanh,梯度接近0。
Xavier initializatio是Glorot等人为了解决随机初始化的问题提出来的另一种初始化方法,他们的思想很简单,就是尽可能的让输入和输出服从相同的分布,这样就能够避免后面层的激活函数的输出值趋向于0,适用于激活函数是sigmoid和tanh。
W = np.random.randn(input_layer_neurons, hidden_layer_neurons)* sqrt(1/input_layer_neurons)
He初始化方法适用于激活函数Relu,在初始化使得正向传播时,状态值的方差保持不变;反向传播时,关于激活值的梯度的方差保持不变。
其初始化方法为:W~N(0,√(2/n)),其中,n为第 l 层神经元个数。
W = np.random.randn(input_layer_neurons,hidden_layer_neurons)* sqrt(2/input_layer_neurons)
随着网络层数的增加,分布逐渐发生偏移,之所以收敛慢,是因为整体分布往非线性函数取值区间的上下限靠近。这会导致反向传播时梯度消失。BN就是通过规范化的手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值0方差1的标准正态分布,使得激活输入值落入非线性函数中比较敏感的区域。可以让梯度变大,学习收敛速度快,能大大加快收敛速度。
由于现在batch norm比较流行所以可能对初始化要求不是那么高,一般用relu的话使用he初始化会好一些,或者直接用norm batch。
使用 BN 时,减少了网络对参数初始值尺度的依赖,此时使用较小的标准差(例如0.01)进行初始化即可,初始化方法没有那么重要了。
这是训练模型最重要的超参数之一,用于调节着学习过程。如果学习率设置得太小,则模型很可能需要很长时间来收敛,如果设置得过大,损失可能将会极高。通常出事设置为0.01 的学习率比较保险。但是这不是一个严格的标准,最优学习率与实际训练模型相关。
相比固定学习率,可以采用逐渐降低学习率训练方法。虽然这能更快地训练,但需要人工决定新的学习率。一般来说,学习率可以在每个周期后减半。最近研究发展,出现了自适应学习率(adaptive learning rates)。
目前,我们有许多选择,从老式动能方法( Momentum Method ),到 Adagrad、Adam 、RMSProp 等等。
网格搜索(Grid Search )在经典机器学习中十分普遍。但它在寻找 DNN 的最优超参数方面效率较低。这主要是因为 DNN 尝试不同超参数组合所耗费的时间。随着超参数不断增长,网格搜索需要的计算性能会指数级增长。超参数组合通常在期望范围之内、从均匀分布中被选择出来。加入之前获得的知识来进一步缩小搜寻空间,也是有可能的(比如,学习率不应该太大也不应该太小)。大家发现,随机搜索比网格搜索高效地多。
有两种解决办法:
(1)如果我们的团队有之前的经验,我们可以人工对部分常见超参数调参,比如学习率、隐层数目;
(2)采用随机搜索(random search),或者随机采样代替网格搜索,来选择最优超参数。
随机梯度下降( Stochastic Gradient Descent )的老方法也许对于 DNN 不是那么有效率(有例外)。最近,有许多研究聚焦于开发更灵活的优化算法,比如 Adagrad、Adam,、AdaDelta,、RMSProp 等等。在提供自适应学习率之外,这些复杂的方法还对于模型的不同参数使用不同的学习率,通常能有更平滑的收敛。把这些当做超参数是件好事,你应该每次都在训练数据的子集上试试它们。
不管你进行的是 NLP(自然语言处理)、计算机视觉还是语音识别等任务,无监督预训练永远能帮助你训练监督、或其他无监督模型:NLP 中词向量就(Word Vectors)无所不在;你可以用 ImageNet 的数据库,使用无监督方式对你的模型预训练,或是对于两个类别的监督分类;或是更大频域的音频样本,来在扬声器消崎模型(speaker disambiguation model)中使用该信息。
Mini-Batch(小批量)相对于Batch梯度下降法,是对整个训练集的一次遍历(称为一个 epoch)能做 mini-batch 个数个梯度下降。并可以一直遍历训练集,直到最后收敛到一个合适的精度。
那么如何选择mini-batch 大小?
首先,如果训练集较小,小于2000个样本,直接使用batch梯度下降法,样本集较小就没必要使用mini-batch梯度下降法。
对于样本数量较多的情况,实践经验上,一般的mini-batch大小设置为32到256,用于CPU训练环境,32到1024用于GPU训练环境,考虑到计算机内存设置和使用的方式,如果mini-batch大小是2^n,训练会运行地快一些,获得 mini-batch 的方法如下:
m = X.shape[1]
permutation = list(np.random.permutation(m))
shuffled_X = X[:, permutation]
shuffled_Y = Y[:, permutation].reshape((1,m))
训练神经网络模型的主要目的是学习合适的参数,即训练生输入到输出的最优映射。这些参数依据每个训练样本进行调参,不管你决定使用 batch、mini-batch 还是随机学习。
mini-batch 的大小为 1,即是随机梯度下降法(stochastic gradient descent),每个样本都是独立的 mini-batch;
mini-batch 的大小为 m(数据集大小),即是 batch 梯度下降法。
这来自于信息理论(Information Theory)——“学习到一件不太可能发生的事却发生了,比学习一件很可能发生的事已经发生,包含更多的信息。”同样的,把训练样例的顺序随机化(在不同周期,或者 mini-batch),会导致更快的收敛。如果模型看到的很多样例不在同一种顺序下,运算速度会有小幅提升。
使用Dropout,就是每次随机选择一些神经元不参与训练,只有在预测的时候这些神经元才生效。
如果有数百万的参数需要学习,正则化就是避免 DNN 过拟合的必须手段。你也可以继续使用 L1/L2 正则化,但 Dropout 是检查 DNN 过拟合的更好方式。执行 Dropout 很容易,并且通常能带来更快地学习。通常Dropout 的默认值是0.5,如果模型不太复杂,Dropout 值设置0.2或许就可以。
注意,在测试阶段,Dropout 应该被关闭,权重要调整到相应大小。
“对深度学习模型进行多个周期的训练,会得到更好的模型”——我们经常听到这句话。但多少周期才是“多”呢?其实,这里有一个简单的策略:继续按照一个固定的样例数或者周期训练模型,比如两万个样例或者一个周期。在每批样例之后,比较测试误差(test error)和训练误差(train error),如果它们的差距在缩小,那么继续训练。另外,记得在每批训练之后,保存模型的参数,所以训练好之后你可以从多个模型中做选择。
训练深度学习模型有上千种出差错的方式。我猜大家都遇到过这样的场景:模型已经训练了几个小时或者好几天,然而在训练完成之后,才意识到某个地方出问题了。为了不让你自己神经错乱,一定要对训练过程作可视化处理。比较显而易见的措施是保存或打印损失值、训练误差、测试误差等项目的日志。
在此之外,一个很好的措施是采用可视化库(visualization library ),在几个训练样例之后、或者周期之间,生成权重柱状图。这或许能帮助我们追踪深度学习模型中的一些常见问题,比如梯度消失与梯度爆发(Exploding Gradient)。
在我们生活的移动端,如何让深度模型在移动设备上运行,也是模型压缩加速的一大重要目标。Krizhevsky 在 2014 年的文章中,提出了两点观察结论:卷积层占据了大约 90-95% 的计算时间和参数规模,有较大的值;全连接层占据了大约 5-10% 的计算时间,95% 的参数规模,并且值较小。这为后来的研究深度模型的压缩与加速提供了统计依据。
例如有 50 个卷积层的 ResNet-50 需要超过 95MB 的存储器以及 38 亿次浮点运算。在丢弃了一些冗余的权重后,网络仍照常工作,但节省了超过 75% 的参数和 50% 的计算时间。
随着AI的发展,人们对AI应用需求旺盛,如何让你的深度神经网络跑得更快,这是我迫切需要的。我们的深度神经网络发展得益于计算能力的大幅提升,但是由于内存和计算能力有限,随着网络变得越来越深,而且对于移动设备在内的有严格时延要求的有限资源平台而言,神经网络压缩就成为一个关键问题。我们需要在计算能力和深度神经网络之间达到平衡,给出剪枝和共享、低秩分解、紧凑卷积滤波器、知识蒸馏等解决方案。
研究结果告诉我们,通常情况下,参数修剪和共享、低秩分解和知识蒸馏方法可以用于全连接层和卷积层的 CNN,而使用转移/紧凑型卷积核的方法仅支持卷积层。
通俗的说,剪枝就是移除对模型性能影响不大的参数。参数的冗余和稀疏特性对模型性能的影响甚微,而剪枝正好利用了这一特性。例如在训练的很多卷积神经网络中的卷积核并非每个都是完全有用的,也就是说很多卷积核是赘余的,这有两种可能,一是去掉整个卷积核,整个都是赘余的,二是去掉卷积核中的某些权重,因为整个卷积核中的某些权重是赘余的。对于剪枝可以进一步划分为稀疏剪枝、fiters pruning、channel pruning等三类。
(1)稀疏剪枝
在模型训练过程中,基于大小的权重剪枝会逐渐将模型权重设为零,实现模型稀疏性。稀疏模型更易于压缩,我们可以在推断过程中跳过零以缩短延迟时间。
此技术通过模型压缩改进了模型。今后,对此技术的框架支持可以缩短延迟时间。我们发现模型压缩的性能提升了6倍(Tensorflow官方网站2020年https://www.tensorflow.org/model_optimization/guide/pruning),并且对准确率造成的影响极低。
此技术正在各种语音应用(例如语音识别和文字转语音)中接受评估,并已针对各种视觉和翻译模型进行了实验。
一般情况下,可以理解为操作方式就是:
这种方法虽然很多赘余的权重全被减掉了,对于速度,因为剪枝之后计算和存储都相当于稀疏矩阵的方式,所以有特定的加速计算的方式,对于稀疏矩阵,对于存储也是会采用0×次数类似的方式来减小尺寸,所以速度和存储大小都会被优化。
(2)channel pruning/fiters pruning
通道(channel)剪枝是压缩深度神经网络的主要方法之一。通道剪枝要在一个层上移除通道可能会急剧改变下一个层的输入。然而,通道剪枝保持了原始模型的架构,并没有引入额外的层,在GPU上的绝对加速比要更高。
一般情况下,channel pruning过程如下。
keras-surgeon是一个keras的模型剪枝工具,该项目支持神经元,通道以及网络层级别的剪枝操作,下面这个例子使用《Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures》Hengyuan Hu(2016)等人描述的方法来确定要修剪哪些神经元。
我们定义了平均零百分比(APoZ)来度量ReLU映射后的神经元,平均零的百分比(APoZ),其中 O c ( i ) O_{c}^{(i)} Oc(i)表示第i层c通道的输出。
A P o Z c ( i ) = A P o Z ( O c ( i ) ) = ( ∑ k N ∑ j M f ( O c , j ( i ) ( k ) = 0 ) ) / ( N × M ) APoZ_{c}^{(i)}=APoZ(O_{c}^{(i)} )=(∑_{k}^{N}∑_{j}^{M}f(O_{c,j}^{(i)} (k)=0) )/(N×M) APoZc(i)=APoZ(Oc(i))=(∑kN∑jMf(Oc,j(i)(k)=0))/(N×M)
其中,当为真时 f ( ∙ ) = 1 f(∙)=1 f(∙)=1,为假时 f ( ∙ ) = 0 f(∙)=0 f(∙)=0,M表示 O c ( i ) O_{c}^{(i)} Oc(i)输出特征图的维数,N表示验证示例的总数。验证实例越多,APoZ的测量越精确。
https://github.com/BenWhetton/keras-surgeon
from keras.layers import Dense, Conv2D, MaxPool2D, Flatten
from keras.models import Sequential
from keras import layers
from keras import callbacks
from tensorflow.examples.tutorials.mnist import input_data
from kerassurgeon import identify
from kerassurgeon.operations import delete_channels
def main():
training_verbosity = 2
# Download data if needed and import.
mnist = input_data.read_data_sets('/MNIST_data', one_hot=True, reshape=False)
val_images = mnist.validation.images
val_labels = mnist.validation.labels
# Create LeNet model
model = Sequential()
model.add(Conv2D(20,
[3, 3],
input_shape=[28, 28, 1],
activation='relu',
name='conv_1'))
model.add(MaxPool2D())
model.add(Conv2D(50, [3, 3], activation='relu', name='conv_2'))
model.add(MaxPool2D())
model.add(layers.Permute((2, 1, 3)))
model.add(Flatten())
model.add(Dense(500, activation='relu', name='dense_1'))
model.add(Dense(10, activation='softmax', name='dense_2'))
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
early_stopping = callbacks.EarlyStopping(monitor='val_loss',
min_delta=0,
patience=10,
verbose=training_verbosity,
mode='auto')
reduce_lr = callbacks.ReduceLROnPlateau(monitor='val_loss',
factor=0.1,
patience=5,
verbose=training_verbosity,
mode='auto',
epsilon=0.0001,
cooldown=0,
min_lr=0)
# Train LeNet on MNIST
results = model.fit(mnist.train.images,
mnist.train.labels,
epochs=200,
batch_size=128,
verbose=2,
validation_data=(val_images, val_labels),
callbacks=[early_stopping, reduce_lr])
loss = model.evaluate(val_images, val_labels, batch_size=128, verbose=2)
print('original model loss:', loss, '\n')
layer_name = 'dense_1'
# Dead cycle, please add break and exit by yourself
while True:
layer = model.get_layer(name=layer_name)
apoz = identify.get_apoz(model, layer, val_images,bs=128)
high_apoz_channels = identify.high_apoz(apoz)
model = delete_channels(model, layer, high_apoz_channels)
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
loss = model.evaluate(val_images,
val_labels,
batch_size=128,
verbose=2)
print('model loss after pruning: ', loss, '\n')
results = model.fit(mnist.train.images,
mnist.train.labels,
epochs=200,
batch_size=128,
verbose=training_verbosity,
validation_data=(val_images, val_labels),
callbacks=[early_stopping, reduce_lr])
loss = model.evaluate(val_images,
val_labels,
batch_size=128,
verbose=2)
print('model loss after retraining: ', loss, '\n')
if __name__ == '__main__':
main()
安装keras-surgeon剪枝工具:
pip install kerassurgeon。
(3)过滤器剪枝(fiters pruning)
主流的滤波器剪枝(filter pruning)方法都是在第一轮训练完成之后用各种metrics(比如filter norm大小)直接剔除不那么重要的filters,并重新finetune模型以补偿剪枝过程中带来的性能损失。按soft pruning[1]的说法,这类剪枝方法应该叫hard pruning,也就是在训练之后强行将一部分filters剔除。
一般情况下,fiters pruning过程如下。
这种方法可以瞬间减小你的model的尺寸,并且加快速度,因为你的计算量明显减小了,并且model中的卷积核的个数也少了,也就是说你总体的权重少了,剩下来的都是默认有用的卷积核或者权重,但是问题来了,就是精度会大大降低,因为这个也没告诉你怎么剪枝,具体每层每层之间怎么裁剪,只能不断尝试。对于channel,这边的channel就是某个卷积层通过乘以每个卷积核得到的那些feature channel,来判断这些channel哪些是冗余的方式来消除filters。
注意:前面一两层最好不剪枝,或者少量剪枝;和SSD有关联的层尽量不剪枝,或者少剪枝;剪枝之后一定要再训练,acc会上升一些,但是不要次数过多,会过拟合。
(4)量化和二进制化
量化就是将无限域的值转化为有限域离散值的过程。假设我们有一张灰度图像。量化(N级)就是将图像中的每个像素点的颜色用N份权重来表示。而二进制化只会给出图像的两个灰度级别(灰或非灰)。
网络量化通过减少表示每个权重所需的比特数来压缩原始网络。量化限制了可用于我们内核中的不同权重数目。对于N个比特位,可以表示2的N次方个权重。我们的目的是修改内核中的权重只能取2的N次方个值。因此,低于四个比特位参数量化虽然准确率没有受到很大损失,但却很难表示权重。
低秩分解的方法其实就是运用了矩阵分解和矩阵乘法的结合律。实际上就是把较大的卷积核分解为两个级联的行卷积核和列卷积核。常见的就是一个3×3的卷积层,替换为一个3×1的卷积层加上一个1×3的卷积核。
低秩估计的方法的优势在于,没有改变基础运算的结构,不需要额外定义新的操作。分解后的网络仍是用卷积操作来实现的,所以其适用面比较广泛。分解方法多种多样,但一般分解后的网络都需要参数调优,以保证分解后网络模型的准确率。
常见的低秩分解有:奇异值分解SVD、CP分解、Tucker分解、Tensor Train分解和Block Term分解等,用低秩矩阵近似原有权重矩阵。
理想AI模型的目标不是拟合训练数据,而是学习如何泛化到新的数据。所以蒸馏的目标是让student学习到teacher的泛化能力,理论上得到的结果会比单纯拟合训练数据的student要好。另外,对于分类任务,如果soft targets的熵比hard targets高,那显然student会学习到更多的信息。
模型蒸馏也叫知识蒸馏,是Hinton在NIPS2014提出了知识蒸馏(Knowledge Distillation)的概念,最基本的想法就是将大模型学习出来的知识作为先验,将先验知识传递到小规模的神经网络中,之后实际应用中部署小规模的神经网络。这样做有三点依据:
对于网络压缩和加速,模型蒸馏则直接设计了一个简单结构的小网络,那小网络的准确率怎么和大网络比呢?Hinton前辈提出了一个非常简单且有效的方法——网络蒸馏。主要思想是用预训练好的网络,是指通常结构较复杂,准确率较高的网络,来指导小网络的训练,并使小网络达到与复杂网络相近的准确率。大网络类比于老师,小网络类比于学生,老师经过漫长时间的“训练”摸索出一套适用于某个任务的方法,于是将方法提炼成“知识”传授给学生,帮助学生更快地学会处理相似的任务。整个思想中最大的难题在于如何有效地表达“知识”,并有效地指导小网络的训练。