神经网络和深度学习,第3章,第2部分:为什么正则化有助于减少再培训?


从经验上,我们已经看到正则化有助于减少再培训。 这令人鼓舞-但是,遗憾的是,为什么正则化尚不明确。 通常人们以某种​​方式进行解释:从某种意义上说,较小的权重具有较小的复杂性,这为数据提供了更简单,更有效的解释,因此应首选它们。 但是,这只是一个简短的解释,它的某些部分似乎令人怀疑或神秘。 让我们展开这个故事,并用批判的眼光审视它。 为此,假设我们有一个要为其创建模型的简单数据集:


在意义上,这里我们研究现实世界的现象,x和y代表真实数据。 我们的目标是建立一个模型,使我们能够将y预测为x的函数。 我们可以尝试使用神经网络来创建这样的模型,但是我建议更简单一些:我将尝试将y建模为x中的多项式。 我将使用神经网络代替神经网络,因为多项式的使用使解释特别清晰。 一旦处理完多项式的情况,我们将进入国民议会。 上图中有十个点,这意味着我们可以找到一个完全适合数据的唯一 9阶多项式 y = a 0 x 9 + a 1 x 8 + ... + a 9 。 这是该多项式的图。


完美的打击。 但是我们可以使用线性模型y = 2x得到一个很好的近似值


哪个更好? 哪个更可能是正确的? 哪一个可以更好地推广到现实世界中相同现象的其他示例?

难题。 如果没有有关潜在现实现象的其他信息,就无法准确回答它们。 但是,让我们看一下两种可能性:(1)具有9阶多项式的模型真实地描述了现实世界的现象,因此可以完美地概括。 (2)正确的模型为y = 2x,但是我们还有与测量误差相关的其他噪声,因此该模型无法完美拟合。

先验地,不能说两种可能性中的哪一种是正确的(或者说没有第三种可能性是正确的)。 从逻辑上讲,它们中的任何一个都可能是真实的。 它们之间的区别是不平凡的。 是的,根据可用数据,可以说两个模型之间只有微小的差异。 但是假设我们要预测与x的某个较大值相对应的y值,该值比图中所示的任何一个大得多。 如果我们尝试这样做,则两个模型的预测之间将出现巨大差异,因为项x 9在9阶多项式中占主导地位,而线性模型保持线性。

关于正在发生的事情的一种观点是指出,如果可能的话,应该在科学中使用更简单的解释。 当我们找到一个可以解释许多参考点的简单模型时,我们只想大喊:“尤里卡!” 毕竟,不可能仅凭偶然出现简单的解释。 我们怀疑该模型应该产生与该现象相关的一些真相。 在这种情况下,模型y = 2x +噪声似乎比y = a 0 x 9 + a 1 x 8 +更简单。如果偶然出现简单性,这将令人惊讶,因此我们怀疑y = 2x +噪声表示某些基本真理。 从这个角度来看,九阶模型只是研究局部噪声的影响。 尽管9阶模型对于这些特定参考点非常有效,但它不能推广到其他点,因此带有噪声的线性模型将具有更好的预测能力。

让我们看看这个观点对神经网络意味着什么。 假设在我们的网络中,权重通常较低,就像在正规网络中通常那样。 由于其重量较小,因此在此各处更改几个随机输入时,网络行为不会发生太大变化。 结果,正则化网络很难了解数据中存在的局部噪声的影响。 这类似于确保单个证据不会在很大程度上影响整个网络的输出的需求。 取而代之的是,对正则化网络进行训练以响应经常在训练数据中找到的证据。 相反,具有较大权重的网络可以响应输入数据的细微变化而非常强烈地更改其行为。 因此,不规则网络可以使用较大的权重来训练复杂模型,该模型在训练数据中包含大量噪声信息。 简而言之,正则化网络的局限性使其可以基于训练数据中经常发现的模式创建相对简单的模型,并且它们可以抵抗训练数据中的噪声所引起的偏差。 希望这将使我们的网络能够研究现象本身,并更好地概括所获得的知识。

综上所述,优先考虑简单的解释应该会让您感到紧张。 有时人们称这个想法为“奥卡姆剃刀”并热心地应用它,就好像它具有一般科学原理一样。 但是,这当然不是一般的科学原理。 没有先验逻辑上的理由偏向于简单的解释而不是复杂的解释。 有时,更复杂的解释是正确的。

让我描述一个更复杂的解释如何正确的两个例子。 在1940年代,物理学家Marcel Shane宣布发现了一个新粒子。 他工作的公司通用电气很高兴,并广泛分发了该活动的出版物。 但是,物理学家汉斯·贝特对此表示怀疑。 贝特(Bethe)拜访了谢恩(Shane),并研究了谢恩(Shane)的新粒子的痕迹。 Shane先后展示了Beta版,但Bete在他们每个人身上发现了一个问题,表明需要拒绝该数据。 最后,Shane向Beta显示了一条看起来合适的记录。 Bethe说这可能只是统计偏差。 Shane:“是的,但是即使是按照您自己的公式,由于统计数据,也有五分之一的可能性。” Bethe:“但是,我已经查看了五张记录。” 最后,Shane说:“但是您用其他一些理论解释了我的每一个记录,每个好的图像,并且我有一个假设可以一次解释所有的记录,因此我们在谈论一个新粒子。” Bethe回答:“我的解释与您的解释之间的唯一区别是您的解释是错误的,而我的解释是正确的。 您的单一解释不正确,而我所有的解释都是正确的。” 随后,事实证明自然界同意贝丝,而谢恩的粒子蒸发了。

