选自colab.research
作者: David Code 机器之心编译 参与:魔王、思、张倩David Code 有多个身份:他是旅行作家,通晓多国语言,他还是一名「AI 教师」。没有数学和软件背景的 David 用 18 个月的时间通过在线课程和博客自学 AI,并花费两年时间撰写了一篇长文。为了使和他一样没有数学基础的人也能入门人工智能,他在这篇文章中运用了大量类比、例子、故事、图示,将重要的知识点或步骤反复强调。这是一篇真正针对初学者的 AI 教程,不只讲概念,还讲概念的底层原理。由于原文过长,机器之心在编译过程中进行了少量删节。想了解更多细节,请查看原文链接:https://colab.research.google.com/drive/1VdwQq8JJsonfT4SV0pfXKZ1vsoNvvxcH#scrollTo=C810qURdm3hZ 。 想学 AI 又担心没有数学背景或软件背景?没关系,这篇博客非常适合你。 我曾经花费一年半的时间自学各种在线课程和博客,过程中有太多专家、太多信息,而且他们的很多观点还有冲突。我在学习过程中经常充满自我怀疑。 我不想要很多专家帮我学习,我只想要一个老师。我希望有人能够拉着我的手说:「Dave,这就是你需要学的东西,请按照这个顺序学。现在我将用图画、有趣的故事、现实示例和浅显易懂的语言教你学习 AI。」 而现在,我就是那个老师。 为什么是我?和网上的专家不同,我没有数学或编程背景,不过我曾就读于耶鲁大学和普林斯顿大学,环游过 100 多个国家,是一名旅行作家,我曾在《周六夜现场》工作,我的作品得过奖。也就是说,我知道如何通过写作来传达复杂的概念,知道如何讲故事。我热爱教学,也善于发现好的老师。在学习深度学习过程中我遇到了四位优秀的老师,他们是 Andrew Trask、Grant Sanderson、Kalid Azad 和我的导师 Adam Koenig。 最重要的是,我理解你正在经历的痛苦。你听说过「专家盲区」(expert blindness)吗?专家向新手讲授某个学科时,由于他成为专家的时间太久,会忘了初学者对教材的感受。因此专家会快速介绍一些复杂的概念,而这些概念需要分解成小块才能方便初学者掌握。或者他们不使用类比、图画或示例来帮助初学者掌握概念,导致初学者非常受挫。 每一个初学者都想要专家来教他们 AI。而事实上,你需要的不是专家,而是一名老师。 最好的教师就是那个刚刚学过这些知识的人,因为他仍然记得自己挣扎过的地方以及克服方法,并且他可以向你传授捷径。而我就是这个人。我不是专家,但我是个好老师,而且富有激情。 本文使用指南 本文的阅读过程和小说不同,只读一次是无法理解和掌握所有内容的。我学数学的朋友告诉我,他们常常需要读至少 7 遍数学文本才能开始理解。这不是开玩笑…… 为方便讲授,我使用了类比、图画、示例和几何表示。但是请放心,本文在数学层面上是准确而严谨的。请做好阅读本文五遍的准备,无法立刻领会也不要着急。 我在学习复杂材料的时候,通常会设置计时器,每五分钟响一次,不断地提醒自己不要在绝望中沉沦,要微笑、耐心并坚持下去。这真的有效,相信我。 以下是一些宏观要点:
-
神经网络是深度学习中非常流行的前沿技术;
-
深度学习是机器学习的分支;
-
机器学习是人工智能的分支。
-
前馈
-
梯度下降
-
全局最小值
-
反向传播
-
您有猫吗?
-
您喝进口啤酒吗?
-
过去一个月,您是否访问过我们的网站 LitterRip.com?
-
过去一个月,您是否购买过 Litter Rip! 猫砂?
-
前馈:想象一下 1960 年的 IBM 计算机,大到填满整个房间,穿孔卡片从一端输入,答案从另一端输出。上文提到的神经网络以前三个调查问题的数据作为输入,得出预测结果;
-
全局最小值:想象一下桌子上有一个黄色的碗(如上图所示)。桌子表面表示几乎零误差的完美预测结果,那么很显然碗底是最接近完美预测结果的位置,具备最小的误差。与碗整个表面(即「全局表面」(global surface))相比,碗底最接近完美,它具备全局最小误差值。
-
反向传播:想象一位杂技表演者,他能向空中抛接 16 个不同大小和重量的保龄球瓶,并使它们同时悬在空中,甚至可以神奇地调整保龄球瓶的大小和重量。网络在执行预测后,会返回到上一次预测的过程中,查看是否可以做一些调整,以便在下一次预测中缩小误差,推动小球向碗底前进。
-
梯度下降:想象粉色乒乓球沿着碗侧向碗底滚落,碗底即全局最小值(见上图)。网络就像那个球,碗的表面由网络的每一次预测构成。梯度下降就是球沿着碗侧滚落向碗底(即具备全局最小误差的预测)的过程。
-
梯度下降是网络在达到准确预测(即全局最小误差)前的试错过程,就像乒乓球滚落碗底的过程;
-
前馈即执行预测。预测就像给定时刻球停留在碗表面某个位置的定格图像;
-
全局最小值即预测几乎没有误差的完美位置(碗底)。我们的目标是到达碗底。网络将预测结果与真值进行对比,来衡量球目前位置与碗底的距离(误差);
-
反向传播即返回到上一次预测,找出错误并修正。反向传播衡量球现在位置到其下桌面的距离(即误差),并找出推动球向碗底前进的方法。
图源:https://www.youtube.com/watch?v=IHZwWFHWa-w&t=2s&index=3&list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi
上图中红色的碗有一些凸起和凹陷,为什么会这样呢? 首先,我们必须理解红色碗由什么构成。从图中看它似乎是塑料做的……不过并不是,想象它由数百万个红点构成,每个红点都是 3D 网格中的一个点。每个点表示一个可能的调查问题组合,以及该组合中哪个问题对于网络执行预测的等级更高。这个凹凸不平的碗让我们看到了所有可能组合的凹凸不平的表面。 记住这幅图:红色碗是「所有排列组合的表面」,它像山脉一样有高峰、深谷、山岭、河床。网络无法穷尽浩瀚宇宙中每一个可能的排列组合,这会花费太多计算时间。因此,它从你随机「抛掷预测球」的位置出发,一路向下走向谷底(即全局最小值)。网络不需要管它从哪里开始出发,它只需要在意从随机的起始点到达碗底即可。 在解释红色的凹凸不平处之前,我们先来看从红碗右上角开始的白色虚线。这条线的顶端就是网络的第一次预测。还记得上文介绍的红色乒乓球吗?假设乒乓球就在白线顶端处,白色虚线表示网络预测结果从起始点到全局最小值的路径。也就是说乒乓球沿着这条白色虚线走到误差最小、预测准确率最高的地方。 但是为什么这条白色虚线路径如此弯曲呢?原因在于,网络的常规试验需要考虑组合哪些问题、每个问题的权重多大,才能得到误差最小的最佳预测结果。网络的常规目标是尽可能降低误差,即让乒乓球尽快到达红碗底部。因此网络通常采用虚线上球所在点的坡度,来确定哪个方向具备最陡峭的坡度,能够实现最快的向下路径。由于坡度不断改变,这条路径就很曲折。我们来看一个简单的例子: 人类第一眼就会判断这两个问题(「你有猫吗?」和「你访问过我们网站吗?」)是做出预测的基础问题。但是哪个问题对预测准确度影响最大呢?第一个问题影响更大,还是二者五五分? 网络想用这两个问题的不同权重组合进行试验,从而找出实现准确预测的最佳组合。上图红色碗中的每个凸起都表示「走在错误道路上」的问题组合和问题权重,因为每个凸起都使网络的「预测球」愈发偏离底部的全局最小值。而碗中的每个凹陷都表示「走在正确道路上」的问题组合,因为它使预测球离碗底更近。 但是,如果网络找出了三个调查问题的完美权重组合,但预测准确率仍然只有 60% 左右,该怎么办?不要怕,网络还有另一个技巧:推断问题(inferred question)。 下面我们用一个简单示例讲述这个重要概念: 我们回过头看关于进口啤酒的那个问题。神经网络不停尝试不同的问题组合。那么举例来说,或许只有富人才有钱买进口喜力啤酒,而我们都知道养猫的人很可能是富人。(本猫奴的内心:不,我不是,我没有……)那么也许当「你喝进口啤酒吗?」和「你有猫吗?」这两个问题在网络计算过程中是组合问题且权重较高时,预测结果会得到改进。 这里的推断问题就是「富人更有可能购买 Litter Rip 猫砂吗?」。不过我们刚才做了一个(愚蠢的)推断:喝进口喜力啤酒和养猫的人可能比较富有。 但是,随着粉色预测球在红色碗中兜兜转转,它到了凸起处,而原因在于网络使用了这个推断问题进行试验。也就是说该推断问题对于改善预测准确率并没有帮助。(可能是因为猫砂不是奢侈品而是养猫者的必需品。)在接下来的迭代中,预测球离开凸起处,不再使用这个无用的推断问题。但是它也有可能因为有用的推断问题掉进凹陷处,从而帮助它更快地到达碗底。 上文的示例展示了如何测试推断问题能否有效帮助实现更准确的预测结果。人类使用模糊逻辑探索推断问题,而神经网络尝试穷尽每一种排列组合。如果试验获得了较好的预测结果,它会保留该问题。如果试验得到了更糟的预测结果,网络将抛弃该问题。 总之,红色碗表面上每一个红点表示网络使用特定问题组合和权重组合进行的一次试验。每一个凹陷表示「这一步走在了正确的道路上」,网络注意到这一点并将继续保持。每一个凸起处表示「这一步走错了方向」,网络也会注意到并摒弃它。预测球的路径(白色虚线)歪歪扭扭,是因为预测球在通往碗底的路上不断寻找凹陷处、避免凸起处。你可以说白色虚线是预测球通往碗底的最高效路径。 现在我们转回来,更详细地了解一下「前馈」、「全局最小值」、「反向传播」和「梯度下降」。 1.3.1 前馈:将穿孔卡片输入到 1960 年的 IBM 计算机 前馈的目标是创建预测。假设每一次预测是预测球向碗底前进过程中的一个新位置。在上图红色碗中,网络做的第一次预测由白色虚线右上角的点表示,我们假设预测球就在这个点上。前馈是创建第一次预测的过程。白色虚线上的下一个点即是第二次预测,这样预测球移动到下一个点,之后再进行第三次预测。 大部分 AI 课程和博客不会提及,预测球所在的每个位置由红色碗底部白色网格的两个坐标轴决定。红色碗并非在空间中漂移,它位于白色网格之上,该网格具备 X 轴和 Y 轴。 还记得上图介绍全局最小值时所用的图像吗?黄色碗位于桌面上。同理,红色碗位于白色网格上。白色网格即「桌面」,「完美、无误差的预测位置」即红色碗实际位于的点。注意,碗与桌面唯一的连接点是碗底,即白色虚线结束的点(全局最小值)。 预测球在红色碗中的每次停顿,即每次预测点,都由 3 个坐标轴决定:X 轴和 Y 轴表示网格位置,Z 轴表示预测球到网格的距离。 现在让我们跳出抽象描述。3D 空间中这三个坐标轴在现实生活中表示什么? 回忆一下,红色碗表面上每一个红点表示网络使用特定问题组合和权重组合进行的一次试验,那么网络如何执行试验呢?它使用 X 轴和 Y 轴在白色网格上定一个位置,表示特定的问题组合。X 轴和 Y 轴必然会问:「嗨,Z 轴你好,这个组合怎么样?」Z 轴将告诉我们该组合的效果——Z 轴负责衡量误差。 我们之前提到,碗底,即碗与白色网格相连接的位置,是完美的预测。那么碗中每一个红点都表示误差,离白色网格越远,误差就越大。 这点非常重要:白色网格表示网络可以尝试的问题及权重组合。对于白色网格上的每一个点,其上方的红点表示该组合的误差值。也就是说,红色碗上的每个红点表示其下方预测的误差。红色碗就是「误差组成的碗」。只有碗底,红点接触白色网格的地方,二者之间没有距离,也因此没有误差。参见下图: 上图中的锥形黄色箭头表示 Z 轴,用于衡量误差。它很有用,你可以从中了解误差值并学习,以便下次得到更加准确的预测结果。 想象一下,粉色的预测球在白色虚线上移动,黄色箭头总是在预测球下方,并随之移动。粉色球表示网络做出的每次预测,黄色箭头衡量预测球到白色网格的距离,即衡量每次预测的误差。它的运行原理是什么? 你可能会想到前馈——以前三个调查问题作为输入并用不同方式进行问题组合,再执行预测,预测结果为 0 到 1 之间的数字,即概率。想象预测球所在的点表示在白色虚线上的数字。我们知道第四个问题的答案是「1」,因此真值是该顾客买过新款猫砂。真值即是全局最小值,是我们改进预测结果的目标,也是红色碗和白色网格的接触点。因此,网络用 1 减去预测概率。举例来说,如果网络的第一次预测为 0.5,即该顾客有 50% 的可能性购买过这款猫砂,1 减去 50% 所得的数字表示误差,黄色箭头就用于衡量该误差。某种程度上可以说,「真值数字 - 预测球数字 = 黄色箭头数字」。在该示例中,即 1 - 0.5 = 0.5。误差(黄色箭头的长度)为 0.5(误差通常用绝对值表示,不能为负值)。 没有第四个调查问题的话,你无法训练网络,因为你需要真值来测试预测结果。黄色箭头衡量网络预测与真值的距离,即误差,这个例子中真值为 1。我们的第一次预测(0.5)准确率并不高,0.99 的预测才可以说比较准确。 1.3.2 找到全局最小值 我们的目标是训练神经网络找出减少预测误差的最快方式,也就是说让黄色箭头变短,这意味着我们需要预测球从起始点到碗底的最高效路径。碗底即「乌托邦」,在那里黄色箭头(预测误差)的长度几乎为 0(全局最小值),即我们的预测具备最小误差,网络达到非常高的准确率。而预测球到达碗底的最高效路径是那条白色虚线。一旦我们使用第一个数据集完成训练,网络就可以准确预测另一拨潜在新客户购买新款猫砂的概率。 现在想象这个场景:预测球从白色虚线顶端开始,此时黄色箭头(即第一次预测的误差值)等于 0.5。我们如何让球到达碗底呢?也就是说,我们如何调整网络,使预测球(目前 X,Y 坐标为 (3,3))在白色网格上移动到碗底(碗底点的坐标大致为 (3,0))。目前,我们仅能基于顾客 1 的答案执行预测。那么,我们如何基于每个顾客的回答改进之后每一次预测的结果,直到预测误差几乎为 0?也就是预测球到达碗底,网络训练得足够好,可以利用新数据集做出预测。 找到从不那么准确的预测结果 0.5 到最终第 6 万次预测结果 0.9999 的每一步路径,这就是梯度下降过程。 1.3.3 梯度下降和反向传播 梯度下降表示网络的试错学习过程。对于数学 nerd 来说,它可定义为一个总体规划:改变突触权重、在每次迭代时最大程度上降低误差。 对于普通人来说,这里有一个更好的解释:将小球沿着最陡峭的坡度滚下,尽快地到达碗底。反向传播是一种计算梯度的方法,梯度其实就是坡度(slope)。反向传播告诉你预测球所在位置的坡度。 坡度在这里非常重要。所有专家都会使用术语「梯度下降」,而梯度的意思就是「坡度」。找到预测球在碗表面位置点的坡度,可以指示出球尽快到达碗底应该移动的方向。 但是,为什么是坡度呢?请考虑梯度下降的过程: 首先,计算机执行前馈预测。用真值(全局最小值所在位置,即碗底)减去预测得到误差(黄色箭头长度),然后使用反向传播计算误差的坡度。坡度决定了预测球滚动的方向和速度(即网络应该在数字方面做出多大的调整)。 找出坡度是反向传播的重要工具。如果说梯度下降是总体规划,那么反向传播就是达成规划的主要工具。 2. 28 行代码创建神经网络 现在我们来看代码。我将首先展示今天要学习的全部代码,然后再进行详细的步骤讲解。建议在两个窗口中打开本文,一个窗口显示代码,另一个继续往下阅读。从下文代码注释中可以看到,我将构建神经网络的过程分解成了 13 个步骤。现在准备好观看计算机从它所犯的错误中学习并最终识别出模式了吗? 不过,首先我们先来了解矩阵和线性代数的概念。 在下文的代码中,你会看到单词「matrix」或「matrices」(矩阵)。它非常重要:矩阵是这辆汽车的引擎。没有矩阵,神经网络哪儿都去不了。 矩阵是多行数字的集合。你将遇到的第一个矩阵如下所示,它容纳了宠物店顾客调查的数据。[1,0,1],[0,1,1],[0,0,1],[1,1,1]
将每一行想象为一名顾客,那么以上矩阵中共有 4 位顾客,每一行的方括号中包含三个数字(1 和 0 分别表示「是」和「否」)。在 1.2 节中,我们看到顾客 1 对前三个调查问题的答案是「Yes/No/Yes」,因此该矩阵的第一行为 1, 0 ,1。第一列是四位顾客对第一个问题「你有猫吗?」的回答。
为了更详细地介绍矩阵这一概念,请看下图。图中表示的矩阵与上面的矩阵相同,不过它多了一些标签和颜色,用于强调顾客的回复是一行,特征/问题是一列:
上图厘清了一个最初使我非常困惑的点:行与列之间的关系。
在该矩阵中,每一位顾客的数据被表示为一行中的三个数字。在神经网络图示中(神经元和突触格式),输入层是一个包含三个圆形神经元的列。你需要注意到,每个神经元并不表示一位顾客,即矩阵中的一行数据。相反,每个神经元表示一个特征,即矩阵中的一列数据。因此,一个神经元内包含所有顾客对同一个问题/特征的答案。拿第一个问题「你有猫吗?」举例,我们只选取了四位顾客的数据,因此上图中该问题的回复中只有四个数字(两个 0、两个 1),而如果我们选取了一百万名顾客的数据,则上图中该神经元内会包含一百万个数字,表示这些顾客对该问题的回复。
到这里,我希望大家能够理解我们为什么需要矩阵:因为我们有不止一位顾客。在下面的神经网络示例中,我们描述了四位顾客,所以我们需要四行数字。
该网络包括不止一个问题。每个问题需要一列,因此我们有三列,分别表示对前三个调查问题的回答(第四个问题将出现在后面的另一个矩阵中)。
因此我们的矩阵很小,4 行 X 3 列(4 by 3)。不过真实神经网络的矩阵可能拥有数百万位顾客和数百个调查问题。做图像识别的神经网络可能具备数十亿个「顾客」行和特征列。
总结一下,我们需要矩阵使所有数据清晰展现,方便我们在其上进行计算,这样矩阵就把数据组织成美观、整洁的行与列。
下面我们来看代码,代码选取自 Andrew Trask 的博客,不过注释是我写的。
代码
#This is the "3 Layer Network" near the bottom of: #http://iamtrask.github.io/2015/07/12/basic-python-network/#First, housekeeping: import numpy, a powerful library of math tools.5 import numpy as np#1 Sigmoid Function: changes numbers to probabilities and computes confidence to use in gradient descent8 def nonlin(x,deriv=False):9 if(deriv==True):10 return x*(1-x)11 12 return 1/(1+np.exp(-x))#2 The X Matrix: This is the responses to our survey from 4 of our customers, #in language the computer understands. Row 1 is the first customer's set of #Yes/No answers to the first 3 of our survey questions: #"1" means Yes to, "Have cat who poops?" The "0" means No to "Drink imported beer?"#The 1 for "Visited the LitterRip.com website?" means Yes. There are 3 more rows#(i.e., 3 more customers and their responses) below that. #Got it? That's 4 customers and their Yes/No responses #to the first 3 questions (the 4th question is used in the next step below). #These are the set of inputs that we will use to train our network.23 X = np.array([[1,0,1],24 [0,1,1],25 [0,0,1],26 [1,1,1]])#3The y Vector: Our testing set of 4 target values. These are our 4 customers' Yes/No answers #to question four of the survey, "Actually purchased Litter Rip?" When our neural network#outputs a prediction, we test it against their answer to this question 4, which #is what really happened. When our network's#predictions compare well with these 4 target values, that means the network is #accurate and ready to take on our second dataset, i.e., predicting whether our #hot prospects from the (hot) veterinarian will buy Litter Rip!34 y = np.array([[1],35 [1],36 [0],37 [0]])#4 SEED: This is housekeeping. One has to seed the random numbers we will generate#in the synapses during the training process, to make debugging easier.40 np.random.seed(1)#5 SYNAPSES: aka "Weights." These 2 matrices are the "brain" which predicts, learns#from trial-and-error, then improves in the next iteration. If you remember the #diagram of the curvy red bowl above, syn0 and syn1 are the #X and Y axes on the white grid under the red bowl, so each time we tweak these #values, we march the grid coordinates of Point A (think, "moving the yellow arrow")#towards the red bowl's bottom, where error is near zero.47 syn0 = 2*np.random.random((3,4)) - 1 # Synapse 0 has 12 weights, and connects l0 to l1.48 syn1 = 2*np.random.random((4,1)) - 1 # Synapse 1 has 4 weights, and connects l1 to l2.#6 FOR LOOP: this iterator takes our network through 60,000 predictions, #tests, and improvements.52 for j in range(60000): #7 FEED FORWARD: Think of l0, l1 and l2 as 3 matrix layers of "neurons" #that combine with the "synapses" matrices in #5 to predict, compare and improve. # l0, or X, is the 3 features/questions of our survey, recorded for 4 customers.57 l0=X58 l1=nonlin(np.dot(l0,syn0))59 l2=nonlin(np.dot(l1,syn1)) #8 The TARGET values against which we test our prediction, l2, to see how much #we missed it by. y is a 4x1 vector containing our 4 customer responses to question #four, "Did you buy Litter Rip?" When we subtract the l2 vector (our first 4 predictions) #from y (the Actual Truth about who bought), we get l2_error, which is how much #our predictions missed the target by, on this particular iteration.66 l2_error = y - l2 #9 PRINT ERROR--a parlor trick: in 60,000 iterations, j divided by 10,000 leaves #a remainder of 0 only 6 times. We're going to check our data every 10,000 iterations #to see if the l2_error (the yellow arrow of height under the white ball, Point A) #is reducing, and whether we're missing our target y by less with each prediction.72 if (j% 10000)==0:73 print("Avg l2_error after 10,000 more iterations: "+str(np.mean(np.abs(l2_error))))#10 This is the beginning of back propagation. All following steps share the goal of # adjusting the weights in syn0 and syn1 to improve our prediction. To make our # adjustments as efficient as possible, we want to address the biggest errors in our weights. # To do this, we first calculate confidence levels of each l2 prediction by # taking the slope of each l2 guess, and then multiplying it by the l2_error. # In other words, we compute l2_delta by multiplying each error by the slope # of the sigmoid at that value. Why? Well, the values of l2_error that correspond # to high-confidence predictions (i.e., close to 0 or 1) should be multiplied by a # small number (which represents low slope and high confidence) so they change little. # This ensures that the network prioritizes changing our worst predictions first, # (i.e., low-confidence predictions close to 0.5, therefore having steep slope). 88 l2_delta = l2_error*nonlin(l2,deriv=True) #11 BACK PROPAGATION, continued: In Step 7, we fed forward our input, l0, through #l1 into l2, our prediction. Now we work backwards to find what errors l1 had when #we fed through it. l1_error is the difference between the most recent computed l1 #and the ideal l1 that would provide the ideal l2 we want. To find l1_error, we #have to multiply l2_delta (i.e., what we want our l2 to be in the next iteration) #by our last iteration of what we *thought* were the optimal weights (syn1). # In other words, to update syn0, we need to account for the effects of # syn1 (at current values) on the network's prediction. We do this by taking the # product of the newly computed l2_delta and the current values of syn1 to give # l1_error, which corresponds to the amount our update to syn0 should change l1 next time.100 l1_error = l2_delta.dot(syn1.T) #12 Similar to #10 above, we want to tweak this #middle layer, l1, so it sends a better prediction to l2, so l2 will better #predict target y. In other words, tweak the weights in order to produce large #changes in low confidence values and small changes in high confidence values. #To do this, just like in #10 we multiply l1_error by the slope of the #sigmoid at the value of l1 to ensure that the network applies larger changes #to synapse weights that affect low-confidence (e.g., close to 0.5) predictions for l1.109 l1_delta = l1_error * nonlin(l1,deriv=True) #13 UPDATE SYNAPSES: aka Gradient Descent. This step is where the synapses, the true #"brain" of our network, learn from their mistakes, remember, and improve--learning! # We multiply each delta by their corresponding layers to update each weight in both of our #synapses so that our next prediction will be even better.115 syn1 += l1.T.dot(l2_delta)116 syn0 += l0.T.dot(l1_delta)#Print results!119 print("Our y-l2 error value after all 60,000 iterations of training: ")120 print(l2)
2.1 Sigmoid 函数:行 8-12
sigmoid 函数在网络学习过程中起到非常重要的作用。
「nonlin()」是一种 sigmoid 函数类型,叫做 logistic 函数。logistic 函数在科学、统计学和概率论中非常常见。此处该函数的表达有些复杂,因为这里它作为两个函数使用:
第一个函数是将矩阵(此处表示为 x)放入括号内,将每个值转换为 0 到 1 之间的数字(即统计概率)。转换过程通过代码行 12 实现:return 1/(1+np.exp(-x))。
那么为什么需要统计概率呢?神经网络不会预测 0 或 1,它不会直接吼「哇,顾客 1 绝对会买这款猫砂!」,而是预测概率:「顾客 1 有 74% 的可能购买这款猫砂」。
这里的区别很大,如果你直接预测 0 和 1,那么网络就没有改进空间了。要么对要么错。但是使用概率的话就有改进空间。你可以调整系统,每一次使概率增加或减少几个点,从而提升网络的准确率。这是一个受控的增量过程,而不是盲猜。
将数字转换成 0 到 1 之间的数字这一过程非常重要,它带给我们四大优势。下文将详细讨论这些优势,现在我们先来看 sigmoid 函数如何将其括号内每个矩阵的每个数字转换成 0 到 1 之间的数字,并使其落在下图的 S 曲线上:
图源:https://iamtrask.github.io/2015/07/12/basic-python-network/
sigmoid 的第一个函数将矩阵中的每个值转换为统计概率,而它的第二个函数如代码行 9 和 10 所示:
' if(deriv==True):return x*(1-x)'
该函数将给定矩阵中的每个值转换成 sigmoid 函数 S 曲线上特定点处的坡度。该坡度值也叫做置信度(confidence measure)。也就是说,该数值回答了这个问题:我们对该数值能够准确预测结果的自信程度如何?你可能会想:这又能怎么样呢?我们的目标是神经网络可靠地做出准这是我们的标签确的预测。而实现这一目标的最快方式就是修复置信度低、准确率低的预测,不改变置信度高、准确率高的预测。「置信度」这一概念非常重要,下文我们将深入解读它。
接下来我们看第 2 步:
2.2 创建输入 X:行 23-26
代码行 23-26 创建了一个输入值的 4x3 矩阵,可用于训练网络。X 将成为网络的 layer 0(或 l0),现在开始创建神经网络吧!
以下是我们从顾客调查中获得的特征集,只不过用计算机能理解的语言表达出来:
Line 23 creates the X input (which becomes l0, layer 0, in line 57)X: [1,0,1],[0,1,1],[0,0,1],[1,1,1]
我们有四个顾客,他们分别回答了三个问题。前面我们讨论过第一行 1,0,1 是顾客 1 的答案。将这个矩阵的每一行看作即将输入网络的训练样本,每一列就是输入的一个特征。因此矩阵 X 可被描述为下图,该矩阵即为图中的 l0:
你可能会想:矩阵 X 是怎么变成图中的 layer 0 的?稍后我会解释这一点。
接下来,我们将创建正确答案列表。
2.3 创建输出 y:行 34-37
这是我们的标签,即第四个调查问题「你是否购买过新款猫砂?」的答案。看下面这列数字,顾客 1 回答的是「Yes」,顾客 2 回答的是「Yes」,顾客 3 和 4 回答的是「No」。
如果把 y 看作「目标」值的话,我将画一个箭靶。随着网络的改进,它射出的箭离靶心越来越近。一旦网络可以根据矩阵 X 提供的输入准确预测 4 个目标值,则网络准备就绪,可以在其他数据集上执行预测了。
2.4 生成随机数:行 40
这一步是「做家务」。我们必须生成随机数(随机数将在训练过程的下一步生成突触/权重),以使 debug 过程更加简单。你不必理解这行代码的工作原理,只需要使用它就好了。
生成随机数的原因是,你必须从某个地方开始。因此我们从一组捏造数字开始,然后在 6 万次迭代中慢慢改变每一个数字,直到它们输出具备最小误差值的预测。这一步使得测试可重复(即使用同样的输入测试多次,结果仍然相同)。
2.5 创建突触(即权重):行 47-48
第一次看到下图时,你可能会认为神经网络的「大脑」是那些圆圈(神经元)。而事实上,神经大脑的真正核心是那些能够学习和改进的部分——突触,也就是图中那些连接圆圈的线。这两个矩阵——syn0 和 syn1 才是网络的真正大脑。它们是网络用来试错学习、执行预测、对比目标值和预测,进而改善下一次预测结果的部分。
注意代码 syn0 = 2np.random.random((3,4)) - 1 创建了 3x4 矩阵,并为它生成随机数。这将是突触(或权重)的第一层 Synapse 0,它连接 l0 和 l1。该矩阵如下所示:
我曾经在这里犯了一个错:我无法理解为什么 syn0 应该是 3x4 矩阵。我认为它应该是 4x3 矩阵,因为 syn0 必须与 l0 相乘,而后者是 4x3 矩阵,我们为什么不让两个矩阵的数字按行与列排列整齐呢?
而这就是我的错误:4x3 乘 4x3 能够使数字排列整齐?这是错的。事实上,如果我们想要数字排列整齐,我们就应该让 4x3 乘 3x4。这是矩阵乘法里的一项基础且重要的规则。仔细查看下图中的第一个神经元,该神经元内是每位顾客对「你有猫吗?」这个问题的回复。以下是 4x3 layer0 矩阵的第一列:
[1][0][0][1]
现在注意,有四条线(突触)将 l0 中「你有猫吗?」这个神经元与 l1 的四个神经元连接起来。这意味着上述列 1,0,0,1 中的每个数字都要与四个不同权重相乘,因此得到 16 个值。l1 确实是一个 4x4 矩阵。
注意,现在我们要对第二个神经元内的四个数字执行同样的操作,也得到 16 个值。我们将这 16 个值中的每个值与刚才创建的值中的对应值相加。
重复这个步骤,直到将第三个神经元中的四个数字也处理完毕。这样 4x4 l1 矩阵就有 16 个值,每个值是三次乘法所得值的对应相加结果。
也就是说,3 个调查问题 x 4 位顾客 = 3 个神经元 x 4 个突触 = 3 个特征 x 4 个权重 = 3x4 矩阵。
看起来很复杂?习惯就好了。而且计算机会替你执行乘法操作。我只是想帮助大家理解其下的底层操作。当你看到下图时,这些线不会说谎。
也许你会疑惑,代码行 47 中的「2」和「-1」是哪儿来的。np.random.random 函数生成均匀分布于 0 到 1 之间的随机数(对应的平均值为 0.5),而我们想让随机数初始值的平均值为 0。这样该矩阵中的初始权重不会存在偏向于 1 或 0 的先验偏置(在最开始的时候,网络不知道接下来会发生什么,因此它对其预测结果是没有信心的,直到我们在每个迭代后更新它)。
那么,我们如何将一组平均值为 0.5 的数字转变为平均值为 0 的数字呢?首先,将所有随机数乘 2(这样所有数字分布在 0 到 2 之间,平均值为 1),然后减去 1(这样所有数字分布在-1 到 1 之间,平均值为 0)。这就是「2」和「-1」出现的原因。
接下来看下一行代码:syn1 = 2np.random.random((4,1)) - 1 创建了一个 4x1 向量,并为它生成随机数。这就是网络第二层的权重 Synapse 1,它连接 l1 和 l2。
for loop 使网络执行 6 万次迭代。每次迭代中,网络使用顾客调查数据 X 作为输入,基于该数据得出对顾客购买新款猫砂概率的最佳预测。然后将预测与真值进行对比,再从错误中学习,在下次迭代中做出更好的预测。该过程持续 6 万次,直到网络通过试错学会如何准确预测。然后这个网络可以使用任意输入数据,并准确预测可能购买新款猫砂的顾客。
2.6 For Loop:行 52
for loop 使网络执行 6 万次迭代。每次迭代中,网络使用顾客调查数据 X 作为输入,基于该数据得出对顾客购买新款猫砂概率的最佳预测。然后将预测与真值进行对比,再从错误中学习,在下次迭代中做出更好的预测。该过程持续 6 万次,直到网络通过试错学会如何准确预测。然后这个网络可以使用任意输入数据,并准确预测可能购买新款猫砂的顾客。
3. 前馈:做出有根据的猜测,60000 次迭代
网络在这一步骤开始执行预测。这是深度学习过程中最令人兴奋的部分,所以我打算从三个不同角度介绍这个概念:
-
关于前馈的迷人童话
-
关于前馈的美丽画作
-
为什么矩阵乘法是前馈的引擎
return 1/(1+np.exp(-x))
这里的前馈使用了我们的一个训练样本,l0 的第 1 行,即「顾客 1 对 3 个调查问题的回复」:[1,0,1]。因此,我们首先将 l0 的第一个值与 syn0 的第一个值相乘。假设 syn0 矩阵已经过多次训练迭代(我们在 2.4 节已经进行过初始化了),现在该矩阵如下所示:
syn0: [ 3.66 -2.88 3.26 -1.53][-4.84 3.54 2.52 -2.55] [ 0.16 -0.66 -2.82 1.87]
到这里,或许你会疑惑:为什么 syn0 的值与 2.4 节创建的 syn0 如此不同?好问题。前面介绍的很多矩阵有逼真的初始值,就好像它们刚被计算机通过随机种子创建出来一样。而这里和下面的矩阵并非初始值。它们是经过多次训练迭代的,因此它们的值已经在学习过程中经过了更新和改变。
现在,我们用 l0 的第一个值 1 与 syn0 的第一个值 3.66 相乘,看看会得到什么:
下面是前馈的伪代码,你可以借助上图(从左至右的顺序)帮助理解。
下面,我们再从另一个角度看前馈过程的底层数学原理。
3.3 前馈的数学原理
l0 x syn0 = l1LH,在这个示例中即 1 x 3.66 = 3.66,不要忘记还要加上另外两个 l0 值和 syn0 对应权重的乘积。在该示例中,l0,2 x syn0,2= 0 x something = 0,l0,3 x syn0,3 中 l0,3=1,从上一节中我们知道 syn0,3 = 0.16,因此 l0,3 x syn0,3 = 1 x 0.16 = 0.16。因此 l0,1 x syn0,1 + l0,3 x syn0,3 = 3.66 + 0.16 = 3.82,即
l1_LH = 3.82。
接下来,我们将 l1_LH 输入 nonlin() 函数,将该数字转换为 0 到 1 之间的概率。Nonlin(l1_LH) 使用代码 return 1/(1+np.exp(-x)),因此该示例中,1/(1+(2.718^-3.82))=0.98,l1 = 0.98。
那么,在公式 1/(1+np.exp(-x)) = [1/(1+2.718^-3.82))] = 0.98 中发生了什么呢?计算机使用代码 return 1/(1+np.exp(-x)) 代替了我们的人工计算,我们可以通过下图看到在 sigmoid 曲线上 x = 3.82 所对应的 y 值:
图源:https://iamtrask.github.io/2015/07/12/basic-python-network/
注意,X 轴上 3.82 在蓝色曲线上对应点的对应 y 值是 0.98,而代码将 3.82 转换为 0 到 1 之间的概率。上图有助于大家了解该计算并不神秘,也不抽象,计算机只不过做了和我们一样的事:它使用数学查看上图中 X 轴上 3.82 对应的 y 值,仅此而已。
同理,我们重复以上步骤,计算出 l2_LH 和 l2 的值。至此,我们就完成了第一次前馈。
现在,为了简洁起见,我们把所有变量放在一起,如下所示:
l0=1syn0,1=3.66l1_LH=3.82l1=0.98syn1,1=12.21l2_LH=0l2=~0.5y=1 (this is a "Yes" answer to survey Question 4, "Actually bought Litter Rip?")l2_error = y-l2 = 1-0.5 = 0.5
现在,我们来看让这一切实现的矩阵乘法(对矩阵乘法和线性代数陌生的朋友,可以先学习 Grant Sanderson 的课程:https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab)。
首先,在代码行 58,我们将 4x3 l0 和 3x4 Syn0 相乘,创建(隐藏层)l1,它是一个 4x4 矩阵:
在第 58 行代码中,我们将 l1 输入 nonlin() 函数,得到一个 0 到 1 之间的概率值:
1/(1 + 2.781281^-x)This creates layer 1, the hidden layer of our neural network:l1: [0.98 0.03 0.61 0.58][0.01 0.95 0.43 0.34][0.54 0.34 0.06 0.87][0.27 0.50 0.95 0.10]
看不懂也没关系,我们来看一个简单的训练示例。第一行(顾客 1 的调查数据)[1,0,1] 是一个 1x3 矩阵。我们将其乘以 syn0(3x4 矩阵),得到的 l1 是 1x4 矩阵。过程如下所示:
注意,在代码第 58 行 l1=nonlin(np.dot(l0,syn0)),我们将 l1 输入 sigmoid 函数,因为我们需要一个 0 到 1 的数值。
从这行代码中,我们可以看到 sigmoid 函数的第一大优势。当我们将 l0 和 syn0 的点乘矩阵输入到 nonlin() 函数时,sigmoid 函数将矩阵中的每个值转换成 0 到 1 之间的概率。
3.4 核心重点:隐藏层中的推断问题
我们为什么要关心统计概率呢?我认为原因在于,统计概率是让一堆沉默矩阵焕发生机并像小孩一样学习的主要因素!
在第一层中,当我们将 l0 乘以 syn0 时,为什么要尝试给 syn0 的权重赋不同的值呢?因为我们想尝试不同的特征问题组合,发现对预测结果帮助最大的问题组合。上文举过一些拙劣的例子,推断出有猫且喝进口啤酒的人更有可能购买新款猫砂,从而利用权重强化这两个特征组合。
另一个例子:如果一些顾客没有养猫,但他们喝进口啤酒且访问过 Litter Rip.com (http://rip.com/) 网站,我们可以推断出这些顾客热衷技术:他们喝进口啤酒因为他们欣赏其供应链物流,他们访问网站,各种各样的网站,说明他们很显然热衷技术。因此我们可以推断出,这些顾客可能会出于对这款猫砂先进技术的欣赏而购买它,尽管他们可能实际上并没有养猫。所以我们或许需要调整 syn0 的权重,强化这些特征之间的连接。
现在你明白了吗?当我们将调查问题的回复 l0 与 syn0 中的权重(每个权重表示我们对一个推断问题对预测结果的重要程度的最佳猜测)相乘时,我们是在尝试不同的答案组合,以查看哪些组合对预测结果最有帮助。很明显,在 6 万次迭代中,访问过猫砂网站的猫主人更有可能购买这款猫砂,因此在迭代的过程中它们对应的权重会增加,即其统计概率更接近 1 而不是 0。而喝进口啤酒却没有猫的人购买这款猫砂的可能性较低,因此对应的权重较小,即其统计概率更接近 0 而不是 1。是不是很奇妙?这就像数字谱写的诗歌,这些是会推理和思考的矩阵!
这也是我为什么大费周章介绍这个的原因。你是否经历过,一些傲慢的软件工程师或制造恐慌的记者告诉你,神经网络隐藏层是潘多拉的魔盒?而事实上它的底层并没有什么「神奇魔法」。这些数学知识清晰、优雅而美丽。你可以掌握它,只要有耐心肯坚持。
接下来我们继续看矩阵乘法。
3.5 用神经元和突触的形式可视化矩阵乘法
我们用神经元和突触的形式可视化 l0 和 syn0,如下图所示:
上图展示了输入 l0 的行 1 如何完成在网络中的第一步。第一行代表顾客 1 对三个调查问题的答案。这三个数字需要乘以 syn0 的 12 个值,然后网络还要对其他三位顾客的答案进行同样的处理,那么如何清晰地展现这些数字并进行计算呢?
这里的关键在于,将四位顾客看作一个「batch」(批次),他们在其中堆叠在一起,即一共 4 个 stack。那么,最上面的那个 stack 就是第一行(顾客 1),依此类推。如上图所示,你将第一行的三个数字和 syn0 的 12 个值相乘,再相加,最后得到 l1 最上面 stack 的四个值。
接着是 batch 中的第二个 stack——顾客 2 的回答是 0,1,1。将这三个数字和 syn0 的 12 个值相乘,再相加,最后得到 l1 第二个 stack 的四个值。
依此类推。其核心在于一次计算一个 stack,这样不管 batch 中有多少个 stack,四个还是四百万个,你都可以很好地处理。你可能会说每个特征都有一个 batch,在猫砂这个示例中,每个调查问题(特征)的 batch 为 4,因为只有四位顾客的答案。但它也可能是四百万。「full batch configuration」的概念是非常常见的模型,接下来我将解释这一点。
其实认为给定特征具备一个 batch 的值是最容易理解的。当你看到一个特征时,你知道它底下有一个 batch 的值。
就像代码行 59 所示,我们把 4x4 l1 和 4x1 syn1 相乘,然后输入 sigmoid 函数得出 4x1 l2,其每个值都是 0 到 1 的统计概率。
l1 (4x4): [0.98 0.03 0.61 0.58] [ 12.21][0.01 0.95 0.43 0.34] X [ 10.24] =[0.54 0.34 0.06 0.87] [ -6.31][0.27 0.50 0.95 0.10] [-14.52]Then pass the above 4x1 product through "nonlin()" and you get l2, our prediction:l2: [ 0.50] [ 0.90] [ 0.05] [ 0.70]
那么这四个预测结果告诉我们什么呢?预测值距离 1 越近,则该顾客购买这款猫砂的可能性越高;预测值距离 0 越近,则该顾客购买这款猫砂的可能性越低。
现在我们完成了前馈部分。接下来我们将看 6 万次迭代过程中如何调整网络权重,使预测结果越来越好。
4. 从试错中学习:梯度下降
4.1 梯度下降概览
梯度下降的目的是什么?是为了更好地调整网络权重,从而在下次迭代中获得更好的预测结果。也就是说,网络的突触矩阵中的某些值要被增减。为了调整这些值,我们必须回答以下两个重要问题:
我应该按什么方向调整数字?应该增加还是减少数值?正方向还是负方向?……
数值应该增减多少?
下面我们将详细解释这两个基础问题。还记得上文的红色碗吗?「梯度」就是「坡度」,「梯度下降」即计算出使小球从碗表面上的某个点尽快下降到碗底的最优坡度。
梯度下降的第一步即,计算当前的预测结果与真值 y(1/yes 或 0/no)之间的差距。
4.2 预测结果与调查问题 4 的答案相比有多大差距?
代码行 66:
l2_error = y - l2
第一次预测结果距离目标值「Yes/1」(顾客 1 对第四个调查问题的回答真值是「购买过」)有多远距离呢?我们需要将 l2 预测值与 y 值(1)进行对比,即 y 值减去 l2 得到的就是 l2_error——「预测值距离目标值 y 的距离」。
因此,我们可以想象这幅图景:网络使用每位顾客的回复作为输入,并操作这些数据,以得到顾客是否购买猫砂的预测结果。
我们有四位顾客,网络就做了四次预测。因此 l2_error 是四次误差的向量(每个误差针对一次预测)。接下来我们将打印出该误差:
打印误差:行 72-73
72 if (j% 10000)==0:73 print("Avg l2_error after 10,000 more iterations: "+str(np.mean(np.abs(l2_error))))
行 72 使计算机每隔一万次迭代打印一次 l2_error。这有助于我们每隔一万次查看网络的学习效果和进展。if (j% 10000)==0: 表示「如果你的迭代器所处的迭代次数除以 10000 后没有余数,则……」。j%10000 共有六次没有余数的情况:迭代数为 10000、20000、30000、40000、50000、60000 时。该打印输出会帮助我们很好地了解网络的学习进展。
代码 + str(np.mean(np.abs(l2_error)))) 取误差的绝对值,然后求平均数并打印出来,从而简化了打印过程。
现在我们已经知道预测结果(l2)距离真值(y)的距离,并打印了出来。但是,我们和城堡的距离实在太远,我们要如何降低目前令人失望的预测误差 0.5,最终到达目的地呢?
一步步来。接下来,我们将了解调整网络的哪一部分才能改进下一次预测的结果,之后会讨论如何调整网络。
4.3 我们需要调整网络的哪一部分?
先看下图,神经网络真正用来学习和记忆的核心部分是突触,而不是神经元。我们的网络中有 16 个变量:3x4 矩阵 syn0 中的 12 个变量和 4x1 向量 syn1 中的 4 个变量。查看下图,你会发现每一条线(「边」或「突触」)表示一个变量,它包含一个数值,这就是权重。
我们可以控制这 16 个权重。
输入 l0 是固定的,不能改变。l1 由 syn0 中的权重决定(l1 = syn0 x l0),l2 由 syn1 中的权重决定(l2 = syn1 x l1)。上图中的 16 条线(突触、权重)是神经网络达到目标过程中唯一能够调整的数字。
现在我们知道了预测值与真值的距离,知道了 l2_error。那么我们如何利用这些数值调整两个矩阵中的 16 个权重呢?这个答案就是 AI 最神奇的属性之一:这是一个统计学和概率概念,叫做置信度。
4.4 置信度:使冷冰冰的数字像人类一样思考
代码行 88:
l2_delta = l2_error*nonlin(l2,deriv=True)
「置信度」这个术语可能比较抽象,但是它实际上就是我们每天都在用的东西。我用一个有趣的故事来提醒你:
先知城堡 2:你必须明白你其实一直在使用置信度!
在先知城堡的故事中,你执行前馈,驱使 l2 向最佳预测 y(城堡)前进,但是当你到达 l2 后却发现你虽然距离城堡更近了,却仍未到达。第二天早上你发现自己在家中醒来(回到 l0),然后开始再一次的旅程(新的迭代)。
你该如何改进驾驶方向,才能做有用功呢?
首先,在你今天的旅程结束时,你急切地询问当地的骑士这个地方距离城堡还有多远,骑士告诉你答案(即 l2_error)。每天的旅程结束时,你都要计算如何改变权重才能使明天的 l2 预测比今天更好,并最终使你抵达城堡。这就是 l2_delta(参见下图),即要想使明天的 l2 抵达城堡,今天你需要对权重做出的改变。
重点来了:
注意 l2_delta 与 l2_error 不同,l2_error 仅告诉你与城堡的距离,而 l2_delta 则影响你对方向的信心。你在衡量信心。这些置信度数字就是导数(不过,这里暂且忘记微积分吧,我们暂时使用这个词「坡度」),即 l2 每个值的坡度。这些坡度就是你对今天旅程中每一次转向的自信程度。有些方向你非常确定且自信,而有些则不然。
使用置信度这一概念来计算明天能到哪里似乎有点抽象?实际上,你每时每刻都在使用置信度导航,只不过你没意识到而已。接下来我们就让你「恢复」这一意识。
回忆一下你的迷路经历。一开始,你按照自己以为正确的道路前进,你非常自信。但是慢慢地你发现旅途似乎比预期更加漫长,你开始怀疑是否忘记拐弯。你没最初那么自信了。随着时间的流逝,你原本应该已经到达目的地了,然而你还在路上,此时你更加确定错过了一次拐弯。这时候你的自信度很低。你知道自己没有到达目的地,但你不确定如何从现在的位置到达目的地。于是你决定停下来问路,但是被问路的女士告诉你的拐弯和路标太多,你没记全,你沿着她指的方向走到一半又不知道怎么走了。这时你会再次问路,不过这一次你离目的地更近,前进的方向也更加简单,你沿着方向前进最终抵达目的地。
在这段讲述中,你需要注意以下几件事:
首先,你通过试错进行学习,你的信心在不停变化。稍后我会解释为什么置信度使得网络试错学习,以及 sigmoid 函数如何提供重要的置信度。
其次,注意你的旅程分为两部分:第一部分从你启程到第一次问路(l1),第二部分是从第一次问路到 l2(你以为已经到达目的地了)。但是后来你意识到这里并不是终点,不得不询问离真正的终点还有多远。
你看到置信度在旅程中的角色了吗?一开始,你确定自己走在正确的路上,然后你怀疑自己少拐了个弯,之后你确定自己少拐了个弯,决定停下来问路。图中这两个分割的部分就像狗腿一样弯,但随着每日旅程的改进,狗腿将一点点变直。
这和你前往城堡的过程一样:
每天,你(3 层神经网络)沿着一组方向(syn0)前往城堡。结束时你发现自己停在了 l1,就询问接下来的方向(syn1)。这些方向使得你到达当天的终点(预测结果),你以为那里就是目的地城堡。但事实上你面前并没有城堡。于是你询问骑士:「我离城堡还有多远?」(l2_error 的值是多少?)你是一个天才,于是你将 l2_error 与你对每一次转向的置信度(l2 的坡度)相乘,得出了明天可以到达的地方(l2_delta)。
注意:这个比喻有一点不恰当,即当你停在 l1 问路时,女士告诉你新的方向。而事实上,你的方向(syn1 值)是你准备更新上一次迭代时就已经选择好的。因此准确来说,那位女士并没有和你说话,只是举起了你昨天在那里消失之前给她的路牌,因为你知道今天会再次路过她身旁。
在继续之前,你需要厘清 3 项事实:
-
你到达的地方,即当前位置(l2);
-
你距离城堡的距离 (l2_error);
-
要想提高对明天旅程更接近城堡的信心,你需要做出的方向改变 (l2_delta)。
y: l2: l2_error: [1] [0.50] [ 0.50][1] _ [0.90] = [ 0.10][0] [0.05] [-0.05][0] [0.70] [-0.70]
下面的公式是理解神经网络学习原理的重点:
l2 slopes after nonlin(): l2_error: l2_delta: [0.25] Not Confident [ 0.50] Big Miss [ 0.125] Big change[0.09] Fairly Confident X [ 0.10] Small Miss = [ 0.009] Small-ish Change[0.05] Confident [-0.05] Tiny miss [-0.003] Tiny change[0.21] Not Confident [-0.70] Very Big Miss [-0.150] Huge Change
注意,大误差即 l2_error 中值较大的数字,而低准确率的 l2 值的坡度也最陡,即它们在 nonlin(l2,deriv=True) 中的数字最大。因此,当我们用大误差乘以低准确率 l2 值时,就是大数字与大数字相乘,因此也将得到 l2_delta 向量中最大的数字。
l2_delta 即「下一次迭代中我们希望看到的 l2 改变」。较大的 l2_delta 值意味着下一次迭代中 l2 预测会有很大的改变,而这正是通过大幅改变对应的 syn1 和 syn0 值来实现的。将这些大数值与 syn1 中已有的值相加,得到的更新权重将在下一次迭代中带来更好的 l2 预测结果。
为什么要使用 l2 的坡度呢?
此举的目的是为了更快地修复 16 个权重中最不合适的那些。上文我们提到 sigmoid 函数的 S 曲线图时说过,l2 坡度与置信度相关。也就是说,大坡度值等于低置信度,最小坡度值等于最高置信度。因此,将小数字与 l2_error 中的对应值相乘不会给 l2_delta 带来大的变化,而我们也恰好不想改变那些权重,它们已经做得很好了。
但是置信度最低的 l2 预测具备最陡的坡度,坡度值也最大。当我们将这个大数字乘以 l2_error 时,l2_delta 的数字也会很大。之后当我们更新 syn1 时,大的乘数意味着大的积、大的改变或调整。正该如此,因为我们想最大程度地改变置信度最低的权重,这也是调整权重大小时使我们获得最大收益的地方。总之,l2 坡度指示了每个 l2 预测值的置信度,从而允许我们决定哪些数字最需要调整,以及如何最快地调整它们。
但是精彩的部分还未结束,回到关于推断问题这一重点上。
在 3.4 节我们讨论了,改变 syn0 的权重类似于改变对预测最有用的推断问题。例如,访问过猫砂网站的养猫人更有可能购买这款猫砂,而喝进口啤酒但不养猫的人购买猫砂的几率相对较低。这就是 l2_delta 的用武之地。l2_delta 增加最有用推断问题的重要性(权重),降低不那么有用推断问题的重要性。接下来,我们将了解如何确定隐藏层 1 中每个推断问题的效用。
不过,首先我们先确定自己是否完全了解 l2 置信度如何修改 syn1 中的权重。sigmoid 函数随机选取矩阵中的任一数字,
-
将它转换为统计概率;
-
将概率转换为置信度;
-
对突触进行或大或小的调整;
-
该调整(几乎)总是朝着提升置信度和准确率的方向,从而降低 l2_error。
l1_error = = l2_delta.dot(syn1.T)
城堡的故事非常有用:在那幅图中,我们可以看到 l2_error 代表 l2 预测与城堡的距离,也可以了解到 l2_delta 是目前的预测与下一次迭代中预测结果之间的距离。刚刚介绍的「学习大误差/小误差」的方法可以帮助我们理解,如何调整当前的 syn1(见代码行 115)。
现在你已经准备好用来更新 syn1 的 l2_delta 了,那么为什么不也更新一下 syn0 呢?
怎么做?回到 4.2 节,我们找到了 l2_error,知道 l2,也知道我们要追寻的「完美 l2」——y。但是,在 l1_error 这里,情况变了,我们不知道「完美 l1」是什么。
如何找到「完美 l1」呢?让我们再次回到推断问题。
回到 3.4 和 4.5 节,关于推断问题(特征)的概念,以及特征问题组合。当我们改变 syn1 中的权重时,我们真正在做的其实是用我们想赋予该问题或问题组合的重要性进行试验。对于 syn0 也是一样,我们想找出能够帮助确定 l1_error 的数学方法,因为 l1_error 将告诉我们哪些问题的优先级被错误分配了或者哪些问题被错误地组合在了一起。当我们接着计算 l1_delta 时,l1_delta 将告诉我们下一次迭代中更好的问题优先级和问题组合。
例如,syn0 最初的权重提示推断问题「这个人富有吗?」是重要的问题。然而,随着训练的开展,网络意识到猫砂并非奢侈品,是否富有并不重要,因此网络决定调整 syn0 的值,以降低推断问题「这个人富有吗」的重要性。如何降低呢?将 syn0 中该问题的对应值乘以较大的 l1_delta 值,不过该值前面带有一个负号。然后网络将更新新的推断问题「这个人对猫屎过敏吗?」。更新方法是将该问题对应的 syn0 值乘以较大的 l1_delta 值(正值)。
这两个推断问题的优先级不断变化。但是另一个推断问题「这个人知道如何网络购物吗」一直都很重要,因为目标受众是喜欢在线购物的人。因此网络将该问题对应的 syn0 值乘以较小的 l1_delta 值。这样,其优先级就会保持不变。
另一个谜题是你需要多少推断问题?在这个网络中,我们一次只能容纳四个推断问题。这是由 syn0 的大小决定的。如果你没有足够的推断问题,那网络可能就无法做出准确的预测。而问题太多则会浪费过多精力。
现在,我们知道改变 syn0 的值会改变某个推断问题对于 l1 的贡献,进而影响 l1 对 l2 准确预测的贡献程度。syn1 使用特征问题组合,并进一步优化从而取得更加准确的 l2。
我在这个阶段提出的问题是:为什么要使用 l2 的坡度?
上文提到,我们使用 l2 坡度来更快地调整权重:l2 坡度告诉我们每个 l2 预测的置信度,从而允许我们决定哪些数字最需要调整,以及如何最快地调整它们。
我们来看数学部分。现在我们知道下一次迭代中 l2 预测要做的改变是 l2_delta,也知道到达目前预测结果的 syn1 值。那么重要的问题来了:如果我们用 l2_delta 乘以这次迭代的 syn1 值会怎么样?
这就像好莱坞编剧一样:你写了一个悲伤结局的电影,主角被龙喷出的火焰灼伤然后被吃掉,所以未能到达城堡。导演看了剧本满屋咆哮:「我要 happy ending,我要主角打败恶龙,发现生命的意义,然后转身离开!」
你同意按老板的意思修改剧本。你知道了要达到的结局,就转回去寻找哪个动作出了错。哪个情节使得你没有成为英雄反而被龙吃掉?
数学就是在做这样的事情:如果你把 l2_delta(期望的完美结局)乘以 syn1(导致错误结局的情节),那么你将收获 l1_error。改变造成错误结局的情节,下一版剧本将变得更好。
再次提醒:如果你将下一次想去的地方(l2_delta)与旅程第二部分的错误方向(现在的 syn1)相乘,则乘积将是「旅程第一部分出错的地方」,即 l1_error。
知道了 l1_error,你就可以计算在下次迭代中将 l1 改变多少才能得到更好的 l2 预测,而这就是 l1_delta。
代码行 109:
l1_delta = l1_error * nonlin(l1,deriv=True)
l1_delta 的计算方法和 l2_delta 一样,此处不再赘述。
4.7 更新突触
代码行 115-116:
115 syn1 += l1.T.dot(l2_delta) 116 syn0 += l0.T.dot(l1_delta)
代码的最后部分是高光时刻:所有的工作完成了,我们恭敬地将 l1_delta 和 l2_delta 搬到神圣的领导者「突触国王」面前,他才是所有操作的真正核心。
计算 syn0 更新的第一步是 l1_delta 乘输入 l0,然后将乘积与目前的 syn0 相加。这就使得 syn0 的构成部分发生很大改变,从而对 l1 产生更强影响。也就是说,我们降低了这次迭代中误认为重要的问题或问题组合的重要性。它们并没有我们想象的那么重要,因此用 l1_delta 在正确的方向上修正错误,从而使我们在下一次迭代中距离城堡更近。
「试错学习」,这就是网络所做的事情。以下介绍了网络如何利用试错来学习:
-
尝试:网络执行 6 万次前馈,即尝试 6 万次不同的特征问题组合,只为输出最佳的预测结果;
-
错误:网络将目前的预测与真值进行对比,找出误差;
-
学习:网络使用置信度(梯度下降)找出下一次迭代需要在正确的方向上做出多少改变,即 delta。然后网络使用新的 delta,并将它乘以旧的 l1 或 l2,然后将乘积加在旧的 syn0 或 syn1 上。你明白为什么说突触是神经网络的大脑了吗?它们在试错中学习,和人类一样。
-
和前文图一样的白色圆圈,从左至右表示蝴蝶效应;
-
图下方方框中是反向传播链式法则中的变化率。这些方框从右至左反向移动,即最右方的变化率改变会通过其他变化率最终影响到最左侧的变化率。
-
彩色箭头起连接作用。
l0= 1syn0,1= 3.66l1_LH= 3.82l1= 0.98syn1,1= 12.21l2_LH= 0.00l2= 0.50y= 1 (This is a "Yes" answer to survey question 4, "Ever bought Litter Rip?" which corresponds to training example #1, i.e., row 1 of l0)l2_error = y-l2 = 1-0.5 = 0.5
5.6.1 不要重蹈我的覆辙
在解决变化率时,千万不要和我犯同样的错误。我最初是这么计算变化率的(错误示范):
那么上图有什么问题呢?
5.6.2 不是为了计算变化率,而是为了计算变化率的变化:坡度
在上图中,我忘记了此时的目标是计算相对变化。看到上图中每个变量前面的 d 了吗?我忽略了它们,只写下了每个变量的当前值。
而那些 d 表示 delta,delta 非常重要,不能忽略。
我们想预测未来。我们想知道 syn0,1 改变多少会最终导致下一次迭代中 l2_error 减小。也就是说,我们不是要对比 syn0,1 中单个数值 3.66 对 l2_error(0.5)的影响,而是想知道 syn0,1 中 3.66 应该如何变化才能影响到 5 个连锁变化率,从而最终产生更好的 l2_error。我们不想要一堆变化率,我们想要的是能改变 delta 的变化率。
5.6.3 计算变化率的公式:x_current 和 x_nearby
「current」表示每个变量的当前值,「nearby」表示我们想提供的与当前数字接近的数字。nearby 数字减去 current 数字,会得到一个很小的数字。如果两个点在一条曲线上,这方便计算出更准确的坡度值。接下来我们过一遍上述 5 个变化率,练习如何寻找每个变化率的 nearby 数字。
下图展示了完整的反向传播:
5.6.4 示例
从后往前,我们要先计算的 Ratio 1 是 Ripple 5:d l2_error / d l2。那么「current」和「nearby」从哪里来呢?
-
x_current 是前馈计算出的 l2——0.5;
-
y_current 是 y-l2 = 1-0.5 = 0.5,即 l2_error;
-
x_nearby 是我们举的一个简单例子。如果 l2 为 0.6,距离 x_current(0.5)较近,则 y-0.6 = 0.4;
-
也就是 x_nearby = 0.6,y_nearby = 0.4;
-
知道这四个变量后,计算就很简单了,坡度即敏感度= -1;
-
这意味着 l2 每增加 0.1,l2_error 就减 0.1。
-
当你试着根据记忆写出代码时,你发现你忘记的地方恰好是不理解的地方。全部理解后,所有代码会永远存在你的大脑中;
-
这些代码是构建所有深度学习网络的基础。掌握了这些代码,你以后学习每一个网络、每一篇论文都会更加清晰和简单。