DNN的整个步骤流程如图所示。NN和很多经典机器学习模型(如KNN、SVM)不同,它经过训练后在训练集上的表现未必会很好,这是因为它优化的损失函数是非凸的,训练停止时可能会停在局部最优点、鞍点或平坦点(即各个方向梯度都约等于零的点)。
因此我们训练好一个网络后,要先检验它在训练集上的效果如何,若效果不好,则要修改训练方法重新训练,若效果较好,则我们再观察在测试集上的效果,若效果不好,说明模型过拟合,需要修改原来算法(如进行正则化或者修改激活函数等),若效果好,则我们得到一个不错的模型。
上图就是一个例子。当我们只看到右图的时候,很多人的第一反应是56层的复杂模型在测试集上的表现还不如20层的简单模型,因此一定是发生了过拟合,但结合左图来看,会发现56层的模型在训练集上的表现依然不如20层的模型,这显然是因为没有训练好导致的,毕竟56层模型对应的function set是包含20层模型对应的function set的,若充分拟合了训练数据理应产生更高的精度。
这个例子强调了训练一个在训练集上表现良好的网络模型是第一步,在训练集上表现好了,我们才能接下来考虑是否发生过拟合的问题。
1、训练在训练集上表现好的网络
之前我们提到,训练的网络在训练集上表现不好的原因是最后停在了鞍点或平坦点等不好的点,但实际上,更差的情形是,模型训练太慢导致参数更新到最后也只变化了一点点,距离最优点还有很远的距离。
上图展示的是梯度消失问题,当我们把各层的激活函数设置为sigmoid函数的时候就会出现这样的问题,即靠近输入的各层参数会更新得很慢。直觉上来看,sigmoid函数的形状决定了,当input产生较大变化的时候,对应的output的变化也是不大的。因此即使我们把第一层的权重增加一个很大的,其经过sigmoid后表现在第二层的input就被缩小了,然后传递到第三层的变化就会进一步缩小,以此类推,当网络比较深的时候,对最终输出的改变量是微乎其微的,因此我们在训练网络的时候靠近输出层的参数更新比较快,而靠近输入层的参数则会更新很慢,因此最终得到的模型会仅通过改变最后几层的参数来收敛到一个局部最优点(或鞍点、平坦点等),而靠近输入的几层参数和最初随机初始化相比没有太大变化,这显然是非常不好的结果。
怎么解决这个问题呢?换激活函数!
从诞生起直到现在依然很常用的ReLU就是很好的解决梯度消失问题的激活函数。
ReLU的优点除了可以解决梯度消失问题以外,还有计算简单,更适合用于深度神经网络的训练。
但是直觉上看来ReLU会使得其中一些输出为0的神经元不起作用,而剩下的起作用的神经元的激活函数都是线性的,这样整个神经网络不就变成一个线性函数了吗?
实际上并不是这样,我们只是一次只更新与输出非零的神经元相连的权重而已,当权重改变后,下一轮训练时可能输出为0的神经元就发生变化了,也可以看作是网络架构发生了变化。从这个角度来说,使用ReLU训练出来的神经网络依然是一个非线性函数。
但是ReLU函数依然是存在缺点的,比如“神经元死亡”的问题。所谓神经元死亡就是这个神经元永远不会再被激活了,相当于这一层就少了一个神经元。这个问题的产生也很直观,以上面第一张图中第二层的第三个输出为0的神经元来说,首先在当前轮次中与之相连的权重都是不会更新的,而与之相连的第一层的两个输出为0的神经元的输出也会保持为0,因为这两个神经元与输入相连的权重并没有发生变化。因此,第三层的这个神经元要想被再次激活,只能寄希望于第一层前两个神经元的输出发生较大变化,但由于与这两个神经元连接的权重可能只是随机初始化的比较小的值,因此第三层的这个神经元要想被再次激活是很困难的。
综上所述,改进ReLU的思路应该是让z<0时输出不为0,而是一个较小的数。
上述两种ReLU是很直观的,后来Goodfellow提出了一个更一般性的激活函数Maxout:
maxout的思路也很简单,其将神经元进行分组,然后输出每组神经元输出的max值。上图是一个每组两个神经元的例子,需要注意的是,这里的一个分组实际上可以看作一个大的神经元,其输出是组里各个小神经元的输出的最大值。
这样说可能有点抽象,但图形化后很容易理解:
如图所示,某个神经元的激活函数实际上就是其组内几个小神经元对应的线性函数的最大值,图中绿色的线就是激活函数的图像。
显然,maxout可以得到ReLU激活函数,而且随着一组内神经元个数的增多,还可以得到形式更复杂的激活函数。如下图所示:
应用maxout激活函数的神经网络要如何训练呢?
其实大体思路和ReLU是相同的,也就是说每次训练的时候我们都只看各组里最大值所对应的神经元以及和它相连的权重,然后对这些权重进行更新即可。由于权重的改变可能导致下一次训练时最大值对应的神经元发生变化,可以视作网络结构发生了改变。
如果我们使用了形式很好的激活函数,但网络在训练集上的表现依然不理想呢?此时我们还可以考虑调整学习率。上面我们说过,maxout实际上可以自动学得一个激活函数的形式,这可以视为确定了网络超参数的一部分。除了激活函数,我们还可以考虑改变学习率,毕竟学习的快慢直接与学习率相关。
之前我们提到过一个动态改变学习率的方法——AdaGrad,实际上还有一些更强大的调整学习率的算法可以加速训练过程。
回忆一下AdaGrad做的事,实际上就是给各个梯度陡峭程度不同的维度分别设置学习率,以避免统一学习率造成的问题。
但上面的损失函数等高线图属于形式比较整齐的,如果遇到下图的情形该怎么办呢?
可以看到这个等高线图中,即使在某一个方向上,梯度也是有时陡峭有时平缓的,这就要求我们在某个方向上也要可以动态地调整学习率。
RMSProp算法据说是Hinton在他的公开课上提出的(大神真任性啊……这可以发篇paper了啊……),具体流程如下:
可以看到,相比AdaGrad引入的分母,这里新引入了一个参数,这个参数用于表示我们对过去梯度和当前梯度的重视程度,若较小,则说明我们比较重视新的(最近几轮)梯度的影响,若较大,则说明我们比较重视老的(先前几轮)梯度的影响。
有了调整学习率的算法,我们就可以比较快地进行训练了,但我们依然会不可避免地陷入平坦点、鞍点或局部最优点,我们能不能想一些办法让模型有一定概率可以跳出这些点呢?
下面提到的动量方法就是这样的一种方法。李宏毅老师的讲解非常形象,就像是物理世界中的惯性一样,如下图所示:
当小球从高处滑下的时候,可能不会因为一个小的凹槽停下来,因为其之前经历了很陡峭的斜坡,其有一个较大的动量,因此会倾向于继续上坡直到动量为0,把这样的机制引入梯度下降法中就可以以一定概率逃离平坦点、鞍点或局部最优点。
下面是原梯度下降法和引入动量后的梯度下降法的对比:
上面两张图已经把动量方法解释得非常清晰了,我们可以看到,每次计算动量时,就是在之前动量上乘以一个系数(我认为这个系数在0到1之间,起到衰减作用,因为上一步已经沿该方向走了一段距离,会消耗部分动量),然后再减去学习率乘以梯度。
至于动量和梯度重要性的trade-off,就体现在和两个参数上了。
可以看到,引入动量的梯度下降法有一定的希望可以逃离局部最优点。
而将动量方法与RMSProp方法结合起来,就得到了常用的经典方法——Adam。
2、避免神经网络的过拟合
上面我们已经介绍了一些经典的帮助训练神经网络在训练集上表现较好的方法。接下来就要关注过拟合问题了,因为神经网络尤其是深度神经网络是非常powerful的模型,因此是很容易发生过拟合的,因此解决过拟合问题是神经网络泛化性能的重要保证。
常用的解决过拟合的方法有经典的early stopping、正则化以及神奇的dropout。
2.1、early stopping
首先是early stopping,早停法不仅适用于神经网络的学习,也适用于很多经典的机器学习算法,它的思路就是训练到训练集上的误差仍在下降但验证集上的误差开始回升的点就停止训练。
2.2、regulation
正则化也是很经典的方法,正则化分为很多种,常用的有和正则化。
说到正则化,这里补充一个点,也是之前没有认真考虑过的,那就是在正则化中通常是不考虑偏置的,这是因为正则化是为了让学得的函数尽可能“平滑”(smooth),而偏置只是将函数上下平移,并不影响函数的平滑程度。
在神经网络的损失函数中加入正则项并使用梯度下降法可以得到上图结果,可以看出,相比不加正则项,梯度下降法只是在每次运行过程中先进行了一次“权重衰减”,即给当前训练的参数乘上一个略小于1的数值。于是算法倾向于得到各个权重都不太大的一个平滑函数。
下面我们看一下正则化:
给当前训练的参数乘上一个略小于1的数值,而是给当前训练参数改变一个固定的较小的值,当参数为正,减去,当参数为负,加上。
考虑某个很大的,比如100000,在中这个参数会在训练过程中乘上一些系数而变得很小;但在中这个参数每次只是减去一个,训练结束后可能依然很大。
而对于某个很小的,比如0.1,在中这个参数会在训练过程中乘上一些系数,但变化量非常小,因此一般不会到接近0的位置;但在中这个参数每次只是减去一个,训练结束后可能非常接近0。
综上所示,倾向于得到更加稀疏的权重矩阵,而更倾向于得到各个权重较为平均的权重矩阵。
实际应用中我们可以根据需要选择不同的正则化方法,但常常比较受欢迎。
上图是人在成长过程中神经元连接的变化情况,可以看到和我们网络的训练过程很像,而之后的神经元连接变少的过程则很像使用使权重矩阵变稀疏的过程。
2.3、Dropout
最后讨论一下神奇的Dropout方法。
Dropout的思路很简单,就是在训练过程中每一层都drop一些神经元,具体实现过程是让每个神经元都有的概率被drop。
Dropout后的网络架构可能有很多种,说到这里,感觉和之前的ReLU以及Maxout干的事情差不多啊,都是让一些神经元消失掉,然后形成不同的网络架构。每次只训练当前网络架构中出现的权重。
值得注意的是,当我们训练完毕进行test的时候,要把所有的权重值都乘上概率,这样做的原因直观上理解就是我们训练这些权重的时候各层的神经元个数较少,因此我们倾向于训练出较大的权重,因此最后应用的时候要乘上一个。
道理我都懂,算法流程也很简单,但是为什么这个东西管用呢?
直观上说,我们每次用一个比较简单的网络(总网络的一个子集)进行训练,这就相当于把在这部分权重训练到最好,让它们有自己上战场也能顶得住的觉悟,每部分权重都这样训练,最后效果自然会强大一些。
理论上来说,可以把Dropout视为一种很多网络架构训练结果的ensemble,因此相比单一结构的神经网络表现会有所提升。