[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning

第一周:深度学习的实用层面(Practical aspects of Deep Learning)

训练,验证,测试集(Train / Dev / Test sets)

在配置训练、验证和测试数据集的过程中做出正确决策会在很大程度上帮助大家创建高效的神经网络。训练神经网络时,我们需要做出很多决策,例如:

  1. 神经网络分多少层
  2. 每层含有多少个隐藏单元
  3. 学习速率是多少
  4. 各层采用哪些激活函数

创建新应用的过程中,我们不可能从一开始就准确预测出这些信息和其他超级参数。实际上,应用型机器学习是一个高度迭代的过程,通常在项目启动时,我们会先有一个初步想法,比如构建一个含有特定层数,隐藏单元数量或数据集个数等等的神经网络,然后编码,并尝试运行这些代码,通过运行和测试得到该神经网络或这些配置信息的运行结果,你可能会根据输出结果重新完善自己的想法,改变策略,或者为了找到更好的神经网络不断迭代更新自己的方案。因此循环该过程的效率是决定项目进展速度的一个关键因素,而创建高质量的训练数据集,验证集和测试集也有助于提高循环效率。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第1张图片

在机器学习发展的小数据量时代,常见做法是将所有数据三七分,就是人们常说的 70% 训练集,30% 测试集,如果你没有一个明确的 dev 集,也可以按照 60% 训练集,20% 验证集和 20% 测试集来划分。这是前几年机器学习领域普遍认可的最好的实践方法。如果只有 100 条,1000 条或者 1 万条数据,那么上述比例划分是非常合理的。但是在大数据时代,我们现在的数据量可能是百万级别,那么验证集和测试集占数据总量的比例会趋向于变得更小。比如我们有 100 万条数据,那么取 1 万条数据便足以进行评估,找出其中表现最好的 1-2 种算法。假设我们有 100 万条数据,其中 1 万条作为验证集,1 万条作为测试集,100 万里取 1 万,比例是 1%,即:训练集占 98%,验证集和测试集各占 1%。对于数据量过百万的应用,训练集可以占到 99.5%,验证和测试集各占 0.25%,或者验证集占 0.4%,测试集占 0.1%。

cross validation set = development set, for brevity, call this the dev set. but all of these terms mean roughly the same thing.

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)

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第2张图片

上图左:数据集拟合一条直线,可能得到一个逻辑回归拟合,但它并不能很好地拟合该数据,这是高偏差(high bias)的情况,我们称为“欠拟合”(underfitting)。

上图右:数据集拟合一个非常复杂的分类器,比如深度神经网络或含有隐藏单元的神经网络,看起来也不是一种很好的拟合方式,分类器方差较高(high variance),数据过度拟合(overfitting)。

上图中:在两者之间,可能还有一些像上面中间那幅图那样的,复杂程度适中,数据拟合适度的分类器,这个数据拟合看起来更加合理,我们称之为“适度拟合”(just right)是介于过度拟合和欠拟合中间的一类。

在这样一个只有 \(x_1\)\(x_2\) 两个特征的二维数据集中,我们可以绘制数据,将偏差和方差可视化。在多维空间数据中,绘制数据和可视化分割边界无法实现,但我们可以通过几个指标,来研究偏差和方差。

理解偏差和方差的两个关键数据是训练集误差(Train set error)和验证集误差(Dev set error)。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第3张图片

我们使用猫咪图片分类的例子,左边一张是猫咪图片,右边一张不是。

一般来说,最优误差 也被称为 贝叶斯误差。假设人眼辨别猫的错误率接近 0%,那么最优误差也接近 0%。

这种情况下,假定训练集误差是 1%,而验证集误差是 11%,可以看出训练集误差非常接近最优误差,所以它拟合得非常好,而验证集拟合相对较差,我们可能过度拟合了训练集,像这种情况,我们称之为 "高方差"。

假设训练集误差是15%,验证集误差是16%,可以看到训练误差远高于最优误差,说明算法并没有在训练集中得到很好训练,如果训练数据的拟合度不高,就是数据欠拟合,就可以说这种算法 "高偏差"。相反,它对于验证集产生的结果却是合理的,验证集中的错误率只比训练集的多了 1%,所以这种算法偏差高,因为它甚至不能很好的拟合训练集。