在第二个例子中,1859年,天文学家Urbain Jean Joseph Le Verrier发现水星轨道的形状与牛顿的万有引力理论不符。 与该理论的偏差很小,因此提出了解决该问题的几种选择,这归结为牛顿理论作为一个整体是正确的,只需要稍作改动即可。 1916年,爱因斯坦证明,可以使用他的广义相对论很好地解释这种偏差,它与牛顿引力有根本不同,并且基于更复杂的数学。 尽管存在这种额外的复杂性,但爱因斯坦的解释在今天是公认的,并且即使采用修改的形式 ,牛顿引力也是不正确的。 尤其是发生这种情况,是因为今天我们知道爱因斯坦的理论解释了牛顿理论难以解决的许多其他现象。 而且,更令人惊讶的是,爱因斯坦的理论准确地预测了牛顿引力根本无法预测的几种现象。 但是,这些令人印象深刻的品质在过去并不明显。 从单纯性的角度来看,牛顿理论的某些修改形式看起来会更具吸引力。

从这些故事中可以得出三种道德。 首先,有时很难确定两种解释中的哪一种“更容易”。 其次,即使我们做出了这样的决定,也必须非常仔细地指导简单性! 第三,对模型的真实检验不是简单性,而是模型在新的行为条件下预测新现象的能力。

考虑到所有这些并谨慎对待,我们将接受一个经验事实-正规化的NS通常比非正规的NS更好。 因此,在本书的后面,我们将经常使用正则化。 所提到的故事只需要说明为什么还没有人对正则化有助于网络泛化的原因提出一个完全令人信服的理论解释。 研究人员继续发表作品,尝试不同的正则化方法,进行比较,寻找最有效的方法,并试图理解为什么不同的方法效果更好或更差。 因此正则化可以像一样对待。 当它经常提供帮助时,我们对所发生的事情没有完全令人满意的系统理解,只有不完整的启发式规则和实践规则。

这里存在着一系列深层的问题,这些问题已成为科学的核心。 这是一个普遍性问题。 正则化可以为我们提供一个计算魔术棒,可以帮助我们的网络更好地概括数据,但是它并不能使人们对概括的工作原理以及最佳的实现方法有基本的了解。

这些问题可以归结为归纳问题,这是苏格兰哲学家戴维·休姆David Hume)在《 人类认知研究 》(1748)一书中进行的著名解释。 归纳问题是David Walpert和William Macredie (1977)提出的“ 缺乏免费膳食定理 ”的主题。

这尤其令人讨厌,因为在日常生活中,人们非常有能力概括数据。 向孩子显示大象的一些图像,他将很快学会识别其他大象。 当然,他有时可能会犯一个错误,例如将犀牛与大象混为一谈,但总的来说,此过程的效果出奇地准确。 现在,我们有了一个系统-人脑-拥有大量的免费参数。 在向他显示一个或多个训练图像后,系统将学习将其概括为其他图像。 从某种意义上说,我们的大脑非常擅长正规化! 但是我们该怎么做呢? 目前,这对我们来说是未知的。 我认为,将来我们将在人工神经网络中开发更强大的正则化技术,这些技术最终将使国民议会能够基于更小的数据集来归纳数据。

实际上,我们的网络已经比预期的先验性更好地推广了。 具有100个隐藏神经元的网络具有近80,000个参数。 训练数据中只有50,000张图像。 这与尝试在50,000个参考点上拉伸80,000阶的多项式相同。 所有迹象表明,我们的网络必须进行严格的重新培训。 但是,正如我们所看到的,这样的网络实际上可以很好地推广。 为什么会这样呢? 这还不是很清楚。 据推测 ,“多层网络中梯度下降的学习动力学受到自我调节。” 这是一个巨大的财富,但也是一个令人不安的事实,因为我们不知道为什么会这样。 同时,我们将采取务实的方法,并尽可能使用正则化。 这将对我们的国民议会有利。

让我结束本节,回到我之前未解释的内容:L2的正则化并不限制位移。 自然地,更改正则化过程很容易,以便它可以对位移进行正则化。 但是从经验上讲,这通常不会以任何明显的方式改变结果,因此,在某种程度上,处理偏差的正则化是一个协议问题。 但是,值得注意的是,大位移不会使神经元对大重量的输入敏感。 因此,我们无需担心较大的偏移量,该偏移量使我们的网络能够学习训练数据中的噪声。 同时,通过允许大位移,我们使我们的网络在行为上更加灵活-特别是,大位移促进了我们想要的神经元饱和。 因此,我们通常不将偏移量包含在正则化中。

其他正则化技术


除了L2之外,还有许多正则化技术。 实际上,已经开发了许多技术,以至于我无法简单地描述所有这些技术。 在本节中,我将简要介绍减少减少再训练的其他三种方法:正则化L1, 退出和人为地增加训练集。 我们不会像以前的主题那样深入地研究它们。 相反,我们只是了解它们,同时欣赏各种现有的正则化技术。

正则化L1


在这种方法中,我们通过添加权重的绝对值之和来修改不规则成本函数:

C=C0+ frac lambdan sumw|w|\标95



从直觉上讲,这类似于L2的正则化,后者对较大的权重进行罚款,并使网络更喜欢较低的权重。 当然,正则项L1与正则项L2不同,因此您不应期望完全相同的行为。 让我们尝试了解使用正则化L1训练的网络与使用正则化L2训练的网络的行为有何不同。

