【4.1.2】神经网络基础

一、二分类变量

当你要构建一个神经网络,有些技巧是相当重要的。例如,m个样本的训练集,你可能会习惯性的去用一个for循环来遍历这m个样本,但事实上,实现一个神经网络,如果你要遍历整个数据集,并不需要直接使用for循环。还有就是,神经网络的计算过程中中,通常有一个正向过程,或者正向传播步骤,接着会有一个反向过程,也叫反向传播步骤。

二分分类案例

使用logistic回归来阐述,以便能更好的理解。 logistic回归是一个用于二分分类的算法。例如举个例子,输入一张图像,我们要输入y,是猫(1)或者不为猫(0)。计算机保存一张图片,要保存三个独立矩阵,分别对应图片中的红绿蓝三个颜色通道,如果输入图片是64*64像素的,就有三个64*64的矩阵,分别对应图片中的红绿蓝三个像素的亮度。为了方便表示,这里用三个小矩阵5*4的来表示,要把这些像素亮度值放进一个特征向量中,就要把这些像素值都提出来,放入一个特征向量x。为了把这些像素值取出来放入特征向量,就要像下面一样定义一个特征向量x以表示这张图片,我们把所有的像素值都取出来,把三层矩阵上的所有数都放入一个向量中,最后得到一个很长的特征向量,把图片中所有的红绿蓝像素强度值都列出来。

如果图片是64*64的,那么向量x的总维度为64*64*3,因为这是三个矩阵的元素数量,乘出来结果为12288,我们用Nx=12288来表示输入特征向量x的维度,有时候为了简洁,我们直接用小写的n,来表示特征向量的维度。

在二分类问题,结果是一个离散值。

例如:

  • 帐户黑客攻击(1)或妥协(0)
  • 恶性肿瘤(1)或良性肿瘤(0)

例如: 判断是否为猫

使用logistic回归来阐述,以便能更好的理解。 logistic回归是一个用于二分分类的算法。例如举个例子,输入一张图像,我们要输入y,是猫(1)或者不为猫(0)。计算机保存一张图片,要保存三个独立矩阵,分别对应图片中的红绿蓝三个颜色通道,如果输入图片是64*64像素的,就有三个64*64的矩阵,分别对应图片中的红绿蓝三个像素的亮度。为了方便表示,这里用三个小矩阵5*4的来表示,要把这些像素亮度值放进一个特征向量中,就要把这些像素值都提出来,放入一个特征向量x。为了把这些像素值取出来放入特征向量,就要像下面一样定义一个特征向量x以表示这张图片,我们把所有的像素值都取出来,把三层矩阵上的所有数都放入一个向量中,最后得到一个很长的特征向量,把图片中所有的红绿蓝像素强度值都列出来。

如果图片是64*64的,那么向量x的总维度为64*64*3,因为这是三个矩阵的元素数量,乘出来结果为12288,我们用Nx=12288来表示输入特征向量x的维度,有时候为了简洁,我们直接用小写的n,来表示特征向量的维度。

由于图片在电脑中是通过 红绿蓝三色矩阵表示的。64*64像素的一张图片,输入x的向量 nx = 64*64*3 = 12288

相关符号

后面课程中需要用到的符号。

  • 用一对(x,y)来表示一个独立的样本,x是Nx维的特征向量,标签y: 值为1或0。
  • 训练集由m个训练样本构成,$ ( x^{(1)},y^{(1)} ) $ 表示样本一的输入和输出,$ ( x^{(2)},y^{(2)} ) $ 表示样本二的输入和输出,$ ( x^{(m)},y^{(m)} ) $ 表示最后一个样本m的输入和输出,$ { ( x^{(1)},y^{(1)} ) .... ( x^{(m)},y^{(m)} )}$)}这些一起就表示整个训练集。
  • m表示整个训练集的样本数,有时候为了强调这是训练样本的个数,可以写作m=m_train,当说到测试集时,可以用m_test来表示测试集的样本数。
  • 最后用更紧凑的符号表示训练集,定义一个矩阵,用大写X来表示,它由训练集中的x1,x2这些组成,像这样写成矩阵的列。每个训练集x^(1)..x^(m)则分别为这个矩阵的1到m列,所以这个矩阵有m列,m是训练集的样本数,这个矩阵的高度记为 Nx。要注意的是,有时候X的定义,训练数据作为行向量堆叠,而不是这样的列向量堆叠。
  • 但是构建神经网络时,用列向量堆叠这个约定形式,会让构建过程简单的多。