再举一个例子,训练集误差是 15%,偏差相当高,而且,验证集的评估结果更糟糕,错误率达到 30%,在这种情况下,我会认为这种算法偏差高,因为它在训练集上结果不理想,而且方差也很高,这是方差偏差都很糟糕的情况。

最后一个例子,训练集误差是 0.5%,验证集误差是 1%,用户看到这样的结果会很开心,猫咪分类器只有 1% 的错误率,偏差和方差都很低。

以上这些分析都是基于一种假设预测的,即假设人眼辨别的错误率接近 0%,也就是最优误差接近 0%的情况下。

如果最优误差或贝叶斯误差非常高,比如 15%。我们再看看这个分类器(训练误差15%,验证误差16%),那么此时,15% 的错误率对训练集来说也是非常合理的,偏差不高,方差也非常低。

当所有分类器都不适用时,如何分析偏差和方差呢?比如,图片很模糊,即使是人眼,或者没有系统可以准确无误地识别图片,在这种情况下,最优误差会更高,那么分析过程就要做些改变了,我们暂时先不讨论这些细微差别,重点是通过查看训练集误差,我们可以判断数据拟合情况,至少对于训练数据是这样,可以判断是否有偏差问题,然后查看错误率有多高。当完成训练集训练,开始使用验证集验证时,从训练集到验证集的这个过程中,我们可以判断方差是否过高。

以上分析的前提都是假设最优误差很小,训练集和验证集数据来自相同分布,如果没有这些假设作为前提,分析过程更加复杂,我们将会在稍后课程里讨论。

机器学习基础(Basic Recipe for Machine Learning)

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第4张图片

这是我在训练神经网络时用到的基本方法,初始模型训练完成后,我首先要知道算法的偏差高不高。如果偏差的确很高,甚至无法拟合训练集,那么你要做的就是选择一个新的网络,比如含有更多隐藏层或者隐藏单元的网络,或者花费更多时间来训练网络,或者尝试更先进的优化算法,后面我们会讲到这部分内容。你也可以尝试其他方法,后面我们会看到许多不同的神经网络架构,或许你能找到一个更合适解决此问题的新的网络架构,其中一条就是你必须去尝试,它们可能有用,也可能没用,不过采用规模更大的网络通常都会有所帮助,延长训练时间不一定有用,但也没什么坏处。训练学习算法时,我会不断尝试这些方法,直到解决掉 偏差问题,这是 最低标准,反复尝试,直到可以拟合数据为止,至少能够拟合训练集。一旦偏差降低到可以接受的数值,检查一下方差有没有问题,为了评估方差,我们要查看验证集性能,我们能从一个性能理想的训练集推断出验证集的性能是否也理想,如果方差高,最好的解决办法就是采用更多数据,如果你能做到,会有一定的帮助,但有时候,我们无法获得更多数据,我们也可以尝试通过正则化来减少过拟合,这个我们下节课会讲。有时候我们不得不反复尝试,但是,如果能找到更合适的神经网络框架,有时它可能会一箭双雕,同时减少方差和偏差。如何实现呢?想系统地说出做法很难,总之就是不断重复尝试,直到找到一个低偏差,低方差的框架,这时你就成功了。

有两点需要大家注意:

第一点,高偏差和高方差是两种不同的情况,我们后续要尝试的方法也可能完全不同,我通常会用训练验证集来诊断算法是否存在偏差或方差问题,然后根据结果选择尝试部分方法。举个例子,如果算法存在高偏差问题,准备更多训练数据其实也没什么用处,至少这不是更有效的方法,所以大家要清楚存在的问题是偏差还是方差,还是两者都有问题,明确这一点有助于我们选择出最有效的方法。

第二点,在机器学习的初期阶段,关于所谓的偏差方差权衡的讨论屡见不鲜,原因是我们能尝试的方法有很多。可以增加偏差,减少方差,也可以减少偏差,增加方差,但是在深度学习的早期阶段,我们没有太多工具可以做到只减少偏差或方差却不影响到另一方。但在当前的深度学习和大数据时代,只要持续训练一个 更大的网络,只要准备了 更多数据,那么也并非只有这两种情况。我们假定只要正则适度,那么通常构建一个更大的网络便可以在 不影响方差的同时减少偏差,而采用更多数据通常可以在 不过多影响偏差的同时减少方差。这两步实际要做的工作是:训练网络,选择网络或者准备更多数据。我觉得这就是 深度学习 对监督式学习大有裨益的一个重要原因,也是我们不用太过关注如何平衡偏差和方差的一个重要原因。