为此,请查看成本函数的偏导数。 与众不同(95),我们获得:

 frac\部C\部w= frac\部C0\部w+ frac lambdan\, rmsgnw\标96



其中sgn(w)是w的符号,即,如果w为正,则为+1,如果w为负,则为-1。 使用该表达式,我们稍微修改了反向传播,以便它使用正则化L1执行随机梯度下降。 L1标准化网络的最终更新规则:

w rightarroww=w frac eta lambdan mboxsgnw eta frac\部C0\部w\标97



像往常一样,可以使用小型数据包的平均值来估算∂C/∂w。 将此与正则化更新规则L2(93)进行比较:

w rightarroww=w left1 frac eta lambdan right eta frac\部C0\部w\标98



在两个表达式中,正则化的效果都是减轻权重。 这与两种正则化都会惩罚较大权重的直观概念相吻合。 但是,减轻重量的方式有所不同。 在L1的正则化中,权重减少一个恒定值,趋于0。在L2的正则化中,权重减少一个与w成正比的值。 因此,当某个权重具有较大的值| w |时,L1的正则化将使权重降低的程度不及L2。 反之亦然,当| w | 较小的L1正则化比L2正则化减轻了更多的重量。 结果,L1的正则化倾向于将网络权重集中在数量相对较少的高重要性键中,而其他权重则趋于零。

我在前面的讨论中略微解决了一个问题-当w = 0时未定义偏导数∂C/∂w。 这是因为函数| w | 在w = 0处存在一个急性“扭结”;因此,无法在此处进行区分。 但这并不可怕。 当w = 0时,我们仅对随机梯度下降应用通常的不规则规则。 直观上,这没什么不对–正则化应该减少权重,并且显然不能减少已经等于0的权重。更精确地,我们将使用等式(96)和(97),条件是sgn(0)= 0。 这将为我们提供一个方便且紧凑的规则,用于正则化为L1的随机梯度下降。

异常[辍学]


一个例外是完全不同的正则化技术。 与L1和L2的正则化不同,该异常不处理成本函数的更改。 相反,我们正在更改网络本身。 让我先解释一个异常操作的基本机制,然后再研究异常的工作原理以及产生的结果。

假设我们正在尝试训练网络:


特别是,我们有训练输入x和相应的期望输出y。 通常,我们将通过在网络上直接分布x进行训练,然后反向传播以确定梯度的贡献。 异常会修改此过程。 我们从随机和临时删除网络中一半的隐藏神经元开始,使输入和输出神经元保持不变。 之后,我们将拥有大约一个这样的网络。 请注意,图中仍标出了被排除的神经元,即那些被临时去除的神经元:


我们通过在更改后的网络上进行直接分发来传递x,然后在更改后的网络上也将结果反向分发。 在使用示例迷你包完成此操作之后,我们将更新相应的权重和偏移量。 然后,我们重复此过程,首先恢复被排除的神经元,然后选择一个新的随机隐藏神经元子集进行删除,评估另一个迷你数据包的梯度,并更新网络的权重和偏移。

一次又一次地重复此过程,我们得到的网络已经了解了一些权重和位移。 当然,这些重量和位移是在排除了一半隐藏神经元的条件下得知的。当我们全面启动网络时,我们将拥有两倍数量的活动隐藏神经元。为了弥补这一点,我们将来自隐藏神经元的权重减半。

排除程序可能看起来很奇怪和任意。她为什么要帮助正规化?为了解释正在发生的事情,我希望您暂时忘记这一例外,并以标准的方式介绍国民议会的培训。特别是,假设我们使用相同的训练数据来训练几个不同的NS。当然,网络一开始可能会有所不同,有时培训会产生不同的结果。在这种情况下,我们可以应用某种平均或表决方案来决定接受哪些输出。例如,如果我们训练了五个网络,其中三个将数字分类为“ 3”,那么这可能是真实的三个。而其他两个网络可能只是错误的。这种平均方案通常是减少再培训的有用方法(尽管很昂贵)。原因是不同的网络可以以不同的方式进行重新训练,而平均可以帮助消除这种重新训练。

所有这些与异常有什么关系?启发式地,当我们排除不同组的中子时,就好像我们在训练不同的NS一样。因此,排除过程类似于在大量不同网络上平均效果。不同的网络以不同的方式进行再培训,因此希望排除的平均效果会减少再培训。最早的作品之一

给出了有关排除的好处的启发式解释使用这种技术:“这种技术减少了神经元的复杂关节适应性,因为神经元不能依赖某些邻居的存在。最后,他必须学习更可靠的特征,这些特征可以与神经元的许多不同随机子集一起工作。”换句话说,如果我们把国民议会想象成一个做出预测的模型,那么例外将是一种保证模型稳定的方法,以免丢失个别证据。从这个意义上讲,该技术类似于L1和L2的正则化,它们试图减少权重,并以此方式使网络更能抵抗网络中任何单个连接的丢失。

自然,排除有用性的真正衡量标准是其在提高神经网络效率方面的巨大成功。在原作中在介绍此方法的地方,它已应用于许多不同的任务。我们特别感兴趣的是,作者使用类似于我们研究的简单直接分配网络,将异常应用于MNIST的数字分类。该白皮书指出,在此之前,这种架构的最佳结果是98.4%的准确性。他们结合排除法和修正形式的正则化L2将其提高到98.7%。同样,许多其他任务也获得了令人印象深刻的结果,包括模式和语音识别以及自然语言处理。在训练经常出现重新训练问题的大型深度网络时,此异常特别有用。

人为地扩展培训数据集


