神经网络和深度学习-第二周-吴恩达 Deep Learning
二元分类(Binary Classification)
二元分类是经典的分类问题,在程序实现中希望算法可以决定某件事物的结果是或者不是某两种状态中的一种。为了计算的方便通常将是用 1 来表示,而不是用 0 来表示,例如:
- 明天天气是晴还是阴
- 某张图片里包含还是不包含猫
- 摄像头所捕获的脸是还是不是某个要追踪的对象
Logistic Regression
Logistic 回归是经典的二元分类方法,它的实现过程可以比较全面的展示神经网络的构建过程,因此基本上是所有神经网络课程的第一讲,其可以解决的经典问题类似于:
-
体重每增加一公斤以及吸烟数量的增加是否提高了患者罹患肺癌的风险?
-
日常饮食的能量摄入、脂肪摄入、性别、年龄是否影响心脏病的发病率?
课程中用到的相关参数说明如下:
-
x 代表一个特征向量,本课程中默认为列向量,每一个特征向量中有 nx 个特征
-
y 代表一个训练样本的已知取值,在二元分类问题中只有0,1两个选择
-
输入的权重(weights)w 也是一个 nx 维列向量
-
b 为一个实数,m 为训练样本集中的样本数量,i 为第 i 个训练样本
Logistic 回归的实现过程是先将输入特征进行线性求和 z = wTx + b,由于这一求和的结果的值域属于 [−∞, +∞] ,为了使得最终的输出结果的值域处于 [0,1] 之间,并依据此判断输出为 1 还是 0,需要将线性求和的结果传递给一个激活函数(Activation Function)。本节课中使用的是 Sigmoid 函数,其数学描述为:σ(z) = 1 / (1 + e-z) 。进而对应每一个特征向量 x,函数的输出结果为:
- ŷ = σ(z) = σ(wTx + b),为了表示方便,后文常用 a 代替 ŷ , 其中 a 取自 activation
激活函数和 Sigmoid 函数
神经网络中常用的激活函数有很多种,需要根据实际情况进行选取。之所以需要引入激活函数,是因为这一类函数可以为神经网络提供非线性的建模能力。因为即便在一个多层的神经网络中,如果各层之间的结果都是靠线性关系传播,那么可以通过合并同类项将其整理为单层的线性模型,而这样的神经网络明显不能对复杂的函数关系进行描述,具体证明可以参见 Shuaib Ahmed 的回答。
Sigmoid 函数的图像如下,它非常适合 Logistic Regression 的一个重要原因是其可以将前面所说的由线性求和得到的结果完美的映射到 (0,1) 之间。
sigmoid(z)损失函数(Loss function)、误差函数(Error function)和成本函数(Cost Function)
在神经网络的训练中,我们需要定义一个损失函数,也称误差函数,用于衡量针对每一个训练样本得到的预期输出和实际输出间的误差(error)。对于整体训练样本集而言,则可以定义一个成本函数。由于在神经网络构建过程中会先将已有的特征信息作为网络的输入,此时决定误差唯一影响因素就是分布在网络各个层中的权重参数。对应不同的参数选择,预测结果和实际标签值之间的差异不同,而对神经网络训练的目的就是要找到合适的 w 和 b 使得这个成本函数取得最小值。与激活函数类似,损失函数和成本函数也可以有多种定义方式,本课中 Andrew 使用的是下面这两个函数,原因请见交叉熵和最大似然估计。
-
损失函数: L(ŷ, y) = L(a, y) = - (ylog(a) + (1 - y) log(1 - a)),注意这里的 log 的底数为自然基底 e,但由于在很多计算工具中都将 ln 的实现方法定义为 log,所以公式和课堂上采用的是 log
-
成本函数:样本集中全部 m 个样本的损失函数的均值
J = (1 / m)ΣL(ŷ(i), y(i)) = (1 / m)Σ(-y(i)logŷ(i) - (1 - y(i))log(1 - ŷ(i))),其中 i = 1, 2, 3, ... , m,m 为样本的数量
之所以称之为误差函数和成本函数在我看来因为在实际计算中,我们都是采用一个近似的模型来模拟世界,因此不可能完全准确,因此合理定义的误差函数和成本函数对这个误差提供了一个有效的定量描述,我们所能接受的最大的错误成本就是我们希望算法找到的成本函数的最小值。
梯度下降(Gradient Descent)
这一部分需要用到较多的导数的知识,Andrew 在后面课堂里专门进行了讲解,这里仅做一个简单的铺垫。如果实在不明白的话可以先试着跟着课堂的节奏走下去,或者以尽量少的时间将相应的知识了解到一个自己觉得有信心继续学习的程度,不要过分扩大学习范围,比如去读整本的微积分教材。如果想快速的理解微积分,可以阅读这个我个人最喜欢的博客 BetterExplained ,或者 Khan Academy 关于偏导数和梯度的讲解 可以给你提供更多关于微积分的直观解释。
一个简单的例子:针对一元函数(只有一个自变量 x ) 如 y = x2,由于 y 作为因变量会随着 x 的值的变化而变化,并且对于不同的 x 的取值,y 值的变化速率不同,体现在图形上也就是曲线不同点的“陡峭”程度不同,对应数学上的描述则是不同 x 点的导数不同,或者说切线的“斜率”不同。
例如在 x = 3 这一点 y = 9,而如果对 x 做一个小小的增加或减少,如变为 3.001 则 y 值会变为 9.006,对应 y 的值的变动为 0.006。如果将 y 的变动值和 x 的变动做一个除法,即用 0.006 / 0.001 则可以得到 x = 3 这一点取值变动而造成的 y 值变动的变化率为 6,正好为 3 的两倍,而这不是一个巧合,即如果经过严肃的数学证明可以得到 y = x2 在任意一点的导数均为 2x,对应 x = 3 这一点的导数为 6。
而对于如我们前面所定义的 J(w, b) 这个成本函数,由于其有多个自变量,且自变量的变化可以沿着不同的方向来进行。注意这里面 w 可以有多个维度,但由于普通人只能直观的理解三维的图像,因此假定 w 是一维的,则可以将 J 表示成两个变量 w 和 b 的函数,对应的图形如下:
Illustration of J as a function of single dimension w and b从图中可以看出 J 不仅可以通过单独改变 w 或 b 的值而取得不同的值,也可以沿着和两个坐标轴成一定夹角的方向取得不同的值(w 和 b 同时变动),对应单个和多个自变量变化而造成的 J 的取值变化速度的数学描述就是“偏导数”和“方向导数”。由于我们需要寻找的是使得 J 能够尽快取得最小值所对应的 w 和 b,而为了尽快找到这一点,我们不会漫无目的的让计算机随机遍历无数个 w 和 b,而是要通过寻求一种算法来快速实现这一目的。
想象我们自己站在一座山上,我们能够看到的达到山底的最短的路径就是希望算法所走的路径(图中红色箭头的路径)。对应的实现过程如下:在算法初始的时候我们会赋予 w 和 b 一个初始值,并使得算法从初始位置朝着在该点看来“最陡”的下坡方向走一小步,对应这个“最陡”的方向的数学描述就是“梯度”,梯度的方向是方向导数取得最大值的方向,梯度的值是方向导数的最大值。相应的利用梯度来更新权重参数进而找到函数最小值点的算法实现过程就是“梯度下降”过程,其核心就在于求取误差函数的偏导数。从初始点开始,每沿梯度方向前进一步就会在此基础上更新 w 和 b 的值,再朝着新的 w 和 b 的梯度的方向前进一个梯度的距离。
这里需要注意的是,在梯度的基础上,每前进一步的过程里还可以加一个控制前进速度的参数 α ,称为学习速率(learning rate)。 对于如何选择这个参数,后续会专门做介绍。还有一点就是由于成本函数可以有多种选择,为了在梯度下降过程中函数可以达到总体的最小值,所以最好选择凸函数(convex function)做为目标函数,而不是有多个局部极值点的函数,后者容易在计算中取得局部最小值而非整体的最小值。
计算图(Computational Graph)
对神经网络中各个节点的 w 和 b 的计算更新过程中体现了神经网络的训练过程:先通过赋予参数 w 和 b 一个初始值,并计算总体样本集的成本函数(前向传播,forward propagation),再借助成本函数来计算相关参数的梯度,并用这个梯度来指导参数更新(后向传播,backward propagation),再循环往复。为了更清楚的展现这个过程,聪明的计算机科学家们使用计算图来可视化这个过程。
Logistic Regression中的梯度下降 - 单个样本
为方便计算,假设 nx = 2 , 则经计算图辅助计算可知模型中所需要的导数值及其计算公式如下:
Computation chart- dz = a - y
- dw1 = x1dz
- dw2 = x2dz
- db = dz
在上述导数计算中,由于是单个训练样本,因此最后一个函数为损失函数,而非成本函数。同时,之所以要将公式整理成 a 和 y 的函数是因为在前向传播计算中这些值都已经得到,因此可以直接代入而最终求得相关的导数值。在此基础上,考虑学习速率 α 后,程序实现中参数的更新过程则为:
- w1 = w1 - αdw1
- w2 = w2 - αdw2
- b = b - αdb
Logistic Regression中的梯度下降 - m 个样本
当训练样本集中有 m 个样本时,需要在初始值 J = 0,dw1 = 0, dw2 = 0, db = 0 的基础上对于每个样本计算 dw1 , dw2 , db 并最终取均值,将均值的结果代入前述的参数更新过程:
- w1 = w1 - αdw1
- w2 = w2 - αdw2
- b = b - αdb
以上的步骤实际上只完成了梯度下降的一次参数更新,实际计算中这一参数更新过程需要重复多次,直到最终 J 的计算满足条件,所以幸好我们有计算机来完成这一重复任务,否则就只能望洋兴叹了。
向量化(Vectorization)
当训练特征和训练样本集非常多的时候,采用显示的 for
循环遍历就显得过于复杂,算法的性能也因此下降,因此如 Andrew 所言在“大数据”的背景下,数据的向量化就不再是一个可有可无的操作,而是整个算法实现中重要的一环。
尽管这又是一个新的术语,但无需紧张,在具体的算法实现中,可以通过 Numpy 这个库,借助其中的 n 维数组这个数据结构来实现向量化。在数据科学领域惯常的做法是用 np 来重新命名 Numpy 库,即 import numpy as np
,后续就可以通过np.some_method()
的形式使用 Numpy 中的方法。假定原有的输入为一个包含 1,000,000 个元素的列表 long_list,通过 Numpy 来实现向量化只需要将其传递给 np.array() 这个函数即可:vectorized_list = np.array(long_list)
。
在神经网络的训练中最重要的一个处理就是将输入样本集 X,样本集标签 y,w 和 dw 向量化,对于样本集和标签的向量化只需要将对应的数据传递给 np.array() 即可,而 w 和 dw 的维数则需要根据输入特征的数量 nx 来确定,即 w = np.zeros((nx, 1))
,dw = np.zeros((nx, 1))
,相应的对于单一的训练样本 z = wTx + b 在代码中则为 z = np.dot(w.T, x) + b
,而整个训练样本集则为 Z = np.dot(w.T, X) + b
,注意 Z 和 X 为大写时表示相应向量构成的矩阵,在这里 b 作为一个实数可以参与矩阵计算是因为 Numpy 默认内置了 Broadcasting 功能。
这一部分如果只看视频的话过于抽象,需要通过编程练习才能真正理解,如果没有时间做练习,只需要记住一句话:
Whenever possible, avoid using explicit for-loops in neural network programming.
Jupyter Notebook
在讲课中 Andrew 用到了 Jupyter Notebook,为了可以更方便的使用这个工具,这里推荐安装 Anaconda ——这是一个集成了众多数据科学计算工具的Python开发环境,具体的搭建及操作可以参考 Anaconda入门指南 以及 Jupyter入门指南。