从下节课开始,我们将讲解正则化,先让 正则适度,然后争取去训练一个更大的网络,大型神经网络的主要代价是计算时间。

正则化(Regularization)

L2 = \(\frac{\lambda}{2m}||w||^2_2=\frac{\lambda}{2m}\sum\limits_{j=1}^{n_x} w_j^2=W^{\top}W\),欧几里得范数的平方
L1 = \(\frac{\lambda}{m}||w||_1=\frac{\lambda}{m}\sum\limits_{j=1}^{n_x} |w_j|\)

为什么只正则化参数 \(w\)?为什么不再加上参数 \(b\) 呢?你可以这么做,只是我习惯省略不写,因为 \(w\) 通常是一个高维参数矢量,已经可以表达高偏差问题,\(w\) 可能包含有很多参数,我们不可能拟合所有参数,而 \(b\) 只是单个数字,所以 \(w\) 几乎涵盖所有参数,如果加了参数 \(b\),其实也没太大影响,因为 \(b\) 只是众多参数中的一个,所以我通常省略不计,如果你想加上这个参数,完全没问题。

\(L2\) 正则化是最常见的正则化类型,如果用的是 \(L1\) 正则化,\(w\) 最终会是稀疏的,也就是说 \(w\) 向量中会有很多 0,有人说这样有利于压缩模型,因为集合中参数均为 0,存储模型所占用的内存更少。实际上,虽然 \(L1\) 正则化使模型变得稀疏,却没有降低太多存储内存,所以我认为这并不是 \(L1\) 正则化的目的,至少不是为了压缩模型,人们在训练网络时,越来越倾向于使用 \(L2\) 正则化。

在Python编程语言中,\(\lambda\) 是一个保留字段,编写代码时,我们从 lambda 中删掉 a,写成 lambd,以免与Python中的保留字段冲突。

\(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=\sum\limits_{i=1}^{n^{[l]}}\sum\limits_{j=1}^{n^{[l-1]}}(w_{ij}^{[l]})^2\)

该矩阵范数被称作 "弗罗贝尼乌斯范数(Frobenius norm)",用下标 F 标注,鉴于线性代数中一些神秘晦涩的原因,我们不称之为 "矩阵 \(L2\) 范数",而称它为 "弗罗贝尼乌斯范数",矩阵 \(L2\) 范数听起来更自然,但鉴于一些大家无须知道的特殊原因,按照惯例,我们称之为 "弗罗贝尼乌斯范数",它表示一个矩阵中所有元素的平方和。

具体,应该如何使用 \(L2\) 范数实现梯度下降呢 ?

\(W^{[l]} := W^{[l]} - \alpha \big[ dW^{[l]} + \frac{\lambda}{m} W^{[l]} \big] =(1-\frac{\lambda}{m})W^{[l]} - \alpha dW^{[l]}\)

\(W^{[l]}\) 被更新为少了 \(\alpha\) 乘以 backprop 输出的最初梯度值,同时 \(W\) 也乘以了一个小于 1 的系数,因此 \(L2\) 正则化也被称为 "权重衰减"。

为什么正则化有利于预防过拟合呢?(Why regularization reduces overfitting?)

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第5张图片

直观上理解就是如果正则化参数 \(\lambda\) 设置得足够大,权重矩阵 \(W\) 被设置为接近于 0 的值,可以理解为把许多的隐藏单元的权重设为 0,最终这个网络会变得更简单。我们直觉上认为大量隐藏单元被完全消除了,其实不然,实际上是该神经网络的所有隐藏单元依然存在,但是它们的影响变得更小了。这个被大大简化了的神经网络会变成一个很小的网络,小到如同一个逻辑回归单元,可是深度却很大,它会使这个网络从过度拟合的状态更接近左图的高偏差状态。但是 \(\lambda\) 会存在一个中间值,于是会有一个接近 "Just Right" 的中间状态。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第6张图片

从另一个角度来理解下,正则化为什么可以预防过拟合。如上图,假设我们用的是双曲正切激活函数。那么当 \(\lambda\) 很大时,\(W\) 会很小,\(Z\) 也会很小,如果 \(Z\) 的范围总是活跃在如上图中红色线所示的区域,那么激活函数相当于是线性的,第一节课我们讲过,如果每层都是线性的,那么整个网络就是一个线性网络,即使是一个非常深的深层网络,因具有线性激活函数的特征,最终我们只能计算线性函数,因此,它不适用于非常复杂的决策,以及过度拟合数据集的非线性决策边界。相反,如果 \(Z\) 扩展成为非常大或者非常小的值,那么激活函数就会开始变得非线性,就有可能产生过拟合。