早先我们看到,当我们仅使用1,000个训练图像时,我们的MNIST分类准确度下降到80%。难怪-有了更少的数据,我们的网络将遇到更少的人工写数字的选择。让我们尝试使用不同数量的训练集来观察效率的变化,以训练我们的30个隐藏神经元的网络。我们使用最小数据包大小10,学习速度η= 0.5,正则化参数λ= 5.0和具有交叉熵的代价函数进行训练。我们将使用一套完整的数据来训练一个包含30个纪元的网络,并与减少训练数据量成比例地增加纪元数。为了保证不同训练数据集的权重降低因子相同,我们将使用正则化参数λ= 5,完整的训练集为0,并随着数据量的减少按比例减少。


可以看出,分类精度随着训练数据的增加而显着提高。随着销量的进一步增长,这种增长可能会继续。当然,从上图可以看出,我们正在接近饱和。但是,假设我们将该图重做为对数依赖于训练数据的数量:


可以看出,最终图表仍趋于上升。这表明,如果我们获取大量数据(例如,数百万甚至数十亿个手写示例,而不是50,000个),那么即使是很小的数据,我们也可能会得到一个更好的工作网络。

获取更多培训数据是一个好主意。不幸的是,这可能很昂贵,因此在实践中并不总是可能的。但是,还有另一个想法几乎可以起作用-人为地增加数据集。例如,假设我们从MNIST拍摄了一张5的图像,并将其旋转了15度:



这显然是同一个人。但是在像素级别,它与MNIST数据库中可用的图像有很大不同。可以合理地假设将此图像添加到训练数据集可以帮助我们的网络更多地了解图像分类。此外,我们显然不限于仅添加一张图像的功能。我们可以对MNIST的所有训练图像进行一些细微调整,然后使用扩展的训练数据集来提高网络效率,从而扩展训练数据。

这个想法非常有力,并且被广泛使用。让我们看一下科学研究的结果他将此想法的几种变体应用于MNIST。他们考虑的网络架构之一与我们使用的架构类似-具有800个隐藏神经元的直接分配网络,使用具有交叉熵的成本函数。通过使用标准MNIST训练集启动此网络,他们获得了98.4%的分类精度。但是随后他们不仅使用上述旋转量,而且还使用图像的传输和失真扩展了训练数据。在对网络进行了高级数据培训之后,他们将其准确性提高到98.9%。他们还尝试了所谓的“弹性变形”是一种特殊的图像变形,旨在消除手部肌肉的随机振动。使用弹性变形扩展数据,它们的精度达到了99.3%。从本质上讲,他们扩展了网络经验,给她真实手写中发现的各种手写变化。

这种想法的变体不仅可以用于手写识别,还可以用于改善许多学习任务的性能。一般原则是通过对训练数据应用反映实际遇到的变化的操作来扩展训练数据。这样的变化很容易想到。假设我们正在创建用于语音识别的NS。即使背景噪声等失真,人们也可以识别语音。因此,您可以通过添加背景噪声来扩展数据。我们还能够识别出语音加速和慢速。这是扩展训练数据的另一种方法。并非总是使用这些技术-例如,与其通过添加噪声来扩展训练集,不如通过将噪声滤波器应用于输入来清理输入,可能会更加有效。但是,值得记住的是扩大培训范围的想法,并寻找使用方法。

锻炼身体


  • 如上所述,从MNIST扩展训练数据的一种方法是使用训练图片的小旋转。如果我们允许以任何角度旋转图片,会出现什么问题?

大数据离题和分类精度比较的意义


让我们再次看一下NS的准确性如何根据训练集的大小而变化:


假设不使用NS,而是使用另一种机器学习技术对数字进行分类。例如,让我们尝试使用我们在第1章中简要介绍的支持向量机(SVM)方法。届时,如果您不熟悉SVM,请不必担心,我们不需要了解其详细信息。我们将通过scikit-learn库使用SVM。这是SVM的有效性随训练集大小而变化的方式。为了进行比较,我提出了国民议会的时间表和结果。


可能第一件事引起您的注意-在任何规模的训练集中,NS都超过SVM。这很好,尽管不值得从中得出深远的结论,因为我使用了预定义的scikit-learn设置,并且我们在NS上非常认真地工作。从图表中得出的一个不太生动但更有趣的事实是,如果我们使用50,000张图像训练我们的SVM,它将比使用5000张图像训练的NS(94.48%的精度)更好地工作(94.48%的精度)。 93.24%)。换句话说,训练数据量的增加有时会补偿MO算法中的差异。

更有趣的事情可能会发生。假设我们试图使用两个算法MO,A和B解决问题。有时,在一组训练数据上算法A领先于算法B,而在另一组训练数据上算法B领先于算法A。我们没有在上面看到这一点-然后图形会相交-但这发生了。这个问题的正确答案是:“算法A是否优于算法B?”实际上,这是:“您使用的是什么训练数据集?”

在开发过程中和阅读科学论文时,都必须考虑所有这些因素。许多作品集中于寻找新的技巧,以在标准测量数据集上获得更好的结果。 “我们的超级食品技术使我们在标准比较组Y上提高了X%”-这项研究中的规范申请表。有时这样的陈述实际上很有趣,但是值得理解它们仅适用于特定的训练集。想象一下另一个故事,其中最初创建比较集的人们获得了更大的研究经费。他们可以使用额外的钱来收集其他数据。超级duper技术的“改进”可能会在更大的数据集上消失。换句话说,改进的本质可能只是偶然。因此,应将以下道德准则纳入实际应用领域:我们既需要改进的算法,又需要改进的训练数据。寻找改进的算法并没有错,但是请确保您不要专注于此,而忽略通过增加训练数据的数量或质量来赢得胜利的更简单方法。