总结一下,X是一个 Nx*m 的矩阵,当用python实现时,会看到X.shape,这是一条python命令,用来输出矩阵的维度,即(nx,m),表示X是一个nx*m的矩阵,这就是如何将训练样本,即输入x用矩阵表示,那输出标签y呢,同样为了简单的构建一个神经网络,将y标签也放入列中,Y = [y^(1),y^(2),…y^(m)],这里的Y是一个1*m的矩阵,同样的,在python里面,Y.shape等于(1,m)。后面的课程中你会发现,好的习惯符号能够将不同训练样本的数据联系起来,这里说的数据,不仅有x和y还会有其他的量。将不同的训练样本数据取出来,放到不同的列上,就像刚才处理x和y一样。在logistic回归和神经网络,要用到的符号就是这些了。

二、logistc回归

这是一个学习算法,用在监督学习中,输出y标签是0或1时,这是一个二分分类的问题。已知输入特征向量是x,可能是一张图,你希望把识别出这是不是猫图,你需要一个算法,可以给出一个预测值 $ \hat{y}$ ,就是你对y的预测。更正式的说,你希望$ \hat{y}$是一个概率,当输入特征满足条件时,y就是1,所以换句话说,如果x是图片,例如之前判读图片是否有猫的例子,你希望y是一张图片是猫图的概率,x是一个nx维的向量,已知logistic回归的参数是w,也是一个nx维向量,而b就是一个实数,所以已知输入x和参数w和b,如何计算$ \hat{y}$

也许可以尝试,y = w^T * x + b, 一个输入x的线性函数,事实上,如果你做线性回归,就是这么算的,但这并不是一个非常好的二元分类算法,因为你希望$ \hat{y}$ 是y=1的概率,而不是计算y的值,所以$ \hat{y}$应该介于0到1之间,但实际上很难实现,因为w^T*x+b可能比1大得多,甚至是负值,这样的概率是没有意义的。所以在logistc回归中,我们把输出变成 $ \hat{y}$ 等于sigmoid函数作用到这个量y上,这就是sigmoid函数的图形,横轴是Z,函数是从0到1的光滑函数,与数轴的交点在0.5处,这就是sigmoid(z)的图形,用z来表示w^T * x + b这个量。

$$ s = σ(w^{T}x + b) = σ(z) = \frac {1}{ 1+ e^{-z}} $$

要注意的是,如果z非常大,那么e^(-z)就很接近0,那么sigmoid(z)就是1/(1+某个接近0的量),所以这就接近1。相反如果z非常小,或者是非常大的负数,那么sigmoid(z)就很接近于0。

所以当你要实现logistic回归时,你要做的是学习参数w和b,这样 $ \hat{y}$ 就变成了比较好的估计。对y=1的比较好的估计。当我们对神经网络编程时,我们通常会把w和b分开,这里b对应一个拦截器,在其他课程中,可能看到过不同的表示。在一些符号约定中,定义一个额外的特征向量x0并且等于1,所以出现x就是R^(nx+1)维向量,然后将$ \hat{y}$定义为σ(θ^T*X),在这种符号定义中,出现了一个向量参数θ,这个θ是由向量[θ0,θ1,θ2…θnx]组成的列向量。所以θ0就是扮演的b的角色,这是一个实数,而θ1直到θnx的作用和w一样。事实上,当你实现你的神经网络时,将b和w看成独立的参数可能更好。