注意,当 \(J\) 中添加了正则化项之后,它已经有了一个全新的定义。所以,在你调试梯度下降,画出 \(J\) 的变化曲线时,如果你用的是原来函数 \(J\)(不带正则化项),你可能看不到单调递减现象,为了调试梯度下降,请务必使用新定义的函数 \(J\),它包含第二个正则化项,否则函数 \(J\) 可能不会呈现出单调递减的趋势,从而导致误判断。

dropout 正则化(Dropout Regularization)

除了 \(L2\) 正则化,还有一个非常实用的正则化方法——"Dropout"(随机失活),我们来看看它的工作原理。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第7张图片

假设你在训练上图这样的神经网络,它存在过拟合,这就是 dropout 所要处理的,我们复制这个神经网络,dropout 会遍历网络的每一层,并设置消除神经网络中节点的概率。假设网络中的每一层,每个节点都以抛硬币的方式设置概率,每个节点得以保留和消除的概率都是0.5,设置完节点概率,我们会消除一些节点,然后删除掉从该节点进出的连线,最后得到一个节点更少,规模更小的网络,然后用backprop方法进行训练。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第8张图片

上图是网络节点精简后的一个样本,对于其它样本,我们照旧以抛硬币的方式设置概率,保留一类节点集合,删除其它类型的节点集合。对于每个训练样本,我们都将采用一个精简后神经网络来训练它,这种方法似乎有点怪,单纯遍历节点,编码也是随机的,可它真的有效。不过可想而知,我们针对每个训练样本训练规模极小的网络,最后你可能会认识到为什么要正则化网络,因为我们在训练极小的网络。

如何实施 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-probkeep-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 实现该算法的话,则是一个布尔型数组,值为 true 和 false,而不是 1 和 0,乘法运算依然有效,python 会把 true 和 false 翻译为 1 和 0。

最后,我们向外扩展,用它除以 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。

如何在测试阶段训练算法,在测试阶段,我们已经给出了 \(x\),或是想预测的变量,用的是标准计数法。我用 \(a^{[0]}\),第 0 层的激活函数标注为测试样本,我们在测试阶段不使用 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)}=...\)

以此类推直到最后一层,预测值为 \(\hat{y}\)

显然在测试阶段,我们并未使用 dropout,自然也就不用抛硬币来决定失活概率,以及要消除哪些隐藏单元了,因为在测试阶段进行预测时,我们不期望输出结果是随机的,如果测试阶段应用 dropout 函数,预测会受到干扰。理论上,你只需要多次运行预测处理过程,每一次,不同的隐藏单元会被随机归零,预测处理遍历它们,但计算效率低,得出的结果也几乎相同,与这个不同程序产生的结果极为相似。

Inverted dropout 函数在除以 keep-prob 时可以记住上一步的操作,目的是确保即使在测试阶段不执行 dropout 来调整数值范围,激活函数的预期结果也不会发生变化,所以没必要在测试阶段额外添加尺度参数,这与训练阶段不同。

理解 dropout(Understanding Dropout)

Dropout 可以随机删除网络中的神经单元,做法有点疯狂,它为什么可以通过正则化发挥这么大作用呢?

上节课我们已经对 drop-out 有了一个直观的理解,好像是每次迭代之后,神经网络都会变得比以前更小。因此采用一个较小的神经网络好像和正则化的效果是一样的。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第9张图片

第二个直观认识是,我们从单个神经元入手,如图,这个单元的工作就是输入并生成一些有意义的输出。通过 dropout,该单元的输入几乎被消除,有时这两个单元会被删除,有时会删除其它单元,就是说,我用紫色圈起来的这个单元,它不能依靠任何特征,因为特征都有可能被随机清除,或者说该单元的输入也都可能被随机清除。我不愿意把所有赌注都放在一个节点上,不愿意给任何一个输入加上太多权重,因为它可能会被删除,因此该单元将通过这种方式积极地传播开,并为单元的四个输入增加一点权重,通过传播所有权重,dropout 将产生收缩权重的平方范数的效果,和我们之前讲过的 \(L2\) 正则化类似,实施 dropout 的结果是它会压缩权重,并完成一些预防过拟合的外层正则化。

