[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning
第一周:深度学习的实用层面(Practical aspects of Deep Learning)
训练,验证,测试集(Train / Dev / Test sets)
本周,我们将继续学习如何有效运作神经网络,内容涉及超参数调优,如何构建数据,以及如何确保优化算法快速运行,从而使学习算法在合理时间内完成自我学习。第一周,我们首先说说神经网络机器学习中的问题,然后是随机失活神经网络,还会学习一些确保神经网络正确运行的技巧,带着这些问题,我们开始今天的课程。
在配置训练、验证和测试数据集的过程中做出正确决策会在很大程度上帮助大家创建高效的神经网络。训练神经网络时,我们需要做出很多决策,例如:
-
神经网络分多少层
-
每层含有多少个隐藏单元
-
学习速率是多少
-
各层采用哪些激活函数
创建新应用的过程中,我们不可能从一开始就准确预测出这些信息和其他超级参数。实际上,应用型机器学习是一个高度迭代的过程,通常在项目启动时,我们会先有一个初步想法,比如构建一个含有特定层数,隐藏单元数量或数据集个数等等的神经网络,然后编码,并尝试运行这些代码,通过运行和测试得到该神经网络或这些配置信息的运行结果,你可能会根据输出结果重新完善自己的想法,改变策略,或者为了找到更好的神经网络不断迭代更新自己的方案。
现如今,深度学习已经在自然语言处理,计算机视觉,语音识别以及结构化数据应用等众多领域取得巨大成功。结构化数据无所不包,从广告到网络搜索。其中网络搜索不仅包括网络搜索引擎,还包括购物网站,从所有根据搜索栏词条传输结果的网站。再到计算机安全,物流,比如判断司机去哪接送货,范围之广,不胜枚举。
我发现,可能有自然语言处理方面的人才想踏足计算机视觉领域,或者经验丰富的语音识别专家想投身广告行业,又或者,有的人想从电脑安全领域跳到物流行业,在我看来,从一个领域或者应用领域得来的直觉经验,通常无法转移到其他应用领域,最佳决策取决于你所拥有的数据量,计算机配置中输入特征的数量,用 GPU 训练还是 CPU,GPU 和 CPU 的具体配置以及其他诸多因素。
目前为止,我觉得,对于很多应用系统,即使是经验丰富的深度学习行家也不太可能一开始就预设出最匹配的超级参数,所以说,应用深度学习是一个典型的迭代过程,需要多次循环往复,才能为应用程序找到一个称心的神经网络,因此循环该过程的效率是决定项目进展速度的一个关键因素,而创建高质量的训练数据集,开发集和测试集也有助于提高循环效率。
假设这是训练数据,我用一个长方形表示,我们通常会将这些数据划分成几部分,一部分作为训练集,一部分作为简单交叉验证集,有时也称之为开发集,方便起见,我就叫它开发集,其实都是同一个概念(cross validation set = development set, for brevity, call this the dev set. but all of these terms mean roughly the same thing.),最后一部分则作为测试集。
接下来,我们开始对训练集执行算法,通过开发集或简单交叉验证集选择最好的模型,经过充分验证,我们选定了最终模型,然后就可以在测试集上进行评估了,为了无偏评估算法的运行状况。
在机器学习发展的小数据量时代,常见做法是将所有数据三七分,就是人们常说的 70% 训练集,30% 测试集。如果明确设置了开发集,也可以按照 60% 训练集,20% 开发集和 20% 测试集来划分。这是前几年机器学习领域普遍认可的最好的实践方法。
如果只有 100 条,1000 条或者 1 万条数据,那么上述比例划分是非常合理的。
但是在大数据时代,我们现在的数据量可能是百万级别,那么开发集和测试集占数据总量的比例会趋向于变得更小。因为开发集的目的就是验证不同的算法,检验哪种算法更有效,因此,开发集只要足够大到能评估不同的算法,比如 2 个甚至 10 个不同算法,并迅速判断出哪种算法更有效。我们可能不需要拿出 20% 的数据作为开发集。
比如我们有 100 万条数据,那么取 1 万条数据便足以进行评估,找出其中表现最好的 1-2 种算法。同样地,根据最终选择的分类器,测试集的主要目的是正确评估分类器的性能,所以,如果拥有百万数据,我们只需要 1000 条数据,便足以评估单个分类器,并且准确评估该分类器的性能。假设我们有 100 万条数据,其中 1 万条作为开发集,1 万条作为测试集,100 万里取 1 万,比例是 1%,即:训练集占 98%,开发集和测试集各占 1%。对于数据量过百万的应用,训练集可以占到 99.5%,验证和测试集各占 0.25%,或者开发集占 0.4%,测试集占 0.1%。
总结一下,在机器学习中,我们通常将样本分成 训练集,开发集 和 测试集 三部分,数据集规模相对较小,适用传统的划分比例,数据集规模较大的,开发集和测试集要小于数据总量的 20% 或 10%。后面我会给出如何划分开发集和测试集的具体指导。
现代深度学习的另一个趋势是越来越多的人在训练和测试集分布不匹配的情况下进行训练,假设你要构建一个用户可以上传大量图片的应用程序,目的是找出并呈现所有猫咪图片,可能你的用户都是爱猫人士,训练集可能是从网上下载的猫咪图片,而开发集和测试集是用户在这个应用上上传的猫的图片,就是说,训练集可能是从网络上抓下来的图片。而开发集和测试集是用户上传的图片。结果许多网页上的猫咪图片分辨率很高,很专业,后期制作精良,而用户上传的照片可能是用手机随意拍摄的,像素低,比较模糊,这两类数据有所不同,针对这种情况,根据经验,我建议大家要确保开发集和测试集的数据来自同一分布,关于这个问题我也会多讲一些。因为你们要用开发集来评估不同的模型,尽可能地优化性能。如果开发集和测试集来自同一个分布就会很好。
但由于深度学习算法需要大量的训练数据,为了获取更大规模的训练数据集,我们可以采用当前流行的各种创意策略,例如,网页抓取,代价就是训练集数据与开发集和测试集数据有可能不是来自同一分布。但只要遵循这个经验法则,你就会发现机器学习算法会变得更快。我会在后面的课程中更加详细地解释这条经验法则。
最后一点,就算没有测试集也不要紧,测试集的目的是对最终所选定的神经网络系统做出无偏估计(Remember the goal of the test set is to give you a unbiased estimate of the performance of you final network that you selected.),如果不需要无偏估计,也可以不设置测试集。所以如果只有开发集,没有测试集,我们要做的就是,在训练集上训练,尝试不同的模型框架,在开发集上评估这些模型,然后迭代并选出适用的模型。因为开发集中已经涵盖测试集数据,其不再提供无偏性能评估。当然,如果你不需要无偏估计,那就再好不过了。
在机器学习中,如果只有一个训练集和一个开发集,而没有独立的测试集,遇到这种情况,训练集还被人们称为训练集,而开发集则被称为测试集,不过在实际应用中,人们只是把测试集当成简单交叉验证集使用,并没有完全实现该术语的功能,因为他们把开发集数据过度拟合到了测试集中。如果某团队跟你说他们只设置了一个训练集和一个测试集,我会很谨慎,心想他们是不是真的有训练开发集,因为他们把开发集数据过度拟合到了测试集中,让这些团队改变叫法,改称其为 "训练开发集",而不是 "训练测试集",可能不太容易。即便我认为 "训练开发集" 在专业用词上更准确。实际上,如果你不需要无偏评估算法性能,那么这样是可以的。
所以说,搭建训练开发集和测试集能够加速神经网络的集成,也可以更有效地衡量算法地偏差和方差,从而帮助我们更高效地选择合适方法来优化算法。
偏差,方差(Bias /Variance)
我注意到,几乎所有机器学习从业人员都期望深刻理解偏差和方差,这两个概念易学难精,即使你自己认为已经理解了偏差和方差的基本概念,却总有一些意想不到的新东西出现。关于深度学习的误差问题,另一个趋势是对偏差和方差的权衡研究甚浅,你可能听说过这两个概念,但深度学习的误差很少权衡二者,我们总是分别考虑偏差和方差,却很少谈及偏差和方差的权衡问题,下面我们来一探究竟。
(下图一)数据集拟合一条直线,可能得到一个逻辑回归拟合,但它并不能很好地拟合该数据,这是高偏差(high bias)的情况,我们称为 "欠拟合"(underfitting)。
(下图三)数据集拟合一个非常复杂的分类器,比如深度神经网络或含有隐藏单元的神经网络,看起来也不是一种很好的拟合方式,分类器方差较高(high variance),数据过度拟合(overfitting)。
在两者之间,可能还有一些像上面中间那幅图那样的,复杂程度适中,数据拟合适度的分类器,这个数据拟合看起来更加合理,我们称之为 "适度拟合"(just right)是介于过度拟合和欠拟合中间的一类。
在这样一个只有 x1 和 x2 两个特征的二维数据集中,我们可以绘制数据,将偏差和方差可视化。在多维空间数据中,绘制数据和可视化分割边界无法实现,但我们可以通过几个指标,来研究偏差和方差。
理解偏差和方差的两个关键数据是训练集误差(Train set error)和开发集误差(Dev set error)。
我们使用猫咪图片分类的例子,左边一张是猫咪图片,右边一张不是。
一般来说,最优误差 也被称为 贝叶斯误差。假设人眼辨别猫的错误率接近 0%,那么最优误差也接近 0%。
这种情况下,假定训练集误差是 1%,而开发集误差是 11%,可以看出训练集误差非常接近最优误差,所以它拟合得非常好,而开发集拟合相对较差,我们可能过度拟合了训练集,像这种情况,我们称之为 "高方差"。
通过查看训练集误差和开发集误差,我们便可以诊断算法是否具有高方差。也就是说衡量训练集和开发集误差就可以得出不同结论。
假设训练集误差是15%,开发集误差是16%,可以看到训练误差远高于最优误差,说明算法并没有在训练集中得到很好训练,如果训练数据的拟合度不高,就是数据欠拟合,就可以说这种算法 "高偏差"。相反,它对于开发集产生的结果却是合理的,开发集中的错误率只比训练集的多了 1%,所以这种算法偏差高,因为它甚至不能很好的拟合训练集。
再举一个例子,训练集误差是 15%,偏差相当高,而且,开发集的评估结果更糟糕,错误率达到 30%,在这种情况下,我会认为这种算法偏差高,因为它在训练集上结果不理想,而且方差也很高,这是方差偏差都很糟糕的情况。
最后一个例子,训练集误差是 0.5%,开发集误差是 1%,用户看到这样的结果会很开心,猫咪分类器只有 1% 的错误率,偏差和方差都很低。
有一点我先在这个简单提一下,具体的留在后面课程里讲,这些分析都是基于假设预测的,假设人眼辨别的错误率接近 0%,一般来说,最优误差也被称为贝叶斯误差,所以,最优误差接近 0%,我就不在这里细讲了,如果最优误差或贝叶斯误差非常高,比如 15%。我们再看看这个分类器(训练误差 15%,验证误差 16%),那么这个时候 15% 的错误率对训练集来说也是非常合理的,偏差不高,方差也非常低。
当所有分类器都不适用时,如何分析偏差和方差呢?比如,图片很模糊,即使是人眼,或者没有系统可以准确无误地识别图片,在这种情况下,最优误差会更高,那么分析过程就要做些改变了,我们暂时先不讨论这些细微差别,重点是通过查看训练集误差,我们可以判断数据拟合情况,至少对于训练数据是这样,可以判断是否有偏差问题,然后查看错误率有多高。当完成训练集训练,开始使用开发集验证时,我们可以判断方差是否过高,从训练集到开发集的这个过程中,我们可以判断方差是否过高。
以上分析的前提都是假设基本误差很小,训练集和开发集数据来自相同分布,如果没有这些假设作为前提,分析过程更加复杂,我们将会在稍后课程里讨论。
刚刚我们讲了高偏差和高方差的情况,大家应该对优质分类器有了一定的认识,偏差和方差都高是什么样子呢?这种情况对于两个衡量标准来说都是非常糟糕的。
我们之前讲过,这样的分类器(图中蓝色直线),会产生高偏差,因为它的数据拟合度低,像这种接近线性的分类器,数据拟合度低。但是如果我们稍微改变一下分类器,我用紫色笔画出,它会过度拟合部分数据,用紫色线画出的分类器具有高偏差和高方差,偏差高是因为它几乎是一条线性分类器,并未拟合数据。
这种二次曲线能够很好地拟合数据(虚线)。
这条曲线中间部分灵活性非常高(紫色线),却过度拟合了这两个样本,这类分类器偏差很高,因为它几乎是线性的。
而采用曲线函数或二次元函数(虚线)会产生高方差,因为它曲线灵活性太高以致拟合了这两个错误样本和中间这些活跃数据。
这看起来有些不自然,从两个维度上看都不太自然,但对于高维数据,有些数据区域偏差高,有些数据区域方差高,所以在高维数据中采用这种分类器看起来就不会那么牵强了。
总结一下,我们讲了如何通过分析在训练集上训练算法产生的误差和开发集上验证算法产生的误差来诊断算法是否存在高偏差和高方差,是否两个值都高,或者两个值都不高,根据算法偏差和方差的具体情况决定接下来你要做的工作,下节课,我会根据算法偏差和方差的高低情况讲解一些机器学习的基本方法,帮助大家更系统地优化算法,我们下节课见。
机器学习基础(Basic Recipe for Machine Learning)
上节课我们讲的是如何通过训练误差和开发集误差判断算法偏差或方差是否偏高,帮助我们更加系统地在机器学习中运用这些方法来优化算法性能。
这是我在训练神经网络时用到的基本方法,初始模型训练完成后,我首先要知道算法的偏差高不高。如果偏差的确很高,甚至无法拟合训练集,那么你要做的就是选择一个新的网络,比如含有更多隐藏层或者隐藏单元的网络,或者花费更多时间来训练网络,或者尝试更先进的优化算法,后面我们会讲到这部分内容。你也可以尝试其他方法,后面我们会看到许多不同的神经网络架构,或许你能找到一个更合适解决此问题的新的网络架构,其中一条就是你必须去尝试,它们可能有用,也可能没用,不过采用规模更大的网络通常都会有所帮助,延长训练时间不一定有用,但也没什么坏处。训练学习算法时,我会不断尝试这些方法,直到解决掉 偏差问题,这是 最低标准,反复尝试,直到可以拟合数据为止,至少能够拟合训练集。一旦偏差降低到可以接受的数值,检查一下方差有没有问题,为了评估方差,我们要查看开发集性能,我们能从一个性能理想的训练集推断出开发集的性能是否也理想,如果方差高,最好的解决办法就是采用更多数据,如果你能做到,会有一定的帮助,但有时候,我们无法获得更多数据,我们也可以尝试通过正则化来减少过拟合,这个我们下节课会讲。有时候我们不得不反复尝试,但是,如果能找到更合适的神经网络框架,有时它可能会一箭双雕,同时减少方差和偏差。如何实现呢?想系统地说出做法很难,总之就是不断重复尝试,直到找到一个低偏差,低方差的框架,这时你就成功了。
有两点需要大家注意:
第一点,高偏差和高方差是两种不同的情况,我们后续要尝试的方法也可能完全不同,我通常会用训练开发集来诊断算法是否存在偏差或方差问题,然后根据结果选择尝试部分方法。举个例子,如果算法存在高偏差问题,准备更多训练数据其实也没什么用处,至少这不是更有效的方法,所以大家要清楚存在的问题是偏差还是方差,还是两者都有问题,明确这一点有助于我们选择出最有效的方法。
第二点,在机器学习的初期阶段,关于所谓的偏差方差权衡的讨论屡见不鲜,原因是我们能尝试的方法有很多。可以增加偏差,减少方差,也可以减少偏差,增加方差,但是在深度学习的早期阶段,我们没有太多工具可以做到只减少偏差或方差却不影响到另一方。但在当前的深度学习和大数据时代,只要持续训练一个 更大的网络,只要准备了 更多数据,那么也并非只有这两种情况。我们假定只要正则适度,那么通常构建一个更大的网络便可以在 不影响方差的同时减少偏差,而采用更多数据通常可以在 不过多影响偏差的同时减少方差。这两步实际要做的工作是:训练网络,选择网络或者准备更多数据。我觉得这就是 深度学习 对监督式学习大有裨益的一个重要原因,也是我们不用太过关注如何平衡偏差和方差的一个重要原因。
从下节课开始,我们将讲解正则化,训练一个更大的网络几乎没有任何负面影响,而训练一个大型神经网络的主要代价也只是计算时间,前提是网络是比较规范化的。
今天我们讲了如何通过组织机器学习来诊断偏差和方差的基本方法,然后选择解决问题的正确操作,希望大家有所了解和认识。我在课上不止一次提到了正则化,它是一种非常实用的减少方差的方法,正则化时会出现偏差方差权衡问题,偏差可能略有增加,如果网络足够大,增幅通常不会太高,我们下节课再细讲,以便大家更好理解如何实现神经网络的正则化。
正则化(Regularization)
深度学习可能存在过拟合问题,如果你怀疑神经网络过度拟合了数据,即存在高方差问题,那么最先想到的方法可能是 正则化,另一个解决高方差的方法就是 准备更多数据,这也是非常可靠的办法,但你可能无法时时准备足够多的训练数据,或者说,获取更多数据的成本很高。正则化有助于避免过度拟合,或者减少网络误差,下面我们就来讲讲正则化的作用原理。
逻辑回归(Logistic Regression)中实现 \(L2\) 正则化
先从最小化成本函数 \(\underset{w,b}{\mathrm{min}} \ J(w,b)\) 开始,参数包含一些训练数据和不同数据中个体预测的损失,\(w \in \mathbb{R}^{n_x}\) 和 \(b \in \mathbb{R}\) 是逻辑回归的两个参数,\(w\) 是一个多维度参数矢量(向量 vector),\(b\) 是一个实数。在逻辑回归函数中加入正则化,只需添加一个超参数 \(\lambda\),也就是正则化参数,一会儿再详细讲。
\(J(w,b)=\frac{1}{m}\sum\limits_{i=1}^{m}\mathcal{L}(\hat{y}^{(i)},y^{(i)})+\frac{\lambda}{2m}{||w||}^2_2\)
\({||w||}_2^2\) 是 \(w\) 的欧几里德范数(2 范数)的平方,等于 \(w_j\)(\(j\) 值从 1 到 \(n_{x}\))平方的和,也可表示为 \(w^{T}w\)。注意:逻辑回归中的 \(w\)(\(n_x \times 1\))是一个列向量,和神经网络中的 \(W\)(\(n^{[l]} \times n^{[l-1]}\))矩阵(matrix)不同,所以神经网络中不能使用 \(w^{T}w\) 来算计。此方法称为 \(L2\) 正则化,因为这里用了欧几里德范数,被称为向量参数 \(w\) 的 \(L2\) 范数。
\(L2\) Regularization = \(\frac{\lambda}{2m}||w||^2_2=\frac{\lambda}{2m}\sum\limits_{j=1}^{n_x} w_j^2=w^{T}w\)
为什么只正则化参数 \(w\)?为什么不再加上参数 \(b\) 呢?你可以这么做,只是我习惯省略不写,因为 \(w\) 通常是一个高维参数矢量,已经可以表达高偏差问题,\(w\) 可能包含有很多参数,我们不可能拟合所有参数,而 \(b\) 只是单个数字,所以 \(w\) 几乎涵盖所有参数,如果加了参数 \(b\),其实也没太大影响,因为 \(b\) 只是众多参数中的一个,所以我通常省略不计,如果你想加上这个参数,完全没问题。
\(L2\) 正则化是最常见的正则化类型,你们可能听说过 \(L1\) 正则化,加的不是 \(L2\) 范数,而是正则项 \(\frac{\lambda}{m}\) 乘以 \(||w||_1\),\(||w||_1\) 也被称为参数 \(w\) 向量的 \(L1\) 范数,无论分母是 \(m\) 还是 \(2m\),它都是一个比例常量。\(L2\) 范数中有个平方,在计算梯度求导的时候会产生一个系数 2,可以和 \(\frac{\lambda}{2m}\) 中的分母 2 约分划掉,如此可以起到一种简化公式、简化程序的编写、简化程序的计算量的效果。而 \(L1\) 范数用没有平方,所以乘上 \(\frac{\lambda}{m}\) 也是比较合适的。
\(L1\) Regularization = \(\frac{\lambda}{m}||w||_1=\frac{\lambda}{m}\sum\limits_{j=1}^{n_x} |w_j|\)
如果用的是 \(L1\) 正则化,\(w\) 最终会是稀疏的,也就是说 \(w\) 向量中会有很多 0,有人说这样有利于压缩模型,因为集合中参数均为 0,存储模型所占用的内存更少。实际上,虽然 \(L1\) 正则化使模型变得稀疏,却没有降低太多存储内存,所以我认为这并不是 \(L1\) 正则化的目的,至少不是为了压缩模型,人们在训练网络时,越来越倾向于使用 \(L2\) 正则化。
我们来看最后一个细节,\(\lambda\) 是正则化参数,我们通常使用开发集或交叉验证集来配置这个参数,尝试各种各样的数据,寻找最好的参数,我们要考虑训练集之间的权衡,把参数设置为较小值,这样可以避免过拟合,所以 \(\lambda\) 是另外一个需要调整的超级参数。
顺便说一下,为了方便写代码,在 Python 编程语言中,lambda 是一个保留字段,编写代码时,我们从 lambda 中删掉 a 写成 lambd,以免与 Python 中的保留字段冲突。
神经网络中实现 \(L2\) 正则化
神经网络含有一个成本函数,该函数包含 \(W^{[1]}\),\(b^{[1]}\) 到 \(W^{[l]}\),\(b^{[l]}\) 所有参数,字母 \(L\) 是神经网络所含的层数,因此成本函数等于 \(m\) 个训练样本损失函数的总和乘以 \(\frac{1}{m}\),正则项为 \(\frac{\lambda}{2m}\sum\limits_{l=1}^{L}{||W^{[l]}||}^2_F\),即:
\(J(W^{[1]},b^{[1]},...W^{[L]},b^{[L]})=\frac{1}{m}\sum\limits_{i=1}^{m}\mathcal{L}(\hat{y}^{(i)},y^{(i)})+\frac{\lambda}{2m}\sum\limits_{l=1}^{L}||W^{[l]}||^2_F\)
\({||W^{[l]}||}^2_F\) 被定义为矩阵中所有元素的平方和,即:
\(||W^{[l]}||^2_F=\sum\limits_{i=1}^{n^{[l-1]}}\sum\limits_{j=1}^{n^{[l]}}(w_{ij}^{[l]})^2\)
我们看下求和公式的具体参数,第一个求和符号其值 \(i\) 从 1 到 \(n^{[l - 1]}\),第二个其 \(j\) 值从 1 到 \(n^{[l]}\),因为 \(W\) 是一个 \(n^{[l]}\times n^{[l-1]}\) 的多维矩阵,\(n^{[l]}\) 表示 \(l\) 层单元的数量,\(n^{[l-1]}\) 表示第 \(l-1\) 层隐藏单元的数量。
该矩阵范数被称作 "弗罗贝尼乌斯范数"(Frobenius norm),用下标 F 标注,鉴于线性代数中一些神秘晦涩的原因,我们不称之为 "矩阵 \(L2\) 范数",而称它为 "弗罗贝尼乌斯范数",矩阵 \(L2\) 范数听起来更自然,但鉴于一些大家无须知道的特殊原因,按照惯例,我们称之为 "弗罗贝尼乌斯范数",它表示一个矩阵中所有元素的平方和。
使用 \(L2\) 范数实现梯度下降
不包含 \(L2\) 正则化的梯度下降做法是用 backprop 计算出 \(dW\) 的值,backprop 会给出 \(J\) 对 \(W\) 的偏导数,实际上是 \(W^{[l]}\),然后再把 \(W^{[l]}\) 替换为 \(W^{[l]}\) 减去学习率乘以 \(dW^{[l]}\),即:
\(W^{[l]}\) := \(W^{[l]} - \alpha dW^{[l]}\)
\(\frac{\lambda}{2m}\sum\limits_{l=1}^{L}||W^{[l]}||^2_F\) 是之前我们额外增加的正则化项,既然已经增加了这个正则项,现在我们要做的就是给 \(dW^{[l]}\) 加上这一项 \(\frac{\lambda}{m}W^{[l]}\),然后使用这个新定义的 \(dW^{[l]}+\frac{\lambda}{m}W^{[l]}\) 来计算更新项,即:
\(W^{[l]}:=W^{[l]}-\alpha\big[dW^{[l]}+\frac{\lambda}{m}W^{[l]}\big]=W^{[l]}-\frac{\alpha\lambda}{m}W^{[l]}-\alpha dW^{[l]}=(1-\frac{\alpha\lambda}{m})W^{[l]}-\alpha dW^{[l]}\)
\(dW^{[l]}+\frac{\lambda}{m}W^{[l]}\) 的定义含有相关参数代价函数导数和,以及最后添加的额外正则项,该正则项说明,不论 \(W^{[l]}\) 是什么,我们都试图让它变得更小,实际上,相当于我们给矩阵 \(W^{[l]}\) 乘以 \((1-\alpha\frac{\lambda}{m})\) 倍的权重,而 \((1-\alpha\frac{\lambda}{m})\) 小于 1,因此 \(L2\) 范数正则化也被称为 "权重衰减",因为它就像一般的梯度下降一样,\(W^{[l]}\) 被更新为少了 \(\alpha dW^{[l]}\),但与此同时 \(W^{[l]}\) 还额外乘以了这个小于 1 的系数,它比一般的梯度下降更新减少的更多,所以 \(L2\) 正则化也被称为 "权重衰减"。我真的不想这么叫它,但之所以叫它 "权重衰减" 是因为 \(W^{[l]}-\frac{\alpha\lambda}{m}W^{[l]}\) 和 \((1-\frac{\alpha\lambda}{m})W^{[l]}\) 这两项相等,权重指标乘以了一个小于 1 的系数。
直观的理解 \(L2\) 产生的效果:假设梯度下降未使用正则化已经计算出了使 \(J\) 最小的参数 \(W\),但是当前这个参数 \(W\) 可能导致了网络有过拟合的现象,那么好,现在我们让这个 \(W\) 减去一个值 \(\frac{\alpha\lambda}{m}\),其结果就是让使得 \(J\) 最小的参数 \(W\) 要么偏大一些,要么偏小一些,反正你别在最好的位置上。其结果就会使得 \(J\) 的值在最小的基础上提高一些,往高偏差的方向走一点,总之就是别停留在 \(J\) 值最小的地方,因为这个位置上网络拟合过度了,我们需要让网络的表现差一点。最后到底是让 \(W\) 偏大多少或者偏小多少,就由超参数 \(\alpha\) 和 \(\lambda\) 决定了。
以上就是在神经网络中应用 \(L2\) 正则化的过程,有人会问我,为什么正则化可以预防过拟合,我们放在下节课讲,同时直观感受一下正则化是如何预防过拟合的。
为什么正则化有利于预防过拟合呢?(Why regularization reduces overfitting?)
为什么正则化有利于预防过拟合呢?为什么它可以减少方差问题?我们通过两个例子来直观体会一下。
左图是高偏差,右图是高方差,中间是 Just Right,这几张图我们在前面课程中看到过。
现在我们来看下这个庞大的深度拟合神经网络。我知道这张图不够大,深度也不够,但你可以想象这是一个过拟合的神经网络。这是我们的代价函数 \(J\),含有参数 \(W\),\(b\)。我们添加正则项,它可以避免数据权值矩阵过大,这就是弗罗贝尼乌斯范数,为什么压缩 \(L2\) 范数,或者弗罗贝尼乌斯范数或者参数可以减少过拟合?
直观上理解就是如果正则化参数 \(\lambda\) 设置得足够大,权重矩阵 \(W\) 被设置为接近于 0 的值,可以理解为把许多的隐藏单元的权重设为 0,最终这个网络会变得更简单,以至于这个神经网络越来越接近逻辑回归。我们直觉上认为大量隐藏单元被完全消除了,其实不然,实际上是该神经网络的所有隐藏单元依然存在,但是它们的影响变得更小了。这个被大大简化了的神经网络会变成一个很小的网络,小到如同一个逻辑回归单元,可是深度却很大,它会使这个网络从过度拟合的状态更接近左图的高偏差状态。但是 \(\lambda\) 会存在一个中间值,于是会有一个接近 "Just Right" 的中间状态。
将神经网络变得更简单,貌似这样更不容易发生过拟合,我不确定这个直觉经验是否有用,不过在编程中执行正则化时,你实际看到一些方差减少的结果。
我们再从另一个角度来理解下,为什么正则化可以预防过拟合。假设我们用的是双曲正切激活函数,用 g(z) 表示 tanh(z)。
我们发现如果 z 的值只在很小范围内活动(图中原点附近的红色区域),那么此时的 g(z) 会大致呈线性(图中红色斜直线),这里我们利用了双曲正切函数的线性状态。但是如果 \(z\) 的值可以扩展为更大值或者更小值(图中原点附近两边的区域),那么激活函数将开始变得非线性。
对于正则化来说,如果正则化参数 \(\lambda\) 很大,那么代价函数中的总体参数 \(W\) 也会变大,而被分配到激活函数的参数 \(W\) 就会相对被压榨的较小,如此,相对来说,\(z\) 也会很小。特别是,如果 \(z\) 的值最终落在原点附近的这个小范围内,都是相对较小的值,那么 \(g(z)\) 大致呈线性,神经网络中的每层几乎都是线性的,和线性回归函数一样。之前的课程中我们讲过了,如果每层都是线性的,那么整个网络就是一个线性网络,即使是一个非常深的深层网络,因具有线性激活函数的特征,最终我们只能计算线性函数,因此,它不是一个极复杂的高度非线性函数,所以不会发生过拟合,也因此它不适用于非常复杂的决策,比如说过度拟合数据集的非线性决策边界,如上图三中所示的过度拟合高方差的情况。
相反的,如果 \(z\) 扩展成为非常大或者非常小的值,那么激活函数就会开始变得非线性,就有可能产生过拟合。
大家在编程作业里实现正则化的时候,会亲眼看到这些结果。
最后给大家一个程序执行方面的小建议,在调试梯度下降时,其中一步就是把代价函数 \(J\) 设计成一个迭代次数的函数,它代表随着迭代次数的增加,梯度下降的所表现出的调幅数量。可以看到,正常来讲,代价函数对于梯度下降的每个调幅都是单调递减的。
而我们的代价函数 \(J\) 现在已经添加了一个正则项 \(\frac{\lambda}{2m}\sum\limits_{l=1}^{L}||W^{[l]}||^2_F\),所以再调试梯度下降计算 \(J\) 值得时候,务必带上这个正则项,否则你可能看不到 \(J\) 在所有调幅范围内都单调递减的现象。
这就是 \(L2\) 正则化,它是我在训练深度学习模型时最常用的一种方法。在深度学习中,还有一种正则化的方法,就是 dropout 正则化,我们下节课再讲。
dropout 正则化(Dropout Regularization)
除了 \(L2\) 正则化,还有一个非常实用的正则化方法叫 "Dropout(随机失活)",我们来看看它的工作原理。
假设你在训练一个神经网络,它存在过拟合,dropout 所要处理的是,它会遍历网络的每一层,并设置消除神经网络中节点的概率。假设网络中每一层中的每个节点都以抛硬币的方式设置概率,每个节点得以保留和消除的概率都是 0.5,设置完节点概率,我们会消除一些节点,然后删除掉从该节点进出的连线,最后得到一个节点更少,规模更小的网络,然后用 backprop 方法进行训练。
图中是对一个训练样本进行网络节点精简后的样子,对于其它的训练样本,我们照旧以抛硬币的方式设置概率,保留一类节点集合,删除其它类型的节点集合。对于每个训练样本,我们都将采用一个精简后神经网络来训练它,这种方法似乎有点怪,单纯遍历节点,编码也是随机的,可它真的有效。最后你可能会认识到为什么 dropout 正则化起作用,因为我们在训练一个规模小得多的网络。
如何实施 dropout 呢?方法有几种,接下来我要讲的是最常用的方法,即 inverted dropout(反向随机失活),出于完整性考虑,我们用一个三层(\(L=3\))网络来举例说明。编码中会有很多涉及到 3 的地方。我只举例说明如何在某一层中实施 dropout。
首先要定义向量 \(d\),\(d^{[3]}\) 表示神经网络第三层的 dropout 向量:
d3 = np.random.rand(a3.shape[0], a3.shape[1]) < keep-prob
然后看 d3 是否小于某数,我们称之为 keep-prob, keep-prob 是一个具体数字,上个示例中它是 0.5,而本例中它是 0.8,它表示保留某个隐藏单元的概率,此处 keep-prob 等于 0.8,它意味着消除任意一个隐藏单元的概率是 0.2,它的作用就是生成随机矩阵,如果对 \(a^{[3]}\) 进行因子分解,效果也是一样的。\(d^{[3]}\) 是一个矩阵,每个样本和每个隐藏单元,其中 \(d^{[3]}\) 中的对应值为 1 的概率都是 0.8,对应为 0 的概率是 0.2,随机数字小于 0.8。它等于 1 的概率是 0.8,等于 0 的概率是 0.2。
接下来要做的就是从第三层中获取激活函数,这里我们叫它 \(a^{[3]}\),\(a^{[3]}\) 含有要计算的激活函数,\(a^{[3]}\) 等于上面的 \(a^{[3]}\) 乘以 \(d^{[3]}\):
a3 = np.multiply(a3,d3)
这里是元素相乘,如果 \(d^{[3]}\) 中元素为 1 与 \(a^{[3]}\) 中对应元素相乘后,\(a^{[3]}\) 中对应元素值不变。但如果 \(d^{[3]}\) 中元素为 0,那么相乘之后, \(a^{[3]}\) 中对应元素会被归 0。另,\(d^{[3]}\) 中各个元素等于 0 的概率是 20%。
如果用 python 实现该算法的话,\(d^{[3]}\) 则是一个布尔型数组,值为 true 和 false,而不是 1 和 0,乘法运算依然有效,python 会把 true 和 false 翻译为 1 和 0。
最后,我们向外扩展 \(a^{[3]}\),用它除以 0.8,或者是除以我们的 keep-prob 参数:
a3 /= keep-prob
下面我解释一下为什么要这么做,为方便起见,我们假设第三隐藏层上有 50 个单元或 50 个神经元,在一维上 \(a^{[3]}\) 是 50,我们通过因子分解将它拆分成 \(50 \times m\) 维的,保留和删除它们的概率分别为 80% 和 20%,这意味着最后被删除或归零的单元平均有 10(50 × 20% = 10)个,现在我们看下 \(z^{[4]}\),\(z^{[4]}=w^{[4]}a^{[3]}+b^{[4]}\),我们的预期是,\(a^{[3]}\) 减少 20%,也就是说 \(a^{[3]}\) 中有 20% 的元素被归零,为了不影响 \(z^{[4]}\) 的期望值,我们需要用 \(w^{[4]}a^{[3]} / keep-prob\),它将会修正或弥补我们所需的那 20%,\(a^{[3]}\) 的期望值不会变。
它的功能是,不论 keep-prop 的值是多少,0.8,0.9 甚至是 1。当然如果 keep-prop 被设置为 1,那么就不存在 dropout,因为它会保留所有节点。反向随机失活(inverted dropout)方法可以通过除以 keep-prob,以确保 \(a^{[3]}\) 的期望值不变。
事实证明,在测试阶段,当我们评估一个神经网络时,反向随机失活方法,使测试阶段变得更容易,因为它的数据扩展问题变少,我们将在下节课讨论。
据我了解,目前实施 dropout 最常用的方法就是 Inverted dropout,建议大家动手实践一下。Dropout 在早期的版本中都没有除以 keep-prob,所以在测试阶段,平均值会变得越来越复杂,不过那些版本已经不再使用了。
现在你使用的 \(d\) 是向量,你会发现,不同的训练样本,清除不同的隐藏单元也不同。实际上,如果你通过相同训练集多次传递数据,每次训练数据的梯度不同,则随机对不同隐藏单元归零,有时却并非如此。比如,需要将相同隐藏单元归零,第一次迭代梯度下降时,把一些隐藏单元归零,第二次迭代梯度下降时,也就是第二次遍历训练集时,对不同类型的隐藏层单元归零。向量 \(d\) 或 \(d^{[3]}\) 用来决定第三层中哪些单元归零,无论用 fore-prop 还是 back-prop,这里我们只介绍了 fore-prob。
如何在测试阶段训练算法(Making predictions at test time),在测试阶段,我们已经给出了 \(x\),或是想预测的变量,用的是标准计数法。我将第 0 层的激活函数 \(a^{[0]}\) 标注为测试样本 \(x\),我们在测试阶段不使用 dropout 函数,尤其是像下列情况:
\(z^{[1]} = w^{[1]} a^{[0]} + b^{[1]}\)
\(a^{[1]} = g^{[1]}(z^{[1]})\)
\(z^{[2]} = \ w^{[2]} a^{[1]} + b^{[2]}\)
\(a^{[2]} = \ldots\)
以此类推直到最后一层,预测值为 \(\hat{y}\)。
显然在测试阶段,我们并未使用 dropout,自然也就不用抛硬币来决定失活概率,以及要消除哪些隐藏单元了,因为在测试阶段进行预测时,我们不期望输出结果是随机的,如果测试阶段应用 dropout 函数,预测会受到干扰。理论上,你只需要多次运行预测处理过程,每一次,不同的隐藏单元会被随机归零,预测处理遍历它们,但计算效率低,得出的结果也几乎相同,与这个不同程序产生的结果极为相似。
Inverted dropout 函数在除以 keep-prob 时可以记住上一步的操作,目的是确保即使在测试阶段不执行 dropout 来调整数值范围,激活函数的预期结果也不会发生变化,所以没必要在测试阶段额外添加尺度参数,这与训练阶段不同。
这就是 dropout,大家可以通过本周的编程练习来执行这个函数,亲身实践一下。为什么 dropout 会起作用呢?下节课我们将更加直观地了解 dropout 的具体功能。
理解 dropout(Understanding Dropout)
Dropout 可以随机删除网络中的神经单元,做法有点疯狂,它为什么可以通过正则化发挥这么大作用呢?让我们来更直观的理解一下。
上节课我们已经对 drop-out 随机删除网络中的神经元有了一个直观的理解,好像每次迭代之后,神经网络都会变得比以前更小。因此采用一个较小的神经网络好像和正则化的效果是一样的。
第二个直观认识是,我们从单个神经元入手,如图,这个单元的工作就是输入并生成一些有意义的输出。通过 dropout,该单元的输入几乎被消除,有时这两个单元会被删除,有时会删除其它单元,就是说,我用紫色圈起来的这个单元,它不能依靠任何特征,因为特征都有可能被随机清除,或者说该单元的输入也都可能被随机清除。我不愿意把所有赌注都放在一个节点上,不愿意给任何一个输入加上太多权重,因为它可能会被删除,因此该单元将通过这种方式积极地传播开,并为单元的四个输入增加一点权重,通过传播所有权重,dropout 将产生收缩权重的平方范数的效果,和我们之前讲过的 \(L2\) 正则化类似,实施 dropout 的结果是它会压缩权重,并完成一些预防过拟合的外层正则化。
事实证明,dropout 被正式地作为一种正则化的替代形式,\(L2\) 对不同权重的衰减是不同的,它取决于 倍增的激活函数的大小。
总结一下,dropout 的功能类似于 \(L2\) 正则化,与 \(L2\) 正则化不同的是,被应用的方式不同,dropout 也会有所不同,甚至更适用于不同的输入范围。
实施 dropout 的另一个细节是,这是一个拥有三个输入特征的网络,其中一个要选择的参数是 keep-prob,它代表每一层上保留单元的概率。所以不同层的 keep-prob 也可以变化。第一层,矩阵 \(W^{[1]}\) 是 \(7 \times 3\),第二个权重矩阵 \(W^{[2]}\) 是 \(7 \times 7\),第三个权重矩阵 \(W^{[3]}\) 是 \(3 \times 7\),以此类推,\(W^{[2]}\) 是最大的权重矩阵,因为 \(W^{[2]}\) 拥有最大参数集,即 \(7 \times 7\),为了预防矩阵的过拟合,对于这一层,我认为这是第二层,它的 keep-prob 值应该相对较低,假设是 0.5。对于其它层,过拟合的程度可能没那么严重,它们的 keep-prob 值可能高一些,可能是 0.7,这里是 0.7。如果在某一层,我们不必担心其过拟合的问题,那么 keep-prob 可以为 1,为了表达清除,我用紫色线笔把它们圈出来,每层 keep-prob 的值可能不同。
注意 keep-prob 的值是 1,意味着保留所有单元,并且不在这一层使用 dropout,对于有可能出现过拟合,且含有诸多参数的层,我们可以把 keep-prob 设置成比较小的值,以便应用更强大的 dropout,有点像在处理 \(L2\) 正则化的正则化参数 \(\lambda\),我们尝试对某些层施行更多正则化,从技术上讲,我们也可以对输入层应用 dropout,我们有机会删除一个或多个输入特征,虽然现实中我们通常不这么做,keep-prob 的值为 1,是非常常用的输入值,也可以用更大的值,或许是 0.9。但是消除一半的输入特征是不太可能的,如果我们遵守这个准则,keep-prob 会接近于 1,即使你对输入层应用 dropout。
总结一下,如果你担心某些层比其它层更容易发生过拟合,可以把某些层的 keep-prob 值设置得比其它层更低,缺点是为了使用交叉验证,你要搜索更多的超级参数,另一种方案是在一些层上应用 dropout,而有些层不用 dropout,应用 dropout 的层只含有一个超级参数,就是 keep-prob。
结束前分享两个实施过程中的技巧,实施 dropout,在计算机视觉领域有很多成功的第一次。计算视觉中的输入量非常大,输入太多像素,以至于没有足够的数据,所以 dropout 在计算机视觉中应用得比较频繁,有些计算机视觉研究人员非常喜欢用它,几乎成了默认的选择,但要牢记一点,dropout 是一种正则化方法,它有助于预防过拟合,因此除非算法过拟合,不然我是不会使用 dropout 的,所以它在其它领域应用得比较少,主要存在于计算机视觉领域,因为我们通常没有足够的数据,所以一直存在过拟合,这就是有些计算机视觉研究人员如此钟情于 dropout 函数的原因。直观上我认为不能概括其它学科。
dropout 一大缺点就是代价函数 \(J\) 不再被明确定义,每次迭代,都会随机移除一些节点,如果再三检查梯度下降的性能,实际上是很难进行复查的。定义明确的代价函数 \(J\) 每次迭代后都会下降,因为我们所优化的代价函数 \(J\) 实际上并没有明确定义,或者说在某种程度上很难计算,所以我们失去了调试工具来绘制这样的图片。我通常会关闭 dropout 函数,将 keep-prob 的值设为 1,运行代码,确保 \(J\) 函数单调递减。然后打开 dropout 函数,希望在 dropout 过程中,代码并未引入 bug。我觉得你也可以尝试其它方法,虽然我们并没有关于这些方法性能的数据统计,但你可以把它们与 dropout 方法一起使用。
其他正则化方法(Other regularization methods)
除了 \(L2\) 正则化和随机失活(dropout)正则化,还有几种方法可以减少神经网络中的过拟合。即:Data augmentation 和 Early stopping。
数据扩增(Data augmentation)
假设你正在拟合猫咪图片分类器,如果你想通过扩增训练数据来解决过拟合,但扩增数据代价高,而且有时候我们无法扩增数据,但我们可以通过添加这类图片来增加训练集。例如,水平翻转图片,并把它添加到训练集。所以现在训练集中有原图,还有翻转后的这张图片,所以通过水平翻转图片,训练集则可以增大一倍,因为训练集有冗余,这虽然不如我们额外收集一组新图片那么好,但这样做节省了获取更多猫咪图片的花费。除了水平翻转图片,你也可以随意裁剪图片,这张图是把原图旋转并随意放大后裁剪的,仍能辨别出图片中的猫咪。通过随意翻转和裁剪图片,我们可以增大数据集,额外生成假训练数据。和全新的,独立的猫咪图片数据相比,这些额外的假的数据无法包含像全新数据那么多的信息,但我们这么做基本没有花费,代价几乎为零,除了一些对抗性代价。以这种方式扩增算法数据,进而正则化数据集,减少过拟合比较廉价。像这样人工合成数据的话,我们要通过算法验证,图片中的猫经过水平翻转之后依然是猫。大家注意,我并没有垂直翻转,因为我们不想上下颠倒图片,也可以随机选取放大后的部分图片,猫可能还在上面。
对于光学字符识别,我们还可以通过添加数字,随意旋转或扭曲数字来扩增数据,把这些数字添加到训练集,它们仍然是数字。为了方便说明,我对字符做了强变形处理,所以数字4看起来是波形的,其实不用对数字4做这么夸张的扭曲,只要轻微的变形就好,我做成这样是为了让大家看的更清楚。实际操作的时候,我们通常对字符做更轻微的变形处理。因为这几个 4 看起来有点扭曲。所以,数据扩增可作为正则化方法使用,实际功能上也与正则化相似。
early stopping
还有另外一种常用的方法叫作 early stopping,运行梯度下降时,我们可以绘制训练误差,或只绘制代价函数 \(J\) 的优化过程,在训练集上用 0-1 记录分类误差次数。呈单调下降趋势,如图。
因为在训练过程中,我们希望训练误差,代价函数 \(J\) 都在下降,通过 early stopping,我们不但可以绘制上面这些内容,还可以绘制开发集误差,它可以是开发集上的分类误差,或开发集上的代价函数,逻辑损失和对数损失等,你会发现,开发集误差通常会先呈下降趋势,然后在某个节点处开始上升,early stopping 的作用是,你会说,神经网络已经在这个迭代过程中表现得很好了,我们在此停止训练吧,得到开发集误差。
那它是怎么发挥作用的呢?
当你还未在神经网络上运行太多迭代过程的时候,参数 \(w\) 接近0,因为随机初始化 \(w\) 值时,它的值可能都是较小的随机值,所以在你长期训练神经网络之前依然很小,在迭代过程和训练过程中的值会变得越来越大,比如在这儿,神经网络中参数 \(w\) 的值已经非常大了,所以 early stopping 要做就是在中间点停止迭代过程,我们得到一个值中等大小的弗罗贝尼乌斯范数,与 \(L2\) 正则化相似,选择参数w范数较小的神经网络,但愿你的神经网络过度拟合不严重。术语 early stopping 代表提早停止训练神经网络,训练神经网络时,我有时会用到 early stopping,但是它也有一个缺点,我们来了解一下。
我认为机器学习过程包括几个步骤,其中一步是选择一个算法来优化代价函数 \(J\),我们有很多种工具来解决这个问题,如梯度下降,后面我会介绍其它算法,例如 Momentum,RMSprop 和 Adam 等等,但是优化代价函数之后,我也不想发生过拟合,也有一些工具可以解决该问题,比如正则化,扩增数据等等。
在机器学习中,超级参数激增,选出可行的算法也变得越来越复杂。我发现,如果我们用一组工具优化代价函数,机器学习就会变得更简单,在重点优化代价函数 \(J\) 时,你只需要留意 \(w\) 和 \(b\),\(J(w, b)\) 的值越小越好,你只需要想办法减小这个值,其它的不用关注。然后,预防过拟合还有其他任务,换句话说就是减少方差,这一步我们用另外一套工具来实现,这个原理有时被称为"正交化"。思路就是在一个时间做一个任务,后面课上我会具体介绍正交化,如果你还不了解这个概念,不用担心。
但对我来说 early stopping 的主要缺点就是你不能独立地处理这两个问题,因为提早停止梯度下降,也就是停止了优化代价函数 \(J\),因为现在你不再尝试降低代价函数 \(J\),所以代价函数 \(J\) 的值可能不够小,同时你又希望不出现过拟合,你没有采取不同的方式来解决这两个问题,而是用一种方法同时解决两个问题,这样做的结果是我要考虑的东西变得更复杂。
如果不用 early stopping,另一种方法就是 \(L2\) 正则化,训练神经网络的时间就可能很长。我发现,这导致超级参数搜索空间更容易分解,也更容易搜索,但是缺点在于,你必须尝试很多正则化参数 \(\lambda\) 的值,这也导致搜索大量值的计算代价太高。
Early stopping 的优点是,只运行一次梯度下降,你可以找出 \(w\) 的较小值,中间值和较大值,而无需尝试 \(L2\) 正则化超级参数 \(\lambda\) 的很多值。
如果你还不能完全理解这个概念,没关系,下节课我们会详细讲解正交化,这样会更好理解。
虽然 \(L2\) 正则化有缺点,可还是有很多人愿意用它。吴恩达老师个人更倾向于使用 \(L2\) 正则化,尝试许多不同的值,假设你可以负担大量计算的代价。而使用 early stopping 也能得到相似结果,还不用尝试这么多 \(\lambda\) 值。
这节课我们讲了如何使用数据扩增,以及如何使用 early stopping 降低神经网络中的方差或预防过拟合。
标准化输入(Normalizing inputs)
训练神经网络,其中一个加速训练的方法就是归一化输入。
假设一个训练集有两个特征,所以输入特征 \(x\) 是二维的,如下图,最左边的图是数据的散点图。
归一化输入特征需要两个步骤:
- 零均值化(subtract out or zero out the mean)
\(\mu=\frac{1}{m}\sum\limits_{i=1}^{m}x^{(i)}\)
\(x := x - \mu\)
\(\mu\) 是一个向量,\(x\) 等于每个训练数据 \(x\) 减去 \(\mu\),意思是移动训练集,直到它完成零均值化,见下图中间那副图。 - 归一化方差(normalize the variances)
注意特征 \(x_1\) 的方差比特征 \(x_2\) 的方差要大得多(下图中间那幅图蓝色箭头所示),我们要做的是给 \(\sigma\) 赋值,\(\sigma^2=\frac{1}{m}\sum\limits_{i=1}^{m}\left(x^{(i)}\right)^2\),\(\sigma^2\) 是每个特征的方差组成的向量(如果 x.shape = (n,m),那么 \(\sigma^2\).shape = (n, 1),即:对每行元素的平方求和),注意,我们已经完成零值均化,我们把所有数据除以方差向量 \(\sigma^2\),最后变成下图最右边图所示的样子。\(x_1\) 和 \(x_2\) 的方差都等于 1。
提示一下,如果你用它来调整训练数据,那么你也需要用相同的 \(\mu\) 和 \(\sigma^2\) 来归一化测试集。
我们为什么要这么做呢?为什么我们想要归一化输入特征,回想一下下面所定义的代价函数:
\(J(w, b)=\frac{1}{m}\sum\limits_{i=1}^{m}\mathcal{L}\left(\hat{y}^{(i)},y^{(i)}\right)\)
如果你使用非归一化的输入特征,代价函数会像下图中左上角的那幅图所示的样子,这是一个非常细长狭窄的代价函数,你要找的最小值在这里(红色箭头所示)。但如果特征值在不同范围,假如特征 \(x_1\) 取值范围从 1 到 1000,特征 \(x_2\) 的取值范围从 0 到 1,结果是参数 \(w_1\) 和 \(w_2\) 值的范围或比率将会非常不同,这些数据轴应该是 \(w_1\) 和 \(w_2\),但直观理解,我标记为 \(w\) 和 \(b\)。代价函数就有点像狭长的碗一样,如果你能画出该函数的部分轮廓,它会是像下图左下角的那幅图所示的样子,是一个狭长的函数。
然而如果你归一化特征,代价函数平均起来看更对称(下面右侧图所示),如果你在下面左侧图这样的代价函数上运行梯度下降法,你必须使用一个非常小的学习率。因为如果起始位置在某个狭长的一边开始,那么梯度下降法可能需要多次迭代过程,直到最后找到最小值。但如果函数是一个更圆的球形轮廓,那么不论从哪个位置开始,梯度下降法都能够更直接地找到最小值,你可以在梯度下降法中使用较大步长,而不需要像在左图中那样反复执行。
当然,实际上 \(w\) 是一个高维向量,因此用二维绘制 \(w\) 并不能正确地传达并直观理解,但总的直观理解是代价函数会更圆一些,而且更容易优化,前提是特征都在相似范围内,不是从 1 到 1000,0 到 1 的范围,而是在 -1 到 1 范围内或相似偏差,这使得代价函数 \(J\) 优化起来更简单快速。
实际上如果假设特征 \(x_1\) 范围在 0 - 1 之间,\(x_2\) 的范围在 -1 到 1 之间,\(x_3\) 的范围在 1 - 2 之间,它们是相似范围,所以会表现得很好。但如果它们在非常不同的取值范围内,如其中一个从 1 到 1000,另一个从 0 到 1,这对优化算法是非常不利的。将它们设置为零均值化,方差归一化,从而确保所有特征都在相似范围内,通常可以帮助学习算法运行得更快。
所以如果输入特征处于不同范围内,可能有些特征值从 0 到 1,有些从 1 到 1000,那么归一化特征值就非常重要了。如果特征值处于相似范围内,那么归一化就不是很重要了。执行这类归一化并不会产生什么危害,我通常会做归一化处理,虽然我不确定它能否提高训练或算法速度。
这就是归一化特征输入,下节课我们将继续讨论提升神经网络训练速度的方法。
梯度消失/梯度爆炸(Vanishing / Exploding gradients)
训练神经网络,尤其是深度神经所面临的一个问题就是梯度消失或梯度爆炸,也就是你训练神经网络的时候,导数或坡度有时会变得非常大,或者非常小,甚至于以指数方式变小,这加大了训练的难度。这节课,你将会了解梯度消失或梯度爆炸的真正含义,以及如何更明智地选择随机初始化权重,从而避免这个问题。
假设你正在训练这样一个(如下图)极深的神经网络,为了节约幻灯片上的空间,我画的神经网络每层只有两个隐藏单元,但现实中可能含有更多。这个神经网络有参数 \(W^{[1]},W^{[2]},W^{[3]}...W^{[L]}\),为了简单起见,假设我们使用线性激活函数,即 \(g(z)=z\),也就是线性激活函数,同时我们也忽略 \(b\),令 \(b=0\),如此,如果那样的话,最终的输出 \(\hat{y} = W^{[L]}W^{[L-1]}W^{[L-2]}...W^{[3]}W^{[2]}W^{[1]}x\)。
具体推导步骤如下:
因为 \(b=0\),所以 \(z^{[1]}=W^{[1]}x\)
因为是线性激活函数,所以 \(a^{[1]}=g(z^{[1]})=z^{[1]}=W^{[1]}x\),所以 \(W^{[1]}x = a^{[1]}\)
通过继续推导,你会得出 \(W^{[2]}W^{[1]}x = a^{[2]}\),\(W^{[3]}W^{[2]}W^{[1]}x = a^{[3]}\) ......
以此类推,最终得出 \(\hat{y} = a^{[L]} = W^{[L]}W^{[L-1]}W^{[L-2]}...W^{[3]}W^{[2]}W^{[1]}x\)
假设每一个权重矩阵 \(W^{[l]}=\begin{bmatrix}1.5 & 0 \\ 0 & 1.5 \end{bmatrix}\),从技术上来讲,最后一项,即输出项有不同的维度,所以 \(W^{[l]}\) 代表是的除 \(W^{[l]}\) 以外的所有 \(W\) 的权重矩阵。因为我们假设所有的权重矩阵都等于 \(\begin{bmatrix}1.5 & 0\\0 & 1.5\end{bmatrix}\),所以 \(\hat{y}=W^{[L]}\begin{bmatrix}1.5 & 0\\0 & 1.5\end{bmatrix}^{(L-1)}x\),假设我们忽略 \(W^{[L]}\),那么 \(\hat{y}=1.5^{(L-1)}x\)。对于一个深度神经网络来说 \(L\) 值较大,那么 \(\hat{y}\) 的值也会非常大,实际上它呈指数级增长的,它的增长比率是 \(1.5^L\),因此对于一个深度神经网络,\(\hat{y}\) 的值将爆炸式增长。
相反的,如果权重是 0.5,即 \(W^{[l]}=\begin{bmatrix}0.5 & 0\\0 & 0.5\end{bmatrix}\),它比 1 小,那么变化的比率也就变成了 \(0.5^L\),再次忽略 \(W^{[L]}\),那么最终 \(\hat{y}=\frac{1}{2^L}x\),所以它是一个与网络层数 \(L\) 相关的函数,随着层数增加,\(\hat{y}\) 呈指数级递减。
以上,希望你得到的直观理解是,权重只比 1 略大一点,或者说只是比单位矩阵大一点,深度神经网络的激活函数将爆炸式增长,如果比 1 略小一点,可能是 \(\begin{bmatrix}0.9 & 0\\0 & 0.9\end{bmatrix}\),那么,深度神经网络的激活函数将以指数级递减。
最近 Microsoft 对 152 层神经网络的研究取得了很大进展。对于一个神经网络,假设 \(L=150\),在这样一个深度神经网络中,如果激活函数或梯度函数以与 \(L\) 相关的指数递增或递减,它们的值将会变得极大或极小,从而导致训练难度上升,特别是当你的梯度已经到达 \(L\) 的指数级小时(梯度濒临消失),梯度下降算法的步长会非常非常小,梯度下降算法将花费很长时间来学习。
总结一下,我们讲了深度神经网络是如何产生梯度消失或爆炸问题的,实际上,在很长一段时间内,它曾是训练深度神经网络的阻力,虽然有一个不能彻底解决此问题的解决方案,但是已在如何选择初始化权重问题上提供了很多帮助。
神经网络的权重初始化(Weight Initialization for Deep Networks)
上节课,我们学习了深度神经网络如何产生梯度消失和梯度爆炸问题,最终针对该问题,我们想出了一个不完整的解决方案,虽然不能彻底解决问题,却很有用,有助于我们为神经网络更谨慎地选择随机初始化参数,为了更好地理解它,我们先举一个神经单元初始化地例子,然后再演变到整个深度网络。
我们来看看只有一个神经元的情况,然后才是深度网络。
单个神经元可能有 4 个输入特征,从 \(x_1\) 到 \(x_2\),经过 \(a=g(z)\) 处理,最终得到 \(\hat{y}\),稍后讲深度网络时,这些输入表示为 \(a^{[l]}\),暂时我们用 \(x\) 表示。
\(z=w_1x_1+w_2x_2+...+w_nx_n\),\(b=0\),暂时忽略 \(b\),为了预防 \(z\) 值过大或过小,你可以看到 \(n\) 越大,你希望 \(w_i\) 越小,因为 \(z\) 是 \(w_ix_i\) 的和,如果你把很多此类项相加,希望每项值更小,最合理的方法就是设置 \(Variance(w_i)=\frac{1}{n}\),\(n\) 表示神经元的输入特征数量,实际上,你要做的就是设置某层权重矩阵 \(w^{[l]}=\) np.random.randn(shape) * np.sqrt\(\left(\frac{1}{n^{(l-1)}}\right)\),\(n^{[l-1]}\) 就是我喂给第 \(l\) 层神经单元的数量(即第 \(l-1\) 层神经元数量)。
如果你是用的是 Relu 激活函数,将方差设置为 \(\frac{2}{n}\) 效果会更好一些。你常常发现,初始化时,尤其是使用 Relu 激活函数时,\(g^{[l]}(z)=\mathrm{Relu}(z)\),它取决于你对 np.random 的熟悉程度,randn 表示高斯随机变量,然后乘以方差 \(\frac{2}{n^{[l-1]}}\) 的平方根。这里,我用的是 \(n^{[l-1]}\),因为本例中,逻辑回归的特征是不变的。但一般情况下 \(l\) 层上的每个神经元都有 \(n^{[l-1]}\) 个输入。如果激活函数的输入特征被零均值和标准方差化,方差是 1,\(z\) 也会调整到相似范围,这虽然没有彻底解决掉梯度消失和爆炸的问题,但它确实降低了梯度消失和爆炸的速度,因为它给权重矩阵 \(w\) 设置了合理值,你也知道,它不能比 1 大很多,也不能比 1 小很多,所以梯度不会爆炸或消失的过快。
我提到了其它变体函数,刚刚提到的函数是 Relu 激活函数,一篇由 Herd 等人撰写的论文曾介绍过。对于几个其它变体函数,如 tanh 激活函数,有篇论文提到,常量 1 比常量 2 的效率更高,对于 tanh 函数来说,它是 \(\sqrt{\frac{1}{n^{[l-1]}}}\),这里平方根的作用与这个公式作用相同 np.sqrt\(\left(\frac{1}{n^{[l-1]}}\right)\),它适用于 tanh 激活函数,被称为 Xavier 初始化。Yoshua Bengio 和他的同事还提出另一种方法,你可能在一些论文中看到过,它们使用的是公式 \(\sqrt{\frac{1}{n^{[l-1]}+n^{[l]}}}\),其它理论已对此证明。但对我来讲,如果使用 Relu 激活函数,也就是最常用的激活函数,我会用这个公式 np.sqrt\(\left(\frac{1}{n^{[l-1]}}\right)\),如果使用 tanh 函数,可以用公式 \(\sqrt{\frac{1}{n^{[l-1]}}}\),有些作者也会使用这个函数。
实际上,我认为所有这些公式只是给你一个起点,它们给出初始化权重矩阵的方差的默认值,如果你想添加方差,方差参数则是另一个你需要调整的超级参数,可以给公式 \(\sqrt{\frac{1}{n^{[l-1]}}}\) 添加一个乘数参数,调优作为超级参数激增一份子的乘子参数。有时调优该超级参数效果一般,这并不是我想调优的首要超级参数,但我发现调优过程中会产生一些问题,虽然调优该参数能起到一定作用,但考虑到相比调优其它超级参数的重要性,我通常把它的优先级放得比较低。
希望你现在对梯度消失或爆炸问题以及如何为权重初始化合理值已经有了一个直观认识,希望你设置的权重矩阵既不会增长过快,也不会太快下降到 0,从而训练出一个权重或梯度不会增长或消失过快的深度网络。我们在训练深度网络时,这也是一个加快训练速度的技巧。
梯度的数值逼近(Numerical approximation of gradients)
在实施 backprop 时,有一个测试叫做梯度检验,它的作用是确保 backprop 正确实施。因为有时候,你虽然写下了这些方程式,却不能 100% 确定,执行 backprop 的所有细节都是正确的。为了逐渐实现梯度检验,我们首先说说如何计算梯度的数值逼近,下节课,我们将讨论如何在 backprop 中执行梯度检验,以确保 backprop 正确实施。
单边公差(one sided difference)
我们先画出函数 \(f\),标记为 \(f(\theta)\),\(f(\theta)=\theta^3\)。先看一下 \(\theta\) 的值,假设 \(\theta=1\),不增大 \(\theta\) 的值,而是在 \(\theta\) 右侧,设置一个 \(\theta+\varepsilon\),假设 \(\varepsilon\) 的值为 0.01,因为 \(\theta=1\),所以 \(\theta+\varepsilon=1.01\),我们用这个小三角形的高比上宽就是对 \(f'(\theta)\) 的数值估计。它的高是 \(f(\theta+\varepsilon) - f(\theta)\),宽就是 \(\varepsilon\),所以高宽比为 \(\frac{f(\theta+\varepsilon)-f(\theta)}{\varepsilon}\),它约等于 \(f'(\theta)\)。 我们把具体数值带入看一下,\(\frac{f(\theta+\varepsilon)-f(\theta)}{\varepsilon}=\frac{(1.01)^3- 1^3}{0.01}=3.0301\)。而 \(f'(\theta)\) 用公式求导结果为:\(f'(\theta)=\frac{\mathrm{d}}{\mathrm{d}\theta}f(\theta)=3\theta^2\),当 \(\theta=1\) 时,\(f'(\theta)=3 \cdot (1)^2=3\),可以看出 3.0301 \(\approx\) 3,逼近误差(approximation error)为 0.0301。这就是使用单边公差来进行梯度数值逼近的情况。
双边公差(two sided difference)
同样,我们先画出函数 \(f\),标记为 \(f(\theta)\),\(f(\theta)=\theta^3\),先看一下 \(\theta\) 的值,假设 \(\theta=1\),不增大 \(\theta\) 的值,而是在 \(\theta\) 右侧,设置一个 \(\theta+\varepsilon\),同时,我们在 \(\theta\) 左侧,设置 \(\theta-\varepsilon\)。因此 \(\theta=1\),\(\theta+\varepsilon=1.01,\;\theta-\varepsilon=0.99\),跟以前一样,\(\varepsilon\) 的值为 0.01,看下上图中的红色三角形,选择 \(f\) 函数在 \(\theta+\varepsilon\) 和 \(\theta-\varepsilon\) 上的这两个点,用这个较大红色三角形的高比上宽,技术上的原因我就不详细解释了,较大三角形的高宽比值更接近于 \(\theta\) 的导数,把右上角的三角形下移,好像有了两个三角形(绿色竖线阴影所示),右上角有一个,左下角有一个,我们通过这个红色大三角形同时考虑了这两个小三角形。所以我们得到的不是一个单边公差而是一个双边公差。
我们写一下数据算式:
公式求导:\(f'(\theta)=\frac{\mathrm{d}}{\mathrm{d}\theta}f(\theta)=3\theta^2\),当 \(\theta=1\) 时,\(f'(\theta)=3 \cdot (1)^2=3\)
数值逼近:图中红色三角形上边的点的值是 \(f(\theta+\varepsilon)\),下边的点是 \(f(\theta-\varepsilon)\),这个三角形的高度是 \(f(\theta+\varepsilon)-f(\theta-\varepsilon)\),这两个宽度都是 \(\varepsilon\),所以三角形的宽度是 \(2\varepsilon\),高宽比值为 \(\frac{f(\theta+\varepsilon)-f(\theta-\varepsilon)}{2\varepsilon}\),它的期望值接近 \(f'(\theta)\),\(f(\theta)=\theta^3\) 传入参数值,\(\frac{f(\theta+\varepsilon)-f(\theta-\varepsilon)}{2\varepsilon}=\frac{(1.01)^3-(0.99)^3}{2 \times 0.01}=3.0001\),当 \(\theta=1\) 时,\(f'(\theta)=3\theta^2=3\),所以这两个值非常接近,逼近误差(approximation error)为 0.0001,上面一小节中我们只考虑了单边公差,即从 \(\theta\) 到 \(\theta+\varepsilon\) 之间的误差,\(f'(\theta)\) 的值为 3.0301,逼近误差是 0.0301,不是 0.0001,所以使用双边误差的方法更逼近导数,其结果更接近于 3。
现在我们更加确信,\(f'(\theta)\) 可能是 \(f\) 导数的正确实现,在梯度检验和反向传播中使用该方法时,最终,它与运行两次单边公差的速度一样,实际上,我认为这种方法还是非常值得使用的,因为它的结果更准确。
导数的官方定义是针对值很小的 \(\varepsilon\),即: 求 \(\varepsilon \to 0\) 时的极限。导数的官方定义使用的是单边公差,但是它使用的是极限,即 \(\varepsilon\) 趋近于 0。我们这种数值逼近没有求极限,所以使用双边公差会更准确一些。
使用双边公差时,对于一个非零的 \(\varepsilon\),它的逼近误差可以写成 \(O(\varepsilon^2)\),\(\varepsilon\) 值非常小,如果 \(\varepsilon=0.01\),\(\varepsilon^2=0.0001\),大写符号 \(O\) 的含义是指逼近误差其实是一些常量乘以 \(\varepsilon^2\),但它的确是很准确的逼近误差,所以大写的 \(O\) 常量有时是1。然而,如果我们使用单边公差公式逼近误差就是 \(O(\varepsilon)\),当 \(\varepsilon\) 小于 1 时,实际上 \(\varepsilon\) 比 \(\varepsilon^2\) 大很多,所以这个公式近似值远没有双边公差公式的准确,所以在执行梯度检验时,我们使用双边公差,而不使用单边公差,因为它不够准确。
今天我们讲了如何使用双边公差来判断别人给你的函数 \(g(\theta)\),是否正确实现了函数 \(f\) 的偏导,现在我们可以使用这个方法来检验反向传播是否得以正确实施,如果不正确,它可能有 bug 需要你来解决。
梯度检验(Gradient checking)
梯度检验帮我们节省了很多时间,也多次帮我发现 backprop 实施过程中的 bug,接下来,我们看看如何利用它来调试或检验 backprop 的实施是否正确。
假设你的网络中含有下列参数,\(W^{[1]}\) 和 \(b^{[1]}\)……\(W^{[l]}\) 和 \(b^{[l]}\),为了执行梯度检验,首先要做的就是,把所有参数转换成一个巨大的向量数据,你要做的就是把矩阵 \(W\) 转换成一个向量,把所有 \(W\) 矩阵转换成向量之后,做连接运算,得到一个巨型向量 \(\theta\),该向量表示为参数 \(\theta\),代价函数 \(J\) 是所有 \(W\) 和 \(b\) 的函数,现在你得到了一个 \(\theta\) 的代价函数 \(J\)(即 \(J(\theta)\))。接着,你得到与 \(W\) 和 \(b\) 顺序相同的数据,你同样可以把 \(dW^{[1]}\) 和 \({db}^{[1]}\)……\({dW}^{[l]}\) 和 \({db}^{[l]}\) 转换成一个新的向量,用它们来初始化大向量 \(d\theta\),它与 \(\theta\) 具有相同维度。
同样的,把 \(dW^{[1]}\) 转换成矩阵,\(db^{[1]}\) 已经是一个向量了,直到把 \({dW}^{[l]}\) 转换成矩阵,这样所有的 \(dW\) 都已经是矩阵,注意 \(dW^{[1]}\) 与 \(W^{[1]}\) 具有相同维度,\(db^{[1]}\) 与 \(b^{[1]}\) 具有相同维度。经过相同的转换和连接运算操作之后,你可以把所有导数转换成一个大向量 \(d\theta\),它与 \(\theta\) 具有相同维度,现在的问题是 \(d\theta\) 和代价函数 \(J\) 的梯度或坡度有什么关系?
这就是实施梯度检验的过程,英语里通常简称为 "grad check",首先,我们要清楚 \(J\) 是超参数 \(\theta\) 的一个函数,你也可以将J函数展开为 \(J(\theta_{1},\theta_{2},\theta_{3},\ldots\ldots)\),不论超级参数向量 \(\theta\) 的维度是多少,为了实施梯度检验,你要做的就是循环执行,从而对每个 \(i\) 也就是对每个 \(\theta\) 组成元素计算 \(d\theta_{\text{approx}}[i]\) 的值,我使用双边误差,也就是
\(d\theta_{\text{approx}}\left[i \right] = \frac{J\left( \theta_{1},\theta_{2},\ldots\theta_{i} + \varepsilon,\ldots \right) - J\left( \theta_{1},\theta_{2},\ldots\theta_{i} - \varepsilon,\ldots \right)}{2\varepsilon}\)
只对 \(\theta_{i}\) 增加 \(\varepsilon\),其它项保持不变,因为我们使用的是双边误差,对另一边做同样的操作,只不过是减去 \(\varepsilon\),\(\theta\) 其它项全都保持不变。
从上节课中我们了解到这个值(\(d\theta_{\text{approx}}\left[i \right]\))应该逼近 \(d\theta\left[i \right]\) = \(\frac{\partial J}{\partial\theta_{i}}\),\(d\theta\left[i \right]\) 是代价函数的偏导数,然后你需要对 \(i\) 的每个值都执行这个运算,最后得到两个向量,得到 \(d\theta\) 的逼近值 \(d\theta_{\text{approx}}\),它与 \(d\theta\) 具有相同维度,它们两个与 \(\theta\) 具有相同维度,你要做的就是验证这些向量是否彼此接近。
具体来说,如何定义两个向量是否真的接近彼此?我一般做下列运算,计算这两个向量的距离,\(d\theta_{\text{approx}}\left[i \right] - d\theta[i]\) 的欧几里得范数,注意这里(\({||d\theta_{\text{approx}} -d\theta||}_{2}\))没有平方,它是误差平方之和,然后求平方根,得到欧式距离,然后用向量长度归一化,使用向量长度的欧几里得范数。分母只是用于预防这些向量太小或太大,分母使得这个方程式变成比率,我们实际执行这个方程式,\(\varepsilon\) 可能为 \(10^{-7}\),使用这个取值范围内的 \(\varepsilon\),如果你发现计算方程式得到的值为 \(10^{-7}\) 或更小,这就很好,这就意味着导数逼近很有可能是正确的,它的值非常小。
如果它的值在 \(10^{-5}\) 范围内,我就要小心了,也许这个值没问题,但我会再次检查这个向量的所有项,确保没有一项误差过大,可能这里有 bug。
如果左边这个方程式结果是 \(10^{-3}\),我就会担心是否存在 bug,计算结果应该比 \(10^{- 3}\) 小很多,如果比 \(10^{-3}\) 大很多,我就会很担心,担心是否存在 bug。这时应该仔细检查所有 \(\theta\) 项,看是否有一个具体的 \(i\) 值,使得 \(d\theta_{\text{approx}}\left[i \right]\) 与 \(d\theta[i]\) 大不相同,并用它来追踪一些求导计算是否正确,经过一些调试,最终结果会是这种非常小的值(\(10^{-7}\)),那么,你的实施可能是正确的。
在实施神经网络时,我经常需要执行 foreprop 和 backprop,然后我可能发现这个梯度检验有一个相对较大的值,我会怀疑存在 bug,然后开始调试,调试,调试,调试一段时间后,我得到一个很小的梯度检验值,现在我可以很自信的说,神经网络实施是正确的。
现在你已经了解了梯度检验的工作原理,它帮助我在神经网络实施中发现了很多 bug,希望它对你也有所帮助。
梯度检验应用的注意事项(Gradient Checking Implementation Notes)
这节课,分享一些关于如何在神经网络实施梯度检验的实用技巧和注意事项。
-
Don't use in training - only to debug
不要在训练中使用梯度检验,它只用于调试。我的意思是,计算所有 \(i\) 值的 \(d\theta_{\text{approx}}\left[i\right]\) 是一个非常漫长的计算过程,为了实施梯度下降,你必须使用 \(W\) 和 \(b\) backprop 来计算 \(d\theta\),并使用 backprop 来计算导数,只要调试的时候,你才会计算它,来确认数值是否接近 \(d\theta\)。完成后,你会关闭梯度检验,梯度检验的每一个迭代过程都不执行它,因为它太慢了。 -
If algorithm fails grad check, look at components to try to identify bug
如果算法的梯度检验失败,要检查所有项,检查每一项,并试着找出 bug,也就是说,如果 \(d\theta_{\text{approx}}\left[i\right]\) 与 \(d\theta[i]\) 的值相差很大,我们要做的就是查找不同的 \(i\) 值,看看是哪个导致 \(d\theta_{\text{approx}}\left[i\right]\) 与 \(d\theta\left[i\right]\) 的值相差这么多。举个例子,如果你发现,相对某些层或某层的 \(\theta\) 或 \(d\theta\) 的值相差很大,但是 \(\text{dw}^{[l]}\) 的各项非常接近,注意 \(\theta\) 的各项与 \(b\) 和 \(w\) 的各项都是一一对应的,这时,你可能会发现,在计算参数 \(b\) 的导数 \(db\) 的过程中存在 bug。反过来也是一样,如果你发现它们的值相差很大,\(d\theta_{\text{approx}}\left[i\right]\) 的值与 \(d\theta\left[i\right]\) 的值相差很大,你会发现所有这些项目都来自于 \(dw\) 或某层的 \(dw\),可能帮你定位 bug 的位置,虽然未必能够帮你准确定位 bug 的位置,但它可以帮助你估测需要在哪些地方追踪 bug。 -
Remember regularization
在实施梯度检验时,如果使用正则化,请注意正则项。如果代价函数 \(J(\theta) = \frac{1}{m}\sum_{}^{}{L(\hat y^{(i)},y^{(i)})} + \frac{\lambda}{2m}\sum\limits_{L}^{l=1}{||W^{[l]}||}^{2}\),这就是代价函数 \(J\) 的定义,\(d\theta\) 等于与 \(\theta\) 相关的 \(J\) 函数的梯度,包括这个正则项,记住一定要包括这个正则项。 -
Doesn't work with dropout
梯度检验不能与 dropout 同时使用,因为每次迭代过程中,dropout 会随机消除隐藏层单元的不同子集,难以计算 dropout 在梯度下降上的代价函数 \(J\)。因此 dropout 可作为优化代价函数 \(J\) 的一种方法,但是代价函数J被定义为对所有指数极大的节点子集求和。而在任何迭代过程中,这些节点都有可能被消除,所以很难计算代价函数 \(J\)。你只是对成本函数做抽样,用 dropout,每次随机消除不同的子集,所以很难用梯度检验来双重检验 dropout 的计算,所以我一般不同时使用梯度检验和 dropout。如果你想这样做,可以把 dropout 中的 keepprob 设置为 1.0,然后打开 dropout,并寄希望于 dropout 的实施是正确的,你还可以做点别的,比如修改节点丢失模式确定梯度检验是正确的。实际上,我一般不这么做,我建议关闭 dropout,用梯度检验进行双重检查,在没有 dropout 的情况下,你的算法至少是正确的,然后打开 dropout。 -
Run at random initialization; perhaps again after some training
最后一点,也是比较微妙的一点,现实中几乎不会出现这种情况。当 \(w\) 和 \(b\) 接近 0 时,梯度下降的实施是正确的,在随机初始化过程中……,但是在运行梯度下降时,\(w\) 和 \(b\) 变得更大。可能只有在 \(w\) 和 \(b\) 接近 0 时,backprop 的实施才是正确的。但是当 \(W\) 和 \(b\) 变大时,它会变得越来越不准确。你需要做一件事,我不经常这么做,就是在随机初始化过程中,运行梯度检验,然后再训练网络,\(w\) 和 \(b\) 会有一段时间远离 0,如果随机初始化值比较小,反复训练网络之后,再重新运行梯度检验。