挑战赛


  • . ? . – , , , . , . - ? , .

总结


我们已经完成了对再培训和正规化的沉浸。 当然,我们将回到这些问题。 正如我多次提到的,再培训在NS领域是一个大问题,尤其是随着计算机变得越来越强大并且我们可以训练更大的网络时。 结果,迫切需要开发有效的正则化技术以减少再培训,因此该领域今天非常活跃。

重量初始化


创建NS时,需要选择权重和偏移量的初始值。 到目前为止,我们已经按照第1章中简要介绍的准则选择了它们。我记得我们是根据独立的高斯分布选择权重和偏移量的,数学期望为0,标准差为1。这种方法效果很好,但似乎相当武断,所以值得对其进行修改,并考虑是否有可能找到一种更好的方法来分配初始权重和位移,并且也许可以帮助我们的NS更快地学习。

事实证明,与归一化的高斯分布相比,可以大大改善初始化过程。 为了理解这一点,我们使用一个带有大量输入神经元(例如1000)的网络进行工作。我们使用归一化的高斯分布来初始化连接到第一个隐藏层的权重。 到目前为止,我将只关注将输入神经元连接到隐藏层中第一个神经元的尺度,而忽略网络的其余部分:


为简单起见,让我们想象一下,我们正在尝试使用输入x训练网络,其中输入神经元的一半处于打开状态,即它们的值为1,而输入神经元的一半处于禁用状态,即它们的值为0。在这个特定的例子上会理解他的。 考虑隐藏神经元的输入的加权和z = ∑ j w j x j + b。 总和的500个成员消失,因为对应的x j为0。因此,z是501个归一化的高斯随机变量,500个权重和1个附加偏移的和。 因此,z值本身具有高斯分布,其数学期望为0,标准偏差为√501≈22.4。 也就是说,z具有相当宽的高斯分布,没有尖峰:


特别是,该图表明| z |可能很大,即z≫ 1或z≫ -1。 在这种情况下,隐藏神经元σ(z)的输出将非常接近1或0。这意味着我们的隐藏神经元将饱和。 正如我们已经知道的那样,当这种情况发生时,重量的微小变化将在隐藏神经元的激活中产生微小的变化。 这些微小的变化实际上将不会影响网络中剩余的中子,我们将在成本函数中看到相应的微小变化。 结果,当我们使用梯度下降算法时,这些权重将被非常缓慢地训练。 这类似于本章已经讨论的任务,在该任务中,输出神经元的值不正确会导致学习变慢。 我们曾经通过巧妙地选择成本函数来解决这个问题。 不幸的是,尽管这对饱和输出神经元有所帮助,但对隐藏神经元的饱和完全没有帮助。

现在,我讨论了第一个隐藏层的传入比例。 自然,相同的论点也适用于以下隐藏层:如果使用规范化的高斯分布初始化后面的隐藏层中的权重,则它们的激活通常将接近0或1,并且训练将非常缓慢。

有没有一种方法可以为权重和偏移量选择最佳的初始化选项,以使我们不会陷入饱和状态,并且可以避免学习延迟? 假设我们有一个神经元,传入权重为n in 。 然后我们需要用随机高斯分布初始化这些权重,数学期望为0,标准偏差为1 /√n( in) 。 也就是说,我们压缩高斯信号,并减少神经元饱和的可能性。 然后,我们为位移选择一个高斯分布,其数学期望为0,标准偏差为1,原因是我稍后再讲。 做出选择之后,我们再次发现z = ∑ j w j x j + b将是一个随机变量,其高斯分布的数学期望为0,但峰值比以前明显得多。 像以前一样,假设500个输入为0,而500个输入为1。则很容易证明(见下面的练习)z具有高斯分布,其数学期望为0,标准偏差为√(3/2)= 1.22 ...该图的峰值更加锐利,以至于即使在下面的图片中,情况也有些被低估了,因为与上一张图相比,我不得不更改垂直轴的比例:


这样的神经元将以低得多的概率饱和,因此,它不太可能遇到学习减慢的情况。

锻炼身体


  • 确认与上一段的z = ∑ j w j x j + b的标准偏差为√(3/2)。 支持以下考虑:独立随机变量之和的方差等于各个随机变量的方差之和; 方差等于标准偏差的平方。

上面我提到过,我们将像以前一样继续基于独立的高斯分布初始化位移,其数学期望为0,标准偏差为1。这是正常现象,因为它不会大大增加神经元饱和的可能性。 实际上,如果我们设法避免饱和问题,则偏移量的初始化并不重要。 有些人甚至尝试将所有偏移量初始化为零,并依靠梯度下降可以学习适当偏移量这一事实。 但是由于这将影响某事的可能性很小,因此我们将继续使用与以前相同的初始化过程。

让我们比较一下使用MNIST的数字分类任务来初始化权重的旧方法和新方法的结果。 和以前一样,我们将使用30个隐藏的神经元,一个大小为10的微型数据包,一个正则化参数&lambda = 5.0,以及具有交叉熵的代价函数。 我们将逐渐将学习速度从η= 0.5降低到0.1,因为这样一来,结果在图上的显示效果会更好一些。 您可以使用旧的权重初始化方法来学习:

>>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper() >>> import network2 >>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.large_weight_initializer() >>> net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True) 

您还可以学习使用新方法初始化权重。 这甚至更简单,因为默认情况下network2使用新方法初始化权重。 这意味着我们可以更早地省略net.large_weight_initializer()调用:

 >>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True) 

我们绘图(使用程序weight_initialization.py):


在这两种情况下,分类精度均达到96%。 两种情况下产生的精度几乎相同。 但是新的初始化技术可以更快地达到这一点。 在上一个训练时代的末期,用于初始化权重的旧方法达到了87%的准确性,而新方法已经达到93%。 显然,一种初始化权重的新方法从一个更好的位置开始,因此我们可以更快地获得良好的结果。 如果我们构建具有100个神经元的网络的结果,则会观察到相同的现象:


在这种情况下,不会出现两条曲线。 但是,我的实验表明,如果您添加更多的纪元,那么准确性就会开始几乎重合。 因此,根据这些实验,可以说改进权重的初始化只会加快训练速度,而不会改变整体网络效率。 但是,在第4章中,我们将看到NS的示例,这些示例中的权重通过1 /√n 进行初始化,从而大大提高了长期效率。 因此,它不仅提高了学习速度,而且有时还提高了效果。

通过1 /√nin初始化权重的方法有助于改善神经网络的训练。 已经提出了用于初始化权重的其他技术,其中许多技术都基于该基本思想。 我不会在这里考虑它们,因为1 /√n对于我们的目的而言效果很好。 如果您有兴趣,我建议阅读Yoshua Benggio在2012年的论文中第14和15页的讨论。

挑战赛


  • 正则化和改进的权重初始化方法的结合。 有时L2的正则化会自动为我们提供类似于初始化权重的新方法的结果。 假设我们使用旧的方法来初始化权重。 概述证明以下观点的启发式论点:(1)如果λ不太小,则在训练的最初时期,权重的减弱将几乎完全占据主导地位; (2)如果ηλ≪ n,则权重将在时代减少e- ηλ/ m倍; (3)如果λ不太大,当权重减小到大约1 /√n时,权重的减弱将减慢,其中n是网络中权重的总数。 在本节中为其构造图的示例中证明满足这些条件。


返回手写识别:代码


让我们实现本章中描述的想法。 我们将开发一个新程序network2.py,它是我们在第1章中创建的network.py程序的改进版本。如果您很长一段时间没有看到其代码,则可能需要快速浏览一下。 这些只是74行代码,很容易理解。

与network.py一样,network2.py的核心是Network类,我们用它来表示我们的NS。 我们使用相应网络层的大小列表初始化类实例,并选择成本函数,默认情况下它将是交叉熵:

 class Network(object): def __init__(self, sizes, cost=CrossEntropyCost): self.num_layers = len(sizes) self.sizes = sizes self.default_weight_initializer() self.cost=cost 

__init__方法的前几行与network.py相同,并且可以自己理解。 接下来的两行是新的,我们需要详细了解它们在做什么。

让我们从default_weight_initializer方法开始。 他使用一种改进的新方法来初始化权重。 如我们所见,在这种方法中,进入神经元的权重是基于独立的高斯分布进行初始化的,其数学期望为0,标准差为1除以与神经元的传入链接数的平方根。 同样,此方法将使用高斯分布(平均数为0,标准偏差为1)来初始化偏移。下面是代码:

  def default_weight_initializer(self): self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]] self.weights = [np.random.randn(y, x)/np.sqrt(x) for x, y in zip(self.sizes[:-1], self.sizes[1:])] 

要理解它,您需要记住np是一个处理线性代数的Numpy库。 我们在程序开始时将其导入。 还要注意,我们没有在神经元的第一层初始化位移。 第一层是入站的,因此不使用偏移量。 network.py也是如此。

除了default_weight_initializer方法之外,我们还将创建一个large_weight_initializer方法。 它使用第1章中的旧方法来初始化权重和偏移量,其中权重和偏移量是基于独立的高斯分布进行初始化的,其数学期望为0,标准差为1。此代码与default_weight_initializer的区别不大:

  def large_weight_initializer(self): self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(self.sizes[:-1], self.sizes[1:])] 

我之所以加入这种方法,主要是因为它使我们可以更方便地比较本章和第1章的结果。我无法想象有任何实际的选择建议使用它!

__init__方法的第二个新颖之处是cost属性的初始化。 为了了解其工作原理,让我们看一下用来表示交叉熵代价函数的类(@staticmethod指令告诉解释器此方法独立于对象,因此self参数不会传递给fn和delta方法)。

 class CrossEntropyCost(object): @staticmethod def fn(a, y): return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a))) @staticmethod def delta(z, a, y): return (ay) 

让我们弄清楚。 在这里可以看到的第一件事是,尽管从数学的角度来看,交叉熵是一个函数,但我们将其实现为python类,而不是python函数。 我为什么决定这样做? 在我们的网络中,价值扮演着两个不同的角色。 显而易见-它是输出激活a与所需输出y对应程度的度量。 此角色由CrossEntropyCost.fn方法提供。 (顺便说一句,请注意,在CrossEntropyCost.fn中调用np.nan_to_num可确保Numpy正确处理接近零的数字的对数)。 但是,成本函数在第二种方式用于我们的网络中。 我们从第2章回想起,在启动反向传播算法时,我们需要考虑网络δL的输出误差 输出误差的形式取决于成本函数:不同的成本函数将具有不同形式的输出误差。 对于交叉熵,如公式(66)所示,输出误差将等于:

 d Ë 一个大号 = 一个大号 - Ÿ \标99