事实证明,dropout 被正式地作为一种正则化的替代形式,\(L2\) 对不同权重的衰减是不同的,它取决于倍增的激活函数的大小。

总结一下,dropout 的功能类似于 \(L2\) 正则化,与 \(L2\) 正则化不同的是,被应用的方式不同,dropout 也会有所不同,甚至更适用于不同的输入范围。

实施 dropout 的另一个细节是,这是一个拥有三个输入特征的网络,其中一个要选择的参数是 keep-prob,它代表每一层上保留单元的概率。所以不同层的 keep-prob 也可以变化。第一层,矩阵 \(W^{[1]}\) 是 7×3,第二个权重矩阵 \(W^{[2]}\) 是 7×7,第三个权重矩阵 \(W^{[3]}\) 是 3×7,以此类推,\(W^{[2]}\) 是最大的权重矩阵,因为 \(W^{[2]}\) 拥有最大参数集,即 7×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)

假设你正在拟合猫咪图片分类器,如果你想通过扩增训练数据来解决过拟合,但扩增数据代价高,而且有时候我们无法扩增数据,但我们可以通过添加这类图片来增加训练集。例如,水平翻转图片,并把它添加到训练集。所以现在训练集中有原图,还有翻转后的这张图片,所以通过水平翻转图片,训练集则可以增大一倍,因为训练集有冗余,这虽然不如我们额外收集一组新图片那么好,但这样做节省了获取更多猫咪图片的花费。除了水平翻转图片,你也可以随意裁剪图片,这张图是把原图旋转并随意放大后裁剪的,仍能辨别出图片中的猫咪。通过随意翻转和裁剪图片,我们可以增大数据集,额外生成假训练数据。和全新的,独立的猫咪图片数据相比,这些额外的假的数据无法包含像全新数据那么多的信息,但我们这么做基本没有花费,代价几乎为零,除了一些对抗性代价。以这种方式扩增算法数据,进而正则化数据集,减少过拟合比较廉价。像这样人工合成数据的话,我们要通过算法验证,图片中的猫经过水平翻转之后依然是猫。大家注意,我并没有垂直翻转,因为我们不想上下颠倒图片,也可以随机选取放大后的部分图片,猫可能还在上面。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第10张图片

对于光学字符识别,我们还可以通过添加数字,随意旋转或扭曲数字来扩增数据,把这些数字添加到训练集,它们仍然是数字。为了方便说明,我对字符做了强变形处理,所以数字4看起来是波形的,其实不用对数字4做这么夸张的扭曲,只要轻微的变形就好,我做成这样是为了让大家看的更清楚。实际操作的时候,我们通常对字符做更轻微的变形处理。因为这几个 4 看起来有点扭曲。所以,数据扩增可作为正则化方法使用,实际功能上也与正则化相似。

early stopping

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第11张图片

还有另外一种常用的方法叫作 early stopping,运行梯度下降时,我们可以绘制训练误差,或只绘制代价函数 \(J\) 的优化过程,在训练集上用 0-1 记录分类误差次数。呈单调下降趋势,如上图。

因为在训练过程中,我们希望训练误差,代价函数 \(J\) 都在下降,通过 early stopping,我们不但可以绘制上面这些内容,还可以绘制验证集误差,它可以是验证集上的分类误差,或验证集上的代价函数,逻辑损失和对数损失等,你会发现,验证集误差通常会先呈下降趋势,然后在某个节点处开始上升,early stopping 的作用是,你会说,神经网络已经在这个迭代过程中表现得很好了,我们在此停止训练吧,得到验证集误差。

那它是怎么发挥作用的呢?

当你还未在神经网络上运行太多迭代过程的时候,参数 \(w\) 接近0,因为随机初始化 \(w\) 值时,它的值可能都是较小的随机值,所以在你长期训练神经网络之前依然很小,在迭代过程和训练过程中的值会变得越来越大,比如在这儿,神经网络中参数 \(w\) 的值已经非常大了,所以 early stopping 要做就是在中间点停止迭代过程,我们得到一个值中等大小的弗罗贝尼乌斯范数,与 \(L2\) 正则化相似,选择参数w范数较小的神经网络,但愿你的神经网络过度拟合不严重。术语 early stopping 代表提早停止训练神经网络,训练神经网络时,我有时会用到 early stopping,但是它也有一个缺点,我们来了解一下。

