收藏ShowMeAI查看更多精彩内容
本系列为吴恩达老师《深度学习专业课程》学习与总结整理所得,对应的课程视频可以在这里查看。
在ShowMeAI前一篇文章 深层神经网络中我们对以下内容进行了介绍:
本篇内容对应吴恩达老师深度学习系列第2门课《Improving Deep Neural Networks: Hyperparameter tuning, Regularization and Optimization》。在第2门课中,吴恩达老师展开讨论和讲解了如何优化神经网络模型,例如调整超参数,提高算法运行速度等等。
实际应用深度学习是一个迭代过程。
在构建一个神经网络的时候,我们需要设置许多超参数,例如神经网络的层数(#Layers)、每个隐藏层包含的神经元个数(#Hidden Units)、学习速率(,Learning Rates)、激活函数(Activation Functions)的选择等。实际上很难在第一次设置的时候就选择到这些最佳的超参数,而是需要通过不断地迭代更新来获得。
循环迭代过程是如下这样的:
上述迭代过程中,决定整个训练过程快慢的关键在于单次循环所花费的时间,单次循环越快,训练过程越快。而设置合适的训练集(Training sets)、验证集(Development sets)、测试集(Test sets)大小,能有效提高训练效率。上述数据部分来源于建立模型的过程中,我们对于总体数据的划分:
在小数据量的时代,如100、1000、10000的数据量大小,可以将数据集按照以下比例进行划分:
而在如今的大数据时代,对于一个问题,我们拥有的数据集的规模可能是百万级别的,所以验证集和测试集所占的比重会趋向于变得更小。
验证集的目的是为了验证不同的算法哪种更加有效,所以验证集只要足够大到能够验证大约2-10种算法哪种更好,而不需要使用20%的数据作为验证集。如百万数据中抽取1万的数据作为验证集就可以了。
测试集的主要目的是评估模型的效果,如在单个分类器中,往往在百万级别的数据中,我们选择其中10000条数据也足以评估单个模型的效果。
我们针对不同量级的大数据场景,可以采用如下的训练集(Training sets)、验证集(Development sets)、测试集(Test sets)数据划分方式:
建议验证集要和训练集来自于同一个分布(数据来源一致),可以使得机器学习算法变得更快并获得更好的效果。
假设你开发一个手机app,可以让用户上传图片,然后app识别出猫的图片。在app识别算法中,你的训练样本可能来自网络下载,而你的验证和测试样本可能来自不同用户的上传。从网络下载的图片一般像素较高而且比较正规,而用户上传的图片往往像素不稳定,且图片质量不一。这种情况下验证集和测试集的作用就受影响了。
如果不需要用无偏估计来评估模型的性能,则可以不需要测试集。
Test sets测试集的目标主要是进行无偏估计。我们可以通过Train sets训练不同的算法模型,然后分别在Dev sets上进行验证,根据结果选择最好的算法模型。这样也是可以的,不需要再进行无偏估计了。如果只有Train sets和Dev sets,通常也有人把这里的Dev sets称为Test sets,我们要注意加以区别。
偏差(Bias)和方差(Variance)是机器学习领域非常重要的两个概念和需要解决的问题。在传统的机器学习算法中,Bias和Variance是对立的,分别对应着欠拟合和过拟合,我们常常需要在Bias和Variance之间进行权衡。而在深度学习中,我们可以同时减小Bias和Variance,构建最佳神经网络模型。
我们先来梳理一下上面提到的概念:
如图是二维平面上二分类问题对应的几种模型状态(High Bias,Just Right,High Variance)示例图。
其中,High Bias对应着欠拟合,而High Variance对应着过拟合。在欠拟合(underfitting)的情况下,出现高偏差(High Bias)的情况,即不能很好地对数据进行分类。
这个例子中输入特征是二维的,High Bias和High Variance可以直接从图中分类线看出来。而对于输入特征是高维的情况,如何来判断是否出现了High Bias或者High Variance呢?
这就要特别借助于上一节我们提到几个数据集的评估来完成了(关于模型的评估也可以参考ShowMeAI文章 图解机器学习 | 模型评估方法与准则)
一般般来说,训练集错误率体现了是否出现Bias(偏差),验证集(和训练集差异)错误率体现了是否出现Variance(方差)。当训练出一个模型以后:
神经网络模型甚至可能出现High Bias and High Variance的糟糕状态,如下图所示:
模型可能处于上述提到的不同状态中,在我们对模型状态评估完毕之后,针对不同的状态,优化方式如下:
在深度学习的早期阶段,没有太多方法能做到只减少偏差或方差而不影响到另外一方。而在大数据时代,深度学习对监督式学习大有裨益,使得我们不用像以前一样太过关注如何平衡偏差和方差的权衡问题,通过以上方法可以在不增加某一方的前提下减少另一方的值。
如果模型出现了过拟合(High Variance)状态,可以通过正则化Regularization来缓解解决。虽然扩大训练样本数量也是减小High Variance的一种方法,但是通常获得更多训练样本的成本太高,比较困难。所以,更可行有效的办法就是使用正则。
我们先回顾一下之前介绍过的逻辑回归模型,我们在Cost Function里添加了L2 Regularization(详见ShowMeAI文章 图解机器学习 | 逻辑回归算法详解),表达式如下:
\( J(w,b)=\frac1m \sum_{i=1}^m L(\hat y^{(i)},y^{(i)})+\frac{\lambda}{2m}||w||_2^2 \)
为什么只对\(w\)进行正则化而不对\(b\)进行正则化呢?其实也可以对\(b\)进行正则化。但是一般\(w\)的维度很大,而\(b\)只是一个常数。相比较来说,参数很大程度上由\(w\)决定,改变\(b\)值对整体模型影响较小。所以,一般为了简便,就忽略对\(b\)的正则化了。
除了L2正则化之外,我们在逻辑回归中也可以添加L1正则化。表达式如下:
\[J(w,b)=\frac1m\sum_{i=1}^mL(\hat y^{(i)},y^{(i)})+\frac{\lambda}{2m}||w||_1 \]与L2正则化相比,L1正则化更容易得到稀疏的\(w\)解,即最后训练得到的权重\(w\)中有很多为零值。L1正则化优点是节约存储空间(因为大部分\(w\)为0)。但实际上L1正则化在解决过拟合问题上并不优于L2正则化,且L1的在微分求导方面比较复杂。所以一般更常用的还是L2正则化。
L1、L2正则化中的\(\lambda\)就是正则化参数(超参数的一种)。可以设置\(\lambda\)为不同的值,在验证集Dev set中进行验证,选择最佳的\(\lambda\)。
在深度学习模型中,L2正则化的表达式如图所示:
通常,我们把\(||w^{[l]}||^2\)称为弗罗贝尼乌斯范数(Frobenius Norm),记为\(||w^{[l]}||_F^2\)。
由于在Cost Function中加入了正则化项,梯度下降算法中的\(dw^{[l]}\)计算表达式需要做如下修改:
\[dw^{[l]}=dw^{[l]}_{before}+\frac{\lambda}{m}w^{[l]} \]\[w^{[l]}:=w^{[l]}-\alpha\cdot dw^{[l]} \]大家有时候也会听到L2正则化被称做weight decay。这是因为,由于加上了正则项,\(dw^{[l]}\)有个增量,在更新\(w^{[l]}\)的时候,会多减去这个增量,使得\(w^{[l]}\)比没有正则项的值要小一些。不断迭代更新,不断地减小。
\[\begin{aligned} w^{[l]} :& = w^{[l]}-\alpha\cdot dw^{[l]}\\ & = w^{[l]}-\alpha\cdot(dw^{[l]}_{before}+\frac{\lambda}{m}w^{[l]})\\ & = (1-\alpha\frac{\lambda}{m})w^{[l]}-\alpha\cdot dw^{[l]}_{before} \end{aligned} \]其中,\((1-\alpha\frac{\lambda}{m})<1\)。
我们回到上面模型状态的那张图,从左到右,分别表示了欠拟合、刚好拟合、过拟合三种情况。选择图中的复杂神经网络模型,那么不添加正则化的情况下,我们可能得到图中的过拟合分类边界。
如果使用\(L2正则化,当\)\lambda\(很大时,\)w{[l]}\approx0$。$w{[l]}$近似为零,意味着该神经网络模型中的某些神经元实际的作用很小,可以忽略。从效果上来看,其实是将某些神经元给忽略掉了。这样原本过于复杂的神经网络模型就变得不那么复杂了,而变得非常简单化了。
如图所示,整个简化的神经网络模型变成了一个逻辑回归模型。问题就从High Variance变成了High Bias了。
因此,总结一下,直观的一种理解是:正则化因子设置的足够大的情况下,为了使成本函数最小化,权重矩阵\(W\)就会被设置为接近于0的值,直观上相当于消除了很多神经元的影响,那么大的神经网络就会变成一个较小的网络。当然,实际上隐藏层的神经元依然存在,但是其影响减弱了,过拟合可能性大大减小。
假设神经元中使用的激活函数为\(g(z) = tanh(z)\)(sigmoid同理)。(关于激活函数的知识回顾可以查看ShowMeAI文章 浅层神经网络 )
在加入正则化项后,当\(\lambda\)增大,导致\(W^{[l]}\)减小,\(Z^{[l]} = W^{[l]}a^{[l-1]} + b^{[l]}\)便会减小。通过上图我们会发现,在\(z\)较小(接近于0)的区域里,\(tanh(z)\)函数近似线性,所以每层的函数就近似线性函数,整个网络就成为一个简单的近似线性的网络,因此不会发生过拟合。
在权值\(w^{[L]}\)变小之下,输入样本\(X\)随机的变化不会对神经网络模造成过大的影响,神经网络受局部噪音的影响的可能性变小。这就是正则化能够降低模型方差的原因。
在神经网络中,另外一种很有效的正则化方式叫做Dropout(随机失活),它是指在神经网络的隐藏层为每个神经元结点设置一个随机关闭的概率,保留下来的神经元形成一个结点较少、规模较小的网络用于训练。网络模型得到简化,从而避免发生过拟合。
Dropout有不同的实现方法,一种常用的方法是Inverted Dropout。假设对于第l层神经元,设定保留神经元比例概率keep_prob=0.8,即该层有20%的神经元停止工作。\(dl\)为Dropout向量,设置\(dl\)为随机vector,其中80%的元素为1,20%的元素为0。
Dropout vector的生成python代码如下所示:
keep_prob = 0.8 # 设置神经元保留概率 dl = np.random.rand(al.shape[0], al.shape[1]) < keep_prob al = np.multiply(al, dl) al /= keep_prob
al /= keep_prob
是因为\(a^{[l]}\)中的一部分元素失活(相当于被归零),为了在下一层计算时不影响\(Z^{[l+1]} = W^{[l+1]}a^{[l]} + b^{[l+1]}\)的期望值,因此除以一个keep_prob
。Inverted Dropout的另外一个好处就是在对该Dropout后的神经网络进行测试时能够减少scaling问题。因为在训练时,使用scale up保证al的期望值没有大的变化,测试时就不需要再对样本数据进行类似的尺度伸缩操作了。
注意:使用Dropout训练结束后,在测试和实际应用模型时,不需要进行Dropout和随机删减神经元,所有的神经元都在工作。
Dropout通过每次迭代训练时,随机选择不同的神经元,相当于每次都在不同的神经网络上进行训练,类似机器学习中Bagging的方法(详细思想可以阅读ShowMeAI文章 图解机器学习 | 随机森林分类模型详解),能够防止过拟合。
第2个理解的视角是Dropout会减小权重\(w\)的值。
对于某个神经元来说,某次训练时,它的某些输入在Dropout的作用被过滤了。而在下一次训练时,又有不同的某些输入被过滤。经过多次训练后,某些输入被过滤,某些输入被保留。这样,神经元不会再特别依赖于任何一个输入特征。也就是说,对应的权重\(w\)不会很大。这从效果上来说,与L2 正则化是类似的,都是对权重\(w\)进行「惩罚」,减小了\(w\)的值。
因此,通过传播过程,Dropout将产生和\(L2\)正则化相同的收缩权重的效果。
一般来说,神经元多的隐藏层,keep_prob
可以设置得小一些,例如0.5;神经元越少的隐藏层,keep_out
可以设置的大一些,例如0.8,设置是1。
实际应用中,不建议对输入层进行Dropout,如果输入层维度很大,例如图片,那么可以设置Dropout,但keep_prob
应设置的大一些,例如0.8,0.9。
总体来说,就是越容易出现overfitting的隐藏层,其keep_prob
就设置的相对小一些。没有准确固定的做法,通常可以根据validation进行选择。
注意:Dropout的一大缺点是成本函数无法被明确定义。因为每次迭代都会随机消除一些神经元结点的影响,因此无法确保成本函数单调递减。因此,使用Dropout时,先将keep_prob
全部设置为1.0后运行代码,确保\(J(w, b)\)函数单调递减,再打开Dropout。
数据扩增(Data Augmentation)是深度学习中常见和有效的技巧,特别的,在计算机视觉领域,它指的通过图片的一些变换(翻转,局部放大后切割等),得到更多的训练集和验证集。如下图所示:
因为深度学习的训练过程是一个不断迭代优化训练集cost function的过程,但是迭代次数过多会导致模型过度拟合训练集而对其他数据泛化能力变弱。一个处理方法是使用早停止法(Early Stopping)。
在早停止法(Early Stopping)中,我们会把训练集和验证集进行梯度下降时的成本变化曲线画在同一个坐标轴内。当训练集误差降低但验证集误差升高,两者开始发生较大偏差时及时停止迭代,并返回具有最小验证集误差的连接权和阈值,以避免过拟合。
Early Stopping也有其自身缺点。
回顾我们应用机器学习训练模型有两个目标:① 优化Cost Function,尽量减小\(J\);② 防止过拟合,希望在新数据上有好的泛化能力。这两个目标彼此对立的,即减小\(J\)的同时可能会造成过拟合,反之亦然。
前面提到过,在深度学习中,神经网络可以同时减小Bias和Variance,构建最佳模型。但是,Early Stopping的做法通过减少得带训练次数来防止过拟合,这样\(J\)就不会足够小。也就是说,Early Stopping将上述两个目标融合在一起,同时优化,但可能没有「分而治之」的效果好。
对比Early Stopping,L2正则化可以实现「分而治之」的效果:迭代训练足够多,减小\(J\),而且也能有效防止过拟合。而L2正则化的缺点之一是最优的正则化参数\(\lambda\)的选择比较复杂,这点上Early Stopping比较简单。
总体上\(L2\)正则化更加常用一些。
在训练神经网络时,对输入标准化可以提高训练的速度。标准化就是对训练数据集进行归一化的操作,即将原始数据减去其均值\(\mu\)后,再除以其方差\(\sigma^2\):
\[\mu=\frac1m\sum_{i=1}^mX^{(i)} \]\[\sigma^2=\frac1m\sum_{i=1}^m(X^{(i)})^2 \]\[X:=\frac{X-\mu}{\sigma^2} \]下图展示二维数据的归一化过程及其分布变化:
注意:实际建模应用时,对于测试集,应该使用训练集同样的\(\mu\)和\(\sigma^2\)对其进行标准化处理。这样保证了训练集和测试集的标准化操作一致。
标准化输入可以让所有输入调整到同样的尺度scale上,方便进行梯度下降算法时能够更快更准确地找到全局最优解。
以二维数据为例,如果输入数据有\(x_1\)和\(x_2\)两个维度,\(x_1\)的范围是\([1,1000]\),\(x_2\)的范围是\([0,1]\)。
不进行标准化处理的情况下,\(x_1\)与\(x_2\)之间分布极不平衡,训练得到的\(w_1\)和\(w_2\)也会在数量级上差别很大。这种情形下的Cost Function与\(w\)和\(b\)的关系可能是一个非常细长的椭圆形碗,如图左所示。
对这种Cost Function进行梯度下降优化时,由于\(w_1\)和\(w_2\)数值差异很大,只能选择很小的学习因子\(\alpha\),来避免\(J\)发生振荡。一旦\(\alpha\)较大,必然发生振荡,\(J\)不再单调下降。
如果进行了标准化操作,\(x_1\)与\(x_2\)分布均匀,\(w_1\)和\(w_2\)数值差别不大,得到的Cost Function与\(w\)和\(b\)的关系是类似圆形碗,如右图所示。对其进行梯度下降优化时,\(\alpha\)可以选择相对大一些,且\(J\)一般不会发生振荡,保证了\(J\)是单调下降的。
如果输入特征之间的范围本来就比较接近,那么不进行标准化操作也是没有太大影响的。但是,标准化处理在大多数场合下还是值得做的。
在深度神经网络里,我们在计算损失函数梯度时,有时会出现以指数级递增或者递减的情况,它们分别对应神经网络的梯度爆炸和梯度消失问题。
举个例子来说明,假设一个多层的每层只包含两个神经元的深度神经网络模型,如下图所示:
为了简化复杂度,便于分析,我们令各层的激活函数为线性函数,且忽略各层常数项\(b\)的影响,即假定\(g(z) = z\),\(b^{[l]} = 0\),对于目标输出\(\hat{y}\)有:
这种叠乘会带来下面2种情况:
计算梯度是一个类似的过程,根据求导链式法则,也会有叠乘情况出现,梯度函数会以指数级递增或递减,导致训练导数难度上升,梯度下降算法的步长会变得非常小,需要训练的时间将会非常长。
那么怎么改善梯度消失和爆炸问题呢?一种方法是对权重\(w\)进行一些初始化处理。(其他的一些方法如ShowMeAI文章 经典CNN网络实例详解 中提到的ResNet等网络结构调整)
深度神经网络模型中,以单个神经元为例,其输出计算为 \(\hat{y}\):
为了让\(\hat{y}\)不会过大或者过小,思路是让\(w\)与\(n\)有关,且\(n\)越大,\(w\)应该越小才好。一种方法是在初始化w时,令其方差为\(1/n\),这里称为Xavier initialization。
# 针对tanh激活函数的Xavier初始化 WL = np.random.randn(WL.shape[0], WL.shape[1]) * np.sqrt(1/n)
其中\(n\)是输入的神经元个数,即WL.shape[1]
。
这样,激活函数的输入\(x\)近似设置成均值为0,标准方差为1,神经元输出\(z\)的方差就正则化到1了。虽然没有解决梯度消失和爆炸的问题,但其在一定程度上确实减缓了梯度消失和爆炸的速度。
如果使用的是ReLU激活函数,权重\(w\)的初始化一般令其方差为\(2/n\)对应python代码如下:
w[l] = np.random.randn(n[l],n[l-1])*np.sqrt(2/n[l-1])
我们知道梯度下降法会大程度依赖梯度来完成,在数学上,我们可以基于微分的定义,使用极限的计算去逼近导数,我们有如下的「单边误差法」和「双边误差法」,其中后者精度要高一些。
当\(\varepsilon\)越小时,结果越接近真实的导数,也就是梯度值。可以使用这种方法来判断反向传播进行梯度下降时,是否出现了错误。
当我们计算出数值梯度后,要进行梯度检查,来验证训练过程中是否有问题。
将\(W^{[1]}\),\(b^{[1]}\),…,\(W^{[L]}\),\(b^{[L]}\)全部连接出来,成为一个巨型向量\(\theta\)。
同时,对\(dW^{[1]}\),\(db^{[1]}\),…,\(dW^{[L]}\),\(db^{[L]}\)执行同样的操作得到巨型向量\(d\theta\),它和\(\theta\)有同样的维度。
现在,我们需要找到\(d\theta\)和代价函数\(J\)的梯度的关系。
求得一个梯度逼近值\(d\theta_{approx}[i]\),应该\(\approx d\theta[i] = \frac{\partial J}{\partial \theta_i}\)。
因此,我们用梯度检验值检验反向传播的实施是否正确。其中,\({||x||}_2\)表示向量\(x\)的2-范数(也称「欧几里德范数」)。
如果梯度检验值和$\varepsilon $的值相近,说明神经网络的实施是正确的,否则要去检查代码是否存在bug。
在进行梯度检查的过程中有几点需要注意的地方: