本教程的作者为 neuronme 的 CTO 兼首席深度学习工程师 Rishabh Shukla,从非数学的角度介绍了深度神经网络的训练方法,并提供了一些实用的建议。
GitHub 地址:http://rishy.github.io//ml/2017/01/05/how-to-train-your-dnn/
深度学习中,为了有效地训练深度神经网络,有一些值得我们强烈推荐的做法。在本文中,我将介绍一些最常用的方法,从高质量训练数据的重要性,到超参数的选择,再到更快的做出 DNN 原型的通用技巧。这些方法的大多数由学术界和工业界的研究验证过,并且在诸如 Yann LeCun 等人写的《Efficient BackProp》和 Yoshua Bengio 的《Practical Recommendations for Deep Architectures》中也给出了数学和实验证明。
你会注意到,我没有在这篇文章中提到任何数学证明。在这里建议的所有要点,更多地应该被看作是 DNN 的最佳训练方法中获得的总结。为了更深入地了解,我强烈建议您最后看一遍上面提到的研究论文和后面提到的参考文献。
训练数据
许多 机器学习从业者习惯于在任何深度神经网络(DNN)中使用原始(raw)训练数据。为什么不这样做呢,任何 DNN(大概)仍会给出好的结果,对吧?但是,说出「给出正确的数据类型,一个十分简单的模型会比一个复杂的 DNN 更快地给出更好的结果」(虽然,这可能会有例外)并不是很守旧派的。因此,无论你是在做计算机视觉,还是在做自然语言处理或统计建模等,你都应该尝试预处理您的原始数据。你可以采取以下几个方法来获得更好的训练数据:
* 尽可能大的数据集(DNN 对数据的需求是相当大的:越多越好)
* 删除所有具有损坏数据的训练样本(短文本、高失真图像、虚假输出标签、具有大量空值的特征等)
* 数据增强——创建新样本(如果是图像,可以重新缩放、添加噪声等)
选择适当的激活函数
激活函数在任何神经网络中都是重要组成部分之一。激活将大家非常期望的非线性效果引入到了模型中。多年来,sigmoid 激活函数一直是最好的选择。但是 sigmoid 函数本质上有两个缺陷:
1.sigmoid 尾部的饱和(会进一步导致梯度消失问题)
2.sigmoid 不是以 0 为中心的。
一个更好的选择是 tanh 函数——在数学上,tanh 只是一个重新缩放和移位的 sigmoid,tanh(x) = 2*sigmoid(x) - 1。虽然 tanh 仍然可能遭受梯度消失问题,但好消息是 tanh 是以零为中心的。因此,使用 tanh 为激活函数可以更快地收敛。我使用中也发现使用 tanh 作为激活函数通常比使用 sigmoid 函数好。
你可以根据具体任务进一步探索其它选择,如已经表现出可以改善一些问题的 ReLU,SoftSign 等函数。
隐含单元和隐含层的数量
使用比最佳隐含单元数更多的数量通常是安全的。因为,任何正则化方法在一定程度上都可以处理多余的单元。而另一方面,使用比最佳隐含单元数更少的数量时,发生欠拟合的概率更高一些。
此外,当采用无监督学习预训练的表示(pre-trained representations,在后面部分中描述)时,隐含单元数的最佳数量通常要更大一些。因为,在各种表示中(对于特定的监督任务),预训练表示可能会包含大量的无关信息。通过增加隐含单元的数量,模型将具有足够支持从预训练表示中过滤出最合适的信息的灵活性。
选择最佳隐含层数是相对简单的。正如 Yoshua Bengio 在 Quora 上提到的:「你只需要继续添加层,直到测试错误不再改善为止」。
权重初始化
始终使用小随机数(random numbers)初始化权重,以打破不同单元之间的对称性。但是权重应该多小呢?推荐的上限是多少?使用什么概率分布来生成随机数?此外,当使用 sigmoid 激活函数时,如果权重被初始化为非常大的数,则 sigmoid 函数将会饱和(尾部区域),导致死亡神经元(dead neurons)。如果权重非常小,则梯度也会很小。因此,最好在中间范围选择权重,并且使它们围绕平均值均匀分布。
幸运的是,目前已经有很多关于初始权重的适当值的研究,这对于有效的收敛是非常重要的。为了初始化得到均匀分布的权重,uniform distribution 可能是最好的选择之一。此外,如论文(Glorot and Bengio, 2010)所示,具有更多传入连接(fan_in)的单元应具有相对较小的权重。
由于所有这些深入的实验,现在我们有一个测量公式,可以直接用于权重初始化;例如从~ Uniform(-r, r) 范围获得权重,对于 tanh 作为激活函数的时候,r=sqrt(6/(fan_in+fan_out));而对于 sigmoid 作为激活函数的时候,r=4*(sqrt(6/fan_in+fan_out)),其中 fan_in 是上一层的大小,fan_out 是下一层的大小。
训练速率
这可能是最重要的超参数之一,决定了整个学习过程。如果设置的学习速率太小,你的模型可能需要几年才能收敛;如果学习速率太大,在开始训练几个样本之后,你的损失值(loss)可能会迅速增加。一般来说,0.01 的学习速率是安全的,但这不应被视为一个严格的规则;因为最佳学习速率应该根据具体任务来调整。
相比之下,在每个 epoch 之后选择固定的学习率(learning rate)或者逐渐降低学习率(learning rate)是另一个选择。虽然这可能有助于训练得更快,但需要人工确定新的学习率。一般来说,学习率可以在每个 epoch 后减半——这几类策略在几年前相当普遍。
幸运的是,现在我们有更好的基于动量(momentum based methods)方法来改变学习率,就是基于误差函数的曲率。这种方法也可以帮助我们为模型中的各个参数设置不同的学习率;使得一些参数可能以相对较慢或较快的速率学习。
近期大量针对优化方法的研究也产生了自适应学习率(adaptive learning rate)方法。现在,我们有很多可选择的方法,从老牌的动量方法(Momentum Method)到 Adagrad、Adam(我最喜欢的方法)、RMSProp 等。像 Adagrad 或 Adam 这样的方法有效地避免了手动选择初始学习速率,并且模型能在一定的时间内顺利地收敛(当然,如果选择好的初始速率会进一步帮助模型收敛)。
超参数微调:旋转的网格搜索——拥抱随机搜索
网格搜索(Grid Search)是经典的机器学习方法。但是,在寻找 DNN 的最佳超参数时,网格搜索并不高效。主要是因为尝试不同的 DNN 超参数组合所花费的时间太长。随着超参数的数量不断增加,网格搜索所需的计算也呈指数增长。
这里有两种方法:
1. 根据经验手动调整一些常用的超参数,如学习率、层数(number of layer)等。
2. 使用随机搜索/随机抽样(Random Search/Random Sampling)来选择最优超参数。超参数的组合通常从可行域的均匀分布中选择。还可以加入先验知识以进一步减少搜索空间(例如学习速率不应该太大或太小)。经证明,随机搜索比网格搜索更有效率。
学习方法
老牌的随机梯度下降(Stochastic Gradient Descent)可能不像 DNN 一样有效(这不是一个严格的规则),因此最近很多人正研究开发更灵活的优化算法。例如:Adagrad、Adam、AdaDelta、RMSProp 等。这些复杂的方法除了提供自适应学习率之外,还对不同的模型参数应用不同的速率,使得收敛曲线更加平滑。将学习率、层数等作为超参数是很好的,建议在训练数据的子集上进行尝试。
使权重的维度以 2 的指数幂级增加
即使用最新的硬件资源处理最新的深度学习模型时,内存管理仍然是以字节衡量的;所以,尽量将参数的大小设置为 2 的幂指数,如 64、128、512、1024。这可能有助于分割矩阵、权重等,从而略微提高学习效率。这个现象在使用 GPU 时变得更加显著。
无监督预训练
无论你是否用 NLP、机器视觉(Computer Vision)、语音识别等技术,无监督预训练总能帮助有监督模型和无监督模型的训练。词向量(Word Vector)在 NLP 中无处不在;对于有监督二分类问题,你可以使用 ImageNet 的数据集以无监督的方式预训练模型;或者对于说话者消歧模型(speaker disambiguation model),可以进一步利用语音样本的信息训练模型。
Mini-Batch vs. 随机学习
训练模型的主要目的是学习得到适当的参数,从而产生从输入到输出的最佳映射(mapping)。不管是否使用批处理(batch)、Mini-Batch 或随机学习(stochastic learning),这些参数都会因训练样本的不同而有所调整。在使用随机学习方法时,在每个训练样本之后调整权重梯度,因此将噪声引入梯度。这具有非常理想的效果;即在训练期间引入噪声,模型变得不太容易过度拟合。
然而,现在计算机的计算能力大大提高,使得随机学习方法的效率可能相对较低。随机学习可能浪费了很大一部分资源。如果我们能够计算矩阵与矩阵的乘法(Matrix-Matrix multiplication),那么为什么要局限于使用迭代相加的向量与向量的乘法?因此,为了实现更大的吞吐量(throughput)/更快的学习,建议使用 Mini-Batch 而不是随机学习。
但是,选择适当的批处理大小也具有相同的重要性;因此我们也可以保留一些噪音(通过减少批的数据量实现),并有效利用计算机的计算能力。同样的,一个批的样本量控制在 16 到 128 之间也是一个不错的选择(2 的指数幂)。通常,一旦你找到更重要的超参数(或手动或随机搜索),批处理的大小是选定的。然而,有些情况下(如在线学习/online learning),模型的训练数据是数据流(stream),这时求助于随机学习是一个很好的选择。
重排训练样本
信息论中有这样的话——「学习发生的极小概率事件比学习发生的大概率事件更有信息」。类似地,随机排列训练样本的顺序(不同的迭代或批处理中)将导致更快的收敛。当训练样本的顺序不同时,模型的结果会有轻微的提升。
作为正则化的 dropout
考虑到要学习数百万的参数,正则化成为防止 DNN 过度拟合的必然要求。你也可以继续使用 L1/L2 正则化,但是 dropout 是更好的检查 DNN 过拟合的方法。dropout 在实现方法上比较琐碎,它通常能更快地学习模型。默认值 0.5 是一个好的选择,但是这也取决于具体的任务。如果模型较不复杂,则 0.2 的 dropout 也可能就足够了。
在测试阶段,应该相应地标准化权重,同时暂缓使用舍弃方法,如论文《Dropout: A Simple Way to Prevent Neural Networks from Overfitting》中所做的那样。赋予模型舍弃正则化方法,经过一定的训练时间错误肯定会下降。
训练的迭代数
「用多个迭代(epoch)来训练深度学习模型将产生更好的结果」——但是我们如何量化「多少」?原来,有一个简单的策略 - 只要对训练模型设置固定数量的训练样本或迭代,比如 20,000 个样本或 1 个迭代。在每组训练样本训练完成后,将测试误差与训练误差进行比较,如果它们之间的差距正在减小,则继续训练。除此之外,在每个这样的集合之后,保存这组数据训练模型的参数(以便从多个模型中选择)。
可视化
有一千种可能使得训练的深度学习模型出错。我们都有过这样的经历,当模型训练了几个小时或几天后,在训练完成后我们才意识到模型存在一些问题。为了避免这种问题——总是可视化训练过程。最明显的步骤是打印/保存损失(loss)函数值的日志、训练误差或测试误差等。
除此之外,另一个好的做法是在训练一定样本后或在 epoch 之间使用可视化库绘制权重的直方图。这可能有助于跟踪深度学习模型中的一些常见问题,例如梯度消失(Vanishing Gradient)、梯度爆炸(Exploding Gradient)等。
多核计算机,GPU 集群
随着 GPU、提供向量化操作的库(library)、具有更强计算能力的计算机的出现,这些可能是深度学习成功的最重要的因素。如果你足够耐心,你可以尝试在你的笔记本电脑上运行 DNN(这时你甚至不能打开 10 个 Chrome 浏览器标签),并需要等待很长时间才能得到结果。要么你有非常好的硬件(很昂贵)与至少多个 CPU 核和几百个 GPU 核。GPU 已经彻底改变了深度学习研究(难怪 Nvidia 的股票井喷式涨价),主要是因为 GPU 能够更大规模地执行矩阵操作。
因此,以前在正常的机器上花几个星期的训练,由于并行(parallelization)技术,将训练的时间减少到几天甚至几个小时。
使用具有 GPU 计算和自动微分支持的库
幸运的是,对于快速训练,我们有一些很好的库,如 Theano、Tensorflow、Keras 等。几乎所有这些深度学习库提供对 GPU 计算和自动微分(Automatic Differentiation)的支持。所以,你不必深入了解 GPU 的核心编程(除非你想,绝对有趣);你也不必编写自己的微分代码,这在真正复杂的模型中可能会有点费力(虽然你应该能够做到这一点)。TensorFlow 能进一步支持在分布式架构上训练模型。
以上并不是训练 DNN 的详尽列表。它只包括了最常见的做法,上面已经去掉了如数据标准化、批规范化/层规范化、梯度检查(Gradient Check)等概念。
原文 http://www.jiqizhixin.com/article/2293