第一门课 神经网络和深度学习(Neural Networks and Deep Learning) 第一周:深度学习引言(Introduction to Deep Learning) 1.1 欢迎(Welcome) 第一个视频主要讲了什么是深度学习,深度学习能做些什么事情。以下是吴恩达老师的原话: 深度学习改变了传统互联网业务,例如如网络搜索和广告。但是深度学习同时也使得许多新产品和企业以很多方式帮助人们,从获得更好的健康关注。 深度学习做的非常好的一个方面就是读取X光图像,到生活中的个性化教育,到精准化农业,甚至到驾驶汽车以及其它一些方面。如果你想要学习深度学习的这些工具,并应用它们来做这些令人窒息的操作,本课程将帮助你做到这一点。当你完成 cousera 上面的这一系列专项课程,你将能更加自信的继续深度学习之路。在接下来的十年中,我认为我们所有人都有机会创造一个惊人的世界和社会,这就是 AI (人工智能)的力量。我希望你们能在创建 AI (人工智能)社会的过程中发挥重要作用。 我认为AI是最新的电力,大约在一百年前,我们社会的电气化改变了每个主要行业,从交通运输行业到制造业、医疗保健、通讯等方面,我认为如今我们见到了 AI 明显的令人惊讶的能量,带来了同样巨大的转变。显然,AI的各个分支中,发展的最为迅速的就是深度学习。因此现在,深度学习是在科技世界中广受欢迎的一种技巧。 通过这个课程,以及这门课程后面的几门课程,你将获取并且掌握那些技能。 下面是你将学习到的内容: 在 cousera 的这一系列也叫做专项课程中,在第一门课中( 神经网络和深度学习 ),你将学习神经网络的基础,你将学习神经网络和深度学习,这门课将持续四周,专项课程中的每门课将持续2至4周。 但是在第一门课程中,你将学习如何建立神经网络(包含一个深度神经网络),以及如何在数据上面训练他们。在这门课程的结尾,你将用一个深度神经网络进行辨认猫。 由于某种原因,第一门课会以猫作为对象识别。 接下来在第二门课中,我们将使用三周时间。你将进行深度学习方面的实践,学习严密地构建神经网络,如何真正让它表现良好,因此你将要学习超参数调整、正则化、诊断偏差和方差以及一些高级优化算法,比如 Momentum 和 Adam 算法,犹如黑魔法一样根据你建立网络的方式。第二门课只有三周学习时间。 在第三门课中,我们将使用两周时间来学习如何结构化你的机器学习工程。事实证明,构建机器学习系统的策略改变了深度学习的错误。 举个例子:你分割数据的方式,分割成训练集、比较集或改变的验证集,以及测试集合,改变了深度学习的错误。 所以最好的实践方式是什么呢? 你的训练集和测试集来自不同的贡献度在深度学习中的影响很大,那么你应该怎么处理呢? 如果你听说过端对端深度学习,你也会在第三门课中了解到更多,进而了解到你是否需要使用它,第三课的资料是相对比较独特的,我将和你分享。我们了解到的所有的热门领域的建立并且改良许多的深度学习问题。这些当今热门的资料,绝大部分大学在他们的深度学习课堂上面里面不会教的,我认为它会提供你帮助,让深度学习系统工作的更好。 在第四门课程中,我们将会提到卷积神经网络( CNN(s) ),它经常被用于图像领域,你将会在第四门课程中学到如何搭建这样的模型。 最后在第五门课中,你将会学习到序列模型,以及如何将它们应用于自然语言处理,以及其它问题。 序列模型包括的模型有循环神经网络( RNN )、全称是长短期记忆网络( LSTM )。你将在课程五中了解其中的时期是什么含义,并且有能力应用到自然语言处理( NLP )问题。 总之你将在课程五中学习这些模型,以及能够将它们应用于序列数据。比如说,自然语言就是一个单词序列。你也将能够理解这些模型如何应用到语音识别或者是编曲以及其它问题。 因此,通过这些课程,你将学习深度学习的这些工具,你将能够去使用它们去做一些神奇的事情,并借此来提升你的职业生涯。 吴恩达 1.2 什么是神经网络?(What is a Neural Network) 我们常常用深度学习这个术语来指训练神经网络的过程。有时它指的是特别大规模的神经网络训练。那么神经网络究竟是什么呢?在这个视频中,我会讲解一些直观的基础知识。 让我们从一个房价预测的例子开始讲起。 假设你有一个数据集,它包含了六栋房子的信息。所以,你知道房屋的面积是多少平方英尺或者平方米,并且知道房屋价格。这时,你想要拟合一个根据房屋面积预测房价的函数。 如果你对线性回归很熟悉,你可能会说:“好吧,让我们用这些数据拟合一条直线。”于是你可能会得到这样一条直线。 但有点奇怪的是,你可能也发现了,我们知道价格永远不会是负数的。因此,为了替代一条可能会让价格为负的直线,我们把直线弯曲一点,让它最终在零结束。这条粗的蓝线最终就是你的函数,用于根据房屋面积预测价格。有部分是零,而直线的部分拟合的很好。你也许认为这个函数只拟合房屋价格。 作为一个神经网络,这几乎可能是最简单的神经网络。我们把房屋的面积作为神经网络的输入(我们称之为$x$),通过一个节点(一个小圆圈),最终输出了价格(我们用$y$表示)。其实这个小圆圈就是一个单独的神经元。接着你的网络实现了左边这个函数的功能。 在有关神经网络的文献中,你经常看得到这个函数。从趋近于零开始,然后变成一条直线。这个函数被称作 ReLU 激活函数,它的全称是 Rectified Linear Unit 。rectify(修正)可以理解成$max(0,x)$,这也是你得到一个这种形状的函数的原因。 你现在不用担心不理解 ReLU 函数,你将会在这门课的后面再次看到它。 如果这是一个单神经元网络,不管规模大小,它正是通过把这些单个神经元叠加在一起来形成。如果你把这些神经元想象成单独的乐高积木,你就通过搭积木来完成一个更大的神经网络。 让我们来看一个例子,我们不仅仅用房屋的面积来预测它的价格,现在你有了一些有关房屋的其它特征,比如卧室的数量,或许有一个很重要的因素,一家人的数量也会影响房屋价格,这个房屋能住下一家人或者是四五个人的家庭吗?而这确实是基于房屋大小,以及真正决定一栋房子是否能适合你们家庭人数的卧室数。 换个话题,你可能知道邮政编码或许能作为一个特征,告诉你步行化程度。比如这附近是不是高度步行化,你是否能步行去杂货店或者是学校,以及你是否需要驾驶汽车。有些人喜欢居住在以步行为主的区域,另外根据邮政编码还和富裕程度相关(在美国是这样的)。但在其它国家也可能体现出附近学校的水平有多好。 在图上每一个画的小圆圈都可以是 ReLU 的一部分,也就是指修正线性单元,或者其它稍微非线性的函数。基于房屋面积和卧室数量,可以估算家庭人口,基于邮编,可以估测步行化程度或者学校的质量。最后你可能会这样想,这些决定人们乐意花费多少钱。 对于一个房子来说,这些都是与它息息相关的事情。在这个情景里,家庭人口、步行化程度以及学校的质量都能帮助你预测房屋的价格。以此为例,$x$ 是所有的这四个输入,$y$ 是你尝试预测的价格,把这些单个的神经元叠加在一起,我们就有了一个稍微大一点的神经网络。这显示了神经网络的神奇之处,虽然我已经描述了一个神经网络,它可以需要你得到房屋面积、步行化程度和学校的质量,或者其它影响价格的因素。 神经网络的一部分神奇之处在于,当你实现它之后,你要做的只是输入$x$,就能得到输出$y$。因为它可以自己计算你训练集中样本的数目以及所有的中间过程。所以,你实际上要做的就是:这里有四个输入的神经网络,这输入的特征可能是房屋的大小、卧室的数量、邮政编码和区域的富裕程度。给出这些输入的特征之后,神经网络的工作就是预测对应的价格。同时也注意到这些被叫做隐藏单元圆圈,在一个神经网络中,它们每个都从输入的四个特征获得自身输入,比如说,第一个结点代表家庭人口,而家庭人口仅仅取决于$x_1$和$x_2$特征,换句话说,在神经网络中,你决定在这个结点中想要得到什么,然后用所有的四个输入来计算想要得到的。因此,我们说输入层和中间层被紧密的连接起来了。 值得注意的是神经网络给予了足够多的关于$x$和$y$的数据,给予了足够的训练样本有关$x$和$y$。神经网络非常擅长计算从$x$到$y$的精准映射函数。 这就是一个基础的神经网络。你可能发现你自己的神经网络在监督学习的环境下是如此的有效和强大,也就是说你只要尝试输入一个$x$,即可把它映射成$y$,就好像我们在刚才房价预测的例子中看到的效果。 在下一个视频中,让我们复习一下更多监督学习的例子,有些例子会让你觉得你的网络会十分有用,并且你实际应用起来也是如此。 1.3 神经网络的监督学习(Supervised Learning with Neural Networks) 关于神经网络也有很多的种类,考虑到它们的使用效果,有些使用起来恰到好处,但事实表明,到目前几乎所有由神经网络创造的经济价值,本质上都离不开一种叫做监督学习的机器学习类别,让我们举例看看。 在监督学习中你有一些输入$x$,你想学习到一个函数来映射到一些输出$y$,比如我们之前提到的房价预测的例子,你只要输入有关房屋的一些特征,试着去输出或者估计价格$y$。我们举一些其它的例子,来说明神经网络已经被高效应用到其它地方。 如今应用深度学习获利最多的一个领域,就是在线广告。这也许不是最鼓舞人心的,但真的很赚钱。具体就是通过在网站上输入一个广告的相关信息,因为也输入了用户的信息,于是网站就会考虑是否向你展示广告。 神经网络已经非常擅长预测你是否会点开这个广告,通过向用户展示最有可能点开的广告,这就是神经网络在很多家公司难以置信地提高获利的一种应用。因为有了这种向你展示你最有可能点击的广告的能力,而这一点击的行为的改变会直接影响到一些大型的在线广告公司的收入。 计算机视觉在过去的几年里也取得了长足的进步,这也多亏了深度学习。你可以输入一个图像,然后想输出一个索引,范围从1到1000来试着告诉你这张照片,它可能是,比方说,1000个不同的图像中的任何一个,所以你可能会选择用它来给照片打标签。 深度学习最近在语音识别方面的进步也是非常令人兴奋的,你现在可以将音频片段输入神经网络,然后让它输出文本记录。得益于深度学习,机器翻译也有很大的发展。你可以利用神经网络输入英语句子,接着输出一个中文句子。 在自动驾驶技术中,你可以输入一幅图像,就好像一个信息雷达展示汽车前方有什么,据此,你可以训练一个神经网络,来告诉汽车在马路上面具体的位置,这就是神经网络在自动驾驶系统中的一个关键成分。 那么深度学习系统已经可以创造如此多的价值,通过智能的选择,哪些作为$x$哪些作为$y$,来针对于你当前的问题,然后拟合监督学习部分,往往是一个更大的系统,比如自动驾驶。这表明神经网络类型的轻微不同,也可以产生不同的应用,比如说,应用到我们在上一个视频提到的房地产领域,我们不就使用了一个普遍标准神经网络架构吗? 也许对于房地产和在线广告来说可能是相对的标准一些的神经网络,正如我们之前见到的。对于图像应用,我们经常在神经网络上使用卷积( Convolutional Neural Network ),通常缩写为 CNN 。对于序列数据,例如音频,有一个时间组件,随着时间的推移,音频被播放出来,所以音频是最自然的表现。作为一维时间序列(两种英文说法 one-dimensional time series / temporal sequence ).对于序列数据,经常使用 RNN ,一种递归神经网络( Recurrent Neural Network ),语言,英语和汉语字母表或单词都是逐个出现的,所以语言也是最自然的序列数据,因此更复杂的 RNNs 版本经常用于这些应用。 对于更复杂的应用比如自动驾驶,你有一张图片,可能会显示更多的 CNN 卷积神经网络结构,其中的雷达信息是完全不同的,你可能会有一个更定制的,或者一些更复杂的混合的神经网络结构。所以为了更具体地说明什么是标准的 CNN 和 RNN 结构,在文献中你可能见过这样的图片,这是一个标准的神经网络。 你也可能见过这样的图片,这是一个卷积神经网络的例子。 我们会在后面的课程了解这幅图的原理和实现,卷积网络( CNN )通常用于图像数据。 你可能也会看到这样的图片,而且你将在以后的课程中学习如何实现它。 递归神经网络( RNN )非常适合这种一维序列,数据可能是一个时间组成部分。 你可能也听说过机器学习对于结构化数据和非结构化数据的应用,结构化数据意味着数据的基本数据库。例如在房价预测中,你可能有一个数据库,有专门的几列数据告诉你卧室的大小和数量,这就是结构化数据。或预测用户是否会点击广告,你可能会得到关于用户的信息,比如年龄以及关于广告的一些信息,然后对你的预测分类标注,这就是结构化数据,意思是每个特征,比如说房屋大小卧室数量,或者是一个用户的年龄,都有一个很好的定义。 相反非结构化数据是指比如音频,原始音频或者你想要识别的图像或文本中的内容。这里的特征可能是图像中的像素值或文本中的单个单词。 从历史经验上看,处理非结构化数据是很难的,与结构化数据比较,让计算机理解非结构化数据很难,而人类进化得非常善于理解音频信号和图像,文本是一个更近代的发明,但是人们真的很擅长解读非结构化数据。 神经网络的兴起就是这样最令人兴奋的事情之一,多亏了深度学习和神经网络,计算机现在能更好地解释非结构化数据,这是与几年前相比的结果,这为我们创造了机会。许多新的令人兴奋的应用被使用,语音识别、图像识别、自然语言文字处理,甚至可能比两三年前的还要多。因为人们天生就有本领去理解非结构化数据,你可能听说了神经网络更多在媒体非结构化数据的成功,当神经网络识别了一只猫时那真的很酷,我们都知道那意味着什么。 但结果也表明,神经网络在许多短期经济价值的创造,也是基于结构化数据的。比如更好的广告系统、更好的利润建议,还有更好的处理大数据的能力。许多公司不得不根据神经网络做出准确的预测。 因此在这门课中,我们将要讨论的许多技术都将适用,不论是对结构化数据还是非结构化数据。为了解释算法,我们将在使用非结构化数据的示例中多画一点图片,但正如你所想的,你自己团队里通过运用神经网络,我希望你能发现,神经网络算法对于结构化和非结构化数据都有用处。 神经网络已经改变了监督学习,正创造着巨大的经济价值,事实证明,基本的神经网络背后的技术理念大部分都离我们不遥远,有的是几十年,那么为什么他们现在才刚刚起步,效果那么好,下一集视频中我们将讨论为什么最近的神经网络已经成为你可以使用的强大工具。 1.4 为什么深度学习会兴起?(Why is Deep Learning taking off?) 本节视频主要讲了推动深度学习变得如此热门的主要因素。包括数据规模、计算量及算法的创新。 深度学习和神经网络之前的基础技术理念已经存在大概几十年了,为什么它们现在才突然流行起来呢?本节课程主要讲述一些使得深度学习变得如此热门的主要驱动因素,这将会帮助你在你的组织机构内发现最好的时机来应用这些东西。 在过去的几年里,很多人都问我为什么深度学习能够如此有效。当我回答这个问题时,我通常给他们画个图,在水平轴上画一个形状,在此绘制出所有任务的数据量,而在垂直轴上,画出机器学习算法的性能。比如说准确率体现在垃圾邮件过滤或者广告点击预测,或者是神经网络在自动驾驶汽车时判断位置的准确性,根据图像可以发现,如果你把一个传统机器学习算法的性能画出来,作为数据量的一个函数,你可能得到一个弯曲的线,就像图中这样,它的性能一开始在增加更多数据时会上升,但是一段变化后它的性能就会像一个高原一样。假设你的水平轴拉的很长很长,它们不知道如何处理规模巨大的数据,而过去十年的社会里,我们遇到的很多问题只有相对较少的数据量。 多亏数字化社会的来临,现在的数据量都非常巨大,我们花了很多时间活动在这些数字的领域,比如在电脑网站上、在手机软件上以及其它数字化的服务,它们都能创建数据,同时便宜的相机被配置到移动电话,还有加速仪及各类各样的传感器,同时在物联网领域我们也收集到了越来越多的数据。仅仅在过去的20年里对于很多应用,我们便收集到了大量的数据,远超过机器学习算法能够高效发挥它们优势的规模。 神经网络展现出的是,如果你训练一个小型的神经网络,那么这个性能可能会像下图黄色曲线表示那样;如果你训练一个稍微大一点的神经网络,比如说一个中等规模的神经网络(下图蓝色曲线),它在某些数据上面的性能也会更好一些;如果你训练一个非常大的神经网络,它就会变成下图绿色曲线那样,并且保持变得越来越好。因此可以注意到两点:如果你想要获得较高的性能体现,那么你有两个条件要完成,第一个是你需要训练一个规模足够大的神经网络,以发挥数据规模量巨大的优点,另外你需要能画到$x$轴的这个位置,所以你需要很多的数据。因此我们经常说规模一直在推动深度学习的进步,这里的规模指的也同时是神经网络的规模,我们需要一个带有许多隐藏单元的神经网络,也有许多的参数及关联性,就如同需要大规模的数据一样。事实上如今最可靠的方法来在神经网络上获得更好的性能,往往就是 要么训练一个更大的神经网络,要么投入更多的数据 ,这只能在一定程度上起作用,因为最终你耗尽了数据,或者最终你的网络是如此大规模导致将要用太久的时间去训练,但是仅仅提升规模的的确确地让我们在深度学习的世界中摸索了很多时间。为了使这个图更加从技术上讲更精确一点,我在$x$轴下面已经写明的数据量,这儿加上一个标签(label)量,通过添加这个标签量,也就是指在训练样本时,我们同时输入$x$和标签$y$,接下来引入一点符号,使用小写的字母$m$表示训练集的规模,或者说训练样本的数量,这个小写字母$m$就横轴结合其他一些细节到这个图像中。 在这个小的训练集中,各种算法的优先级事实上定义的也不是很明确,所以如果你没有大量的训练集,那效果会取决于你的特征工程能力,那将决定最终的性能。假设有些人训练出了一个 SVM (支持向量机)表现的更接近正确特征,然而有些人训练的规模大一些,可能在这个小的训练集中 SVM 算法可以做的更好。因此你知道在这个图形区域的左边,各种算法之间的优先级并不是定义的很明确,最终的性能更多的是取决于你在用工程选择特征方面的能力以及算法处理方面的一些细节,只是在某些大数据规模非常庞大的训练集,也就是在右边这个$m$会非常的大时,我们能更加持续地看到更大的由神经网络控制的其它方法,因此如果你的任何某个朋友问你为什么神经网络这么流行,我会鼓励你也替他们画这样一个图形。 所以可以这么说,在深度学习萌芽的初期,数据的规模以及计算量,局限在我们对于训练一个特别大的神经网络的能力,无论是在CPU还是GPU上面,那都使得我们取得了巨大的进步。但是渐渐地,尤其是在最近这几年,我们也见证了算法方面的极大创新。许多算法方面的创新,一直是在尝试着使得神经网络运行的更快。 作为一个具体的例子,神经网络方面的一个巨大突破是从 sigmoid 函数转换到一个 ReLU 函数,这个函数我们在之前的课程里提到过。 如果你无法理解刚才我说的某个细节,也不需要担心,可以知道的一个使用 sigmoid 函数和机器学习问题是,在这个区域,也就是这个 sigmoid 函数的梯度会接近零,所以学习的速度会变得非常缓慢,因为当你实现梯度下降以及梯度接近零的时候,参数会更新的很慢,所以学习的速率也会变的很慢,而通过改变这个被叫做激活函数的东西,神经网络换用这一个函数,叫做 ReLU 的函数(修正线性单元), ReLU 它的梯度对于所有输入的负值都是零,因此梯度更加不会趋向逐渐减少到零。而这里的梯度,这条线的斜率在这左边是零,仅仅通过将 Sigmod 函数转换成 ReLU 函数,便能够使得一个叫做梯度下降( gradient descent )的算法运行的更快,这就是一个或许相对比较简单的算法创新的例子。但是根本上算法创新所带来的影响,实际上是对计算带来的优化,所以有很多像这样的例子,我们通过改变算法,使得代码运行的更快,这也使得我们能够训练规模更大的神经网络,或者是多端口的网络。即使我们从所有的数据中拥有了大规模的神经网络,快速计算显得更加重要的另一个原因是,训练你的神经网络的过程,很多时候是凭借直觉的,往往你对神经网络架构有了一个想法,于是你尝试写代码实现你的想法,然后让你运行一个试验环境来告诉你,你的神经网络效果有多好,通过参考这个结果再返回去修改你的神经网络里面的一些细节,然后你不断的重复上面的操作,当你的神经网络需要很长时间去训练,需要很长时间重复这一循环,在这里就有很大的区别,根据你的生产效率去构建更高效的神经网络。当你能够有一个想法,试一试,看效果如何。在10分钟内,或者也许要花上一整天,如果你训练你的神经网络用了一个月的时间,有时候发生这样的事情,也是值得的,因为你很快得到了一个结果。在10分钟内或者一天内,你应该尝试更多的想法,那极有可能使得你的神经网络在你的应用方面工作的更好、更快的计算,在提高速度方面真的有帮助,那样你就能更快地得到你的实验结果。这也同时帮助了神经网络的实验人员和有关项目的研究人员在深度学习的工作中迭代的更快,也能够更快的改进你的想法,所有这些都使得整个深度学习的研究社群变的如此繁荣,包括令人难以置信地发明新的算法和取得不间断的进步,这些都是开拓者在做的事情,这些力量使得深度学习不断壮大。 好消息是这些力量目前也正常不断的奏效,使得深度学习越来越好。研究表明我们的社会仍然正在抛出越来越多的数字化数据,或者用一些特殊的硬件来进行计算,比如说 GPU ,以及更快的网络连接各种硬件。我非常有信心,我们可以做一个超级大规模的神经网络,而计算的能力也会进一步的得到改善,还有算法相对的学习研究社区连续不断的在算法前沿产生非凡的创新。根据这些我们可以乐观地回答,同时对深度学习保持乐观态度,在接下来的这些年它都会变的越来越好。 1.5 关于这门课(About this Course) 你的学习进度已经快接近这个专项课程的第一门课的第一周结尾了,首先,快速地介绍一下下周的学习内容: 在第一个视频已经提到,这个专项有五门课程,目前正处于第一门课:神经网络与深度学习。在这门课中将教会你最重要的基础知识。当学习到第一门课末尾,你将学到如何建立一个深度神经网络并且使之奏效。 下面是关于第一门课的一些细节,这门课有四周的学习资料: 第一周:关于深度学习的介绍。在每一周的结尾也会有十个多选题用来检验自己对材料的理解; 第二周:关于神经网络的编程知识,了解神经网络的结构,逐步完善算法并思考如何使得神经网络高效地实现。从第二周开始做一些编程训练(付费项目),自己实现算法; 第三周:在学习了神经网络编程的框架之后,你将可以编写一个隐藏层神经网络,所以需要学习所有必须的关键概念来实现神经网络的工作; 第四周:建立一个深层的神经网络。 这段视频即将结束,希望在这段视频之后,你们可以看看课程网站的十道选择题来检查自己的理解,不必复习前面的知识,有的知识是你现在不知道的,可以不断尝试,直到全部做对以理解全部概念。 1.6 课程资源(Course Resources) 我希望你们喜欢这门课程,为了帮助你们完成课程,本次课程将列举一些课程资源。 首先,如果你有任何疑问,或是想和其他同学讨论问题,或是想和包括我在内的教学人员讨论任何问题,或是想要归档一个错误,论坛是最好的去处,我和其他教学人员将定期关注论坛的内容。论坛也是一个你从同学那里得到问题答案的好地方,如果想要回答同学的问题,可以从课程首页来到论坛: 点击论坛标签可以进入论坛 在论坛上是提问的最佳途径,但是出于一些原因,可能要直接联系我们,可以将邮件发送到这个地址,我们会尽力阅读每一份邮件并尝试解决普遍出现的问。由于邮件数量庞大,不一定能迅速回复每一封邮件。另外,一些公司会尝试给员工做深度学习培训,如果你们想对员工负责,聘请专家培训上百甚至更多员工深度学习,请用企业邮箱与我们联系。我们处在大学学术开发的初始阶段,如果你是大学领导或者是管理人员,并且希望在你们学校开设一门深度学习课程,请通过大学邮箱联系我们。邮箱地址如下,祝你们好运! Contact us: feedback@deeplearning.ai Companies: enterprise@deeplearning.ai Universities: academic@deeplearning.ai 第二周:神经网络的编程基础(Basics of Neural Network programming) 2.1 二分类(Binary Classification) 这周我们将学习神经网络的基础知识,其中需要注意的是,当实现一个神经网络的时候,我们需要知道一些非常重要的技术和技巧。例如有一个包含$m$个样本的训练集,你很可能习惯于用一个 for 循环来遍历训练集中的每个样本,但是当实现一个神经网络的时候,我们通常不直接使用 for 循环来遍历整个训练集,所以在这周的课程中你将学会如何处理训练集。 另外在神经网络的计算中,通常先有一个叫做前向暂停( forward pause )或叫做前向传播( foward propagation )的步骤,接着有一个叫做反向暂停( backward pause ) 或叫做反向传播**(backward propagation**)的步骤。所以这周我也会向你介绍为什么神经网络的训练过程可以分为前向传播和反向传播两个独立的部分。 在课程中我将使用逻辑回归(l ogistic regression )来传达这些想法,以使大家能够更加容易地理解这些概念。即使你之前了解过逻辑回归,我认为这里还是有些新的、有趣的东西等着你去发现和了解,所以现在开始进入正题。 逻辑回归是一个用于二分类( binary classification )的算法。首先我们从一个问题开始说起,这里有一个二分类问题的例子,假如你有一张图片作为输入,比如这只猫,如果识别这张图片为猫,则输出标签1作为结果;如果识别出不是猫,那么输出标签0作为结果。现在我们可以用字母 $y$来 表示输出的结果标签,如下图所示: 我们来看看一张图片在计算机中是如何表示的,为了保存一张图片,需要保存三个矩阵,它们分别对应图片中的红、绿、蓝三种颜色通道,如果你的图片大小为64x64像素,那么你就有三个规模为64x64的矩阵,分别对应图片中红、绿、蓝三种像素的强度值。为了便于表示,这里我画了三个很小的矩阵,注意它们的规模为5x4 而不是64x64,如下图所示: 为了把这些像素值放到一个特征向量中,我们需要把这些像素值提取出来,然后放入一个特征向量$x$。为了把这些像素值转换为特征向量 $x$,我们需要像下面这样定义一个特征向量 $x$ 来表示这张图片,我们把所有的像素都取出来,例如255、231等等,直到取完所有的红色像素,接着最后是255、134、…、255、134等等,直到得到一个特征向量,把图片中所有的红、绿、蓝像素值都列出来。如果图片的大小为64x64像素,那么向量 $x$ 的总维度,将是64乘以64乘以3,这是三个像素矩阵中像素的总量。在这个例子中结果为12,288。现在我们用$n_x=12,288$,来表示输入特征向量的维度,有时候为了简洁,我会直接用小写的$n$来表示输入特征向量$x$的维度。所以在二分类问题中,我们的目标就是习得一个分类器,它以图片的特征向量作为输入,然后预测输出结果$y$为1还是0,也就是预测图片中是否有猫: 接下来我们说明一些在余下课程中,需要用到的一些符号。 符号定义  : $x$:表示一个$n_x$维数据,为输入数据,维度为$(n_x,1)$;  $y​$:表示输出结果,取值为$(0,1)​$; $(x^{(i)},y^{(i)})$:表示第$i$组数据,可能是训练数据,也可能是测试数据,此处默认为训练数据;  $X=[x^{(1)},x^{(2)},...,x^{(m)}]$:表示所有的训练数据集的输入值,放在一个 $n_x×m$的矩阵中,其中$m$表示样本数目;  $Y=[y^{(1)},y^{(2)},...,y^{(m)}]$:对应表示所有训练数据集的输出值,维度为$1×m$。 用一对$(x,y)$来表示一个单独的样本,$x$代表$n_x$维的特征向量,$y$ 表示标签(输出结果)只能为0或1。 而训练集将由$m$个训练样本组成,其中$(x^{(1)},y^{(1)})$表示第一个样本的输入和输出,$(x^{(2)},y^{(2)})$表示第二个样本的输入和输出,直到最后一个样本$(x^{(m)},y^{(m)})$,然后所有的这些一起表示整个训练集。有时候为了强调这是训练样本的个数,会写作$M_{train}$,当涉及到测试集的时候,我们会使用$M_{test}$来表示测试集的样本数,所以这是测试集的样本数: 最后为了能把训练集表示得更紧凑一点,我们会定义一个矩阵用大写$X$的表示,它由输入向量$x^{(1)}$、$x^{(2)}$等组成,如下图放在矩阵的列中,所以现在我们把$x^{(1)}$作为第一列放在矩阵中,$x^{(2)}$作为第二列,$x^{(m)}$放到第$m$列,然后我们就得到了训练集矩阵$X$。所以这个矩阵有$m$列,$m$是训练集的样本数量,然后这个矩阵的高度记为$n_x$,注意有时候可能因为其他某些原因,矩阵$X$会由训练样本按照行堆叠起来而不是列,如下图所示:$x^{(1)}$的转置直到$x^{(m)}$的转置,但是在实现神经网络的时候,使用左边的这种形式,会让整个实现的过程变得更加简单: 现在来简单温习一下:$X$是一个规模为$n_x$乘以$m$的矩阵,当你用 Python 实现的时候,你会看到 X.shape ,这是一条 Python 命令,用于显示矩阵的规模,即 X.shape 等于$(n_x,m)$,$X$是一个规模为$n_x$乘以$m$的矩阵。所以综上所述,这就是如何将训练样本(输入向量$X$的集合)表示为一个矩阵。 那么输出标签$y$呢?同样的道理,为了能更加容易地实现一个神经网络,将标签$y$放在列中将会使得后续计算非常方便,所以我们定义大写的$Y$等于${{y}^{\left( 1 \right)}},{{y}^{\left( m \right)}},...,{{y}^{\left( m \right)}}$,所以在这里是一个规模为1乘以$m$的矩阵,同样地使用 Python 将表示为 Y.shape 等于$(1,m)$,表示这是一个规模为1乘以$m$的矩阵。 当你在后面的课程中实现神经网络的时候,你会发现,一个好的符号约定能够将不同训练样本的数据很好地组织起来。而我所说的数据不仅包括 $x$ 或者 $y$ 还包括之后你会看到的其他的量。将不同的训练样本的数据提取出来,然后就像刚刚我们对 $x$ 或者 $y$ 所做的那样,将他们堆叠在矩阵的列中,形成我们之后会在逻辑回归和神经网络上要用到的符号表示。如果有时候你忘了这些符号的意思,比如什么是 $m$,或者什么是 $n$,或者忘了其他一些东西,我们也会在课程的网站上放上符号说明,然后你可以快速地查阅每个具体的符号代表什么意思,好了,我们接着到下一个视频,在下个视频中,我们将以逻辑回归作为开始。 备注:附录里也写了符号说明。 2.2 逻辑回归(Logistic Regression) 在这个视频中,我们会重温逻辑回归学习算法,该算法适用于二分类问题,本节将主要介绍逻辑回归的 Hypothesis Function (假设函数)。 对于二元分类问题来讲,给定一个输入特征向量$X$,它可能对应一张图片,你想识别这张图片识别看它是否是一只猫或者不是一只猫的图片,你想要一个算法能够输出预测,你只能称之为$\hat{y}$,也就是你对实际值 $y$ 的估计。更正式地来说,你想让 $\hat{y}$ 表示 $y$ 等于1的一种可能性或者是机会,前提条件是给定了输入特征$X$。换句话来说,如果$X$是我们在上个视频看到的图片,你想让 $\hat{y}$ 来告诉你这是一只猫的图片的机率有多大。在之前的视频中所说的,$X$是一个$n_x$维的向量(相当于有$n_x$个特征的特征向量)。我们用$w$来表示逻辑回归的参数,这也是一个$n_x$维向量(因为$w$实际上是特征权重,维度与特征向量相同),参数里面还有$b$,这是一个实数(表示偏差)。所以给出输入$x$以及参数$w$和$b$之后,我们怎样产生输出预测值$\hat{y}$,一件你可以尝试却不可行的事是让$\hat{y}={{w}^{T}}x+b$。 这时候我们得到的是一个关于输入$x$的线性函数,实际上这是你在做线性回归时所用到的,但是这对于二元分类问题来讲不是一个非常好的算法,因为你想让$\hat{y}$表示实际值$y$等于1的机率的话,$\hat{y}$ 应该在0到1之间。这是一个需要解决的问题,因为${{w}^{T}}x+b$可能比1要大得多,或者甚至为一个负值。对于你想要的在0和1之间的概率来说它是没有意义的,因此在逻辑回归中,我们的输出应该是$\hat{y}$等于由上面得到的线性函数式子作为自变量的 sigmoid 函数中,公式如上图最下面所示,将线性函数转换为非线性函数。 下图是 sigmoid 函数的图像,如果我把水平轴作为$z$轴,那么关于$z$的 sigmoid 函数是这样的,它是平滑地从0走向1,让我在这里标记纵轴,这是0,曲线与纵轴相交的截距是0.5,这就是关于$z$的 sigmoid 函数的图像。我们通常都使用$z$来表示${{w}^{T}}x+b$的值。 关于 sigmoid 函数的公式是这样的,$\sigma \left( z \right)=\frac{1}{1+{{e}^{-z}}}$,在这里$z$是一个实数,这里要说明一些要注意的事情,如果$z$非常大那么${{e}^{-z}}$将会接近于0,关于$z$的 sigmoid 函数将会近似等于1除以1加上某个非常接近于0的项,因为$e$ 的指数如果是个绝对值很大的负数的话,这项将会接近于0,所以如果$z$很大的话那么关于$z$的 sigmoid 函数会非常接近1。相反地,如果$z$非常小或者说是一个绝对值很大的负数,那么关于${{e}^{-z}}$这项会变成一个很大的数,你可以认为这是1除以1加上一个非常非常大的数,所以这个就接近于0。实际上你看到当$z$变成一个绝对值很大的负数,关于$z$的 sigmoid 函数就会非常接近于0,因此当你实现逻辑回归时,你的工作就是去让机器学习参数$w$以及$b$这样才使得$\hat{y}$成为对$y=1$这一情况的概率的一个很好的估计。 在继续进行下一步之前,介绍一种符号惯例,可以让参数$w$和参数$b$分开。在符号上要注意的一点是当我们对神经网络进行编程时经常会让参数$w$和参数$b$分开,在这里参数$b$对应的是一种偏置。在之前的机器学习课程里,你可能已经见过处理这个问题时的其他符号表示。比如在某些例子里,你定义一个额外的特征称之为${{x} {0}}$,并且使它等于1,那么现在$X$就是一个$n_x$加1维的变量,然后你定义$\hat{y}=\sigma \left( {{\theta }^{T}}x \right)$的 sigmoid 函数。在这个备选的符号惯例里,你有一个参数向量${{\theta } {0}},{{\theta } {1}},{{\theta } {2}},...,{{\theta } {{{n} {x}}}}$,这样${{\theta } {0}}$就充当了$b$,这是一个实数,而剩下的${{\theta } {1}}$ 直到${{\theta } {{{n} {x}}}}$充当了$w$,结果就是当你实现你的神经网络时,有一个比较简单的方法是保持$b$和$w$分开。但是在这节课里我们不会使用任何这类符号惯例,所以不用去担心。 现在你已经知道逻辑回归模型是什么样子了,下一步要做的是训练参数$w$和参数$b$,你需要定义一个代价函数,让我们在下节课里对其进行解释。 2.3 逻辑回归的代价函数(Logistic Regression Cost Function) 在上个视频中,我们讲了逻辑回归模型,这个视频里,我们讲逻辑回归的代价函数(也翻译作成本函数)。 为什么需要代价函数: 为了训练逻辑回归模型的参数参数$w$和参数$b$我们,需要一个代价函数,通过训练代价函数来得到参数$w$和参数$b$。先看一下逻辑回归的输出函数: 为了让模型通过学习调整参数,你需要给予一个$m$样本的训练集,这会让你在训练集上找到参数$w$和参数$b$,,来得到你的输出。 对训练集的预测值,我们将它写成$\hat{y}$,我们更希望它会接近于训练集中的$y$值,为了对上面的公式更详细的介绍,我们需要说明上面的定义是对一个训练样本来说的,这种形式也使用于每个训练样本,我们使用这些带有圆括号的上标来区分索引和样本,训练样本$i$所对应的预测值是${{y}^{(i)}}$,是用训练样本的${{w}^{T}}{{x}^{(i)}}+b$然后通过 sigmoid 函数来得到,也可以把$z$定义为${{z}^{(i)}}={{w}^{T}}{{x}^{(i)}}+b$,我们将使用这个符号$(i)$注解,上标$(i)$来指明数据表示$x$或者$y$或者$z$或者其他数据的第$i$个训练样本,这就是上标$(i)$的含义。 损失函数: 损失函数又叫做误差函数,用来衡量算法的运行情况, Loss function:$L\left( \hat{y},y \right)$. 我们通过这个$L$称为的损失函数,来衡量预测输出值和实际值有多接近。一般我们用预测值和实际值的平方差或者它们平方差的一半,但是通常在逻辑回归中我们不这么做,因为当我们在学习逻辑回归参数的时候,会发现我们的优化目标不是凸优化,只能找到多个局部最优值,梯度下降法很可能找不到全局最优值,虽然平方差是一个不错的损失函数,但是我们在逻辑回归模型中会定义另外一个损失函数。 我们在逻辑回归中用到的损失函数是:$L\left( \hat{y},y \right)=-y\log(\hat{y})-(1-y)\log (1-\hat{y})$ 为什么要用这个函数作为逻辑损失函数?当我们使用平方误差作为损失函数的时候,你会想要让这个误差尽可能地小,对于这个逻辑回归损失函数,我们也想让它尽可能地小,为了更好地理解这个损失函数怎么起作用,我们举两个例子: 当$y=1$时损失函数$L=-\log (\hat{y})$,如果想要损失函数$L$尽可能得小,那么$\hat{y}$就要尽可能大,因为 sigmoid 函数取值$[0,1]$,所以$\hat{y}$会无限接近于1。 当$y=0$时损失函数$L=-\log (1-\hat{y})$,如果想要损失函数$L$尽可能得小,那么$\hat{y}$就要尽可能小,因为 sigmoid 函数取值$[0,1]$,所以$\hat{y}$会无限接近于0。 在这门课中有很多的函数效果和现在这个类似,就是如果$y$等于1,我们就尽可能让$\hat{y}$变大,如果$y$等于0,我们就尽可能让 $\hat{y}$ 变小。 损失函数是在单个训练样本中定义的,它衡量的是算法在单个训练样本中表现如何,为了衡量算法在全部训练样本上的表现如何,我们需要定义一个算法的代价函数,算法的代价函数是对$m$个样本的损失函数求和然后除以$m$: $J\left( w,b \right)=\frac{1}{m}\sum\limits_{i=1}^{m}{L\left( {{{\hat{y}}}^{(i)}},{{y}^{(i)}} \right)}=\frac{1}{m}\sum\limits_{i=1}^{m}{\left( -{{y}^{(i)}}\log {{{\hat{y}}}^{(i)}}-(1-{{y}^{(i)}})\log (1-{{{\hat{y}}}^{(i)}}) \right)}$ 损失函数只适用于像这样的单个训练样本,而代价函数是参数的总代价,所以在训练逻辑回归模型时候,我们需要找到合适的$w$和$b$,来让代价函数 $J$ 的总代价降到最低。 根据我们对逻辑回归算法的推导及对单个样本的损失函数的推导和针对算法所选用参数的总代价函数的推导,结果表明逻辑回归可以看做是一个非常小的神经网络,在下一个视频中,我们会看到神经网络会做什么。 2.4 梯度下降法(Gradient Descent) 梯度下降法可以做什么? 在你测试集上,通过最小化代价函数(成本函数)$J(w,b)$来训练的参数$w$和$b$, 如图,在第二行给出和之前一样的逻辑回归算法的代价函数(成本函数) 梯度下降法的形象化说明 在这个图中,横轴表示你的空间参数$w$和$b$,在实践中,$w$可以是更高的维度,但是为了更好地绘图,我们定义$w$和$b$,都是单一实数,代价函数(成本函数)$J(w,b)$是在水平轴$w$和$b$上的曲面,因此曲面的高度就是$J(w,b)$在某一点的函数值。我们所做的就是找到使得代价函数(成本函数)$J(w,b)$函数值是最小值,对应的参数$w$和$b$。 如图,代价函数(成本函数)$J(w,b)$是一个凸函数( convex function ),像一个大碗一样。 如图,这就与刚才的图有些相反,因为它是非凸的并且有很多不同的局部最小值。由于逻辑回归的代价函数(成本函数)$J(w,b)$特性,我们必须定义代价函数(成本函数)$J(w,b)$为凸函数。 初始化$w$和$b$, 可以用如图那个小红点来初始化参数$w$和$b$,也可以采用随机初始化的方法,对于逻辑回归几乎所有的初始化方法都有效,因为函数是凸函数,无论在哪里初始化,应该达到同一点或大致相同的点。 我们以如图的小红点的坐标来初始化参数$w$和$b$。 2. 朝最陡的下坡方向走一步,不断地迭代 我们朝最陡的下坡方向走一步,如图,走到了如图中第二个小红点处。 我们可能停在这里也有可能继续朝最陡的下坡方向再走一步,如图,经过两次迭代走到第三个小红点处。 3.直到走到全局最优解或者接近全局最优解的地方 通过以上的三个步骤我们可以找到全局最优解,也就是代价函数(成本函数)$J(w,b)$这个凸函数的最小值点。 梯度下降法的细节化说明(仅有一个参数) 假定代价函数(成本函数)$J(w)$ 只有一个参数$w$,即用一维曲线代替多维曲线,这样可以更好画出图像。 迭代就是不断重复做如图的公式: $:=$表示更新参数, $a $ 表示学习率( learning rate ),用来控制步长( step ),即向下走一步的长度$\frac{dJ(w)}{dw}$ 就是函数$J(w)$对$w$ 求导( derivative ),在代码中我们会使用$dw$表示这个结果 对于导数更加形象化的理解就是斜率( slope ),如图该点的导数就是这个点相切于 $J(w)$的小三角形的高除宽。假设我们以如图点为初始化点,该点处的斜率的符号是正的,即$\frac{dJ(w)}{dw}>0$,所以接下来会向左走一步。 整个梯度下降法的迭代过程就是不断地向左走,直至逼近最小值点。 假设我们以如图点为初始化点,该点处的斜率的符号是负的,即$\frac{dJ(w)}{dw}<0$,所以接下来会向右走一步。 整个梯度下降法的迭代过程就是不断地向右走,即朝着最小值点方向走。 梯度下降法的细节化说明(两个参数) 逻辑回归的代价函数(成本函数)$J(w,b)$是含有两个参数的。 $\partial $ 表示求偏导符号,可以读作 round , $\frac{\partial J(w,b)}{\partial w}$ 就是函数$J(w,b)$ 对$w$ 求偏导,在代码中我们会使用$dw$ 表示这个结果, $\frac{\partial J(w,b)}{\partial b}$ 就是函数$J(w,b)$对$b$ 求偏导,在代码中我们会使用$db$ 表示这个结果, 小写字母$d$ 用在求导数( derivative ),即函数只有一个参数, 偏导数符号$\partial $ 用在求偏导( partial derivative ),即函数含有两个以上的参数。 2.5 导数(Derivatives) 这个视频我主要是想帮你获得对微积分和导数直观的理解。或许你认为自从大学毕以后你再也没有接触微积分。这取决于你什么时候毕业,也许有一段时间了,如果你顾虑这点,请不要担心。为了高效应用神经网络和深度学习,你并不需要非常深入理解微积分。因此如果你观看这个视频或者以后的视频时心想:“哇哦,这些知识、这些运算对我来说很复杂。”我给你的建议是:坚持学习视频,最好下课后做作业,成功的完成编程作业,然后你就可以使用深度学习了。在第四周之后的学习中,你会看到定义的很多种类的函数,通过微积分他们能够帮助你把所有的知识结合起来,其中有的叫做前向函数和反向函数,因此你不需要了解所有你使用的那些微积分中的函数。所以你不用担心他们,除此之外在对深度学习的尝试中,这周我们要进一步深入了解微积分的细节。所有你只需要直观地认识微积分,用来构建和成功的应用这些算法。最后,如果你是精通微积分的那一小部分人群,你对微积分非常熟悉,你可以跳过这部分视频。其他同学让我们开始深入学习导数。 一个函数$f(a)=3a$,它是一条直线。下面我们来简单理解下导数。让我们看看函数中几个点,假定$a=2$,那么$f(a)$是$a$的3倍等于6,也就是说如果$a=2$,那么函数$f(a)=6$。假定稍微改变一点点$a$的值,只增加一点,变为2.001,这时$a$将向右做微小的移动。0.001的差别实在是太小了,不能在图中显示出来,我们把它右移一点,现在$f(a)$等于$a$的3倍是6.003,画在图里,比例不太符合。请看绿色高亮部分的这个小三角形,如果向右移动0.001,那么$f(a)$增加0.003,$f(a)$的值增加3倍于右移的$a$,因此我们说函数$f(a)$在$a=2$,.是这个导数的斜率,或者说,当$a=2$时,斜率是3。导数这个概念意味着斜率,导数听起来是一个很可怕、很令人惊恐的词,但是斜率以一种很友好的方式来描述导数这个概念。所以提到导数,我们把它当作函数的斜率就好了。更正式的斜率定义为在上图这个绿色的小三角形中,高除以宽。即斜率等于0.003除以0.001,等于3。或者说导数等于3,这表示当你将$a$右移0.001,$f(a)$的值增加3倍水平方向的量。 现在让我们从不同的角度理解这个函数。 假设$a=5$ ,此时$f(a)=3a=15$。 把$a$右移一个很小的幅度,增加到5.001,$f(a)=15.003$。 即在$a=5$ 时,斜率是3,这就是表示,当微小改变变量$a$的值,$\frac{df(a)}{da}=3$ 。一个等价的导数表达式可以这样写$\frac{d}{da}f(a)$ ,不管你是否将$f(a)$放在上面或者放在右边都没有关系。 在这个视频中,我讲解导数讨论的情况是我们将$a$偏移0.001,如果你想知道导数的数学定义,导数是你右移很小的$a$值(不是0.001,而是一个非常非常小的值)。通常导数的定义是你右移$a$(可度量的值)一个无限小的值,$f(a)$增加3倍(增加了一个非常非常小的值)。也就是这个三角形右边的高度。 那就是导数的正式定义。但是为了直观的认识,我们将探讨右移$a=0.001$ 这个值,即使0.001并不是无穷小的可测数据。导数的一个特性是:这个函数任何地方的斜率总是等于3,不管$a=2$或 $a=5$,这个函数的斜率总等于3,也就是说不管$a$的值如何变化,如果你增加0.001,$f(a)$的值就增加3倍。这个函数在所有地方的斜率都相等。一种证明方式是无论你将小三角形画在哪里,它的高除以宽总是3。 我希望带给你一种感觉:什么是斜率?什么是导函数?对于一条直线,在例子中函数的斜率,在任何地方都是3。在下一个视频让我们看一个更复杂的例子,这个例子中函数在不同点的斜率是可变的。 2.6 更多的导数例子(More Derivative Examples) 在这个视频中我将给出一个更加复杂的例子,在这个例子中,函数在不同点处的斜率是不一样的,先来举个例子: 我在这里画一个函数,$f(a)={{\text{a}}^{\text{2}}}$,如果$a=\text{2}$ 的话,那么$f(a)=4$。让我们稍稍往右推进一点点,现在$a=\text{2}.\text{001}$ ,则$f(a)\approx 4.004$ (如果你用计算器算的话,这个准确的值应该为4.004。0.001 我只是为了简便起见,省略了后面的部分),如果你在这儿画,一个小三角形,你就会发现,如果把$a$往右移动0.001,那么$f(a)$将增大四倍,即增大0.004。在微积分中我们把这个三角形斜边的斜率,称为$f(a)$在点$a=\text{2}$ 处的导数(即为4),或者写成微积分的形式,当$a=\text{2}$ 的时候, $\frac{d}{da}f(a)=4$ 由此可知,函数$f(a)={{a}^{{2}}}$,在$a$取不同值的时候,它的斜率是不同的,这和上个视频中的例子是不同的。 这里有种直观的方法可以解释,为什么一个点的斜率,在不同位置会不同如果你在曲线上,的不同位置画一些小小的三角形你就会发现,三角形高和宽的比值,在曲线上不同的地方,它们是不同的。所以当$a=2$ 时,斜率为4;而当$a=5$时,斜率为10 。如果你翻看微积分的课本,课本会告诉你,函数$f(a)={{a}^{{2}}}$的斜率(即导数)为$2a$。这意味着任意给定一点$a$,如果你稍微将$a$,增大0.001,那么你会看到$f(a)$将增大$2a$,即增大的值为点在$a$处斜率或导数,乘以你向右移动的距离。 现在有个小细节需要注意,导数增大的值,不是刚好等于导数公式算出来的值,而只是根据导数算出来的一个估计值。 为了总结这堂课所学的知识,我们再来看看几个例子: 假设$f(a)={{a}^{\text{3}}}$ 如果你翻看导数公式表,你会发现这个函数的导数,等于$3{{a}^{2}}$。所以这是什么意思呢,同样地举一个例子:我们再次令$a=2$,所以${{a}^{3}}=8$ ,如果我们又将$a$增大一点点,你会发现$f(a)\approx 8.012$, 你可以自己检查一遍,如果我们取8.012,你会发现${{2.001}^{3}}$ ,和8.012很接近,事实上当$a=2$时,导数值为$3×{{2}^{2}}$,即$3×4=12$。所以导数公式,表明如果你将$a$向右移动0.001时,$f(a)$ 将会向右移动12倍,即0.012。 来看最后一个例子,假设$f(a)={{\log }_{e}}a$,有些可能会写作$\ln a$,函数$\log a$ 的斜率应该为$\frac{1}{a}$,所以我们可以解释如下:如果$a$取任何值,比如又取$a=2$,然后又把$a$向右边移动0.001 那么$f(a)$将增大$\frac{\text{1}}{a}\times \text{0}\text{.001}$,如果你借助计算器的话,你会发现当$a=2$时$f(a)\approx \text{0}\text{.69315}$ ;而$a=2.001$时,$f(a)\approx \text{0}\text{.69365}$。所以$f(a)$增大了0.0005,如果你查看导数公式,当$a=2$的时候,导数值$\frac{d}{da}f(a)=\frac{\text{1}}{\text{2}}$。这表明如果你把 增大0.001,$f(a)$将只会增大0.001的二分之一,即0.0005。如果你画个小三角形你就会发现,如果$x$ 轴增加了0.001,那么$y$ 轴上的函数$\log a$,将增大0.001的一半 即0.0005。所以 $\frac{1}{a}$ ,当$a=2$时这里是 ,就是当$a=2$时这条线的斜率。这些就是有关,导数的一些知识。 在这个视频中,你只需要记住两点: 第一点,导数就是斜率,而函数的斜率,在不同的点是不同的。在第一个例子中$f(a)=\text{3}a$ ,这是一条直线,在任何点它的斜率都是相同的,均为3。但是对于函数$f(a)={{\text{a}}^{\text{2}}}$ ,或者$f(a)=\log a$,它们的斜率是变化的,所以它们的导数或者斜率,在曲线上不同的点处是不同的。 第二点,如果你想知道一个函数的导数,你可参考你的微积分课本或者维基百科,然后你应该就能找到这些函数的导数公式。 最后我希望,你能通过我生动的讲解,掌握这些有关导数和斜率的知识,下一课我们将讲解计算图,以及如何用它来求更加复杂的函数的导数。 2.7 计算图(Computation Graph) 可以说,一个神经网络的计算,都是按照前向或反向传播过程组织的。首先我们计算出一个新的网络的输出(前向过程),紧接着进行一个反向传输操作。后者我们用来计算出对应的梯度或导数。计算图解释了为什么我们用这种方式组织这些计算过程。在这个视频中,我们将举一个例子说明计算图是什么。让我们举一个比逻辑回归更加简单的,或者说不那么正式的神经网络的例子。 我们尝试计算函数$J$,$J$是由三个变量$a,b,c$组成的函数,这个函数是$\text{3(a}+\text{bc)}$ 。计算这个函数实际上有三个不同的步骤,首先是计算 $b$ 乘以 $c$,我们把它储存在变量$u$中,因此${u}={bc}$; 然后计算$v=a+u$;最后输出$J=3v$,这就是要计算的函数$J$。我们可以把这三步画成如下的计算图,我先在这画三个变量$a,b,c$,第一步就是计算$u=bc$,我在这周围放个矩形框,它的输入是$b,c$,接着第二步$v=a+u$,最后一步$J=3v$。 举个例子: $a=5,b=3,c=2$ ,$u=bc$就是6,$v=a+u$ ,就是5+6=11。$J$是3倍的 ,因此。即$3×(5+3×2)$。如果你把它算出来,实际上得到33就是$J$的值。 当有不同的或者一些特殊的输出变量时,例如本例中的$J$和逻辑回归中你想优化的代价函数$J$,因此计算图用来处理这些计算会很方便。从这个小例子中我们可以看出,通过一个从左向右的过程,你可以计算出$J$的值。为了计算导数,从右到左(红色箭头,和蓝色箭头的过程相反)的过程是用于计算导数最自然的方式。 概括一下:计算图组织计算的形式是用蓝色箭头从左到右的计算,让我们看看下一个视频中如何进行反向红色箭头(也就是从右到左)的导数计算,让我们继续下一个视频的学习。 2.8 使用计算图求导数(Derivatives with a Computation Graph) 在上一个视频中,我们看了一个例子使用流程计算图来计算函数J。现在我们清理一下流程图的描述,看看你如何利用它计算出函数$J$的导数。 下面用到的公式: $\frac{dJ}{du}=\frac{dJ}{dv}\frac{dv}{du}$ , $\frac{dJ}{db}=\frac{dJ}{du}\frac{du}{db}$ , $\frac{dJ}{da}=\frac{dJ}{du}\frac{du}{da}$ 这是一个流程图: 假设你要计算$\frac{{dJ}}{{dv}}$,那要怎么算呢?好,比如说,我们要把这个$v$值拿过来,改变一下,那么$J$的值会怎么变呢? 所以定义上$J = 3v$,现在$v=11$,所以如果你让$v$增加一点点,比如到11.001,那么$J =3v =33.003$,所以我这里$v$增加了0.001,然后最终结果是$J$上升到原来的3倍,所以$\frac{{dJ}}{{dv}}=3$,因为对于任何 $v$ 的增量$J$都会有3倍增量,而且这类似于我们在上一个视频中的例子,我们有$f(a)=3a$,然后我们推导出$\frac{{df}(a)}{{da}}= 3$,所以这里我们有$J=3v$,所以$\frac{{dJ}}{{dv}} =3$,这里$J$扮演了$f$的角色,在之前的视频里的例子。 在反向传播算法中的术语,我们看到,如果你想计算最后输出变量的导数,使用你最关心的变量对$v$的导数,那么我们就做完了一步反向传播,在这个流程图中是一个反向步骤。 我们来看另一个例子,$\frac{{dJ}}{da}$是多少呢?换句话说,如果我们提高$a$的数值,对$J$的数值有什么影响? 好,我们看看这个例子。变量$a=5$,我们让它增加到5.001,那么对v的影响就是$a+u$,之前$v=11$,现在变成11.001,我们从上面看到现在$J$ 就变成33.003了,所以我们看到的是,如果你让$a$增加0.001,$J$增加0.003。那么增加$a$,我是说如果你把这个5换成某个新值,那么$a$的改变量就会传播到流程图的最右,所以$J$最后是33.003。所以J的增量是3乘以$a$的增量,意味着这个导数是3。 要解释这个计算过程,其中一种方式是:如果你改变了$a$,那么也会改变$v$,通过改变$v$,也会改变$J$,所以$J$值的净变化量,当你提升这个值(0.001),当你把$a$值提高一点点,这就是$J$的变化量(0.003)。 首先a增加了,$v$也会增加,$v$增加多少呢?这取决于$\frac{{dv}}{da}$,然后$v$的变化导致$J$也在增加,所以这在微积分里实际上叫链式法则,如果$a$影响到$v$,$v$影响到$J$,那么当你让$a$变大时,$J$的变化量就是当你改变$a$时,$v$的变化量乘以改变$v$时$J$的变化量,在微积分里这叫链式法则。 我们从这个计算中看到,如果你让$a$增加0.001,$v$也会变化相同的大小,所以$\frac{{dv}}{da}= 1$。事实上,如果你代入进去,我们之前算过$\frac{{dJ}}{{dv}} =3$,$\frac{{dv}}{da} =1$,所以这个乘积3×1,实际上就给出了正确答案,$\frac{{dJ}}{da} = 3$。 这张小图表示了如何计算,$\frac{{dJ}}{{dv}}$就是$J$对变量$v$的导数,它可以帮助你计算$\frac{{dJ}}{da}$,所以这是另一步反向传播计算。 现在我想介绍一个新的符号约定,当你编程实现反向传播时,通常会有一个最终输出值是你要关心的,最终的输出变量,你真正想要关心或者说优化的。在这种情况下最终的输出变量是J,就是流程图里最后一个符号,所以有很多计算尝试计算输出变量的导数,所以输出变量对某个变量的导数,我们就用$dvar$命名,所以在很多计算中你需要计算最终输出结果的导数,在这个例子里是$J$,还有各种中间变量,比如$a、b、c、u、v$,当你在软件里实现的时候,变量名叫什么?你可以做的一件事是,在 python 中,你可以写一个很长的变量名,比如${dFinalOutputvar}_{dvar}$,但这个变量名有点长,我们就用$dJ_dvar$,但因为你一直对$dJ$求导,对这个最终输出变量求导。我这里要介绍一个新符号,在程序里,当你编程的时候,在代码里,我们就使用变量名$dvar$,来表示那个量。 好,所以在程序里是$dvar$表示导数,你关心的最终变量$J$的导数,有时最后是$L$,对代码中各种中间量的导数,所以代码里这个东西,你用$dv$表示这个值,所以$dv=3$,你的代码表示就是$da=3$。 好,所以我们通过这个流程图完成部分的后向传播算法。我们在下一张幻灯片看看这个例子剩下的部分。 我们清理出一张新的流程图,我们回顾一下,到目前为止,我们一直在往回传播,并计算$dv=3$,再次,$dv$是代码里的变量名,其真正的定义是$\frac{{dJ}}{{dv}}$。我发现$da=3$,再次,$da$是代码里的变量名,其实代表$\frac{{dJ}}{da}$的值。 大概手算了一下,两条直线怎么计算反向传播。 好,我们继续计算导数,我们看看这个值$u$,那么$\frac{dJ}{du}$是多少呢?通过和之前类似的计算,现在我们从$u=6$出发,如果你令$u$增加到6.001,那么$v$之前是11,现在变成11.001了,$J$ 就从33变成33.003,所以$J$ 增量是3倍,所以$\frac{{dJ}}{du}= 3$。对$u$的分析很类似对a的分析,实际上这计算起来就是$\frac{{dJ}}{dv}\cdot \frac{{dv}}{du}$,有了这个,我们可以算出$\frac{{dJ}}{dv} =3$,$\frac{{dv}}{du} = 1$,最终算出结果是$3×1=3$。 所以我们还有一步反向传播,我们最终计算出$du=3$,这里的$du$当然了,就是$\frac{{dJ}}{du}$。 现在,我们仔细看看最后一个例子,那么$\frac{{dJ}}{db}$呢?想象一下,如果你改变了$b$的值,你想要然后变化一点,让$J$ 值到达最大或最小,那么导数是什么呢?这个$J$函数的斜率,当你稍微改变$b$值之后。事实上,使用微积分链式法则,这可以写成两者的乘积,就是$\frac{{dJ}}{du}\cdot \frac{{du}}{db}$,理由是,如果你改变$b$一点点,所以$b$变化比如说3.001,它影响J的方式是,首先会影响$u$,它对$u$的影响有多大?好,$u$的定义是$b\cdot c$,所以$b=3$时这是6,现在就变成6.002了,对吧,因为在我们的例子中$c=2$,所以这告诉我们$\frac{{du}}{db}= 2$当你让$b$增加0.001时,$u$就增加两倍。所以$\frac{{du}}{db} =2$,现在我想$u$的增加量已经是$b$的两倍,那么$\frac{{dJ}}{du}$是多少呢?我们已经弄清楚了,这等于3,所以让这两部分相乘,我们发现$\frac{{dJ}}{db}= 6$。 好,这就是第二部分的推导,其中我们想知道 $u$ 增加0.002,会对$J$ 有什么影响。实际上$\frac{{dJ}}{du}=3$,这告诉我们u增加0.002之后,$J$上升了3倍,那么$J$ 应该上升0.006,对吧。这可以从$\frac{{dJ}}{du}= 3$推导出来。 如果你仔细看看这些数学内容,你会发现,如果$b$变成3.001,那么$u$就变成6.002,$v$变成11.002,然后$J=3v=33.006$,对吧?这就是如何得到$\frac{{dJ}}{db}= 6$。 为了填进去,如果我们反向走的话,$db=6$,而$db$其实是 Python 代码中的变量名,表示$\frac{{dJ}}{db}$。 我不会很详细地介绍最后一个例子,但事实上,如果你计算$\frac{{dJ}}{dc} =\frac{{dJ}}{du}\cdot \frac{{du}}{dc} = 3 \times 3$,这个结果是9。 我不会详细说明这个例子,在最后一步,我们可以推出$dc=9$。 所以这个视频的要点是,对于那个例子,当计算所有这些导数时,最有效率的办法是从右到左计算,跟着这个红色箭头走。特别是当我们第一次计算对$v$的导数时,之后在计算对$a$导数就可以用到。然后对$u$的导数,比如说这个项和这里这个项: 可以帮助计算对$b$的导数,然后对$c$的导数。 所以这是一个计算流程图,就是正向或者说从左到右的计算来计算成本函数J,你可能需要优化的函数,然后反向从右到左计算导数。如果你不熟悉微积分或链式法则,我知道这里有些细节讲的很快,但如果你没有跟上所有细节,也不用怕。在下一个视频中,我会再过一遍。在逻辑回归的背景下过一遍,并给你介绍需要做什么才能编写代码,实现逻辑回归模型中的导数计算。 2.9 逻辑回归中的梯度下降(Logistic Regression Gradient Descent) 本节我们讨论怎样通过计算偏导数来实现逻辑回归的梯度下降算法。它的关键点是几个重要公式,其作用是用来实现逻辑回归中梯度下降算法。但是在本节视频中,我将使用计算图对梯度下降算法进行计算。我必须要承认的是,使用计算图来计算逻辑回归的梯度下降算法有点大材小用了。但是,我认为以这个例子作为开始来讲解,可以使你更好的理解背后的思想。从而在讨论神经网络时,你可以更深刻而全面地理解神经网络。接下来让我们开始学习逻辑回归的梯度下降算法。 假设样本只有两个特征${{x} {1}}$和${{x} {2}}$,为了计算$z$,我们需要输入参数${{w} {1}}$、${{w} {2}}$ 和$b$,除此之外还有特征值${{x} {1}}$和${{x} {2}}$。因此$z$的计算公式为: $z={{w} {1}}{{x} {1}}+{{w} {2}}{{x} {2}}+b$ 回想一下逻辑回归的公式定义如下: $\hat{y}=a=\sigma (z)$ 其中$z={{w}^{T}}x+b$ $\sigma \left( z \right)=\frac{1}{1+{{e}^{-z}}}$ 损失函数: $L( {{{\hat{y}}}^{(i)}},{{y}^{(i)}})=-{{y}^{(i)}}\log {{\hat{y}}^{(i)}}-(1-{{y}^{(i)}})\log (1-{{\hat{y}}^{(i)}})$ 代价函数: $J\left( w,b \right)=\frac{1}{m}\sum\nolimits_{i}^{m}{L( {{{\hat{y}}}^{(i)}},{{y}^{(i)}})}$ 假设现在只考虑单个样本的情况,单个样本的代价函数定义如下: $L(a,y)=-(y\log (a)+(1-y)\log (1-a))$ 其中$a$是逻辑回归的输出,$y$是样本的标签值。现在让我们画出表示这个计算的计算图。 这里先复习下梯度下降法,$w$和$b$的修正量可以表达如下: $w:=w-a \frac{\partial J(w,b)}{\partial w}$,$b:=b-a\frac{\partial J(w,b)}{\partial b}$ 如图:在这个公式的外侧画上长方形。然后计算: $\hat{y}=a=\sigma(z)$ 也就是计算图的下一步。最后计算损失函数$L(a,y)$。 有了计算图,我就不需要再写出公式了。因此,为了使得逻辑回归中最小化代价函数$L(a,y)$,我们需要做的仅仅是修改参数$w$和$b$的值。前面我们已经讲解了如何在单个训练样本上计算代价函数的前向步骤。现在让我们来讨论通过反向计算出导数。 因为我们想要计算出的代价函数$L(a,y)$的导数,首先我们需要反向计算出代价函数$L(a,y)$关于$a$的导数,在编写代码时,你只需要用$da$ 来表示$\frac{dL(a,y)}{da}$ 。 通过微积分得到: $\frac{dL(a,y)}{da}=-y/a+(1-y)/(1-a)$ 如果你不熟悉微积分,也不必太担心,我们会列出本课程涉及的所有求导公式。那么如果你非常熟悉微积分,我们鼓励你主动推导前面介绍的代价函数的求导公式,使用微积分直接求出$L(a,y)$关于变量$a$的导数。如果你不太了解微积分,也不用太担心。现在我们已经计算出$da$,也就是最终输出结果的导数。 现在可以再反向一步,在编写 Python 代码时,你只需要用$dz$来表示代价函数$L$关于$z$ 的导数$\frac{dL}{dz}$,也可以写成$\frac{dL(a,y)}{dz}$,这两种写法都是正确的。 $\frac{dL}{dz}=a-y$ 。 因为$\frac{dL(a,y)}{dz}=\frac{dL}{dz}=(\frac{dL}{da})\cdot (\frac{da}{dz})$, 并且$\frac{da}{dz}=a\cdot (1-a)$, 而 $\frac{dL}{da}=(-\frac{y}{a}+\frac{(1-y)}{(1-a)})$,因此将这两项相乘,得到: ${dz} = \frac{{dL}(a,y)}{{dz}} = \frac{{dL}}{{dz}} = \left( \frac{{dL}}{{da}} \right) \cdot \left(\frac{{da}}{{dz}} \right) = ( - \frac{y}{a} + \frac{(1 - y)}{(1 - a)})\cdot a(1 - a) = a - y$ 视频中为了简化推导过程,假设${{n}_{x}}$ 这个推导的过程就是我之前提到过的链式法则。如果你对微积分熟悉,放心地去推导整个求导过程,如果不熟悉微积分,你只需要知道$dz=(a-y)$已经计算好了。 现在进行最后一步反向推导,也就是计算$w$和$b$变化对代价函数$L$的影响,特别地,可以用: $d{{w} {1}}=\frac{1}{m}\sum\limits {i}^{m}{x_{1}^{(i)}}({{a}^{(i)}}-{{y}^{(i)}})$ $d{{w} {2}}=\frac{1}{m}\sum\limits {i}^{m}{x_{2}^{(i)}}({{a}^{(i)}}-{{y}^{(i)}})$ $db=\frac{1}{m}\sum\limits_{i}^{m}{({{a}^{(i)}}-{{y}^{(i)}})}$ 视频中, $d{{w} {1}}$ 表示$\frac{\partial L}{\partial {{w} {1}}}={{x} {1}}\cdot dz$, $d{{w} {\text{2}}}$ 表示$\frac{\partial L}{\partial {{w} {2}}}={{x} {2}}\cdot dz$, $db=dz$。 因此,关于单个样本的梯度下降算法,你所需要做的就是如下的事情: 使用公式$dz=(a-y)$计算$dz$, 使用$d{{w} {1}}={{x} {1}}\cdot dz$ 计算$d{{w} {1}}$, $d{{w} {2}}={{x} {2}}\cdot dz$计算$d{{w} {2}}$, $db=dz$ 来计算$db$, 然后: 更新${{w} {1}}={{w} {1}}-a d{{w} {1}}$, 更新${{w} {2}}={{w} {2}}-a d{{w} {2}}$, 更新$b=b-\alpha db$。 这就是关于单个样本实例的梯度下降算法中参数更新一次的步骤。 现在你已经知道了怎样计算导数,并且实现针对单个训练样本的逻辑回归的梯度下降算法。但是,训练逻辑回归模型不仅仅只有一个训练样本,而是有$m$个训练样本的整个训练集。因此在下一节视频中,我们将这些思想应用到整个训练样本集中,而不仅仅只是单个样本上。 2.10 m 个样本的梯度下降(Gradient Descent on m Examples) 在之前的视频中,你已经看到如何计算导数,以及应用梯度下降在逻辑回归的一个训练样本上。现在我们想要把它应用在$m$个训练样本上。 首先,让我们时刻记住有关于损失函数$J(w,b)$的定义。 $J(w,b)=\frac{1}{m}\sum\limits_{i=1}^{m}{L({{a}^{(i)}},{{y}^{(i)}})}$ 当你的算法输出关于样本$y$的${{a}^{(i)}}$,${{a}^{(i)}}$是训练样本的预测值,即:$\sigma ( {{z}^{(i)}})=\sigma( {{w}^{T}}{{x}^{\left( i \right)}}+b)$。 所以我们在前面的幻灯中展示的是对于任意单个训练样本,如何计算微分当你只有一个训练样本。因此$d{{w} {1}}$,$d{{w} {\text{2}}}$和$db$ 添上上标$i$表示你求得的相应的值。如果你面对的是我们在之前的幻灯中演示的那种情况,但只使用了一个训练样本$({{x}^{(i)}},{{y}^{(i)}})$。 现在你知道带有求和的全局代价函数,实际上是1到$m$项各个损失的平均。 所以它表明全局代价函数对${{w} {1}}$的微分,对${{w} {1}}$的微分也同样是各项损失对${{w}_{1}}$微分的平均。 但之前我们已经演示了如何计算这项,即之前幻灯中演示的如何对单个训练样本进行计算。所以你真正需要做的是计算这些微分,如我们在之前的训练样本上做的。并且求平均,这会给你全局梯度值,你能够把它直接应用到梯度下降算法中。 所以这里有很多细节,但让我们把这些装进一个具体的算法。同时你需要一起应用的就是逻辑回归和梯度下降。 我们初始化$J=0,d{{w} {1}}=0,d{{w} {2}}=0,db=0$ 代码流程: J=0;dw1=0;dw2=0;db=0; for i = 1 to m z(i) = wx(i)+b; a(i) = sigmoid(z(i)); J += -[y(i)log(a(i))+(1-y(i))log(1-a(i)); dz(i) = a(i)-y(i); dw1 += x1(i)dz(i); dw2 += x2(i)dz(i); db += dz(i); J/= m; dw1/= m; dw2/= m; db/= m; w=w-alpha*dw b=b-alpha*db 幻灯片上只应用了一步梯度下降。因此你需要重复以上内容很多次,以应用多次梯度下降。看起来这些细节似乎很复杂,但目前不要担心太多。希望你明白,当你继续尝试并应用这些在编程作业里,所有这些会变的更加清楚。 但这种计算中有两个缺点,也就是说应用此方法在逻辑回归上你需要编写两个 for 循环。第一个 for 循环是一个小循环遍历$m$个训练样本,第二个 for 循环是一个遍历所有特征的 for 循环。这个例子中我们只有2个特征,所以$n$等于2并且${{n} {x}}$ 等于2。 但如果你有更多特征,你开始编写你的因此$d{{w} {1}}$,$d{{w} {2}}$,你有相似的计算从$d{{w} {3}}$一直下去到$d{{w}_{n}}$。所以看来你需要一个 for 循环遍历所有$n$个特征。 当你应用深度学习算法,你会发现在代码中显式地使用 for 循环使你的算法很低效,同时在深度学习领域会有越来越大的数据集。所以能够应用你的算法且没有显式的 for 循环会是重要的,并且会帮助你适用于更大的数据集。所以这里有一些叫做向量化技术,它可以允许你的代码摆脱这些显式的 for 循环。 我想在先于深度学习的时代,也就是深度学习兴起之前,向量化是很棒的。可以使你有时候加速你的运算,但有时候也未必能够。但是在深度学习时代向量化,摆脱for循环已经变得相当重要。因为我们越来越多地训练非常大的数据集,因此你真的需要你的代码变得非常高效。所以在接下来的几个视频中,我们会谈到向量化,以及如何应用向量化而连一个 for 循环都不使用。所以学习了这些,我希望你有关于如何应用逻辑回归,或是用于逻辑回归的梯度下降,事情会变得更加清晰。当你进行编程练习,但在真正做编程练习之前让我们先谈谈向量化。然后你可以应用全部这些东西,应用一个梯度下降的迭代而不使用任何 for 循环。 2.11 向量化(Vectorization) 参考视频: 2.11 向量化 向量化是非常基础的去除代码中 for 循环的艺术,在深度学习安全领域、深度学习实践中,你会经常发现自己训练大数据集,因为深度学习算法处理大数据集效果很棒,所以你的代码运行速度非常重要,否则如果在大数据集上,你的代码可能花费很长时间去运行,你将要等待非常长的时间去得到结果。所以在深度学习领域,运行向量化是一个关键的技巧,让我们举个栗子说明什么是向量化。 在逻辑回归中你需要去计算$z={{w}^{T}}x+b$,$w$、$x$都是列向量。如果你有很多的特征那么就会有一个非常大的向量,所以$w\in {{\mathbb{R}}^{{{n} {x}}}}$ , $x\in{{\mathbb{R}}^{{{n} {x}}}}$,所以如果你想使用非向量化方法去计算${{w}^{T}}x$,你需要用如下方式( python ) z=0 for i in range(n_x): z += w[i]*x[i] z += b 这是一个非向量化的实现,你会发现这真的很慢,作为一个对比,向量化实现将会非常直接计算${{w}^{T}}x$,代码如下: z=np.dot(w,x)+b 这是向量化计算${{w}^{T}}x$的方法,你将会发现这个非常快 让我们用一个小例子说明一下,在我的我将会写一些代码(以下为教授在他的 Jupyter notebook 上写的 Python 代码,) import numpy as np #导入numpy库 a = np.array([1,2,3,4]) #创建一个数据a print(a) # [1 2 3 4] import time #导入时间库 a = np.random.rand(1000000) b = np.random.rand(1000000) #通过round随机得到两个一百万维度的数组 tic = time.time() #现在测量一下当前时间 #向量化的版本 c = np.dot(a,b) toc = time.time() print("Vectorized version:" + str(1000*(toc-tic)) +"ms") #打印一下向量化的版本的时间 #继续增加非向量化的版本 c = 0 tic = time.time() for i in range(1000000): c += a[i]*b[i] toc = time.time() print(c) print("For loop:" + str(1000*(toc-tic)) + "ms")#打印for循环的版本的时间 返回值见图。 在两个方法中,向量化和非向量化计算了相同的值,如你所见,向量化版本花费了1.5毫秒,非向量化版本的 for 循环花费了大约几乎500毫秒,非向量化版本多花费了300倍时间。所以在这个例子中,仅仅是向量化你的代码,就会运行300倍快。这意味着如果向量化方法需要花费一分钟去运行的数据, for 循环将会花费5个小时去运行。 一句话总结,以上都是再说和for循环相比,向量化可以快速得到结果。 你可能听过很多类似如下的话,“大规模的深度学习使用了 GPU 或者图像处理单元实现”,但是我做的所有的案例都是在 jupyter notebook 上面实现,这里只有 CPU , CPU 和 GPU 都有并行化的指令,他们有时候会叫做 SIMD 指令,这个代表了一个单独指令多维数据,这个的基础意义是,如果你使用了 built-in 函数,像 np.function 或者并不要求你实现循环的函数,它可以让 python 的充分利用并行化计算,这是事实在 GPU 和 CPU 上面计算, GPU 更加擅长 SIMD 计算,但是 CPU 事实上也不是太差,可能没有 GPU 那么擅长吧。接下来的视频中,你将看到向量化怎么能够加速你的代码,经验法则是,无论什么时候,避免使用明确的 for 循环。 以下代码及运行结果截图: 2.12 向量化的更多例子(More Examples of Vectorization) 从上节视频中,你知道了怎样通过 numpy 内置函数和避开显式的循环( loop )的方式进行向量化,从而有效提高代码速度。 经验提醒我,当我们在写神经网络程序时,或者在写逻辑(l ogistic )回归,或者其他神经网络模型时,应该避免写循环( loop )语句。虽然有时写循环( loop )是不可避免的,但是我们可以使用比如 numpy 的内置函数或者其他办法去计算。当你这样使用后,程序效率总是快于循环( loop) 。 让我们看另外一个例子。如果你想计算向量$u=Av$,这时矩阵乘法定义为,矩阵乘法的定义就是:$u_{i} =\sum_{j}^{}{A_{\text{ij}}v_{i}}$,这取决于你怎么定义$u_{i}$值。同样使用非向量化实现,$u=np.zeros(n,1)$, 并且通过两层循环$for(i):for(j):$,得到$u[i]=u[i]+A[i][j]*v[j]$ 。现在就有了$i$ 和 $j$ 的两层循环,这就是非向量化。向量化方式就可以用$u=np.dot(A,v)$,右边这种向量化实现方式,消除了两层循环使得代码运行速度更快。 下面通过另一个例子继续了解向量化。如果你已经有一个向量$v$,并且想要对向量$v$的每个元素做指数操作,得到向量$u$等于$e$的$v_1$,$e$的$v_2$,一直到$e$的$v_n$次方。这里是非向量化的实现方式,首先你初始化了向量$u=np.zeros(n,1)$,并且通过循环依次计算每个元素。但事实证明可以通过 python 的 numpy 内置函数,帮助你计算这样的单个函数。所以我会引入 import numpy as np ,执行 $u=np.exp(v)$ 命令。注意到,在之前有循环的代码中,这里仅用了一行代码,向量$v$作为输入,$u$作为输出。你已经知道为什么需要循环,并且通过右边代码实现,效率会明显的快于循环方式。 事实上, numpy 库有很多向量函数。比如 u=np.log 是计算对数函数($log$)、 np.abs() 计算数据的绝对值、 np.maximum(v, 0) 按元素计算$v$中每个元素和和0相比的最大值, v**2 代表获得元素 $v$ 每个值的平方、 1/v 获取 $v$ 中每个元素的倒数等等。所以当你想写循环时候,检查 numpy 是否存在类似的内置函数,从而避免使用循环( loop )方式。 那么,将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看我们是否能简化两个计算过程中的某一步。这是我们逻辑回归的求导代码,有两层循环。在这例子我们有$n$个特征值。如果你有超过两个特征时,需要循环 $dw_1$ 、$dw_2$ 、$dw_3$ 等等。所以 $j$ 的实际值是1、2 和 $n_x$,就是你想要更新的值。所以我们想要消除第二循环,在这一行,这样我们就不用初始化 $dw_1$ , $dw_2$ 都等于0。去掉这些,而是定义 $dw$ 为一个向量,设置 $u=np.zeros(n(x),1)$。定义了一个$x$行的一维向量,从而替代循环。我们仅仅使用了一个向量操作 $dw=dw+x^{(i)}dz^{(i)}$ 。最后,我们得到 $dw=dw/m$ 。现在我们通过将两层循环转成一层循环,我们仍然还有这个循环训练样本。 希望这个视频给了你一点向量化感觉,减少一层循环使你代码更快,但事实证明我们能做得更好。所以在下个视频,我们将进一步的讲解逻辑回归,你将会看到更好的监督学习结果。在训练中不需要使用任何 for 循环,你也可以写出代码去运行整个训练集。到此为止一切都好,让我们看下一个视频。 2.13 向量化逻辑回归(Vectorizing Logistic Regression) 我们已经讨论过向量化是如何显著加速你的代码,在本次视频中我们将讨论如何实现逻辑回归的向量化计算。这样就能处理整个数据集,甚至不会用一个明确的for循环就能实现对于整个数据集梯度下降算法的优化。我对这项技术感到非常激动,并且当我们后面谈到神经网络时同样也不会用到一个明确的 for 循环。 让我们开始吧,首先我们回顾一下逻辑回归的前向传播步骤。所以,如果你有 $m$ 个训练样本,然后对第一个样本进行预测,你需要这样计算。计算 $z$,我正在使用这个熟悉的公式 $z^{(1)}=w^{T}x^{(1)}+b$ 。然后计算激活函数 $a^{(1)}=\sigma (z^{(1)})$ ,计算第一个样本的预测值 $y$ 。 然后对第二个样本进行预测,你需要计算 $z^{(2)}=w^{T}x^{(2)}+b$ , $a^{(2)}=\sigma (z^{(2)})$ 。然后对第三个样本进行预测,你需要计算 $z^{(3)}=w^{T}x^{(3)}+b$ , $a^{(3)}=\sigma (z^{(3)})$ ,依次类推。如果你有 $m$ 个训练样本,你可能需要这样做 $m$ 次,可以看出,为了完成前向传播步骤,即对我们的 $m$ 个样本都计算出预测值。有一个办法可以并且不需要任何一个明确的 for 循环。让我们来看一下你该怎样做。 首先,回忆一下我们曾经定义了一个矩阵 $X$ 作为你的训练输入,(如下图中蓝色 $X$ )像这样在不同的列中堆积在一起。这是一个 $n_x$ 行 $m$ 列的矩阵。我现在将它写为 Python numpy 的形式 $$(n_{x},m)$$ ,这只是表示 $X$ 是一个 $n_x$ 乘以 $m$ 的矩阵 $$R^{n_x \times m}$$。 现在我首先想做的是告诉你该如何在一个步骤中计算 $z_1$、 $z_2$ 、$z_3$ 等等。实际上,只用了一行代码。所以,我打算先构建一个 $1\times m$ 的矩阵,实际上它是一个行向量,同时我准备计算 $z^{(1)}$, $z^{(2)}$ ……一直到 $z^{(m)}$ ,所有值都是在同一时间内完成。结果发现它可以表达为 $w$ 的转置乘以大写矩阵 $x$ 然后加上向量 $[b b...b]$ , $([z^{(1)} z^{(2)}...z^{(m)}]=w^{T}+[bb...b])$ 。$[b b...b]$ 是一个 $1\times m$ 的向量或者 $1\times m$ 的矩阵或者是一个 $m$ 维的行向量。所以希望你熟悉矩阵乘法,你会发现的 $w$ 转置乘以 $x^{(1)}$ , $x^{(2)}$ 一直到 $x^{(m)}$ 。所以 $w$ 转置可以是一个行向量。所以第一项 $w^{T}X$ 将计算 $w$ 的转置乘以 $x^{(1)}$, $w$ 转置乘以$x^{(2)}$ 等等。然后我们加上第二项 $[b b...b]$ ,你最终将 $b$ 加到了每个元素上。所以你最终得到了另一个 $1\times m$ 的向量, $[z^{(1)} z^{(2)}...z^{(m)}]=w^{T}X+[b b...b]=[w^{T}x^{(1)}+b,w^{T}x^{(2)}+b...w^{T}x^{(m)}+b]$ 。 $w^{T}x^{(1)}+b$ 这是第一个元素,$w^{T}x^{(2)}+b$ 这是第二个元素, $w^{T}x^{(m)}+b$ 这是第 $m$ 个元素。 如果你参照上面的定义,第一个元素恰好是 $z^{(1)}$ 的定义,第二个元素恰好是 $z^{(2)}$ 的定义,等等。所以,因为$X$是一次获得的,当你得到你的训练样本,一个一个横向堆积起来,这里我将 $[z^{(1)} z^{(2)} ... z^{(m)}]$ 定义为大写的 $Z$ ,你用小写 $z$ 表示并将它们横向排在一起。所以当你将不同训练样本对应的小写 $x$ 横向堆积在一起时得到大写变量 $X$ 并且将小写变量也用相同方法处理,将它们横向堆积起来,你就得到大写变量 $Z$ 。结果发现,为了计算 $W^{T}X+[b b ... b]$ , numpy 命令是$Z=np.dot(w.T,X)+b$。这里在 Python 中有一个巧妙的地方,这里 $b$ 是一个实数,或者你可以说是一个 $1\times 1$ 矩阵,只是一个普通的实数。但是当你将这个向量加上这个实数时, Python 自动把这个实数 $b$ 扩展成一个 $1\times m$ 的行向量。所以这种情况下的操作似乎有点不可思议,它在 Python 中被称作广播( brosdcasting ),目前你不用对此感到顾虑,我们将在下一个视频中进行进一步的讲解。话说回来它只用一行代码,用这一行代码,你可以计算大写的 $Z$,而大写 $Z$ 是一个包含所有小写$z^{(1)}$ 到 $ z^{(m)}$ 的 $1\times m$ 的矩阵。这就是 $Z$ 的内容,关于变量 $a$ 又是如何呢? 我们接下来要做的就是找到一个同时计算 $[a^{(1)} a^{(2)} ... a^{(m)}]$ 的方法。就像把小写 $x$ 堆积起来得到大写 $X$ 和横向堆积小写 $z$ 得到大写 $Z$ 一样,堆积小写变量 $a$ 将形成一个新的变量,我们将它定义为大写 $A$。在编程作业中,你将看到怎样用一个向量在 sigmoid 函数中进行计算。所以 sigmoid 函数中输入大写 $Z$ 作为变量并且非常高效地输出大写 $A$。你将在编程作业中看到它的细节。 总结一下,在这张幻灯片中我们已经看到,不需要 for 循环,利用 $m$ 个训练样本一次性计算出小写 $z$ 和小写 $a$,用一行代码即可完成。 Z = np.dot(w.T,X) + b 这一行代码:$A=[a^{(1)} a^{(2)} ... a^{(m)}]=\sigma (Z)$ ,通过恰当地运用$\sigma$一次性计算所有 $a$。这就是在同一时间内你如何完成一个所有 $m$ 个训练样本的前向传播向量化计算。 概括一下,你刚刚看到如何利用向量化在同一时间内高效地计算所有的激活函数的所有 $a$值。接下来,可以证明,你也可以利用向量化高效地计算反向传播并以此来计算梯度。让我们在下一个视频中看该如何实现。 2.14 向量化 logistic 回归的梯度输出(Vectorizing Logistic Regression's Gradient) 注:本节中大写字母代表向量,小写字母代表元素 如何向量化计算的同时,对整个训练集预测结果$a$,这是我们之前已经讨论过的内容。在本次视频中我们将学习如何向量化地计算$m$个训练数据的梯度,本次视频的重点是如何 同时 计算 $m$ 个数据的梯度,并且实现一个非常高效的逻辑回归算法**(Logistic Regression**)。 之前我们在讲梯度计算的时候,列举过几个例子, $dz^{(1)}=a^{(1)}-y^{(1)}$,$dz^{(2)}=a^{(2)}-y^{(2)}$ ……等等一系列类似公式。现在,对 $m$个训练数据做同样的运算,我们可以定义一个新的变量 $dZ=[dz^{(1)} ,dz^{(2)} ... dz^{(m)}]$ ,所有的 $dz$ 变量横向排列,因此,$dZ$ 是一个 $1\times m$ 的矩阵,或者说,一个 $m$ 维行向量。在之前的幻灯片中,我们已经知道如何计算$A$,即 $[a^{(1)},a^{(2)} ... a^{(m)}]$,我们需要找到这样的一个行向量 $Y=[y^{(1)} y^{(2)} ... y^{(m)}]$ ,由此,我们可以这样计算 $dZ=A-Y=[a^{(1)}-y^{(1)} a^{(2)}-y^{(2)} ... a^{(m)}-y^{(m)}]$,不难发现第一个元素就是 $dz^{(1)}$,第二个元素就是 $dz^{(2)}$ ……所以我们现在仅需一行代码,就可以同时完成这所有的计算。 在之前的实现中,我们已经去掉了一个 for 循环,但我们仍有一个遍历训练集的循环,如下所示: $dw=0$ $dw + = x^{(1)}*{dz}^{(1)}$ $dw + = x^{(2)}\ *dz^{(2)}$ …………. $dw + = x^{(m)}*{dz}^{(m)}$ $dw = \frac{{dw}}{m}$ $db = 0$ $db + = {dz}^{(1)}$ $db + = {dz}^{(2)}$ …………. $db + = dz^{(m)}$ $db = \frac{{db}}{m}$ 上述(伪)代码就是我们在之前实现中做的,我们已经去掉了一个 for 循环,但用上述方法计算 $dw$ 仍然需要一个循环遍历训练集,我们现在要做的就是将其向量化! 首先我们来看 $db$,不难发现 $$db=\frac{1}{m}\sum_{i=1}^{m}dz^{(i)}$$ , 之前的讲解中,我们知道所有的$dz^{i)}$已经组成一个行向量 $dZ$了,所以在 Python 中,我们很容易地想到$$db=\frac{1}{m} np.sum(dZ)$$;接下来看$dw$,我们先写出它的公式 $$dw=\frac{1}{m} X dz^{T}$$ 其中,$X$ 是一个行向量。因此展开后 $$dw=\frac{1}{m} (x^{(1)}dz^{(1)}+x^{(2)}dz^{(2)}+...+x^{m}dz^{m})$$ 。因此我们可以仅用两行代码进行计算:$$db=\frac{1}{m}*np.sum(dZ)$$, $$dw=\frac{1}{m} X dz^{T}$$。这样,我们就避免了在训练集上使用for循环。 现在,让我们回顾一下,看看我们之前怎么实现的逻辑回归,可以发现,没有向量化是非常低效的,如下图所示代码: 我们的目标是不使用 for 循环,而是向量,我们可以这么做: $Z = w^{T}X + b = np.dot( w.T,X)+b$ $A = \sigma( Z )$ $dZ = A - Y$ ${{dw} = \frac{1}{m} X dz^{T}\ }$ $db= \frac{1}{m}*np.sum( dZ)​$ $w: = w - a*dw$ $b: = b - a*db$ 现在我们利用前五个公式完成了前向和后向传播,也实现了对所有训练样本进行预测和求导,再利用后两个公式,梯度下降更新参数。我们的目的是不使用 for 循环,所以我们就通过一次迭代实现一次梯度下降,但如果你希望多次迭代进行梯度下降,那么仍然需要 for 循环,放在最外层。不过我们还是觉得一次迭代就进行一次梯度下降,避免使用任何循环比较舒服一些。 最后,我们得到了一个高度向量化的、非常高效的逻辑回归的梯度下降算法,我们将在下次视频中讨论 Python 中的 Broadcasting 技术。 2.15 Python 中的广播(Broadcasting in Python) 这是一个不同食物(每100g)中不同营养成分的卡路里含量表格,表格为3行4列,列表示不同的食物种类,从左至右依次为苹果,牛肉,鸡蛋,土豆。行表示不同的营养成分,从上到下依次为碳水化合物,蛋白质,脂肪。 那么,我们现在想要计算不同食物中不同营养成分中的卡路里百分比。 现在计算苹果中的碳水化合物卡路里百分比含量,首先计算苹果(100g)中三种营养成分卡路里总和56+1.2+1.8 = 59,然后用56/59 = 94.9%算出结果。 可以看出苹果中的卡路里大部分来自于碳水化合物,而牛肉则不同。 对于其他食物,计算方法类似。首先,按列求和,计算每种食物中(100g)三种营养成分总和,然后分别用不用营养成分的卡路里数量除以总和,计算百分比。 那么,能否不使用 for 循环完成这样的一个计算过程呢? 假设上图的表格是一个4行3列的矩阵$A$,记为 $A_{3\times 4}$,接下来我们要使用 Python 的 numpy 库完成这样的计算。我们打算使用两行代码完成,第一行代码对每一列进行求和,第二行代码分别计算每种食物每种营养成分的百分比。 在 jupyter notebook 中输入如下代码,按 shift+Enter 运行,输出如下。 下面使用如下代码计算每列的和,可以看到输出是每种食物(100g)的卡路里总和。 其中 sum 的参数 axis=0 表示求和运算按列执行,之后会详细解释。 接下来计算百分比,这条指令将 $3\times 4$的矩阵$A$除以一个$1 \times 4$的矩阵,得到了一个 $3 \times 4$的结果矩阵,这个结果矩阵就是我们要求的百分比含量。 下面再来解释一下 A.sum(axis = 0) 中的参数 axis 。 axis用来指明将要进行的运算是沿着哪个轴执行,在numpy中,0轴是垂直的,也就是列,而1轴是水平的,也就是行。 而第二个 A/cal.reshape(1,4) 指令则调用了 numpy 中的广播机制。这里使用 $3 \times 4$的矩阵$A$除以 $1 \times 4$的矩阵$cal$。技术上来讲,其实并不需要再将矩阵$cal$ reshape (重塑)成 $1 \times 4$,因为矩阵$cal$本身已经是 $1 \times 4$了。但是当我们写代码时不确定矩阵维度的时候,通常会对矩阵进行重塑来确保得到我们想要的列向量或行向量。重塑操作 reshape 是一个常量时间的操作,时间复杂度是$O(1)$,它的调用代价极低。 那么一个 $3 \times 4$ 的矩阵是怎么和 $1 \times 4$的矩阵做除法的呢?让我们来看一些更多的广播的例子。 在numpy中,当一个 $4 \times 1$的列向量与一个常数做加法时,实际上会将常数扩展为一个 $4 \times 1$的列向量,然后两者做逐元素加法。结果就是右边的这个向量。这种广播机制对于行向量和列向量均可以使用。 再看下一个例子。 用一个 $2 \times 3$的矩阵和一个 $1 \times 3$ 的矩阵相加,其泛化形式是 $m \times n$ 的矩阵和 $1 \times n$的矩阵相加。在执行加法操作时,其实是将 $1 \times n$ 的矩阵复制成为 $m \times n$ 的矩阵,然后两者做逐元素加法得到结果。针对这个具体例子,相当于在矩阵的第一列加100,第二列加200,第三列加300。这就是在前一张幻灯片中计算卡路里百分比的广播机制,只不过这里是除法操作(广播机制与执行的运算种类无关)。 下面是最后一个例子 这里相当于是一个 $m \times n$ 的矩阵加上一个 $m \times 1$ 的矩阵。在进行运算时,会先将 $m \times 1$ 矩阵水平复制 $n$ 次,变成一个 $m \times n$ 的矩阵,然后再执行逐元素加法。 广播机制的一般原则如下: 这里我先说一下我本人对 numpy 广播机制的理解,再解释上面这张PPT。 首先是 numpy 广播机制 如果两个数组的后缘维度的轴长度相符或其中一方的轴长度为1,则认为它们是广播兼容的。广播会在缺失维度和轴长度为1的维度上进行。 后缘维度的轴长度: A.shape[-1] 即矩阵维度元组中的最后一个位置的值 对于视频中卡路里计算的例子,矩阵 $A_{3,4}$ 后缘维度的轴长度是4,而矩阵 $cal_{1,4}$ 的后缘维度也是4,则他们满足后缘维度轴长度相符,可以进行广播。广播会在轴长度为1的维度进行,轴长度为1的维度对应 axis=0 ,即垂直方向,矩阵 $$\text{cal} {1,4}$$ 沿 axis=0 (垂直方向)复制成为 $$\text{cal_temp} {3,4}$$ ,之后两者进行逐元素除法运算。 现在解释上图中的例子 矩阵 $A_{m,n}$ 和矩阵 $B_{1,n}$ 进行四则运算,后缘维度轴长度相符,可以广播,广播沿着轴长度为1的轴进行,即 $B_{1,n}$ 广播成为 ${B_{m,n}}'$ ,之后做逐元素四则运算。 矩阵 $A_{m,n}$ 和矩阵 $B_{m,1}$ 进行四则运算,后缘维度轴长度不相符,但其中一方轴长度为1,可以广播,广播沿着轴长度为1的轴进行,即 $B_{m,1}$ 广播成为 ${B_{m,n}}'$ ,之后做逐元素四则运算。 矩阵 $A_{m,1}$ 和常数$ R$ 进行四则运算,后缘维度轴长度不相符,但其中一方轴长度为1,可以广播,广播沿着缺失维度和轴长度为1的轴进行,缺失维度就是 axis=0 ,轴长度为1的轴是 axis=1 ,即$R$广播成为 ${B_{m,1}}'$ ,之后做逐元素四则运算。 最后,对于 Matlab/Octave 有类似功能的函数 bsxfun 。 总结一下 broadcasting ,可以看看下面的图: 2.16 关于 python _ numpy 向量的说明(A note on python or numpy vectors)参考视频: 本节主要讲 Python 中的 numpy 一维数组的特性,以及与行向量或列向量的区别。并介绍了老师在实际应用中的一些小技巧,去避免在 coding 中由于这些特性而导致的 bug 。 Python 的特性允许你使用广播( broadcasting )功能,这是 Python 的 numpy 程序语言库中最灵活的地方。而我认为这是程序语言的优点,也是缺点。优点的原因在于它们创造出语言的表达性, Python 语言巨大的灵活性使得你仅仅通过一行代码就能做很多事情。但是这也是缺点,由于广播巨大的灵活性,有时候你对于广播的特点以及广播的工作原理这些细节不熟悉的话,你可能会产生很细微或者看起来很奇怪的 bug 。例如,如果你将一个列向量添加到一个行向量中,你会以为它报出维度不匹配或类型错误之类的错误,但是实际上你会得到一个行向量和列向量的求和。 在 Python 的这些奇怪的影响之中,其实是有一个内在的逻辑关系的。但是如果对 Python 不熟悉的话,我就曾经见过的一些学生非常生硬、非常艰难地去寻找 bug 。所以我在这里想做的就是分享给你们一些技巧,这些技巧对我非常有用,它们能消除或者简化我的代码中所有看起来很奇怪的 bug 。同时我也希望通过这些技巧,你也能更容易地写没有 bug 的 Python 和 numpy 代码。 为了演示 Python-numpy 的一个容易被忽略的效果,特别是怎样在 Python-numpy 中构造向量,让我来做一个快速示范。首先设置$a=np.random.randn(5)$,这样会生成存储在数组 $a$ 中的5个高斯随机数变量。之后输出 $a$,从屏幕上可以得知,此时 $a$ 的 shape (形状)是一个$(5,)$的结构。这在 Python 中被称作 一个一维数组 。它既不是一个行向量也不是一个列向量,这也导致它有一些不是很直观的效果。举个例子,如果我输出一个转置阵,最终结果它会和$a$看起来一样,所以$a$和$a$的转置阵最终结果看起来一样。而如果我输出$a$和$a$的转置阵的内积,你可能会想:$a$乘以$a$的转置返回给你的可能会是一个矩阵。但是如果我这样做,你只会得到一个数。 所以建议你编写神经网络时,不要使用shape为 (5,) 、 (n,) 或者其他一维数组的数据结构。相反,如果你设置 $a$ 为$(5,1)$,那么这就将置于5行1列向量中。在先前的操作里 $a$ 和 $a$ 的转置看起来一样,而现在这样的 $a$ 变成一个新的 $a$ 的转置,并且它是一个行向量。请注意一个细微的差别,在这种数据结构中,当我们输出 $a$ 的转置时有两对方括号,而之前只有一对方括号,所以这就是1行5列的矩阵和一维数组的差别。 如果你输出 $a$ 和 $a$ 的转置的乘积,然后会返回给你一个向量的外积,是吧?所以这两个向量的外积返回给你的是一个矩阵。 就我们刚才看到的,再进一步说明。首先我们刚刚运行的命令是这个 $(a=np.random.randn(5))$,它生成了一个数据结构$a$,其中$a.shape$是$(5,)$。这被称作 $a$ 的一维数组,同时这也是一个非常有趣的数据结构。它不像行向量和列向量那样表现的很一致,这使得它带来一些不直观的影响。所以我建议,当你在编程练习或者在执行逻辑回归和神经网络时,你不需要使用这些一维数组。 相反,如果你每次创建一个数组,你都得让它成为一个列向量,产生一个$(5,1)$向量或者你让它成为一个行向量,那么你的向量的行为可能会更容易被理解。所以在这种情况下,$a.shape$等同于$(5,1)$。这种表现很像 $a$,但是实际上却是一个列向量。同时这也是为什么当它是一个列向量的时候,你能认为这是矩阵$(5,1)$;同时这里 $a.shape$ 将要变成$(1,5)$,这就像行向量一样。所以当你需要一个向量时,我会说用这个或那个( column vector or row vector ),但绝不会是一维数组。 我写代码时还有一件经常做的事,那就是如果我不完全确定一个向量的维度( dimension ),我经常会扔进一个断言语句( assertion statement )。像这样,去确保在这种情况下是一个$(5,1)$向量,或者说是一个列向量。这些断言语句实际上是要去执行的,并且它们也会有助于为你的代码提供信息。所以不论你要做什么,不要犹豫直接插入断言语句。如果你不小心以一维数组来执行,你也能够重新改变数组维数 $a=reshape$,表明一个$(5,1)$数组或者一个$(1,5)$数组,以致于它表现更像列向量或行向量。 我有时候看见学生因为一维数组不直观的影响,难以定位bug而告终。通过在原先的代码里清除一维数组,我的代码变得更加简洁。而且实际上就我在代码中表现的事情而言,我从来不使用一维数组。因此,要去简化你的代码,而且不要使用一维数组。总是使用 $n \times 1$ 维矩阵(基本上是列向量),或者 $1 \times n$ 维矩阵(基本上是行向量),这样你可以减少很多 assert 语句来节省核矩阵和数组的维数的时间。另外,为了确保你的矩阵或向量所需要的维数时,不要羞于 reshape 操作。 总之,我希望这些建议能帮助你解决一个 Python 中的bug,从而使你更容易地完成练习。 2.17 Jupyter/iPython Notebooks快速入门(Quick tour of Jupyter/iPython Notebooks) 学到现在,你即将要开始处理你的第一个编程作业。但在那之前,让我快速地给你介绍一下在 Coursera 上的 iPython Notebooks 工具。 这就是 Jupyter iPython Notebooks 的界面,你可以通过它连接到 Coursera 。让我快速地讲解下它的一些特性。关于它的说明已经被写入这个 Notebook 中。 这里有一些空白区域的代码块,你可以在这里编写代码。有时,你也会看到一些函数块。而关于这些的说明都已经在 iPython Notebook 的文本中。在 iPython Notebook 中,在这些较长的灰色的区域就是代码块。 有时,你会看到代码块中有像这样的开始代码和结束代码。在进行编程练习时,请确保你的代码写在开始代码和结束代码之间。 比如,编写打印输出 Hello World 的代码,然后执行这一代码块(你可以按 shift +enter 来执行这一代码块)。最终,它就会输出我们想要的 Hello World 。 在运行一个单元格 cell 时,你也可以选择运行其中的一块代码区域。通过点击 Cell 菜单的 Run Cells 执行这部分代码。 也许,在你的计算机上,运行 cell 的键盘快捷方式可能并非是 shift enter 。但是,Mac应该和我的个人电脑一样,可以使用 shift + enter 来运行 cell 。 当你正在阅读指南时,如果不小心双击了它,点中的区域就会变成 markdown 语言形式。如果你不小心使其变成了这样的文本框,只要运行下单元格 cell ,就可以回到原来的形式。所以,点击 cell 菜单的 Run Cells 或者使用 shift + enter ,就可以使得它变回原样。 这里还有一些其他的小技巧。比如当你执行上面所使用的代码时,它实际上会使用一个内核在服务器上运行这段代码。如果你正在运行超负荷的进程,或者电脑运行了很长一段时间,或者在运行中出了错,又或者网络连接失败,这里依然有机会让 Kernel 重新工作。你只要点击 Kernel ,选择 Restart ,它会重新运行 Kernel 使程序继续工作。 所以,如果你只是运行相对较小的工作并且才刚刚启动你的 ipad 或笔记本电脑,这种情况应该是不会发生的。但是,如果你看见错误信息,比如 Kernel 已经中断或者其他信息,你可以试着重启 Kernel 。 当我使用 iPython Notebook 时会有多个代码区域块。尽管我并没有在前面的代码块中添加自己的代码,但还是要确保先执行这块代码。因为在这个例子,它导入了 numpy 包并另命名为 np 等,并声明了一些你可能需要的变量。为了能顺利地执行下面的代码,就必须确保先执行上面的代码,即使不要求你去写其他的代码。 最后,当你完成作业后,可以通过点击右上方蓝色的 Submit Assignment 按钮提交你的作业。 我发现这种交互式的 shell 命令,在 iPython Notebooks 是非常有用的,能使你快速地实现代码并且查看输出结果,便于学习。所以我希望这些练习和 Jupyter iPython Notebooks 会帮助你更快地学习和实践,并且帮助你了解如何去实现这些学习算法。后面一个视频是一个选学视频,它主要是讲解逻辑回归中的代价函数。你可以选择是否观看。不管怎样,都祝愿你能通过这两次编程作业。我会在新一周的课程里等待着你。 2.18 (选修)logistic 损失函数的解释(Explanation of logistic regression cost function) 在前面的视频中,我们已经分析了逻辑回归的损失函数表达式,在这节选修视频中,我将给出一个简洁的证明来说明逻辑回归的损失函数为什么是这种形式。 回想一下,在逻辑回归中,需要预测的结果$\hat{y}$,可以表示为$\hat{y}=\sigma(w^{T}x+b)$,$\sigma$是我们熟悉的$S$型函数 $\sigma(z)=\sigma(w^{T}x+b)=\frac{1}{1+e^{-z}}$ 。我们约定 $\hat{y}=p(y=1|x)$ ,即算法的输出$\hat{y}$ 是给定训练样本 $x$ 条件下 $y$ 等于1的概率。换句话说,如果$y=1$,在给定训练样本 $x$ 条件下$y=\hat{y}$;反过来说,如果$y=0$,在给定训练样本$x$条件下 $y$ 等于1减去$\hat{y}(y=1-\hat{y})$,因此,如果 $\hat{y}$ 代表 $y=1$ 的概率,那么$1-\hat{y}$就是 $y=0$的概率。接下来,我们就来分析这两个条件概率公式。 这两个条件概率公式定义形式为 $p(y|x)$并且代表了 $y=0$ 或者 $y=1$ 这两种情况,我们可以将这两个公式合并成一个公式。需要指出的是我们讨论的是二分类问题的损失函数,因此,$y$的取值只能是0或者1。上述的两个条件概率公式可以合并成如下公式: $p(y|x)={\hat{y}}^{y}{(1-\hat{y})}^{(1-y)}$ 接下来我会解释为什么可以合并成这种形式的表达式:$(1-\hat{y})$的$(1-y)$次方这行表达式包含了上面的两个条件概率公式,我来解释一下为什么。 第一种情况,假设 $y=1$,由于$y=1$,那么${(\hat{y})}^{y}=\hat{y}$,因为 $\hat{y}$的1次方等于$\hat{y}$,$1-{(1-\hat{y})}^{(1-y)}$的指数项$(1-y)$等于0,由于任何数的0次方都是1,$\hat{y}$乘以1等于$\hat{y}$。因此当$y=1$时 $p(y|x)=\hat{y}$(图中绿色部分)。 第二种情况,当 $y=0$ 时 $p(y|x)$ 等于多少呢? 假设$y=0$,$\hat{y}$的$y$次方就是 $$\hat{y}$$ 的0次方,任何数的0次方都等于1,因此 $p(y|x)=1×{(1-\hat{y})}^{1-y}$ ,前面假设 $y=0$ 因此$(1-y)$就等于1,因此 $p(y|x)=1×(1-\hat{y})$。因此在这里当$y=0$时,$p(y|x)=1-\hat{y}$。这就是这个公式(第二个公式,图中紫色字体部分)的结果。 因此,刚才的推导表明 $p(y|x)={\hat{y}}^{(y)}{(1-\hat{y})}^{(1-y)}$,就是 $p(y|x)$ 的完整定义。由于 log 函数是严格单调递增的函数,最大化 $log(p(y|x))$ 等价于最大化 $p(y|x)$ 并且地计算 $p(y|x)$ 的 log对数,就是计算 $log({\hat{y}}^{(y)}{(1-\hat{y})}^{(1-y)})$ (其实就是将 $p(y|x)$ 代入),通过对数函数化简为: $ylog\hat{y}+(1-y)log(1-\hat{y})$ 而这就是我们前面提到的损失函数的负数 $(-L(\hat{y},y))$ ,前面有一个负号的原因是当你训练学习算法时需要算法输出值的概率是最大的(以最大的概率预测这个值),然而在逻辑回归中我们需要最小化损失函数,因此最小化损失函数与最大化条件概率的对数 $log(p(y|x))$ 关联起来了,因此这就是单个训练样本的损失函数表达式。 在 $m$个训练样本的整个训练集中又该如何表示呢,让我们一起来探讨一下。 让我们一起来探讨一下,整个训练集中标签的概率,更正式地来写一下。假设所有的训练样本服从同一分布且相互独立,也即独立同分布的,所有这些样本的联合概率就是每个样本概率的乘积: $P\left(\text{labels in training set} \right) = \prod_{i =1}^{m}{P(y^{(i)}|x^{(i)})}$。 如果你想做最大似然估计,需要寻找一组参数,使得给定样本的观测值概率最大,但令这个概率最大化等价于令其对数最大化,在等式两边取对数: $logp\left( \text{labels in training set} \right) = log\prod_{i =1}^{m}{P(y^{(i)}|x^{(i)})} = \sum_{i = 1}^{m}{logP(y^{(i)}|x^{(i)})} = \sum_{i =1}^{m}{- L(\hat y^{(i)},y^{(i)})}$ 在统计学里面,有一个方法叫做最大似然估计,即求出一组参数,使这个式子取最大值,也就是说,使得这个式子取最大值,$\sum_{i= 1}^{m}{- L(\hat y^{(i)},y^{(i)})}$,可以将负号移到求和符号的外面,$- \sum_{i =1}^{m}{L(\hat y^{(i)},y^{(i)})}$,这样我们就推导出了前面给出的 logistic 回归的成本函数$J(w,b)= \sum_{i = 1}^{m}{L(\hat y^{(i)},y^{\hat( i)})}$。 由于训练模型时,目标是让成本函数最小化,所以我们不是直接用最大似然概率,要去掉这里的负号,最后为了方便,可以对成本函数进行适当的缩放,我们就在前面加一个额外的常数因子$\frac{1}{m}$,即:$J(w,b)= \frac{1}{m}\sum_{i = 1}^{m}{L(\hat y^{(i)},y^{(i)})}$。 总结一下,为了最小化成本函数$J(w,b)$,我们从 logistic 回归模型的最大似然估计的角度出发,假设训练集中的样本都是独立同分布的条件下。尽管这节课是选修性质的,但还是感谢观看本节视频。我希望通过本节课您能更好地明白逻辑回归的损失函数,为什么是那种形式,明白了损失函数的原理,希望您能继续完成课后的练习,前面课程的练习以及本周的测验,在课后的小测验和编程练习中,祝您好运。 第三周:浅层神经网络(Shallow neural networks) 3.1 神经网络概述(Neural Network Overview) 本周你将学习如何实现一个神经网络。在我们深入学习具体技术之前,我希望快速的带你预览一下本周你将会学到的东西。如果这个视频中的某些细节你没有看懂你也不用担心,我们将在后面的几个视频中深入讨论技术细节。 现在我们开始快速浏览一下如何实现神经网络。上周我们讨论了逻辑回归,我们了解了这个模型(见图3.1.1)如何与下面公式3.1建立联系。 图3.1.1 : 公式3.1: $$ \left. \begin{array}{l} x\ w\ b \end{array} \right} \implies{z={w}^Tx+b} $$ 如上所示,首先你需要输入特征$x​$,参数$w​$和$b​$,通过这些你就可以计算出$z​$,公式3.2: $$ \left. \begin{array}{l} x\ w\ b \end{array} \right} \implies{z={w}^Tx+b} \implies{a = \sigma(z)}\ \implies{{L}(a,y)} $$ 接下来使用$z$就可以计算出$a$。我们将的符号换为表示输出$\hat{y}\implies{a = \sigma(z)}$,然后可以计算出 loss function $L(a,y)$ 神经网络看起来是如下这个样子(图3.1.2)。正如我之前已经提到过,你可以把许多 sigmoid 单元堆叠起来形成一个神经网络。对于图3.1.1中的节点,它包含了之前讲的计算的两个步骤:首先通过公式3.1计算出值$z$,然后通过$\sigma(z)$计算值$a$。 图3.1.2 在这个神经网络(图3.1.2)对应的3个节点,首先计算第一层网络中的各个节点相关的数$z^{[1]}$,接着计算$\alpha^{[1]}$,在计算下一层网络同理; 我们会使用符号$^{[m]}$表示第$m$层网络中节点相关的数,这些节点的集合被称为第$m$层网络。这样可以保证$^{[m]}$不会和我们之前用来表示单个的训练样本的$^{(i)}$(即我们使用表示第$i$个训练样本)混淆; 整个计算过程,公式如下: 公式3.3: $$ \left. \begin{array}{r} {x }\ {W^{[1]}}\ {b^{[1]}} \end{array} \right} \implies{z^{[1]}=W^{[1]}x+b^{[1]}} \implies{a^{[1]} = \sigma(z^{[1]})} $$ 公式3.4: $$ \left. \begin{array}{r} \text{$a^{[1]} = \sigma(z^{[1]})$}\ \text{$W^{[2]}$}\ \text{$b^{[2]}$}\ \end{array} \right} \implies{z^{[2]}=W^{[2]}a^{[1]}+b^{[2]}} \implies{a^{[2]} = \sigma(z^{[2]})}\ \implies{{L}\left(a^{[2]},y \right)} $$ 类似逻辑回归,在计算后需要使用计算,接下来你需要使用另外一个线性方程对应的参数计算$z^{[2]}$, 计算$a^{[2]}$,此时$a^{[2]}$就是整个神经网络最终的输出,用 $\hat{y}​$表示网络的输出。 公式3.5: $$ \left. \begin{array}{r} {da^{[1]} = {d}\sigma(z^{[1]})}\ {dW^{[2]}}\ {db^{[2]}}\ \end{array} \right} \impliedby{{dz}^{[2]}={d}(W^{[2]}\alpha^{[1]}+b^{[2]}}) \impliedby{{{da}^{[2]}} = {d}\sigma(z^{[2]})}\ \impliedby{{dL}\left(a^{[2]},y \right)} $$ 我知道这其中有很多细节,其中有一点非常难以理解,即在逻辑回归中,通过直接计算$z$得到结果$a$。而这个神经网络中,我们反复的计算$z$和$a$,计算$a$和$z$,最后得到了最终的输出 loss function 。 你应该记得逻辑回归中,有一些从后向前的计算用来计算导数$da$、$dz$。同样,在神经网络中我们也有从后向前的计算,看起来就像这样,最后会计算$da^{[2]}$ 、$dz^{[2]}$,计算出来之后,然后计算计算$dW^{[2]}$、$db^{[2]}$ 等,按公式3.4、3.5箭头表示的那样,从右到左反向计算。 现在你大概了解了一下什么是神经网络,基于逻辑回归重复使用了两次该模型得到上述例子的神经网络。我清楚这里面多了很多新符号和细节,如果没有理解也不用担心,在接下来的视频中我们会仔细讨论具体细节。 那么,下一个视频讲述神经网络的表示。 3.2 神经网络的表示(Neural Network Representation) 先回顾一下我在上一个视频画几张神经网络的图片,在这次课中我们将讨论这些图片的具体含义,也就是我们画的这些神经网络到底代表什么。 我们首先关注一个例子,本例中的神经网络只包含一个隐藏层(图3.2.1)。这是一张神经网络的图片,让我们给此图的不同部分取一些名字。 图3.2.1 我们有输入特征$x_1$、$x_2$、$x_3$,它们被竖直地堆叠起来,这叫做神经网络的 输入层 。它包含了神经网络的输入;然后这里有另外一层我们称之为 隐藏层 (图3.2.1的四个结点)。待会儿我会回过头来讲解术语"隐藏"的意义;在本例中最后一层只由一个结点构成,而这个只有一个结点的层被称为 输出层 ,它负责产生预测值。解释隐藏层的含义:在一个神经网络中,当你使用监督学习训练它的时候,训练集包含了输入$x$也包含了目标输出$y$,所以术语隐藏层的含义是在训练集中,这些中间结点的准确值我们是不知道到的,也就是说你看不见它们在训练集中应具有的值。你能看见输入的值,你也能看见输出的值,但是隐藏层中的东西,在训练集中你是无法看到的。所以这也解释了词语隐藏层,只是表示你无法在训练集中看到他们。 现在我们再引入几个符号,就像我们之前用向量$x$表示输入特征。这里有个可代替的记号$a^{[0]}$可以用来表示输入特征。$a$表示激活的意思,它意味着网络中不同层的值会传递到它们后面的层中,输入层将$x$传递给隐藏层,所以我们将输入层的激活值称为$a^{[0]}$;下一层即隐藏层也同样会产生一些激活值,那么我将其记作$a^{[1]}$,所以具体地,这里的第一个单元或结点我们将其表示为$a^{[1]} {1}$,第二个结点的值我们记为$a^{[1]} {2}$以此类推。所以这里的是一个四维的向量如果写成 Python 代码,那么它是一个规模为4x1的矩阵或一个大小为4的列向量,如下公式,它是四维的,因为在本例中,我们有四个结点或者单元,或者称为四个隐藏层单元; 公式3.7 $$ a^{[1]} = \left[ \begin{array}{ccc} a^{[1]} {1}\ a^{[1]} {2}\ a^{[1]} {3}\ a^{[1]} {4} \end{array} \right] $$ 最后输出层将产生某个数值$a$,它只是一个单独的实数,所以的$\hat{y}$值将取为$a^{[2]}$。这与逻辑回归很相似,在逻辑回归中,我们有$\hat{y}$直接等于$a$,在逻辑回归中我们只有一个输出层,所以我们没有用带方括号的上标。但是在神经网络中,我们将使用这种带上标的形式来明确地指出这些值来自于哪一层,有趣的是在约定俗成的符号传统中,在这里你所看到的这个例子,只能叫做一个两层的神经网络(图3.2.2)。原因是当我们计算网络的层数时,输入层是不算入总层数内,所以隐藏层是第一层,输出层是第二层。第二个惯例是我们将输入层称为第零层,所以在技术上,这仍然是一个三层的神经网络,因为这里有输入层、隐藏层,还有输出层。但是在传统的符号使用中,如果你阅读研究论文或者在这门课中,你会看到人们将这个神经网络称为一个两层的神经网络,因为我们不将输入层看作一个标准的层。 图3.2.2 最后,我们要看到的隐藏层以及最后的输出层是带有参数的,这里的隐藏层将拥有两个参数$W$和$b$,我将给它们加上上标$^{[1]}$($W^{[1]}$,$b^{[1]}$),表示这些参数是和第一层这个隐藏层有关系的。之后在这个例子中我们会看到$W$是一个4x3的矩阵,而$b$是一个4x1的向量,第一个数字4源自于我们有四个结点或隐藏层单元,然后数字3源自于这里有三个输入特征,我们之后会更加详细地讨论这些矩阵的维数,到那时你可能就更加清楚了。相似的输出层也有一些与之关联的参数$W^{[2]}$以及$b^{[2]}$。从维数上来看,它们的规模分别是1x4以及1x1。1x4是因为隐藏层有四个隐藏层单元而输出层只有一个单元,之后我们会对这些矩阵和向量的维度做出更加深入的解释,所以现在你已经知道一个两层的神经网络什么样的了,即它是一个只有一个隐藏层的神经网络。 在下一个视频中。我们将更深入地了解这个神经网络是如何进行计算的,也就是这个神经网络是怎么输入$x$,然后又是怎么得到$\hat{y}$。 3.3 计算一个神经网络的输出(Computing a Neural Network's output) 在上一节的视频中,我们介绍只有一个隐藏层的神经网络的结构与符号表示。在这节的视频中让我们了解神经网络的输出究竟是如何计算出来的。 首先,回顾下只有一个隐藏层的简单两层 神经网络结构 : 图3.3.1 其中,$x$表示输入特征,$a$表示每个神经元的输出,$W$表示特征的权重,上标表示神经网络的层数(隐藏层为1),下标表示该层的第几个神经元。这是神经网络的 符号惯例 ,下同。 神经网络的计算 关于神经网络是怎么计算的,从我们之前提及的逻辑回归开始,如下图所示。用圆圈表示神经网络的计算单元,逻辑回归的计算有两个步骤,首先你按步骤计算出$z$,然后在第二步中你以 sigmoid 函数为激活函数计算$z$(得出$a$),一个神经网络只是这样子做了好多次重复计算。 图3.3.2 回到两层的神经网络,我们从隐藏层的第一个神经元开始计算,如上图第一个最上面的箭头所指。从上图可以看出,输入与逻辑回归相似,这个神经元的计算与逻辑回归一样分为两步,小圆圈代表了计算的两个步骤。 第一步,计算$z^{[1]}_1,z^{[1]}_1 = w^{[1]T}_1x + b^{[1]}_1$。 第二步,通过激活函数计算$a^{[1]}_1,a^{[1]}_1 = \sigma(z^{[1]}_1)$。 隐藏层的第二个以及后面两个神经元的计算过程一样,只是注意符号表示不同,最终分别得到$a^{[1]}_2、a^{[1]}_3、a^{[1]}_4$,详细结果见下: $z^{[1]}_1 = w^{[1]T}_1x + b^{[1]}_1, a^{[1]}_1 = \sigma(z^{[1]}_1)$ $z^{[1]}_2 = w^{[1]T}_2x + b^{[1]}_2, a^{[1]}_2 = \sigma(z^{[1]}_2)$ $z^{[1]}_3 = w^{[1]T}_3x + b^{[1]}_3, a^{[1]}_3 = \sigma(z^{[1]}_3)$ $z^{[1]}_4 = w^{[1]T}_4x + b^{[1]}_4, a^{[1]}_4 = \sigma(z^{[1]}_4)$ 向量化计算 如果你执行神经网络的程序,用for循环来做这些看起来真的很低效。所以接下来我们要做的就是把这四个等式向量化。向量化的过程是将神经网络中的一层神经元参数纵向堆积起来,例如隐藏层中的$w$纵向堆积起来变成一个$(4,3)$的矩阵,用符号$W^{[1]}$表示。另一个看待这个的方法是我们有四个逻辑回归单元,且每一个逻辑回归单元都有相对应的参数——向量$w$,把这四个向量堆积在一起,你会得出这4×3的矩阵。 因此, 公式3.8: $z^{[n]} = w^{[n]}x + b^{[n]}$ 公式3.9: $a^{[n]}=\sigma(z^{[n]})$ 详细过程见下: 公式3.10: $$ a^{[1]} = \left[ \begin{array}{c} a^{[1]} {1}\ a^{[1]} {2}\ a^{[1]} {3}\ a^{[1]} {4} \end{array} \right] = \sigma(z^{[1]}) $$ 公式3.11: $$ \left[ \begin{array}{c} z^{[1]} {1}\ z^{[1]} {2}\ z^{[1]} {3}\ z^{[1]} {4}\ \end{array} \right] = \overbrace{ \left[ \begin{array}{c} ...W^{[1]T} {1}...\ ...W^{[1]T} {2}...\ ...W^{[1]T} {3}...\ ...W^{[1]T} {4}... \end{array} \right] }^{W^{[1]}} * \overbrace{ \left[ \begin{array}{c} x_1\ x_2\ x_3\ \end{array} \right] }^{input} + \overbrace{ \left[ \begin{array}{c} b^{[1]}_1\ b^{[1]}_2\ b^{[1]}_3\ b^{[1]}_4\ \end{array} \right] }^{b^{[1]}} $$ 对于神经网络的第一层,给予一个输入$x$,得到$a^{[1]}$,$x$可以表示为$a^{[0]}$。通过相似的衍生你会发现,后一层的表示同样可以写成类似的形式,得到$a^{[2]}$,$\hat{y} = a^{[2]}$,具体过程见公式3.8、3.9。 图3.3.3 如上图左半部分所示为神经网络,把网络左边部分盖住先忽略,那么最后的输出单元就相当于一个逻辑回归的计算单元。当你有一个包含一层隐藏层的神经网络,你需要去实现以计算得到输出的是右边的四个等式,并且可以看成是一个向量化的计算过程,计算出隐藏层的四个逻辑回归单元和整个隐藏层的输出结果,如果编程实现需要的也只是这四行代码。 总结 通过本视频,你能够根据给出的一个单独的输入特征向量,运用四行代码计算出一个简单神经网络的输出。接下来你将了解的是如何一次能够计算出不止一个样本的神经网络输出,而是能一次性计算整个训练集的输出。 3.4 多样本向量化(Vectorizing across multiple examples) 在上一个视频,了解到如何针对于单一的训练样本,在神经网络上计算出预测值。 在这个视频,将会了解到如何向量化多个训练样本,并计算出结果。该过程与你在逻辑回归中所做类似。 逻辑回归是将各个训练样本组合成矩阵,对矩阵的各列进行计算。神经网络是通过对逻辑回归中的等式简单的变形,让神经网络计算出输出值。这种计算是所有的训练样本同时进行的,以下是实现它具体的步骤: 图3.4.1 上一节视频中得到的四个等式。它们给出如何计算出$z^{[1]}$,$a^{[1]}$,$z^{[2]}$,$a^{[2]}$。 对于一个给定的输入特征向量$X$,这四个等式可以计算出$\alpha^{[2]}$等于$\hat{y}$。这是针对于单一的训练样本。如果有$m$个训练样本,那么就需要重复这个过程。 用第一个训练样本$x^{[1]}$来计算出预测值$\hat{y}^{[1]}$,就是第一个训练样本上得出的结果。 然后,用$x^{[2]}$来计算出预测值$\hat{y}^{[2]}$,循环往复,直至用$x^{[m]}$计算出$\hat{y}^{[m]}$。 用激活函数表示法,如上图左下所示,它写成$a^{ 2 }$、$a^{ 2 }$和$a^{ 2 }$。 【注】:$a^{ 2 }$,$(i)$是指第$i$个训练样本而$[2]$是指第二层。 如果有一个非向量化形式的实现,而且要计算出它的预测值,对于所有训练样本,需要让$i$从1到$m$实现这四个等式: $z^{ 1 }=W^{ 1 }x^{(i)}+b^{ 1 }$ $a^{ 1 }=\sigma(z^{ 1 })$ $z^{ 2 }=W^{ 2 }a^{ 1 }+b^{ 2 }$ $a^{ 2 }=\sigma(z^{ 2 })$ 对于上面的这个方程中的$^{(i)}$,是所有依赖于训练样本的变量,即将$(i)$添加到$x$,$z$和$a$。如果想计算$m$个训练样本上的所有输出,就应该向量化整个计算,以简化这列。 本课程需要使用很多线性代数的内容,重要的是能够正确地实现这一点,尤其是在深度学习的错误中。实际上本课程认真地选择了运算符号,这些符号只是针对于这个课程的,并且能使这些向量化容易一些。 所以,希望通过这个细节可以更快地正确实现这些算法。接下来讲讲如何向量化这些: 公式3.12: $$ x = \left[ \begin{array}{c} \vdots & \vdots & \vdots & \vdots\ x^{(1)} & x^{(2)} & \cdots & x^{(m)}\ \vdots & \vdots & \vdots & \vdots\ \end{array} \right] $$ 公式3.13: $$ Z^{[1]} = \left[ \begin{array}{c} \vdots & \vdots & \vdots & \vdots\ z^{ 1 } & z^{ 1 } & \cdots & z^{ 1 }\ \vdots & \vdots & \vdots & \vdots\ \end{array} \right] $$ 公式3.14: $$ A^{[1]} = \left[ \begin{array}{c} \vdots & \vdots & \vdots & \vdots\ \alpha^{ 1 } & \alpha^{ 1 } & \cdots & \alpha^{ 1 }\ \vdots & \vdots & \vdots & \vdots\ \end{array} \right] $$ 公式3.15: $$ \left. \begin{array}{r} \text{$z^{ 1 } = W^{ 1 }x^{(i)} + b^{[1]}$}\ \text{$\alpha^{ 1 } = \sigma(z^{ 1 })$}\ \text{$z^{ 2 } = W^{ 2 }\alpha^{ 1 } + b^{[2]}$}\ \text{$\alpha^{ 2 } = \sigma(z^{ 2 })$}\ \end{array} \right} \implies \begin{cases} \text{$A^{[1]} = \sigma(z^{[1]})$}\ \text{$z^{[2]} = W^{[2]}A^{[1]} + b^{[2]}$}\ \text{$A^{[2]} = \sigma(z^{[2]})$}\ \end{cases} $$ 前一张幻灯片中的 for 循环是来遍历所有个训练样本。 定义矩阵$X$等于训练样本,将它们组合成矩阵的各列,形成一个$n$维或$n$乘以$m$维矩阵。接下来计算见公式3.15: 以此类推,从小写的向量$x$到这个大写的矩阵$X$,只是通过组合$x$向量在矩阵的各列中。 同理,$z^{ 1 }$,$z^{ 1 }$等等都是$z^{ 1 }$的列向量,将所有$m$都组合在各列中,就的到矩阵$Z^{[1]}$。 同理,$a^{ 1 }$,$a^{ 1 }$,……,$a^{ 1 }$将其组合在矩阵各列中,如同从向量$x$到矩阵$X$,以及从向量$z$到矩阵$Z$一样,就能得到矩阵$A^{[1]}$。 同样的,对于$Z^{[2]}$和$A^{[2]}$,也是这样得到。 这种符号其中一个作用就是,可以通过训练样本来进行索引。这就是水平索引对应于不同的训练样本的原因,这些训练样本是从左到右扫描训练集而得到的。 在垂直方向,这个垂直索引对应于神经网络中的不同节点。例如,这个节点,该值位于矩阵的最左上角对应于激活单元,它是位于第一个训练样本上的第一个隐藏单元。它的下一个值对应于第二个隐藏单元的激活值。它是位于第一个训练样本上的,以及第一个训练示例中第三个隐藏单元,等等。 当垂直扫描,是索引到隐藏单位的数字。当水平扫描,将从第一个训练示例中从第一个隐藏的单元到第二个训练样本,第三个训练样本……直到节点对应于第一个隐藏单元的激活值,且这个隐藏单元是位于这$m$个训练样本中的最终训练样本。 从水平上看,矩阵$A​$代表了各个训练样本。从竖直上看,矩阵$A​$的不同的索引对应于不同的隐藏单元。 对于矩阵$Z,X$情况也类似,水平方向上,对应于不同的训练样本;竖直方向上,对应不同的输入特征,而这就是神经网络输入层中各个节点。 神经网络上通过在多样本情况下的向量化来使用这些等式。 在下一个视频中,将证明为什么这是一种正确向量化的实现。这种证明将会与逻辑回归中的证明类似。 3.5 向量化实现的解释(Justification for vectorized implementation) 在上一个视频中,我们学习到如何将多个训练样本横向堆叠成一个矩阵$X$,然后就可以推导出神经网络中前向传播( forward propagation )部分的向量化实现。 在这个视频中,我们将会继续了解到,为什么上一节中写下的公式就是将多个样本向量化的正确实现。 我们先手动对几个样本计算一下前向传播,看看有什么规律: 公式3.16: $z^{ 1 } = W^{[1]}x^{(1)} + b^{[1]}$ $z^{ 1 } = W^{[1]}x^{(2)} + b^{[1]}$ $z^{ 1 } = W^{[1]}x^{(3)} + b^{[1]}$ 这里,为了描述的简便,我们先忽略掉 $b^{[1]}$后面你将会看到利用 Python 的广播机制,可以很容易的将$b^{[1]}$ 加进来。 现在 $W^{[1]}$ 是一个矩阵,$x^{(1)},x^{(2)},x^{(3)}$都是列向量,矩阵乘以列向量得到列向量,下面将它们用图形直观的表示出来: 公式3.17: $$ W^{[1]} x = \left[ \begin{array}{ccc} \cdots \ \cdots \ \cdots \ \end{array} \right] \left[ \begin{array}{c} \vdots &\vdots & \vdots & \vdots \\ x^{(1)} & x^{(2)} & x^{(3)} & \vdots\\ \vdots &\vdots & \vdots & \vdots \\ \end{array} \right] = \left[ \begin{array}{c} \vdots &\vdots & \vdots & \vdots \\ w^{(1)}x^{(1)} & w^{(1)}x^{(2)} & w^{(1)}x^{(3)} & \vdots\\ \vdots &\vdots & \vdots & \vdots \\ \end{array} \right] =\\ \left[ \begin{array}{c} \vdots &\vdots & \vdots & \vdots \\ z^{[1](1)} & z^{[1](2)} & z^{[1](3)} & \vdots\\ \vdots &\vdots & \vdots & \vdots \\ \end{array} \right] = Z^{[1]} $$ 视频中,吴恩达老师很细心的用不同的颜色表示不同的样本向量,及其对应的输出。所以从图中可以看出,当加入更多样本时,只需向矩阵$X$中加入更多列。 所以从这里我们也可以了解到,为什么之前我们对单个样本的计算要写成 $z^{ 1 } = W^{[1]}x^{(i)} + b^{[1]}$ 这种形式,因为当有不同的训练样本时,将它们堆到矩阵$X$的各列中,那么它们的输出也就会相应的堆叠到矩阵 $Z^{[1]}$ 的各列中。现在我们就可以直接计算矩阵 $Z^{[1]}$ 加上$b^{[1]}$,因为列向量 $b^{[1]}$ 和矩阵 $Z^{[1]}$的列向量有着相同的尺寸,而 Python 的广播机制对于这种矩阵与向量直接相加的处理方式是,将向量与矩阵的每一列相加。 所以这一节只是说明了为什么公式 $Z^{[1]} =W^{[1]}X + \ b^{[1]}$是前向传播的第一步计算的正确向量化实现,但事实证明,类似的分析可以发现,前向传播的其它步也可以使用非常相似的逻辑,即如果将输入按列向量横向堆叠进矩阵,那么通过公式计算之后,也能得到成列堆叠的输出。 最后,对这一段视频的内容做一个总结: 由公式3.12、公式3.13、公式3.14、公式3.15可以看出,使用向量化的方法,可以不需要显示循环,而直接通过矩阵运算从$X​$就可以计算出 $A^{[1]}​$,实际上$X$可以记为 $A^{[0]}$,使用同样的方法就可以由神经网络中的每一层的输入 $A^{[i-1]}​$ 计算输出 $A^{[i]}$。其实这些方程有一定对称性,其中第一个方程也可以写成$Z^{[1]} = W^{[1]}A^{[0]} + b^{[1]}$,你看这对方程,还有这对方程形式其实很类似,只不过这里所有指标加了1。所以这样就显示出神经网络的不同层次,你知道大概每一步做的都是一样的,或者只不过同样的计算不断重复而已。这里我们有一个双层神经网络,我们在下周视频里会讲深得多的神经网络,你看到随着网络的深度变大,基本上也还是重复这两步运算,只不过是比这里你看到的重复次数更多。在下周的视频中将会讲解更深层次的神经网络,随着层数的加深,基本上也还是重复同样的运算。 以上就是对神经网络向量化实现的正确性的解释,到目前为止,我们仅使用 sigmoid 函数作为激活函数,事实上这并非最好的选择,在下一个视频中,将会继续深入的讲解如何使用更多不同种类的激活函数。 3.6 激活函数(Activation functions) 使用一个神经网络时,需要决定使用哪种激活函数用隐藏层上,哪种用在输出节点上。到目前为止,之前的视频只用过 sigmoid 激活函数,但是,有时其他的激活函数效果会更好。 在神经网路的前向传播中,的$a^{[1]} = \sigma(z^{[1]})$和$a^{[2]} =\sigma(z^{[2]})$这两步会使用到 sigmoid 函数。 sigmoid 函数在这里被称为激活函数。 公式3.18: $a = \sigma(z) = \frac{1}{{1 + e}^{- z}}$ 更通常的情况下,使用不同的函数$g( z^{[1]})$,$g$可以是除了 sigmoid 函数以外的非线性函数。 tanh 函数或者双曲正切函数是总体上都优于 sigmoid 函数的激活函数。 如图,$a = tan(z)$的值域是位于+1和-1之间。 公式3.19: $a= tanh(z) = \frac{e^{z} - e^{- z}}{e^{z} + e^{- z}}$ 事实上, tanh 函数是 sigmoid 的向下平移和伸缩后的结果。对它进行了变形后,穿过了$(0,0)$点,并且值域介于+1和-1之间。 结果表明,如果在隐藏层上使用函数 公式3.20: $g(z^{[1]}) = tanh(z^{[1]}) $ 效果总是优于 sigmoid 函数。因为函数值域在-1和+1的激活函数,其均值是更接近零均值的。在训练一个算法模型时,如果使用 tanh 函数代替 sigmoid 函数中心化数据,使得数据的平均值更接近0而不是0.5. 这会使下一层学习简单一点,在第二门课中会详细讲解。 在讨论优化算法时,有一点要说明:我基本已经不用 sigmoid 激活函数了, tanh 函数在所有场合都优于 sigmoid 函数。 但有一个例外:在二分类的问题中,对于输出层,因为$y​$的值是0或1,所以想让$\hat{y}​$的数值介于0和1之间,而不是在-1和+1之间。所以需要使用 sigmoid 激活函数。这里的 公式3.21: $g(z^{[2]}) = \sigma(z^{[2]})​$ 在这个例子里看到的是,对隐藏层使用 tanh 激活函数,输出层使用 sigmoid 函数。 所以,在不同的神经网络层中,激活函数可以不同。为了表示不同的激活函数,在不同的层中,使用方括号上标来指出$g$上标为$[1]$的激活函数,可能会跟$g$上标为$[2]$不同。方括号上标$[1]$代表隐藏层,方括号上标$[2]$表示输出层。 sigmoid 函数和 tanh 函数两者共同的缺点是,在$z$特别大或者特别小的情况下,导数的梯度或者函数的斜率会变得特别小,最后就会接近于0,导致降低梯度下降的速度。 在机器学习另一个很流行的函数是:修正线性单元的函数( ReLu ), ReLu 函数图像是如下图。 公式3.22: $ a =max( 0,z) $ 所以,只要$z$是正值的情况下,导数恒等于1,当$z$是负值的时候,导数恒等于0。从实际上来说,当使用$z$的导数时,$z$=0的导数是没有定义的。但是当编程实现的时候,$z$的取值刚好等于0.00000001,这个值相当小,所以,在实践中,不需要担心这个值,$z$是等于0的时候,假设一个导数是1或者0效果都可以。 这有一些选择激活函数的经验法则: 如果输出是0、1值(二分类问题),则输出层选择 sigmoid 函数,然后其它的所有单元都选择 Relu 函数。 这是很多激活函数的默认选择,如果在隐藏层上不确定使用哪个激活函数,那么通常会使用 Relu 激活函数。有时,也会使用 tanh 激活函数,但 Relu 的一个优点是:当$z$是负值的时候,导数等于0。 这里也有另一个版本的 Relu 被称为 Leaky Relu 。 当$z$是负值时,这个函数的值不是等于0,而是轻微的倾斜,如图。 这个函数通常比 Relu 激活函数效果要好,尽管在实际中 Leaky ReLu 使用的并不多。 图3.6.1 两者的优点是: 第一,在$z​$的区间变动很大的情况下,激活函数的导数或者激活函数的斜率都会远大于0,在程序实现就是一个 if-else 语句,而 sigmoid 函数需要进行浮点四则运算,在实践中,使用 ReLu 激活函数神经网络通常会比使用 sigmoid 或者 tanh 激活函数学习的更快。 第二, sigmoid 和 tanh 函数的导数在正负饱和区的梯度都会接近于0,这会造成梯度弥散,而 Relu 和 Leaky ReLu 函数大于0部分都为常数,不会产生梯度弥散现象。(同时应该注意到的是, Relu 进入负半区的时候,梯度为0,神经元此时不会训练,产生所谓的稀疏性,而 Leaky ReLu 不会有这问题) $z$在 ReLu 的梯度一半都是0,但是,有足够的隐藏层使得z值大于0,所以对大多数的训练数据来说学习过程仍然可以很快。 快速概括一下不同激活函数的过程和结论。 sigmoid 激活函数:除了输出层是一个二分类问题基本不会用它。 tanh 激活函数: tanh 是非常优秀的,几乎适合所有场合。 ReLu 激活函数:最常用的默认函数,,如果不确定用哪个激活函数,就使用 ReLu 或者 Leaky ReLu 。公式3.23: $a = max( 0.01z,z)$ 为什么常数是0.01?当然,可以为学习算法选择不同的参数。 在选择自己神经网络的激活函数时,有一定的直观感受,在深度学习中的经常遇到一个问题:在编写神经网络的时候,会有很多选择:隐藏层单元的个数、激活函数的选择、初始化权值……这些选择想得到一个对比较好的指导原则是挺困难的。 鉴于以上三个原因,以及在工业界的见闻,提供一种直观的感受,哪一种工业界用的多,哪一种用的少。但是,自己的神经网络的应用,以及其特殊性,是很难提前知道选择哪些效果更好。所以通常的建议是:如果不确定哪一个激活函数效果更好,可以把它们都试试,然后在验证集或者发展集上进行评价。然后看哪一种表现的更好,就去使用它。 为自己的神经网络的应用测试这些不同的选择,会在以后检验自己的神经网络或者评估算法的时候,看到不同的效果。如果仅仅遵守使用默认的 ReLu 激活函数,而不要用其他的激励函数,那就可能在近期或者往后,每次解决问题的时候都使用相同的办法。 3.7 为什么需要非线性激活函数?(why need a nonlinear activation function?) 为什么神经网络需要非线性激活函数?事实证明:要让你的神经网络能够计算出有趣的函数,你必须使用非线性激活函数,证明如下: 这是神经网络正向传播的方程,现在我们去掉函数$g$,然后令$a^{[1]} = z^{[1]}$,或者我们也可以令$g(z)=z$,这个有时被叫做线性激活函数(更学术点的名字是恒等激励函数,因为它们就是把输入值输出)。为了说明问题我们把$a^{[2]} = z^{[2]}$,那么这个模型的输出$y$或仅仅只是输入特征$x$的线性组合。 如果我们改变前面的式子,令: (1) $a^{[1]} = z^{[1]} = W^{[1]}x + b^{[1]}$ (2) $a^{[2]} = z^{[2]} = W^{[2]}a^{[1]}+ b^{[2]}$ 将式子(1)代入式子(2)中,则: $a^{[2]} = z^{[2]} = W^{[2]}(W^{[1]}x + b^{[1]}) + b^{[2]}$ (3) $a^{[2]} = z^{[2]} = W^{[2]}W^{[1]}x + W^{[2]}b^{[1]} + b^{[2]} $ 简化多项式得 $a^{[2]} = z^{[2]} = W^{'}x + b^{'} $ 如果你是用线性激活函数或者叫恒等激励函数,那么神经网络只是把输入线性组合再输出。 我们稍后会谈到深度网络,有很多层的神经网络,很多隐藏层。事实证明,如果你使用线性激活函数或者没有使用一个激活函数,那么无论你的神经网络有多少层一直在做的只是计算线性函数,所以不如直接去掉全部隐藏层。在我们的简明案例中,事实证明如果你在隐藏层用线性激活函数,在输出层用 sigmoid 函数,那么这个模型的复杂度和没有任何隐藏层的标准 Logistic 回归是一样的,如果你愿意的话,可以证明一下。 在这里线性隐层一点用也没有,因为这两个线性函数的组合本身就是线性函数,所以除非你引入非线性,否则你无法计算更有趣的函数,即使你的网络层数再多也不行;只有一个地方可以使用线性激活函数------$g(z)=z$,就是你在做机器学习中的回归问题。$y$ 是一个实数,举个例子,比如你想预测房地产价格,$y$ 就不是二分类任务0或1,而是一个实数,从0到正无穷。如果$y$ 是个实数,那么在输出层用线性激活函数也许可行,你的输出也是一个实数,从负无穷到正无穷。 总而言之,不能在隐藏层用线性激活函数,可以用 ReLU 或者 tanh 或者 leaky ReLU 或者其他的非线性激活函数,唯一可以用线性激活函数的通常就是输出层;除了这种情况,会在隐层用线性函数的,除了一些特殊情况,比如与压缩有关的,那方面在这里将不深入讨论。在这之外,在隐层使用线性激活函数非常少见。因为房价都是非负数,所以我们也可以在输出层使用 ReLU 函数这样你的$\hat{y}$都大于等于0。 理解为什么使用非线性激活函数对于神经网络十分关键,接下来我们讨论梯度下降,并在下一个视频中开始讨论梯度下降的基础——激活函数的导数。 3.8 激活函数的导数(Derivatives of activation functions) 在神经网络中使用反向传播的时候,你真的需要计算激活函数的斜率或者导数。针对以下四种激活,求其导数如下: 1) sigmoid activation function 图3.8.1 其具体的求导如下: 公式3.25: $\frac{d}{dz}g(z) = {\frac{1}{1 + e^{-z}} (1-\frac{1}{1 + e^{-z}})}=g(z)(1-g(z))$ 注: 当$z$ = 10或$z= -10$ ; $\frac{d}{dz}g(z)\approx0$ 当$z $= 0 , $\frac{d}{dz}g(z)\text{=g(z)(1-g(z))=}{1}/{4}$ 在神经网络中$a= g(z)$; $g{{(z)}^{'}}=\frac{d}{dz}g(z)=a(1-a)$ Tanh activation function 图3.8.2 其具体的求导如下: 公式3.26: $g(z) = tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}} $ 公式3.27: $\frac{d}{{d}z}g(z) = 1 - (tanh(z))^{2}$ 注: 当$z$ = 10或$z= -10$ $\frac{d}{dz}g(z)\approx0$ 当$z$ = 0, $\frac{d}{dz}g(z)\text{=1-(0)=}1$ 在神经网络中; 3) Rectified Linear Unit (ReLU) $g(z) =max (0,z)$ $$ g(z)^{'}= \begin{cases} 0& \text{if z < 0}\ 1& \text{if z > 0}\ undefined& \text{if z = 0} \end{cases} $$ 注:通常在$z$= 0的时候给定其导数1,0;当然$z$=0的情况很少 4) Leaky linear unit (Leaky ReLU) 与 ReLU 类似 $$ g(z)=\max(0.01z,z) \ \ \ g(z)^{'}= \begin{cases} 0.01& \text{if z < 0}\ 1& \text{if z > 0}\ undefined& \text{if z = 0} \end{cases} $$ 注:通常在$z = 0$的时候给定其导数1,0.01;当然$z=0$的情况很少。 3.9 神经网络的梯度下降(Gradient descent for neural networks) 在这个视频中,我会给你实现反向传播或者说梯度下降算法的方程组,在下一个视频我们会介绍为什么这几个特定的方程是针对你的神经网络实现梯度下降的正确方程。 你的单隐层神经网络会有$W^{[1]}$,$b^{[1]}$,$W^{[2]}$,$b^{[2]}$这些参数,还有个$n_x$表示输入特征的个数,$n^{[1]}$表示隐藏单元个数,$n^{[2]}$表示输出单元个数。 在我们的例子中,我们只介绍过的这种情况,那么参数: 矩阵$W^{[1]}$的维度就是($n^{[1]}, n^{[0]}$),$b^{[1]}$就是$n^{[1]}$维向量,可以写成$(n^{[1]}, 1)$,就是一个的列向量。 矩阵$W^{[2]}$的维度就是($n^{[2]}, n^{[1]}$),$b^{[2]}$的维度就是$(n^{[2]},1)$维度。 你还有一个神经网络的成本函数,假设你在做二分类任务,那么你的成本函数等于: Cost function : 公式: $J(W^{[1]},b^{[1]},W^{[2]},b^{[2]}) = {\frac{1}{m}}\sum_{i=1}^mL(\hat{y}, y)$ loss function 和之前做 logistic 回归完全一样。 训练参数需要做梯度下降,在训练神经网络的时候,随机初始化参数很重要,而不是初始化成全零。当你参数初始化成某些值后,每次梯度下降都会循环计算以下预测值: $\hat{y}^{(i)},(i=1,2,…,m)$ 公式3.28: $dW^{[1]} = \frac{dJ}{dW^{[1]}},db^{[1]} = \frac{dJ}{db^{[1]}}$ 公式3.29: ${d}W^{[2]} = \frac{{dJ}}{dW^{[2]}},{d}b^{[2]} = \frac{dJ}{db^{[2]}}$ 其中 公式3.30: $W^{[1]}\implies{W^{[1]} - adW^{[1]}},b^{[1]}\implies{b^{[1]} -adb^{[1]}}$ 公式3.31: $W^{[2]}\implies{W^{[2]} - \alpha{\rm d}W^{[2]}},b^{[2]}\implies{b^{[2]} - \alpha{\rm d}b^{[2]}}$ 正向传播方程如下(之前讲过): forward propagation : (1) $z^{[1]} = W^{[1]}x + b^{[1]}$ (2) $a^{[1]} = \sigma(z^{[1]})$ (3) $z^{[2]} = W^{[2]}a^{[1]} + b^{[2]}$ (4) $a^{[2]} = g^{[2]}(z^{[z]}) = \sigma(z^{[2]})$ 反向传播方程如下: back propagation : 公式3.32: $ dz^{[2]} = A^{[2]} - Y , Y = \begin{bmatrix}y^{[1]} & y^{[2]} & \cdots & y^{[m]}\ \end{bmatrix} $ 公式3.33: $ dW^{[2]} = {\frac{1}{m}}dz^{[2]}A^{[1]T} $ 公式3.34: $ {\rm d}b^{[2]} = {\frac{1}{m}}np.sum({d}z^{[2]},axis=1,keepdims=True)$ 公式3.35: $ dz^{[1]} = \underbrace{W^{[2]T}{\rm d}z^{[2]}} {(n^{[1]},m)}\quad*\underbrace{{g^{[1]}}^{'}} {activation ; function ; of ; hidden ; layer}*\quad\underbrace{(z^{[1]})} {(n^{[1]},m)} $ 公式3.36: $dW^{[1]} = {\frac{1}{m}}dz^{[1]}x^{T}$ 公式3.37: ${\underbrace{db^{[1]}} {(n^{[1]},1)}} = {\frac{1}{m}}np.sum(dz^{[1]},axis=1,keepdims=True)$ 上述是反向传播的步骤,注:这些都是针对所有样本进行过向量化,$Y$是$1×m$的矩阵;这里 np.sum 是python的numpy命令, axis=1 表示水平相加求和, keepdims 是防止 python 输出那些古怪的秩数$(n,)$,加上这个确保阵矩阵$db^{[2]}$这个向量输出的维度为$(n,1)$这样标准的形式。 目前为止,我们计算的都和 Logistic 回归十分相似,但当你开始计算反向传播时,你需要计算,是隐藏层函数的导数,输出在使用 sigmoid 函数进行二元分类。这里是进行逐个元素乘积,因为$W^{[2]T}dz^{[2]}$和$(z^{[1]})$这两个都为$(n^{[1]},m)$矩阵; 还有一种防止 python 输出奇怪的秩数,需要显式地调用 reshape 把 np.sum 输出结果写成矩阵形式。 以上就是正向传播的4个方程和反向传播的6个方程,这里我是直接给出的,在下个视频中,我会讲如何导出反向传播的这6个式子的。如果你要实现这些算法,你必须正确执行正向和反向传播运算,你必须能计算所有需要的导数,用梯度下降来学习神经网络的参数;你也可以许多成功的深度学习从业者一样直接实现这个算法,不去了解其中的知识。 3.10(选修)直观理解反向传播(Backpropagation intuition) 这个视频主要是推导反向传播。 下图是逻辑回归的推导: 回想一下逻辑回归的公式(参考公式3.2、公式3.5、公式3.6、公式3.15) 公式3.38: $$ \left. \begin{array}{l} {x }\ {w }\ {b } \end{array} \right} \implies{z={w}^Tx+b} \implies{\alpha = \sigma(z)} \implies{{L}\left(a,y \right)} $$ 所以回想当时我们讨论逻辑回归的时候,我们有这个正向传播步骤,其中我们计算$z$,然后$a$,然后损失函数$L$。 公式3.39: $$ \underbrace{ \left. \begin{array}{l} {x }\ {w }\ {b } \end{array} \right} } {dw={dz}\cdot x, db =dz} \impliedby\underbrace{{z={w}^Tx+b}} {dz=da\cdot g^{'}(z), g(z)=\sigma(z), {\frac{{dL}}{dz}}={\frac{{dL}}{da}}\cdot{\frac{da}{dz}}, {\frac{d}{ dz}}g(z)=g^{'}(z)} \impliedby\underbrace{{a = \sigma(z)} \impliedby{L(a,y)}}_{da={\frac{{d}}{da}}{L}\left(a,y \right)=(-y\log{\alpha} - (1 - y)\log(1 - a))^{'}={-\frac{y}{a}} + {\frac{1 - y}{1 - a}{}} } $$ 神经网络的计算中,与逻辑回归十分类似,但中间会有多层的计算。下图是一个双层神经网络,有一个输入层,一个隐藏层和一个输出层。 前向传播: 计算$z^{[1]}$,$a^{[1]}$,再计算$z^{[2]}$,$a^{[2]}$,最后得到 loss function 。 反向传播: 向后推算出$da^{[2]}$,然后推算出$dz^{[2]}$,接着推算出$da^{[1]}$,然后推算出$dz^{[1]}$。我们不需要对$x$求导,因为$x$是固定的,我们也不是想优化$x$。向后推算出$da^{[2]}$,然后推算出$dz^{[2]}$的步骤可以合为一步: 公式3.40: $dz^{[2]}=a^{[2]}-y;,;dW^{[2]}=dz^{[2]}{a^{[1]}}^{T}$ (注意:逻辑回归中;为什么$a^{[1]T}$多了个转置:$dw$中的$W$(视频里是$W^{[2]}_i$)是一个列向量,而$W^{[2]}$是个行向量,故需要加个转置); 公式3.41: $db^{[2]}=dz^{[2]}$ 公式3.42: $dz^{[1]} = W^{[2]T}dz^{[2]}* g[1]^{'}(z^{[1]})$ 注意:这里的矩阵:$W^{[2]}$的维度是:$(n^{[2]},n^{[1]})$。 $z^{[2]}$ , $dz^{[2]}$的维度都是:$(n^{[2]},1)$,如果是二分类,那维度就是$(1,1)$。 $z^{[1]}$,$dz^{[1]}$的维度都是:$(n^{[1]},1)$。 证明过程: 见公式3.42,其中$W^{[2]T}dz^{[2]}$维度为:$(n^{[1]},n^{[2]})$、$(n^{[2]},1)$相乘得到$(n^{[1]},1)$,和$z^{[1]}$维度相同, $g[1]^{'}(z^{[1]})$的维度为$(n^{[1]},1)$,这就变成了两个都是$(n^{[1]},1)$向量逐元素乘积。 实现后向传播有个技巧,就是要保证矩阵的维度相互匹配。最后得到$dW^{[1]}$和$db^{[1]}$,公式3.43: $dW^{[1]} =dz^{[1]}x^{T},db^{[1]} = dz^{[1]}$ 可以看出$dW^{[1]}$ 和$dW^{[2]}$ 非常相似,其中$x$扮演了$a^{[0]}$的角色,$x^{T}$ 等同于$a^{[0]T}$。 由: $Z^{[1]} = W^{[1]}x + b^{[1]};,;a^{[1]}=g^{[1]}(Z^{[1]})$ 得到: $Z^{[1]} = W^{[1]}x + b^{[1]}, A^{[1]} = g^{[1]}(Z^{[1]})$ $$ Z^{[1]} = \left[ \begin{array}{c} \vdots &\vdots & \vdots & \vdots \ z^{ 1 } & z^{ 1 } & \vdots & z^{ 1 } \ \vdots &\vdots & \vdots & \vdots \ \end{array} \right] $$ 注意:大写的$Z^{[1]}$表示$z^{ 1 },z^{ 1 },z^{ 1 }...z^{ 1 }$的列向量堆叠成的矩阵,以下类同。 下图写了主要的推导过程: 公式3.44: $dZ^{[2]}=A^{[2]}-Y;,;dW^{[2]}={\frac{1}{m}}dZ^{[2]}{A^{[1]}}^{T}$ 公式3.45: $L = {\frac{1}{m}}\sum_i^n{L(\hat{y},y)}$ 公式3.46: $db^{[2]} = {\frac{1}{m}}np.sum(dZ^{[2]},axis=1,keepdims=True)$ 公式3.47: $\underbrace{dZ^{[1]}} {(n^{[1]}, m)} = \underbrace{W^{[2]T}dZ^{[2]}} {(n^{[1]}, m)}*\underbrace{g[1]^{'}(Z^{[1]})}_{(n^{[1]}, m)}$ 公式3.48: $dW^{[1]} = {\frac{1}{m}}dZ^{[1]}x^{T}$ 公式3.49: $db^{[1]} = {\frac{1}{m}}np.sum(dZ^{[1]},axis=1,keepdims=True) $ 吴恩达老师认为反向传播的推导是机器学习领域最难的数学推导之一,矩阵的导数要用链式法则来求,如果这章内容掌握不了也没大的关系,只要有这种直觉就可以了。还有一点,就是初始化你的神经网络的权重,不要都是0,而是随机初始化,下一章将详细介绍原因。 3.11 随机初始化(Random+Initialization) 当你训练神经网络时,权重随机初始化是很重要的。对于逻辑回归,把权重初始化为0当然也是可以的。但是对于一个神经网络,如果你把权重或者参数都初始化为0,那么梯度下降将不会起作用。 让我们看看这是为什么。有两个输入特征,$n^{[0]} = 2$,2个隐藏层单元$n^{[1]}$就等于2。 因此与一个隐藏层相关的矩阵,或者说$W^{[1]}$是2*2的矩阵,假设把它初始化为0的2*2矩阵,$b^{[1]}$也等于 $[0;0]^T$,把偏置项$b$初始化为0是合理的,但是把$w$初始化为0就有问题了。 那这个问题如果按照这样初始化的话,你总是会发现$a_{1}^{[1]}$ 和 $a_{2}^{[1]}$相等,这个激活单元和这个激活单元就会一样。因为两个隐含单元计算同样的函数,当你做反向传播计算时,这会导致$\text{dz} {1}^{[1]}$ 和 $\text{dz} {2}^{[1]}$也会一样,对称这些隐含单元会初始化得一样,这样输出的权值也会一模一样,由此$W^{[2]}$等于$[0;0]$; 图3.11.1 但是如果你这样初始化这个神经网络,那么这两个隐含单元就会完全一样,因此他们完全对称,也就意味着计算同样的函数,并且肯定的是最终经过每次训练的迭代,这两个隐含单元仍然是同一个函数,令人困惑。$dW$会是一个这样的矩阵,每一行有同样的值因此我们做权重更新把权重$W^{[1]}\implies{W^{[1]}-adW}$每次迭代后的$W^{[1]}$,第一行等于第二行。 由此可以推导,如果你把权重都初始化为0,那么由于隐含单元开始计算同一个函数,所有的隐含单元就会对输出单元有同样的影响。一次迭代后同样的表达式结果仍然是相同的,即隐含单元仍是对称的。通过推导,两次、三次、无论多少次迭代,不管你训练网络多长时间,隐含单元仍然计算的是同样的函数。因此这种情况下超过1个隐含单元也没什么意义,因为他们计算同样的东西。当然更大的网络,比如你有3个特征,还有相当多的隐含单元。 如果你要初始化成0,由于所有的隐含单元都是对称的,无论你运行梯度下降多久,他们一直计算同样的函数。这没有任何帮助,因为你想要两个不同的隐含单元计算不同的函数,这个问题的解决方法就是随机初始化参数。你应该这么做:把$W^{[1]}$设为 np.random.randn(2,2) (生成高斯分布),通常再乘上一个小的数,比如0.01,这样把它初始化为很小的随机数。然后$b$没有这个对称的问题(叫做 symmetry breaking problem ),所以可以把 $b$ 初始化为0,因为只要随机初始化$W$你就有不同的隐含单元计算不同的东西,因此不会有 symmetry breaking 问题了。相似的,对于$W^{[2]}$你可以随机初始化,$b^{[2]}$可以初始化为0。 $W^{[1]} = np.random.randn(2,2); ;0.01;,;b^{[1]} = np.zeros((2,1))$ $W^{[2]} = np.random.randn(2,2); ;0.01;,;b^{[2]} = 0$ 你也许会疑惑,这个常数从哪里来,为什么是0.01,而不是100或者1000。我们通常倾向于初始化为很小的随机数。因为如果你用 tanh 或者 sigmoid 激活函数,或者说只在输出层有一个 Sigmoid ,如果(数值)波动太大,当你计算激活值时$z^{[1]} = W^{[1]}x + b^{[1]};,;a^{[1]} = \sigma(z^{[1]})=g^{[1]}(z^{[1]})$如果$W$很大,$z$就会很大或者很小,因此这种情况下你很可能停在 tanh / sigmoid 函数的平坦的地方(见图3.8.2),这些地方梯度很小也就意味着梯度下降会很慢,因此学习也就很慢。 回顾一下:如果$w$很大,那么你很可能最终停在(甚至在训练刚刚开始的时候)$z$很大的值,这会造成 tanh / Sigmoid 激活函数饱和在龟速的学习上,如果你没有 sigmoid / tanh 激活函数在你整个的神经网络里,就不成问题。但如果你做二分类并且你的输出单元是 Sigmoid 函数,那么你不会想让初始参数太大,因此这就是为什么乘上0.01或者其他一些小数是合理的尝试。对于$w^{[2]}$一样,就是 np.random.randn((1,2)) ,我猜会是乘以0.01。 事实上有时有比0.01更好的常数,当你训练一个只有一层隐藏层的网络时(这是相对浅的神经网络,没有太多的隐藏层),设为0.01可能也可以。但当你训练一个非常非常深的神经网络,你可能要试试0.01以外的常数。下一节课我们会讨论怎么并且何时去选择一个不同于0.01的常数,但是无论如何它通常都会是个相对小的数。 好了,这就是这周的视频。你现在已经知道如何建立一个一层的神经网络了,初始化参数,用前向传播预测,还有计算导数,结合反向传播用在梯度下降中。 第四周:深层神经网络(Deep Neural Networks) 4.1 深层神经网络(Deep L-layer neural network) 目前为止我们学习了只有一个单独隐藏层的神经网络的正向传播和反向传播,还有逻辑回归,并且你还学到了向量化,这在随机初始化权重时是很重要。 本周所要做的是把这些理念集合起来,就可以执行你自己的深度神经网络。 复习下前三周的课的内容: 1.逻辑回归,结构如下图左边。一个隐藏层的神经网络,结构下图右边: 注意,神经网络的层数是这么定义的: 从左到右,由0开始定义 ,比如上边右图,${x} {1}$、${x} {2}$、${x}_{3}$,这层是第0层,这层左边的隐藏层是第1层,由此类推。如下图左边是两个隐藏层的神经网络,右边是5个隐藏层的神经网络。 严格上来说逻辑回归也是一个一层的神经网络,而上边右图一个深得多的模型,浅与深仅仅是指一种程度。记住以下要点: 有一个隐藏层的神经网络,就是一个两层神经网络。记住当我们算神经网络的层数时,我们不算输入层,我们只算隐藏层和输出层。 但是在过去的几年中, DLI (深度学习学院 deep learning institute )已经意识到有一些函数,只有非常深的神经网络能学会,而更浅的模型则办不到。尽管对于任何给定的问题很难去提前预测到底需要多深的神经网络,所以先去尝试逻辑回归,尝试一层然后两层隐含层,然后把隐含层的数量看做是另一个可以自由选择大小的超参数,然后再保留交叉验证数据上评估,或者用你的开发集来评估。 我们再看下深度学习的符号定义: 上图是一个四层的神经网络,有三个隐藏层。我们可以看到,第一层(即左边数过去第二层,因为输入层是第0层)有5个神经元数目,第二层5个,第三层3个。 我们用L表示层数,上图:$L=4$,输入层的索引为“0”,第一个隐藏层${n}^{[1]}=5$,表示有5个隐藏神经元,同理${n}^{[2]}=5$,${n}^{[3]}=3$,${{n}^{[4]}}$=${{n}^{[L]}}=1$(输出单元为1)。而输入层,${n}^{[0]}={n}_{x}=3$。 在不同层所拥有的神经元的数目,对于每层 l 都用${a}^{[l]}$来记作 l 层激活后结果,我们会在后面看到在正向传播时,最终能你会计算出${{a}^{[l]}}$。 通过用激活函数 $g$ 计算${z}^{[l]}$,激活函数也被索引为层数$l$,然后我们用${w}^{[l]}$来记作在 l 层计算${z}^{[l]}$值的权重。类似的,${{z}^{[l]}}$里的方程${b}^{[l]}$也一样。 最后总结下符号约定: 输入的特征记作$x$,但是$x$同样也是0层的激活函数,所以$x={a}^{[0]}$。 最后一层的激活函数,所以${a}^{[L]}$是等于这个神经网络所预测的输出结果。 但是如果你忘记了某些符号的意义,请看笔记最后的附件: 《深度学习符号指南》 。 4.2 前向传播和反向传播(Forward and backward propagation) 之前我们学习了构成深度神经网络的基本模块,比如每一层都有前向传播步骤以及一个相反的反向传播步骤,这次视频我们讲讲如何实现这些步骤。 先讲前向传播,输入${a}^{[l-1]}$,输出是${a}^{[l]}$,缓存为${z}^{[l]}$;从实现的角度来说我们可以缓存下${w}^{[l]}$和${b}^{[l]}$,这样更容易在不同的环节中调用函数。 所以前向传播的步骤可以写成: ${z}^{[l]}={W}^{[l]}\cdot{a}^{[l-1]}+{b}^{[l]}$ ​ ${{a}^{[l]}}={{g}^{[l]}}\left( {{z}^{[l]}}\right)$ 向量化实现过程可以写成: ${z}^{[l]}={W}^{[l]}\cdot {A}^{[l-1]}+{b}^{[l]}$ ​ ${A}^{[l]}={g}^{[l]}({Z}^{[l]})$ 前向传播需要喂入${A}^{[0]}$也就是$X$,来初始化;初始化的是第一层的输入值。${a}^{[0]}$对应于一个训练样本的输入特征,而${{A}^{[0]}}$对应于一整个训练样本的输入特征,所以这就是这条链的第一个前向函数的输入,重复这个步骤就可以从左到右计算前向传播。 下面讲反向传播的步骤: 输入为${{da}^{[l]}}$,输出为${{da}^{[l-1]}}$,${{dw}^{[l]}}$, ${{db}^{[l]}}$ 所以反向传播的步骤可以写成: (1)$d{{z}^{[l]}}=d{{a}^{[l]}}*{{g}^{[l]}}'( {{z}^{[l]}})$ (2)$d{{w}^{[l]}}=d{{z}^{[l]}}\cdot{{a}^{[l-1]}}~$ (3)$d{{b}^{[l]}}=d{{z}^{[l]}}~~$ (4)$d{{a}^{[l-1]}}={{w}^{\left[ l \right]T}}\cdot {{dz}^{[l]}}$ (5)$d{{z}^{[l]}}={{w}^{[l+1]T}}d{{z}^{[l+1]}}\cdot \text{ }{{g}^{[l]}}'( {{z}^{[l]}})~$ 式子(5)由式子(4)带入式子(1)得到,前四个式子就可实现反向函数。 向量化实现过程可以写成: (6)$d{{Z}^{[l]}}=d{{A}^{[l]}}*{{g}^{\left[ l \right]}}'\left({{Z}^{[l]}} \right)~~$ (7)$d{{W}^{[l]}}=\frac{1}{m}\text{}d{{Z}^{[l]}}\cdot {{A}^{\left[ l-1 \right]T}}$ (8)$d{{b}^{[l]}}=\frac{1}{m}\text{ }np.sum(d{{z}^{[l]}},axis=1,keepdims=True)$ (9)$d{{A}^{[l-1]}}={{W}^{\left[ l \right]T}}.d{{Z}^{[l]}}$ 总结一下: 第一层你可能有一个 ReLU 激活函数,第二层为另一个 ReLU 激活函数,第三层可能是 sigmoid 函数(如果你做二分类的话),输出值为,用来计算损失;这样你就可以向后迭代进行反向传播求导来求${{dw}^{[3]}}$,${{db}^{[3]}}$ ,${{dw}^{[2]}}$ ,${{db}^{[2]}}$ ,${{dw}^{[1]}}$ ,${{db}^{[1]}}$。在计算的时候,缓存会把${{z}^{[1]}}$ ${{z}^{[2]}}$${{z}^{[3]}}$传递过来,然后回传${{da}^{[2]}}$,${{da}^{[1]}}$ ,可以用来计算${{da}^{[0]}}$,但我们不会使用它,这里讲述了一个三层网络的前向和反向传播,还有一个细节没讲就是前向递归——用输入数据来初始化,那么反向递归(使用 Logistic 回归做二分类)——对${{A}^{[l]}}$ 求导。 忠告:补补微积分和线性代数,多推导,多实践。 4.3 深层网络中的前向传播(Forward propagation in a Deep Network) 跟往常一样,我们先来看对其中一个训练样本$x$如何应用前向传播,之后讨论向量化的版本。 第一层需要计算${{z}^{[1]}}={{w}^{[1]}}x+{{b}^{[1]}}$,${{a}^{[1]}}={{g}^{[1]}} {({z}^{[1]})}$($x$可以看做${{a}^{[0]}}$) 第二层需要计算${{z}^{[2]}}={{w}^{[2]}}{{a}^{[1]}}+{{b}^{[2]}}$,${{a}^{[2]}}={{g}^{[2]}} {({z}^{[2]})}$ 以此类推, 第四层为${{z}^{[4]}}={{w}^{[4]}}{{a}^{[3]}}+{{b}^{[4]}}$,${{a}^{[4]}}={{g}^{[4]}} {({z}^{[4]})}$ 前向传播可以归纳为多次迭代${{z}^{[l]}}={{w}^{[l]}}{{a}^{[l-1]}}+{{b}^{[l]}}$,${{a}^{[l]}}={{g}^{[l]}} {({z}^{[l]})}$。 向量化实现过程可以写成: ${{Z}^{[l]}}={{W}^{[l]}}{{a}^{[l-1]}}+{{b}^{[l]}}$,${{A}^{[l]}}={{g}^{[l]}}{({Z}^{[l]})}$ (${{A}^{[0]}} = X)$ 这里只能用一个显式 for 循环,$l$从1到$L$,然后一层接着一层去计算。下一节讲的是避免代码产生BUG,我所做的其中一件非常重要的工作。 4.4 核对矩阵的维数(Getting your matrix dimensions right) 当实现深度神经网络的时候,其中一个我常用的检查代码是否有错的方法就是拿出一张纸过一遍算法中矩阵的维数。 $w$的维度是(下一层的维数,前一层的维数),即${{w}^{[l]}}$: (${{n}^{[l]}}$,${{n}^{[l-1]}}$); $b$的维度是(下一层的维数,1),即: ${{b}^{[l]}}$ : (${{n}^{[l]}},1)$; ${{z}^{[l]}}$,${{a}^{[l]}}$: $({{n}^{[l]}},1)$; ${{dw}^{[l]}}$和${{w}^{[l]}}$维度相同,${{db}^{[l]}}$和${{b}^{[l]}}$维度相同,且$w$和$b$向量化维度不变,但$z$,$a$以及$x$的维度会向量化后发生变化。 向量化后: ${Z}^{[l]}$可以看成由每一个单独的${Z}^{[l]}$叠加而得到,${Z}^{[l]}=({{z}^{[l][1]}},{{z}^{[l][2]}},{{z}^{[l][3]}},…,{{z}^{[l][m]}})$, $m$为训练集大小,所以${Z}^{[l]}$的维度不再是$({{n}^{[l]}},1)$,而是$({{n}^{[l]}},m)$。 ${A}^{[l]}$:$({n}^{[l]},m)$,${A}^{[0]} = X =({n}^{[l]},m)$ 在你做深度神经网络的反向传播时,一定要确认所有的矩阵维数是前后一致的,可以大大提高代码通过率。下一节我们讲为什么深层的网络在很多问题上比浅层的好。 4.5 为什么使用深层表示?(Why deep representations?) 我们都知道深度神经网络能解决好多问题,其实并不需要很大的神经网络,但是得有深度,得有比较多的隐藏层,这是为什么呢?我们一起来看几个例子来帮助理解,为什么深度神经网络会很好用。 首先,深度网络在计算什么? 首先,深度网络究竟在计算什么?如果你在建一个人脸识别或是人脸检测系统,深度神经网络所做的事就是,当你输入一张脸部的照片,然后你可以把深度神经网络的第一层,当成一个特征探测器或者边缘探测器。在这个例子里,我会建一个大概有20个隐藏单元的深度神经网络,是怎么针对这张图计算的。隐藏单元就是这些图里这些小方块(第一张大图),举个例子,这个小方块(第一行第一列)就是一个隐藏单元,它会去找这张照片里“|”边缘的方向。那么这个隐藏单元(第四行第四列),可能是在找(“—”)水平向的边缘在哪里。之后的课程里,我们会讲专门做这种识别的卷积神经网络,到时候会细讲,为什么小单元是这么表示的。你可以先把神经网络的第一层当作看图,然后去找这张照片的各个边缘。我们可以把照片里组成边缘的像素们放在一起看,然后它可以把被探测到的边缘组合成面部的不同部分(第二张大图)。比如说,可能有一个神经元会去找眼睛的部分,另外还有别的在找鼻子的部分,然后把这许多的边缘结合在一起,就可以开始检测人脸的不同部分。最后再把这些部分放在一起,比如鼻子眼睛下巴,就可以识别或是探测不同的人脸(第三张大图)。 你可以直觉上把这种神经网络的前几层当作探测简单的函数,比如边缘,之后把它们跟后几层结合在一起,那么总体上就能学习更多复杂的函数。这些图的意义,我们在学习卷积神经网络的时候再深入了解。还有一个技术性的细节需要理解的是,边缘探测器其实相对来说都是针对照片中非常小块的面积。就像这块(第一行第一列),都是很小的区域。面部探测器就会针对于大一些的区域,但是主要的概念是,一般你会从比较小的细节入手,比如边缘,然后再一步步到更大更复杂的区域,比如一只眼睛或是一个鼻子,再把眼睛鼻子装一块组成更复杂的部分。 这种从简单到复杂的金字塔状表示方法或者组成方法,也可以应用在图像或者人脸识别以外的其他数据上。比如当你想要建一个语音识别系统的时候,需要解决的就是如何可视化语音,比如你输入一个音频片段,那么神经网络的第一层可能就会去先开始试着探测比较低层次的音频波形的一些特征,比如音调是变高了还是低了,分辨白噪音,咝咝咝的声音,或者音调,可以选择这些相对程度比较低的波形特征,然后把这些波形组合在一起就能去探测声音的基本单元。在语言学中有个概念叫做音位,比如说单词ca,c的发音,“嗑”就是一个音位,a的发音“啊”是个音位,t的发音“特”也是个音位,有了基本的声音单元以后,组合起来,你就能识别音频当中的单词,单词再组合起来就能识别词组,再到完整的句子。 所以深度神经网络的这许多隐藏层中,较早的前几层能学习一些低层次的简单特征,等到后几层,就能把简单的特征结合起来,去探测更加复杂的东西。比如你录在音频里的单词、词组或是句子,然后就能运行语音识别了。同时我们所计算的之前的几层,也就是相对简单的输入函数,比如图像单元的边缘什么的。到网络中的深层时,你实际上就能做很多复杂的事,比如探测面部或是探测单词、短语或是句子。 有些人喜欢把深度神经网络和人类大脑做类比,这些神经科学家觉得人的大脑也是先探测简单的东西,比如你眼睛看得到的边缘,然后组合起来才能探测复杂的物体,比如脸。这种深度学习和人类大脑的比较,有时候比较危险。但是不可否认的是,我们对大脑运作机制的认识很有价值,有可能大脑就是先从简单的东西,比如边缘着手,再组合成一个完整的复杂物体,这类简单到复杂的过程,同样也是其他一些深度学习的灵感来源,之后的视频我们也会继续聊聊人类或是生物学理解的大脑。 Small :隐藏单元的数量相对较少 Deep :隐藏层数目比较多 深层的网络隐藏单元数量相对较少,隐藏层数目较多,如果浅层的网络想要达到同样的计算结果则需要指数级增长的单元数量才能达到。 另外一个,关于神经网络为何有效的理论,来源于电路理论,它和你能够用电路元件计算哪些函数有着分不开的联系。根据不同的基本逻辑门,譬如与门、或门、非门。在非正式的情况下,这些函数都可以用相对较小,但很深的神经网络来计算,小在这里的意思是隐藏单元的数量相对比较小,但是如果你用浅一些的神经网络计算同样的函数,也就是说在我们不能用很多隐藏层时,你会需要成指数增长的单元数量才能达到同样的计算结果。 我再来举个例子,用没那么正式的语言介绍这个概念。假设你想要对输入特征计算异或或是奇偶性,你可以算$x_{1}XOR x_{2} XOR x_{3} XOR ……x_{n}$,假设你有$n$或者$n_{x}$个特征,如果你画一个异或的树图,先要计算$x_{1}$,$x_{2}$的异或,然后是$x_{3}$和$x_{4}$。技术上来说如果你只用或门,还有非门的话,你可能会需要几层才能计算异或函数,但是用相对小的电路,你应该就可以计算异或了。然后你可以继续建这样的一个异或树图(上图左),那么你最后会得到这样的电路来输出结果$y$,$\hat{y}=y$,也就是输入特征的异或,或是奇偶性,要计算异或关系。这种树图对应网络的深度应该是$O(log(n))$,那么节点的数量和电路部件,或是门的数量并不会很大,你也不需要太多门去计算异或。 但是如果你不能使用多隐层的神经网络的话,在这个例子中隐层数为$O(log(n))$,比如你被迫只能用单隐藏层来计算的话,这里全部都指向从这些隐藏单元到后面这里,再输出$y$,那么要计算奇偶性,或者异或关系函数就需要这一隐层(上图右方框部分)的单元数呈指数增长才行,因为本质上来说你需要列举耗尽$2^{n}$种可能的配置,或是$2^{n}$种输入比特的配置。异或运算的最终结果是1或0,那么你最终就会需要一个隐藏层,其中单元数目随输入比特指数上升。精确的说应该是$2^{n-1}$个隐藏单元数,也就是$O(2^{n})$。 我希望这能让你有点概念,意识到有很多数学函数用深度网络计算比浅网络要容易得多,我个人倒是认为这种电路理论,对训练直觉思维没那么有用,但这个结果人们还是经常提到的,用来解释为什么需要更深层的网络。 除了这些原因,说实话,我认为“深度学习”这个名字挺唬人的,这些概念以前都统称为有很多隐藏层的神经网络,但是深度学习听起来多高大上,太深奥了,对么?这个词流传出去以后,这是神经网络的重新包装或是多隐藏层神经网络的重新包装,激发了大众的想象力。抛开这些公关概念重新包装不谈,深度网络确实效果不错,有时候人们还是会按照字面意思钻牛角尖,非要用很多隐层。但是当我开始解决一个新问题时,我通常会从 logistic 回归开始,再试试一到两个隐层,把隐藏层数量当作参数、超参数一样去调试,这样去找比较合适的深度。但是近几年以来,有一些人会趋向于使用非常非常深邃的神经网络,比如好几打的层数,某些问题中只有这种网络才是最佳模型。 这就是我想讲的,为什么深度学习效果拔群的直觉解释,现在我们来看看除了正向传播以外,反向传播该怎么具体实现。 4.6 搭建神经网络块(Building blocks of deep neural networks) 这周的前几个视频和之前几周的视频里,你已经看到过正向反向传播的基础组成部分了,它们也是深度神经网络的重要组成部分,现在我们来用它们建一个深度神经网络。 这是一个层数较少的神经网络,我们选择其中一层(方框部分),从这一层的计算着手。在第$l$层你有参数$W^{[l]}$和$b^{[l]}$,正向传播里有输入的激活函数,输入是前一层$a^{[l-1]}$,输出是$a^{[l]}$,我们之前讲过$z^{[l]} =W^{[l]}a^{[l-1]} +b^{[l]}$,$a^{[l]} =g^{[l]}(z^{[l]})$,那么这就是你如何从输入$a^{[l-1]}$走到输出的$a^{[l]}$。之后你就可以把$z^{[l]}$的值缓存起来,我在这里也会把这包括在缓存中,因为缓存的$z^{[i]}$对以后的正向反向传播的步骤非常有用。 然后是反向步骤或者说反向传播步骤,同样也是第$l$层的计算,你会需要实现一个函数输入为$da^{[l]}$,输出$da^{[l-1]}$的函数。一个小细节需要注意,输入在这里其实是$da^{[l]}$以及所缓存的$z^{[l]}$值,之前计算好的$z^{[l]}$值,除了输出$da^{[l-1]}$的值以外,也需要输出你需要的梯度$dW^{[l]}$和$db^{[l]}$,这是为了实现梯度下降学习。 这就是基本的正向步骤的结构,我把它成为称为正向函数,类似的在反向步骤中会称为反向函数。总结起来就是,在l层,你会有正向函数,输入$a^{[l-1]}$并且输出$a^{[l]}$,为了计算结果你需要用$W^{[l]}$和$b^{[l]}$,以及输出到缓存的$z^{[l]}$。然后用作反向传播的反向函数,是另一个函数,输入$da^{[l]}$,输出$da^{[l-1]}$,你就会得到对激活函数的导数,也就是希望的导数值$da^{[l]}$。$a^{[l-1]}$是会变的,前一层算出的激活函数导数。在这个方块(第二个)里你需要$W^{[l]}$和$b^{[l]}$,最后你要算的是$dz^{[l]}$。然后这个方块(第三个)中,这个反向函数可以计算输出$dW^{[l]}$和$db^{[l]}$。我会用红色箭头标注标注反向步骤,如果你们喜欢,我可以把这些箭头涂成红色。 然后如果实现了这两个函数(正向和反向),然后神经网络的计算过程会是这样的: 把输入特征$a^{[0]}$,放入第一层并计算第一层的激活函数,用$a^{[1]}$表示,你需要$W^{[1]}$和$b^{[1]}$来计算,之后也缓存$z^{[l]}$值。之后喂到第二层,第二层里,需要用到$W^{[2]}$和$b^{[2]}$,你会需要计算第二层的激活函数$a^{[2]}$。后面几层以此类推,直到最后你算出了$a^{[L]}$,第$L$层的最终输出值$\hat y$。在这些过程里我们缓存了所有的$z$值,这就是正向传播的步骤。 对反向传播的步骤而言,我们需要算一系列的反向迭代,就是这样反向计算梯度,你需要把$da^{[L]}$的值放在这里,然后这个方块会给我们${da}^{[L-1]}$的值,以此类推,直到我们得到${da}^{[2]}$和${da}^{[1]}$,你还可以计算多一个输出值,就是${da}^{[0]}$,但这其实是你的输入特征的导数,并不重要,起码对于训练监督学习的权重不算重要,你可以止步于此。反向传播步骤中也会输出$dW^{[l]}$和$db^{[l]}$,这会输出$dW^{[3]}$和$db^{[3]}$等等。目前为止你算好了所有需要的导数,稍微填一下这个流程图。 神经网络的一步训练包含了,从$a^{[0]}$开始,也就是 $x$ 然后经过一系列正向传播计算得到$\hat y$,之后再用输出值计算这个(第二行最后方块),再实现反向传播。现在你就有所有的导数项了,$W$也会在每一层被更新为$W=W-αdW$,$b$也一样,$b=b-αdb$,反向传播就都计算完毕,我们有所有的导数值,那么这是神经网络一个梯度下降循环。 继续下去之前再补充一个细节,概念上会非常有帮助,那就是把反向函数计算出来的$z​$值缓存下来。当你做编程练习的时候去实现它时,你会发现缓存可能很方便,可以迅速得到$W^{[l]}​$和$b^{[l]}​$的值,非常方便的一个方法,在编程练习中你缓存了$z​$,还有$W​$和$b​$对吧?从实现角度上看,我认为是一个很方便的方法,可以将参数复制到你在计算反向传播时所需要的地方。好,这就是实现过程的细节,做编程练习时会用到。 现在你们见过实现深度神经网络的基本元件,在每一层中有一个正向传播步骤,以及对应的反向传播步骤,以及把信息从一步传递到另一步的缓存。下一个视频我们会讲解这些元件具体实现过程,我们来看下一个视频吧。 4.7 参数VS超参数(Parameters vs Hyperparameters) 想要你的深度神经网络起很好的效果,你还需要规划好你的参数以及超参数。 什么是超参数? 比如算法中的 learning rate $a$(学习率)、 iterations (梯度下降法循环的数量)、$L$(隐藏层数目)、${{n}^{[l]}}$(隐藏层单元数目)、 choice of activation function (激活函数的选择)都需要你来设置,这些数字实际上控制了最后的参数$W$和$b$的值,所以它们被称作超参数。 实际上深度学习有很多不同的超参数,之后我们也会介绍一些其他的超参数,如 momentum 、 mini batch size 、 regularization parameters 等等。 如何寻找超参数的最优值? 走 Idea—Code—Experiment—Idea 这个循环,尝试各种不同的参数,实现模型并观察是否成功,然后再迭代。 今天的深度学习应用领域,还是很经验性的过程,通常你有个想法,比如你可能大致知道一个最好的学习率值,可能说$a=0.01$最好,我会想先试试看,然后你可以实际试一下,训练一下看看效果如何。然后基于尝试的结果你会发现,你觉得学习率设定再提高到0.05会比较好。如果你不确定什么值是最好的,你大可以先试试一个学习率$a$,再看看损失函数J的值有没有下降。然后你可以试一试大一些的值,然后发现损失函数的值增加并发散了。然后可能试试其他数,看结果是否下降的很快或者收敛到在更高的位置。你可能尝试不同的$a$并观察损失函数$J$这么变了,试试一组值,然后可能损失函数变成这样,这个$a$值会加快学习过程,并且收敛在更低的损失函数值上(箭头标识),我就用这个$a$值了。 在前面几页中,还有很多不同的超参数。然而,当你开始开发新应用时,预先很难确切知道,究竟超参数的最优值应该是什么。所以通常,你必须尝试很多不同的值,并走这个循环,试试各种参数。试试看5个隐藏层,这个数目的隐藏单元,实现模型并观察是否成功,然后再迭代。这页的标题是,应用深度学习领域,一个很大程度基于经验的过程,凭经验的过程通俗来说,就是试直到你找到合适的数值。 另一个近来深度学习的影响是它用于解决很多问题,从计算机视觉到语音识别,到自然语言处理,到很多结构化的数据应用,比如网络广告或是网页搜索或产品推荐等等。我所看到过的就有很多其中一个领域的研究员,这些领域中的一个,尝试了不同的设置,有时候这种设置超参数的直觉可以推广,但有时又不会。所以我经常建议人们,特别是刚开始应用于新问题的人们,去试一定范围的值看看结果如何。然后下一门课程,我们会用更系统的方法,用系统性的尝试各种超参数取值。然后其次,甚至是你已经用了很久的模型,可能你在做网络广告应用,在你开发途中,很有可能学习率的最优数值或是其他超参数的最优值是会变的,所以即使你每天都在用当前最优的参数调试你的系统,你还是会发现,最优值过一年就会变化,因为电脑的基础设施, CPU 或是 GPU 可能会变化很大。所以有一条经验规律可能每几个月就会变。如果你所解决的问题需要很多年时间,只要经常试试不同的超参数,勤于检验结果,看看有没有更好的超参数数值,相信你慢慢会得到设定超参数的直觉,知道你的问题最好用什么数值。 这可能的确是深度学习比较让人不满的一部分,也就是你必须尝试很多次不同可能性。但参数设定这个领域,深度学习研究还在进步中,所以可能过段时间就会有更好的方法决定超参数的值,也很有可能由于 CPU 、 GPU 、网络和数据都在变化,这样的指南可能只会在一段时间内起作用,只要你不断尝试,并且尝试保留交叉检验或类似的检验方法,然后挑一个对你的问题效果比较好的数值。 近来受深度学习影响,很多领域发生了变化,从计算机视觉到语音识别到自然语言处理到很多结构化的数据应用,比如网络广告、网页搜索、产品推荐等等;有些同一领域设置超参数的直觉可以推广,但有时又不可以,特别是那些刚开始研究新问题的人们应该去尝试一定范围内的结果如何,甚至那些用了很久的模型得学习率或是其他超参数的最优值也有可能会改变。 在下个课程我们会用系统性的方法去尝试各种超参数的取值。有一条经验规律:经常试试不同的超参数,勤于检查结果,看看有没有更好的超参数取值,你将会得到设定超参数的直觉。 4.8 深度学习和大脑的关联性(What does this have to do with the brain?) 深度学习和大脑有什么关联性吗? 关联不大。 那么人们为什么会说深度学习和大脑相关呢? 当你在实现一个神经网络的时候,那些公式是你在做的东西,你会做前向传播、反向传播、梯度下降法,其实很难表述这些公式具体做了什么,深度学习像大脑这样的类比其实是过度简化了我们的大脑具体在做什么,但因为这种形式很简洁,也能让普通人更愿意公开讨论,也方便新闻报道并且吸引大众眼球,但这个类比是非常不准确的。 一个神经网络的逻辑单元可以看成是对一个生物神经元的过度简化,但迄今为止连神经科学家都很难解释究竟一个神经元能做什么,它可能是极其复杂的;它的一些功能可能真的类似 logistic 回归的运算,但单个神经元到底在做什么目前还没有人能够真正可以解释。 深度学习的确是个很好的工具来学习各种很灵活很复杂的函数,学习到从$x$到$y$的映射,在监督学习中学到输入到输出的映射。 但这个类比还是很粗略的,这是一个 logistic 回归单元的 sigmoid 激活函数,这里是一个大脑中的神经元,图中这个生物神经元,也是你大脑中的一个细胞,它能接受来自其他神经元的电信号,比如$x_1,x_2,x_3$,或可能来自于其他神经元$a_1,a_2,a_3$ 。其中有一个简单的临界计算值,如果这个神经元突然激发了,它会让电脉冲沿着这条长长的轴突,或者说一条导线传到另一个神经元。 所以这是一个过度简化的对比,把一个神经网络的逻辑单元和右边的生物神经元对比。至今为止其实连神经科学家们都很难解释,究竟一个神经元能做什么。一个小小的神经元其实却是极其复杂的,以至于我们无法在神经科学的角度描述清楚,它的一些功能,可能真的是类似 logistic 回归的运算,但单个神经元到底在做什么,目前还没有人能够真正解释,大脑中的神经元是怎么学习的,至今这仍是一个谜之过程。到底大脑是用类似于后向传播或是梯度下降的算法,或者人类大脑的学习过程用的是完全不同的原理。 所以虽然深度学习的确是个很好的工具,能学习到各种很灵活很复杂的函数来学到从x到y的映射。在监督学习中,学到输入到输出的映射,但这种和人类大脑的类比,在这个领域的早期也许值得一提。但现在这种类比已经逐渐过时了,我自己也在尽量少用这样的说法。 这就是神经网络和大脑的关系,我相信在计算机视觉,或其他的学科都曾受人类大脑启发,还有其他深度学习的领域也曾受人类大脑启发。但是个人来讲我用这个人类大脑类比的次数逐渐减少了。