因此,我定义了第二种方法CrossEntropyCost.delta,其目的是向网络解释如何计算输出误差。 然后,我们将这两种方法合并为一个类,其中包含我们的网络需要了解的有关成本函数的所有内容。

由于类似的原因,network2.py包含一个代表二次成本函数的类。 将其包括在内以与第一章的结果进行比较,因为将来我们将主要使用交叉熵。 代码如下。 QuadraticCost.fn方法是对与输出a和所需输出y相关的二次成本的简单计算。 QuadraticCost.delta返回的值基于表达式(30)的平方值的输出误差,我们在第二章中得出了该值。

 class QuadraticCost(object): @staticmethod def fn(a, y): return 0.5*np.linalg.norm(ay)**2 @staticmethod def delta(z, a, y): return (ay) * sigmoid_prime(z) 

现在,我们已经找到了network2.py和network2.py之间的主要区别。 一切都非常简单。 我将在下面描述其他一些小的更改,包括L2正则化的实现。 在此之前,让我们看一下完整的network2.py代码。 不必对其进行详细研究,但是值得了解其基本结构,尤其是阅读注释以了解程序的每个部分的功能。 当然,我不会禁止您尽可能多地研究这个问题! 如果迷路了,请尝试在程序后阅读文本,然后再次返回代码。 通常,这里是:

 """network2.py ~~~~~~~~~~~~~~   network.py,            .   –      , ,   .     ,    .   ,       . """ ####  #  import json import random import sys #  import numpy as np ####   ,      class QuadraticCost(object): @staticmethod def fn(a, y): """ ,    ``a``    ``y``. """ return 0.5*np.linalg.norm(ay)**2 @staticmethod def delta(z, a, y): """  delta   .""" return (ay) * sigmoid_prime(z) class CrossEntropyCost(object): @staticmethod def fn(a, y): """ ,    ``a``    ``y``. np.nan_to_num    .  ,   ``a``  ``y``      1.0,   (1-y)*np.log(1-a)  nan. np.nan_to_num ,       (0.0). """ return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a))) @staticmethod def delta(z, a, y): """  delta   .  ``z``    ,          delta     . """ return (ay) ####   Network class Network(object): def __init__(self, sizes, cost=CrossEntropyCost): """  sizes      .  ,      Network      ,     ,     ,    ,  [2, 3, 1].       ,   ``self.default_weight_initializer`` (.  ). """ self.num_layers = len(sizes) self.sizes = sizes self.default_weight_initializer() self.cost=cost def default_weight_initializer(self): """            0    1,       ,       .          0    1.    ,         ,           . """ self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]] self.weights = [np.random.randn(y, x)/np.sqrt(x) for x, y in zip(self.sizes[:-1], self.sizes[1:])] def large_weight_initializer(self): """          0    1.          0    1.    ,         ,           .         1,    .       . """ self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(self.sizes[:-1], self.sizes[1:])] def feedforward(self, a): """  ,  ``a``  .""" for b, w in zip(self.biases, self.weights): a = sigmoid(np.dot(w, a)+b) return a def SGD(self, training_data, epochs, mini_batch_size, eta, lmbda = 0.0, evaluation_data=None, monitor_evaluation_cost=False, monitor_evaluation_accuracy=False, monitor_training_cost=False, monitor_training_accuracy=False): """     -    . ``training_data`` –   ``(x, y)``,       .       ,    ``lmbda``.    ``evaluation_data``,     ,   .         ,     ,   .      :   ,    ,   ,              .  ,      30 ,        30 ,        .     ,   . """ if evaluation_data: n_data = len(evaluation_data) n = len(training_data) evaluation_cost, evaluation_accuracy = [], [] training_cost, training_accuracy = [], [] for j in xrange(epochs): random.shuffle(training_data) mini_batches = [ training_data[k:k+mini_batch_size] for k in xrange(0, n, mini_batch_size)] for mini_batch in mini_batches: self.update_mini_batch( mini_batch, eta, lmbda, len(training_data)) print "Epoch %s training complete" % j if monitor_training_cost: cost = self.total_cost(training_data, lmbda) training_cost.append(cost) print "Cost on training data: {}".format(cost) if monitor_training_accuracy: accuracy = self.accuracy(training_data, convert=True) training_accuracy.append(accuracy) print "Accuracy on training data: {} / {}".format( accuracy, n) if monitor_evaluation_cost: cost = self.total_cost(evaluation_data, lmbda, convert=True) evaluation_cost.append(cost) print "Cost on evaluation data: {}".format(cost) if monitor_evaluation_accuracy: accuracy = self.accuracy(evaluation_data) evaluation_accuracy.append(accuracy) print "Accuracy on evaluation data: {} / {}".format( self.accuracy(evaluation_data), n_data) print return evaluation_cost, evaluation_accuracy, \ training_cost, training_accuracy def update_mini_batch(self, mini_batch, eta, lmbda, n): """    ,          -. ``mini_batch`` –    ``(x, y)``, ``eta`` –  , ``lmbda`` -  , ``n`` -     .""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [(1-eta*(lmbda/n))*w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] def backprop(self, x, y): """  ``(nabla_b, nabla_w)``,      C_x. ``nabla_b``  ``nabla_w`` -    numpy,   ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] #   activation = x activations = [x] #      zs = [] #     z- for b, w in zip(self.biases, self.weights): z = np.dot(w, activation)+b zs.append(z) activation = sigmoid(z) activations.append(activation) # backward pass delta = (self.cost).delta(zs[-1], activations[-1], y) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta, activations[-2].transpose()) """  l      ,      . l = 1    , l = 2 – ,   .    ,   python      . """ for l in xrange(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w) def accuracy(self, data, convert=False): """    ``data``,      .   –        .  ``convert``  False,    –    ( )  True,   .    - ,  ``y`` -     .  ,       .           .          ?     –       ,      .   ,      .       mnist_loader.load_data_wrapper. """ if convert: results = [(np.argmax(self.feedforward(x)), np.argmax(y)) for (x, y) in data] else: results = [(np.argmax(self.feedforward(x)), y) for (x, y) in data] return sum(int(x == y) for (x, y) in results) def total_cost(self, data, lmbda, convert=False): """      ``data``.  ``convert``   False,   –  (),   True,   –   . .    ,      ``accuracy``, . """ cost = 0.0 for x, y in data: a = self.feedforward(x) if convert: y = vectorized_result(y) cost += self.cost.fn(a, y)/len(data) cost += 0.5*(lmbda/len(data))*sum( np.linalg.norm(w)**2 for w in self.weights) return cost def save(self, filename): """    ``filename``.""" data = {"sizes": self.sizes, "weights": [w.tolist() for w in self.weights], "biases": [b.tolist() for b in self.biases], "cost": str(self.cost.__name__)} f = open(filename, "w") json.dump(data, f) f.close() ####  Network def load(filename): """    ``filename``.    Network. """ f = open(filename, "r") data = json.load(f) f.close() cost = getattr(sys.modules[__name__], data["cost"]) net = Network(data["sizes"], cost=cost) net.weights = [np.array(w) for w in data["weights"]] net.biases = [np.array(b) for b in data["biases"]] return net ####   def vectorized_result(j): """  10-    1.0   j     .      (0..9)     . """ e = np.zeros((10, 1)) e[j] = 1.0 return e def sigmoid(z): """.""" return 1.0/(1.0+np.exp(-z)) def sigmoid_prime(z): """ .""" return sigmoid(z)*(1-sigmoid(z)) 