三、logistc回归损失函数

为了训练logistic回归模型的参数w和b,需要定义一个成本函数。为了让模型通过学习调整参数要给一个m个样本的训练集,通过训练集,找到参数b和w,来得到你的输出,在训练集上得到预测值,是预测值$ \hat{y}$更接近于训练集得到的y^(i)。

为了让上面的方程更详细一点,需要说明上面这里定义的$ \hat{y}$(即y头上有个^这个符号),是对一个训练样本x来说的,当然是对每个训练样本,我们使用这些带有圆角的符号方便引用说明,来区分样本。你的训练样本(i),对应的预测值为通过sigmoid函数获得的 $ \hat{y^{i}}$ , 通过函数作用到 W^T*x(i)+b 得到的,所以符号约定就是这个上标(i)来指明数据。

loss function 损失函数, 也叫作误差函数,他们可以用来衡量算法的运行情况,你可以定义损失为$ \hat{y}$和真的类标y的差平方的一半,但通常在logistic回归中,大家都不这么做,因为当你学习这些参数时候,会发现优化问题会变成非凸的,最后会得到很多个局部最优解,所以梯度下降法(gradient descent)可能找不到全局最优值。我们通过定义损失函数L来衡量你的预测输出值$ \hat{y}$和y的实际值有多接近。误差平方看起来似乎是一个合理的选择,但是用这个的话,梯度下降法就不会很有效。在logistic回归中,我们会定义一个不同的损失函数,它有着与误差平方相似的作用,这会给我们一个凸的优化问题,就很容易去做优化。在logistic回归中,我们用到的损失函数如下:

直观地看看为什么这个损失函数能起作用,假设我们使用误差平方越小越好,对于这个logistic回归的损失函数,同样的我们也让它尽可能的小。

  • 当y=1时,我们要使 $ \hat{y}$ 尽可能大,但这是由sigmoid函数估计出来的值,不会超过1,所以要尽可能接近1
  • 当y=0时,要使 $ \hat{y}$ 尽可能小,但是不会小于0,所以尽可能接近0。

损失函数是在单个训练样本中定义的,它衡量了在单个训练样本中的表现。接下来要定义一个成本函数cost function ,它衡量的是在全体训练样本上的表现,这个成本函数J根据之前得到的两个参数w和b,J(w,b) 即所有训练样本的损失函数和。而$ \hat{y}$是用一组特定的参数w,b通过logistc回归算法得出的预测输出值。从而成本函数把L带入得到以下形式:

损失函数只适用于像这样的单个训练样本,而成本函数基于参数的总成本,所以在训练logistic回归模型时,我们要找到合适的参数w和b,让这里的成本函数J尽可能的小。

综上所述,可以得到logistic回归可以被看成一个非常小的神经网络。

四、梯度下降法

如何使用梯度下降法来训练或学习训练集上的参数w和b,成本函数只能用来说明你选择的w和b作用效果好不好,最后误差大不大,参数w和b在训练集上的效果,但是我们不知道一个w和b的关系,如何取值,发现一组不好后如何确定下一组取值,这就需要梯度下降法来研究如何训练w和b使得成本函数最小。