我认为机器学习过程包括几个步骤,其中一步是选择一个算法来优化代价函数 \(J\),我们有很多种工具来解决这个问题,如梯度下降,后面我会介绍其它算法,例如 MomentumRMSpropAdam 等等,但是优化代价函数之后,我也不想发生过拟合,也有一些工具可以解决该问题,比如正则化,扩增数据等等。

在机器学习中,超级参数激增,选出可行的算法也变得越来越复杂。我发现,如果我们用一组工具优化代价函数,机器学习就会变得更简单,在重点优化代价函数 \(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\) 是二维的,如下图,最左边的图是数据的散点图:

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第12张图片

归一化输入特征需要两个步骤:

  1. 零均值化(subtract out or zero out the mean)
    \(\mu=\frac{1}{m}\sum\limits_{i=1}^{m}x^{(i)}\)
    \(x := x - \mu\)
    意思是移动训练集,直到它完成零均值化,见上图中间那副图。
  2. 归一化方差(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)\)

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第13张图片

如果你使用非归一化的输入特征,代价函数会像上图中左上角的那幅图所示的样子,这是一个非常细长狭窄的代价函数,你要找的最小值在这里(红色箭头所示)。但如果特征值在不同范围,假如特征 \(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,这对优化算法是非常不利的。将它们设置为零均值化,方差归一化,从而确保所有特征都在相似范围内,通常可以帮助学习算法运行得更快。执行这类归一化并不会产生什么危害,我通常会做归一化处理,虽然我不确定它能否提高训练或算法速度。

梯度消失/梯度爆炸(Vanishing / Exploding gradients)

训练神经网络,尤其是深度神经所面临的一个问题就是梯度消失或梯度爆炸,也就是你训练神经网络的时候,导数或坡度有时会变得非常大,或者非常小,甚至于以指数方式变小,这加大了训练的难度。这节课,你将会了解梯度消失或梯度爆炸的真正含义,以及如何更明智地选择随机初始化权重,从而避免这个问题。

1546742-20190629213357796-2134441563.png

上图例子中我们假设神经网络每层只有两个隐藏单元,但现实中可能含有更多。这个神经网络有参数 \(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)

上节课,我们学习了深度神经网络如何产生梯度消失和梯度爆炸问题,最终针对该问题,我们想出了一个不完整的解决方案,虽然不能彻底解决问题,却很有用,有助于我们为神经网络更谨慎地选择随机初始化参数,为了更好地理解它,我们先举一个神经单元初始化地例子,然后再演变到整个深度网络。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第14张图片

我们来看看只有一个神经元的情况,然后才是深度网络。

单个神经元可能有 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)

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第15张图片

我们先画出函数 \(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)

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第16张图片

同样,我们先画出函数 \(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\)的梯度或坡度有什么关系?

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第17张图片

这就是实施梯度检验的过程,英语里通常简称为“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​\)其它项全都保持不变。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第18张图片

从上节课中我们了解到这个值(\(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\)具有相同维度,你要做的就是验证这些向量是否彼此接近。

1546742-20190630150901960-1644224859.png

具体来说,如何定义两个向量是否真的接近彼此?我一般做下列运算,计算这两个向量的距离,\(d\theta_{\text{approx}}\left[i \right] - d\theta[i]\)的欧几里得范数,注意这里(\({||d\theta_{\text{approx}} -d\theta||}_{2}\))没有平方,它是误差平方之和,然后求平方根,得到欧式距离,然后用向量长度归一化,使用向量长度的欧几里得范数。分母只是用于预防这些向量太小或太大,分母使得这个方程式变成比率,我们实际执行这个方程式,\(\varepsilon\)可能为\(10^{-7}\),使用这个取值范围内的\(\varepsilon\),如果你发现计算方程式得到的值为\(10^{-7}\)或更小,这就很好,这就意味着导数逼近很有可能是正确的,它的值非常小。

[C2W1] Improving Deep Neural Networks : Practical aspects of Deep Learning_第19张图片

如果它的值在\(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}\)),那么,你的实施可能是正确的。

1546742-20190630150928730-1801087074.png

在实施神经网络时,我经常需要执行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θ[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_{}^{}{||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,如果随机初始化值比较小,反复训练网络之后,再重新运行梯度检验。

转载于:https://www.cnblogs.com/keyshaw/p/11027689.html

你可能感兴趣的:(人工智能,python,数据结构与算法)