其中更有趣的变化是包含L2正则化。 尽管这是一个重大的概念更改,但它易于实现,以至于您可能在代码中没有注意到它。 在大多数情况下,这只是将lmbda参数传递给不同的方法,尤其是Network.SGD。 所有工作都在程序的一行中完成,在Network.update_mini_batch方法的最后一行中进行第四行。 在那里,我们更改了梯度下降更新规则以包括减少重量。 变化很小,但会严重影响结果!

顺便说一下,这在神经网络中实施新技术时经常发生。 我们花了数千个单词讨论正则化。 从概念上讲,这是一件非常微妙且难以理解的事情。 但是,它可以轻松添加到程序中! 出乎意料的是,可以通过少量的代码更改来实现复杂的技术。

代码中的另一个小而重要的变化是向Network.SGD随机梯度下降方法添加了几个可选标志。这些标志使得可以跟踪training_data或Evaluation_data上的成本和准确性,并可以将其传输到Network.SGD。在本章的前面,我们经常使用这些标志,但让我给出一个使用它们的示例,以提醒您:

 >>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper() >>> import network2 >>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.SGD(training_data, 30, 10, 0.5, ... lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True, ... monitor_evaluation_cost=True, ... monitor_training_accuracy=True, ... monitor_training_cost=True) 

我们通过validation_data设置评价数据。但是,我们可以跟踪test_data和任何其他数据集的性能。我们还具有四个标志,这些标志指定需要在评估数据和培训数据上跟踪成本和准确性。这些标志默认情况下设置为False,但是此处包含了这些标志以跟踪网络的有效性。此外,来自network2.py的Network.SGD方法返回表示跟踪结果的四元素元组。您可以像这样使用它:

 >>> evaluation_cost, evaluation_accuracy, ... training_cost, training_accuracy = net.SGD(training_data, 30, 10, 0.5, ... lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True, ... monitor_evaluation_cost=True, ... monitor_training_accuracy=True, ... monitor_training_cost=True) 

因此,例如,evaluation_cost将是30个元素的列表,其中包含每个时代结束时估算数据的成本。这样的信息对于理解神经网络的行为非常有用。这样的信息对于理解网络行为非常有用。例如,它可用于绘制一段时间内的网络学习图。这就是我构建本章中所有图的方式。但是,如果未设置标志之一,则对应的元组元素将为空列表。

其他代码添加包括Network.save方法,该方法将Network对象保存到磁盘,以及将其加载到内存的功能。保存和加载是通过JSON完成的,而不是通过Python pickle或cPickle模块完成的,它们通常用于保存到磁盘并在python中加载。使用JSON需要的代码比pickle或cPickle所需的代码更多。为了理解为什么我选择JSON,请想象一下在将来的某个时候,我们决定更改Network类,以使S型神经元不止于此。为了实现此更改,我们很可能会更改Network .__ init__方法中定义的属性。而且,如果我们仅使用pickle进行保存,则加载功能将无法正常工作。结合使用JSON和显式序列化,我们可以轻松保证可以下载较旧版本的Network对象。

代码中有许多小的变化,但是这些只是network.py的微小变化。最终结果是将我们的74行程序扩展到了功能更强大的152行程序。

挑战赛


  • 通过引入正则化L1来修改下面的代码,并使用它通过具有30个隐藏神经元的网络对MNIST数字进行分类。您可以选择一个正则化参数,与没有正则化的网络相比,该参数可以改善结果吗?
  • Network.cost_derivative method network.py. . ? , ? network2.py Network.cost_derivative, CrossEntropyCost.delta. ?

Source: https://habr.com/ru/post/zh-CN459816/


All Articles