这个图中的横轴表示空间参数w和b,在实践中,w可以是更高维的,但为了方便绘图,我们让w是一个实数,b也是一个实数,成本函数J(w,b)是在水平轴上w和b上的曲面,曲面的高度J(w,b)在某一点的值,我们想要做的就是找到这样的w和b使其对应的成本函数J值,是最小值。 可以看出,成本函数J是一个凸函数,和非凸的函数不一样,非凸函数有很多的局部最优,因此,我们使用为凸函数的J(w,b),凸函数的这个性质是我们为啥使用这个特定的成本函数J做logistic回归一个很重要的原因。为了找到更好的参数值,我们要做的就是用某初始值,初始化w和b。对于logistic回归而言,几乎是任意的初始化方法都有效,通常用0来初始化,但人们一般不这么做,但是因为函数是凸的,无论在哪里初始化,都应该到达到同一点,或大致相同的点。梯度下降法所做的就是,从初始点开始,朝最抖的下坡方向走一步,在梯度下降一步后,也许就会停在碗的最低端,因为它试着沿着最快下降的方向往下走,这是梯度下降的一次迭代。两次迭代可能就到了最低点,或者需要更多次,隐藏在图上的曲线中,很有希望收敛到这个全局最优解,或接近全局最优解,这张图片阐述了梯度下降法。

为了更好的说明如何学习w和b,我们将图像的横轴只有w,变成二维坐标轴,图像如左图,右图的公式代表w如何变化的公式。 α表示学习率,学习率可以控制每一次迭代,或者梯度下降法中的步长,后面会讨论,如何选择学习率α;其次d(J(w))/dw这个数是导数(derivative),这就是对参数w的更新或者说变化量。

当我们开始编写代码,来实现梯度下降,我们会使用到代码中变量名的约定,dw用来表示导数,作为导数的变量名,那么w:=w-α*dw(:=代表变化取值),现在我们确保梯度下降法中更新是有用的。

记住导数的定义,是函数在这个点的斜率。对于一开始就很大的参数w来说,每更新一次就会向左移动,向最小值点更靠近,同样的,假设w很小,在最小值的左边,那么斜率为负值,每次迭代就是w加上一个数,也会逐步的向最小值的w0靠近。

我们想知道,用目前参数的情况下函数的斜率朝下降速度最快的方向走。我们知道,为了让成本函数J走下坡路,下一步更新的方向在哪。当前J(w)的梯度下降法只有参数w,在logistic回归中,你的成本函数是一个含有w和b的函数,在这种情况下,梯度下降的内循环就是这里的这个东西,你必须重复的计算,通过w:=w-α*dw【d(j(w,b)/dw)】更新w,通过b:=b-α*d(J(w,b))/db更新b。这两个公式是实际更新w和b时所作的操作。当然这里的符号d微分也可以是偏导数花哨的α,表示的是函数在w方向的斜率是多小,当函数有两个以上的变量时,应该使用偏导数符号,计算函数关于其中一个变量的在对应点所对应的斜率。

五、倒数

将倒数理解为斜率

六、更多倒数的例子

这两节主要就是关于微积分,导数的内容,通过举例f(x)=3x, f(x)=x^2, f(x)=x^3来说明函数上某一点的导数其实就是在该点的斜率,直线上点的斜率处处相等,但是对于其他非直线的函数,每个点的斜率即导数可能都不相同。 对于f(x)=x^2, f(x)=2x, 当x=2时,f(2)=4, 将x向后移动一点点,比如x=2.001,对应的f(x)=8.004002 约为8.004,发现f(x)增量与x的增量相比,结果为8.004/2.001=4=2*2,这里f(x)我们只是近似的等于4倍,实际上,按照导数的定义,是在x点出,增加一个无穷小量,0.001的增量很明显不能表示无穷小,因此在该点处的切线的斜率就为2x。

七、计算图

可以说,一个神经网络的计算都是按照前向或者反向传播过程来实现的,首先计算出,神经网络的输出,紧接着进行一个反向传播过程,后者我们用来计算出对应的梯度或者导数,以下流程图解释了,为什么要用这样的方式实现。 为了阐明这个计算过程,举一个比logistc回归更加简单的、不那么正式的神经网络的例子。我们尝试计算函数J,它是关于三个变量a,b,c的函数 J(a,b,c) = 3(a+b*c) 计算这个函数,实际上有三个不同的步骤:

  1. 计算b*c,存在在变量u中,u=b*c
  2. 计算a+u,存在变量v中,v=a+u
  3. 最后输出J,J=3*v 我们可以把这个计算过程划成如下的流程图:

这种流程图用起来很方便,有不同的或者一些特殊的输出变量时,比如我们想要优化的J。在logistc回归中,J是想要最小化的成本函数,可以看出,通过一个从左到右的过程,你可以计算出J的值,计算导数就是一个从右到左的过程,刚好与从左到右传播的过程相反。

概括一下:计算图组织计算的形式是用蓝色箭头从左到右的计算,让我们看看下一个视频中如何进行反向红色箭头(也就是从右到左)的导数计算,让我们继续下一个视频的学习。

八、使用计算图求导数(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,你可能需要优化的函数,然后反向从右到左计算导数。如果你不熟悉微积分或链式法则,我知道这里有些细节讲的很快,但如果你没有跟上所有细节,也不用怕。在下一个视频中,我会再过一遍。在逻辑回归的背景下过一遍,并给你介绍需要做什么才能编写代码,实现逻辑回归模型中的导数计算。

九、逻辑回归中的梯度下降(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$个训练样本的整个训练集。因此在下一节视频中,我们将这些思想应用到整个训练样本集中,而不仅仅只是单个样本上。

十、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循环。

十一、向量化(Vectorization)

向量化是非常基础的去除代码中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循环。

以下代码及运行结果截图:

十二 向量化的更多例子(More Examples of Vectorization)

从上节视频中,你知道了怎样通过numpy内置函数和避开显式的循环(loop)的方式进行向量化,从而有效提高代码速度。

经验提醒我,当我们在写神经网络程序时,或者在写逻辑(logistic)回归,或者其他神经网络模型时,应该避免写循环(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() 计算元素$y$中的最大值,你也可以 np.maximum(v,0) 、 $v2$代表获得元素 $y$个值得平方、 $\frac{1}{v}$ 获取元素 $y$的倒数等等。所以当你想写循环时候,检查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 循环,你也可以写出代码去运行整个训练集。到此为止一切都好,让我们看下一个视频。

十三、向量化逻辑回归(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$值。接下来,可以证明,你也可以利用向量化高效地计算反向传播并以此来计算梯度。让我们在下一个视频中看该如何实现。

十四、向量化 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}Xdz^{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}Xdz^{T}$。这样,我们就避免了在训练集上使用for循环。

现在,让我们回顾一下,看看我们之前怎么实现的逻辑回归,可以发现,没有向量化是非常低效的,如下图所示代码:

我们的目标是不使用for循环,而是向量,我们可以这么做:

$Z = w^{T}X + b = np.dot( w.T,X)+b$

$A = \sigma( Z )$

$dZ = A - Y$

${{dw} = \frac{1}{m}Xdz^{T}\ }$

$db= \frac{1}{m}*np.sum( dZ)​$

$w: = w - a*dw$

`$b: = b - a*db$

现在我们利用前五个公式完成了前向和后向传播,也实现了对所有训练样本进行预测和求导,再利用后两个公式,梯度下降更新参数。我们的目的是不使用for循环,所以我们就通过一次迭代实现一次梯度下降,但如果你希望多次迭代进行梯度下降,那么仍然需要for循环,放在最外层。不过我们还是觉得一次迭代就进行一次梯度下降,避免使用任何循环比较舒服一些。

最后,我们得到了一个高度向量化的、非常高效的逻辑回归的梯度下降算法,我们将在下次视频中讨论Python中的Broadcasting技术。

十五、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,可以看看下面的图:

十六、关于 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.shape)$,$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,从而使你更容易地完成练习。

参考资料

$ \hat{y}$

个人公众号,比较懒,很少更新,可以在上面提问题,如果回复不及时,可发邮件给我: tiehan@sina.cn

Sam avatar
About Sam
专注生物信息 专注转化医学