深度学习所使用的神经网络基础技术理念已经存在几十年了,为什么最近这些年才流行起来?通过一个图来了解一下。
下图的水平轴是任务的数据量,垂直轴上是机器学习算法的性能。比如准确率体现在垃圾邮件过滤或者广告点击预测,或者是神经网络在自动驾驶汽车时判断位置的准确性。
如果把一个传统机器学习算法的性能画出来,作为数据量的一个函数,可能得到一个弯曲的线,就像图中红色的线。它的性能一开始随着更多数据时会上升,但是一段变化后它的性能就会像一个天花板一样。假设水平轴拉的很长很长,它们不知道如何处理规模巨大的数据,而过去十年的社会里,遇到的很多问题只有相对较少的数据量。
随着数字化社会的发展,需要解决的问题中数据量都非常巨大。比如在电脑网站上、在手机软件上以及其它数字化的服务,它们都能创建数据,同时便宜的相机被配置到移动电话,还有加速仪及各类各样的传感器,同时在物联网领域也收集到了越来越多的数据。仅仅在过去的20年里对于很多应用,便收集到了大量的数据,远超过机器学习算法能够高效发挥它们优势的规模。
神经网络展现出的是,如果训练一个小型的神经网络,那么性能可能会像图中黄色曲线表示那样;如果训练一个中等规模的神经网络(蓝色曲线),它在某些数据上面的性能会更好一些;如果训练一个非常大的神经网络,就会变成下图绿色曲线那样,并且保持变得越来越好。
因此如果想要获得较高的性能体现,有两个条件要完成:
* **需要训练一个规模足够大的神经网络,以发挥数据规模量巨大的优点;**
* **需要很多的数据,以便把算法的性能提升到图中 _x_ 轴右边位置。**
大家经常说规模一直在推动深度学习的进步,这里的“规模”同时指神经网络的规模。需要带有许多隐藏单元的神经网络,也有许多的参数及关联性,就如同需要大规模的数据一样。
**如今在神经网络上获得更好性能的最可靠方法,是训练一个更大的神经网络,投入更多的数据。** 但这只能在一定程度上起作用,因为最终你将耗尽数据,或者最终你的网络是如此大规模导致将要用太久的时间去训练。仅仅提升规模这一步的的确确地让科学界在深度学习的世界中摸索了很多时间。
**当在面临小的训练集时,各种算法的优先级事实上定义的不是很明确。** 所以如果你没有大量的训练集,算法的效果非常取决于特征工程能力。假设有些人训练出了一个SVM(支持向量机)表现的更接近正确特征,然而有些人训练的规模大一些,可能在这个小的训练集中SVM算法可以做的更好。因此知道在上图区域的左边,各种算法之间的优先级并不是定义的很明确,最终的性能更取决于你在用工程选择特征方面的能力以及算法处理的细节,只是在某些大数据规模非常庞大的训练集,即在右边这个 _m_ 非常大时,我们能更加持续地看到更大的由神经网络控制的其它方法。因此如果你的朋友问为什么神经网络这么流行,我会鼓励你也替他们画这样一个图形。
**在深度学习技术发展的初期,数据规模以及计算量都局限了训练一个特别大的神经网络的能力。** 近年无论CPU还是GPU的发展,都使深度学习取得了巨大的进步。但是渐渐地,尤其是在最近这几年,我们也见证了算法方面的极大创新。许多算法方面的创新,一直在尝试着使得神经网络运行的更快。
作为一个具体的例子,神经网络方面的一个巨大突破是从sigmoid函数转换到一个ReLU函数。
使用Sigmoid函数的一个已知问题是,在函数的两端,Sigmoid函数的梯度会接近零,所以学习的速度会变得非常缓慢。
因为当实现梯度下降以及梯度接近零的时候,参数会更新的很慢,所以学习的速率也会变的很慢。
因而可以改变激活函数,比如现在常用的ReLU的函数(修正线性单元)。ReLU它的梯度对于所有输入的负值都是零,仅仅通过将Sigmod函数转换成ReLU函数,便能够使得梯度下降算法运行更快,这就是一个或许相对比较简单的算法创新的例子。
**但是根本上算法创新所带来的影响,实际上是对计算带来的优化。** 有很多这样的例子,通过改变算法,使得代码运行的更快,这也使得我们能够训练规模更大的神经网络,或者是多端口的网络。
即使我们从所有的数据中拥有了大规模的神经网络,快速计算显得更加重要的另一个原因是,训练神经网络的过程很多时候是凭借直觉的。往往你对神经网络架构有了一个想法,于是你尝试写代码实现你的想法,然后运行一个试验环境来看看这个神经网络效果有多好,通过参考这个结果再去修改神经网络里面的细节,然后不断的重复上面的操作。(如下右图)
当神经网络需要很长时间去训练(需要长时间重复上述循环),在这就有很大的区别了。当你有一个想法,动手试试效果如何,你的神经网络可能花10分钟,或花一整天时间,也可能需要一个月的时间去训练。如果可以在10分钟内或者一天内执行完,你有机会尝试更多的想法,那极有可能使得神经网络工作的更好更快。
提高速度会非常有帮助,那样你就能更快地得到实验结果。同时帮助了神经网络的实验人员在深度学习的工作中迭代的更快,也能够更快的改进想法,所有这些都使得整个深度学习的研究社群变的如此繁荣,包括令人难以置信地发明新的算法和取得不间断的进步,这些都是开拓者在做的事情,这些力量使得深度学习不断壮大。
这些力量目前不断的奏效,使得深度学习越来越好。研究表明社会仍然正在抛出越来越多的数字化数据,或者用一些特殊的硬件来进行计算,比如说GPU、TPU,以及更快的网络连接各种硬件。非常有信心,未来可以实现一个超级大规模的神经网络,而计算的能力也会进一步的得到改善,还有算法相对的学习研究社区连续不断的在算法前沿产生非凡的创新。
这一节将介绍神经网络的基础知识。在神经网络的计算中,通常包括一个前向传播(Forward Propagation)和一个反向传播(Backward Propagation)的步骤。这里会使用逻辑回归(logistic regression)来解释这些想法,以便能够更加容易地理解这些概念。
逻辑回归是一个用于二分类(Binary Classification)问题的算法。看一个二分类问题的例子,假如你有一张图片作为输入,比如这只猫,如果识别这张图片为猫,则输出标签1作为结果;如果识别出不是猫,那么输出标签0作为结果。用字母 _y_ 来表示输出的结果标签,如下图所示:
先看看一张图片在计算机中是如何表示的:
* 程序中为保存一张图片,需要三个矩阵,分别对应图片中的红、绿、蓝三种颜色通道
* 如果图片大小为64x64像素,那么就有三个64x64的矩阵,分别对应图片中红、绿、蓝三种像素的强度值
为了便于表示,下图画了3个很小的矩阵,注意它们的规模为`5 * 4 `而不是`64 * 64`,如下图所示:
为了把这些像素值放到一个特征向量中,需要把这些像素值提取出来,放入一个特征向量 _x_ 。
首先把所有的像素都取出来,如果图片的大小为64x64像素,那么向量 _x_ 的总维度,将是64乘以64乘以3,这是三个像素矩阵中像素的总量。在这个例子中它为12,288。现在我们用 _nx=12,288_ ,来表示输入特征向量的维度。有时候为了简洁,会直接用小写的 _n_ 来表示输入特征向量 _x_ 的维度。
所以在二分类问题中,目标就是得到一个分类器,以图片的特征向量作为输入,然后预测输出结果 _y_ 为1还是0,也就是预测图片中是否有猫。
在这个系列课程里使用到的一些定义如下:
* _x_ :表示一个 _nx_ 维数据,为输入数据,维度为 _(nx, 1)_ ;
* _y_ :表示输出结果,取值为 _(0, 1)_ ;
* _(x(i), y(i))_ :表示第 _i_ 组数据,可能是训练数据,也可能是测试数据,此处默认为训练数据;
* _X = [x(1), x(2), ..., x(m)]_ :表示所有的训练数据集的输入值,放在一个 _nx × m_ 的矩阵中,其中 _m_ 表示样本数目;
* _Y = [y(1), y(2), ..., y(m)]_ :对应表示所有训练数据集的输出值,维度为 _1 × m_ 。
* 一对_(x, y)_ 来表示一个单独的样本, _x_ 代表 _nx_ 维的特征向量, _y_ 表示标签(输出结果)只能为0或1。
定义训练集由 _m_ 个训练样本组成时,有:
* _(x(1),y(1))_ 表示第一个样本的输入和输出
* _(x(m),y(m))_ 表示最后一个样本
* 有时为了强调这是训练样本的个数,会写作 _Mtrain_
* 当涉及到测试集的时候,我们会使用 _Mtest_ 来表示测试集的样本数
定义一个矩阵用大写 _X_ 表示,有:
* 它由输入向量 _x(1)_ 、 _x(2)_ 、...、_x(m)_ 等组成
* 如下图放在矩阵的列中,所以现在把 _x(1)_ 作为第一列放在矩阵中, _x(2)_ 作为第二列, _x(m)_ 放到第 _m_ 列,得到了训练集矩阵 _X_ 。
* 这个矩阵有 _m_ 列, _m_ 是训练集的样本数量,矩阵的高度记为 _nx_ 。
一个好的符号定义能够将不同训练样本的数据很好地组织起来。所说的数据不仅包括 _x_ 或者 _y_ 还包括之后你会看到的其他的量。将不同的训练样本的数据提取出来,将他们堆叠在矩阵的列中,形成之后会在逻辑回归和神经网络上要用到的符号表示。如果之后忘了这些符号的意思,比如什么是 _m_ ,或者什么是 _n_ ,或者其他的,可以随时查阅这里。
[略]。这部分请参考机器学习中[逻辑回归](../machine-learning/logistic-regression.md)的章节。
向量化是非常基础的去除代码中`for`循环的艺术,在深度学习实践中,会经常发现训练大数据集,因为深度学习算法处理大数据集效果很棒,所以代码运行速度非常重要,否则如果在大数据集上,代码可能花费很长时间去运行,你将要等待非常长的时间去得到结果。
在深度学习领域,运行向量化是一个关键的技巧,举个例子说明什么是向量化。
在逻辑回归中你需要去计算 _z = wTx+b_ , _w_ 、 _x_ 都是列向量。如果你有很多的特征那么就会有一个非常大的向量,所以 _w ∈ Rnx_ , _x ∈ Rnx_ ,所以如果你想使用非向量化方法去计算 _wTx_ ,你需要用如下方式(Python):
z = 0
for i in range(n_x):
z += w[i]*x[i]
z += b
这是一个非向量化的实现,你会发现相对运行的很慢,作为一个对比,向量化实现将会非常直接计算 _wTx_ ,代码如下:
z = np.dot(w, x) + b
上面是向量化计算 _wTx_ 的方法,你将会发现向量化的写法运行速度相对会非常快。
让我们用一个小例子说明一下:
import numpy as np
import time
""" 通过round随机得到两个一百万维度的数组 """
a = np.random.rand(1000000)
b = np.random.rand(1000000)
"""现在测量一下当前时间"""
tic = time.time()
""" 向量化的版本 """
c = np.dot(a,b)
toc = time.time()
print("向量化的版本:" + str(1000*(toc-tic)) + "毫秒")
""" 非向量化的版本 """
c = 0
tic = time.time()
for i in range(1000000):
c += a[i]*b[i]
toc = time.time()
print(c)
print("非向量化的版本:" + str(1000*(toc-tic)) + "毫秒")
输入为:
向量化的版本:0.9706020355224609毫秒
非向量化的版本:305.0830364227295毫秒
在两个方法中,向量化和非向量化计算了相同的值,如你所见,向量化版本花费了0.97毫秒,非向量化版本的for循环花费了大约几乎305毫秒,非向量化版本多花费了300倍时间。在这个例子中,仅仅是向量化代码,就会运行快300倍。
此外,你可能听过很多类似如下的话,“大规模的深度学习使用了GPU(图像处理单元)实现”,而上面的例子实际上只用到了CPU。CPU和GPU都有并行化的指令,有时会叫做SIMD指令,这代表了一个单独指令多维数据。如果你使用了built-in函数,np.function或其他不要求实现循环的函数,可以让Python充分利用并行化计算。在GPU和CPU上面计算,GPU更加擅长SIMD计算,但是CPU也不差,只是没有GPU那么擅长。
经验告诉我们,当写神经网络程序时,或者逻辑回归等等程序时,应该避免写循环(loop)语句。
虽然有时写循环(loop)是不可避免的,但是我们可以使用比如`Numpy`的内置函数或者其他办法去计算。
当你这样使用后,程序效率总是快于循环(loop)。
**举一个例子**,如果你想计算向量 _u = A v_ ,矩阵乘法的定义是:_ui = ΣjAijvi_ 。
* 使用非向量化实现, _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_ 的 _v1_ , _e_ 的 _v2_ ,一直到 _e_ 的 _vn_ 次方。
* 非向量化的实现方式(如下图)
* 首先你初始化了向量 _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`是否存在类似的内置函数,从而避免使用循环方式。
下面将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看是否能简化两个计算过程中的某一步。
这是逻辑回归的求导代码,有两层循环。在这例子我们有 _n_ 个特征。
如果有超过两个特征时,需要循环 _dw1_ 、 _dw2_ 、...、 _dwn_ 等等。所以 _j_ 的实际值是1、2和...、 _nx_ ,就是你想要更新的值。
如果你想消除第二个循环,可以考虑:
* 首先定义 _dw_ 为一个向量,设置 `u = np.zeros(n(x), 1)` 。这行代码定义了一个 _x_ 行的一维向量,从而替代循环。
* 其次使用了一个向量操作 _dw += x(i)dz(i)_ 。
* 最后,采用 _dw = dw / m_ 。
现在通过将两层循环转成一层循环,仍然还有这个循环训练样本。
举一个例子来讲解Python中的广播机制。
这是不同食物(每100g)中营养成分的卡路里含量表格,列表示不同的食物种类,行表示不同的营养成分。现在想要计算不同食物中不同营养成分中的卡路里百分比。计算苹果中的碳水化合物卡路里百分比含量,首先计算苹果(100g)中三种营养成分卡路里总和 56 + 1.2 + 1.8 = 59,然后用56/59 = 94.9%算出结果。可以看出苹果中的卡路里大部分来自于碳水化合物,而牛肉则不同。
对于其他食物,计算方法类似。
* 首先,按列求和,计算每种食物中(100g)三种营养成分总和;
* 然后,分别用不同营养成分的卡路里数量除以总和,计算百分比。
那么,能否不使用`for`循环完成这样的一个计算过程呢?
假设上图的表格是一个3行4列的矩阵 _A_ ,记为 _A3×4_ ,接下来要使用Python的`Numpy`库完成这样的计算。打算使用两行代码完成,第一行代码对每一列进行求和,第二行代码分别计算每种食物每种营养成分的百分比。在`Jupyter Notebook`中输入如下代码,按`Shift+Enter`运行。
下面使用如下代码计算每列的和,可以看到输出是每种食物(100g)的卡路里总和。
其中`sum`的参数 `axis=0` 表示求和运算按列执行,之后会详细解释。
接下来计算百分比,这条指令将 _3×4_ 的矩阵 _A_ 除以一个 _1×4_ 的矩阵,得到了一个 _3×4_ 的结果矩阵,这个结果矩阵就是要计算的百分比含量。
`A.sum(axis = 0)`中的参数`axis`指明将要进行的运算是沿着哪个轴执行。在`Numpy`中:
* 0轴是垂直的,也就是列;
* 1轴是水平的,也就是行。
而第二个`A / cal.reshape(1,4)`指令则调用了`Numpy`中的广播机制。这里使用 _3×4_ 的矩阵 _A_ 除以 _1×4_ 的矩阵 _cal_ 。技术上来讲,其实并不需要再将矩阵 _cal_ 重塑(reshape) 成 _1×4_ ,因为矩阵 _cal_ 本身已经是 _1×4_ 了。但是当我们写代码时不确定矩阵维度的时候,通常会对矩阵进行重塑来确保得到我们想要的列向量或行向量。重塑操作reshape是一个常量时间的操作,时间复杂度是 _O(1)_ ,它的调用代价极低。
那么一个 _3×4_ 的矩阵是怎么和 _1×4_ 的矩阵做除法的呢?让我们来看一些更多的广播的例子。
在`Numpy`中,当一个 _4×1_ 的列向量与一个常数做加法时,实际上会将常数扩展为一个 _4×1_ 的列向量,然后两者做逐元素加法。结果就是右边的这个向量。这种广播机制对于行向量和列向量均可以使用。
再看下一个例子。
用一个 _2×3_ 的矩阵和一个 _1×3_ 的矩阵相加,其泛化形式是 _m×n_ 的矩阵和 _1×n_ 的矩阵相加。在执行加法操作时,其实是将 _1×n_ 的矩阵复制成为 _m×n_ 的矩阵,然后两者做逐元素加法得到结果。针对这个具体例子,相当于在矩阵的第一列加100,第二列加200,第三列加300。这就是在前面计算卡路里百分比的广播机制,只不过这里是除法操作(广播机制与执行的运算种类无关)。
最后一个例子:
这里相当于是一个 _m×n_ 的矩阵加上一个 _m×1_ 的矩阵。在进行运算时,会先将 _m×1_ 矩阵水平复制 _n_ 次,变成一个 _m×n_ 的矩阵,然后再执行逐元素加法。
广播机制的一般原则如下:
更多关于广播的介绍可以参考:[广播Broadcasting](https://github.com/loveunk/machine-learning-deep-learning-notes/tree/master/python/numpy#%E5%B9%BF%E6%92%AD-broadcasting)。
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_ 是 _(5,)_ 。这被称作 _a_ 的一维数组,同时这也是一个非常有趣的数据结构。它不像行向量和列向量那样表现的很一致,这也让带来一些不直观的影响。所以我建议,当你在编程练习或者在执行逻辑回归和神经网络时,你不需要使用这些一维数组。
相反,如果你每次创建一个数组,都是创建 _(5,1)_ 向量或者你让它成为一个行向量,那么向量的行为可能会更容易被理解。
* `a = np.random.randn(5, 1)`中 _a.shape_ 等同于 _(5,1)_ ,是一个列向量
* `a = np.random.randn(1, 5)`中 _a.shape_ 等同于 _(1,5)_ ,是一个行向量
所以当你需要一个向量时(column vector or row vector),绝不会是一维数组。
此外,如果我不完全确定一个向量的维度(dimension),我经常会扔进一个断言语句(assertion statement)。
像这样,去确保在这种情况下是一个 _(5,1)_ 向量,或者说是一个列向量。这些断言语句实际上是要去执行的,并且它们也会有助于为你的代码提供信息。所以不论你要做什么,不要犹豫直接插入断言语句。如果你不小心以一维数组来执行,你也能够重新改变数组维数 _a = reshape_ ,表明一个 _(5,1)_ 数组或者一个 _(1,5)_ 数组,以致于它表现更像列向量或行向量。
有时候看见学生因为一维数组不直观的影响,难以定位Bug而告终。通过在原先的代码里清除一维数组,代码变得更加简洁。而且实际上就在代码中表现的事情而言,我从来不使用一维数组。因此,要去简化你的代码,而且不要使用一维数组。总是使用 _n×1_ 维矩阵(基本上是列向量),或者 _1×n_ 维矩阵(基本上是行向量),这样你可以减少很多assert语句来节省核矩阵和数组的维数的时间。另外,为了确保你的矩阵或向量所需要的维数时,不要羞于reshape操作。
Jupyter 的使用比较简单,略过
现在我们开始快速浏览一下如何实现神经网络。
此前我们讨论了逻辑回归,了解了上图的模型如何与下面公式建立联系。
如上所示,首先需要输入特征 _x_,参数 _w_ 和 _b_,通过这些你就可以计算出 _z_:
接下来使用 _z_ 就可以计算出 _a_ 。我们定义符号:
* **输出**: _ŷ = a = σ(z)_
* **代价函数(loss function)**: _L(a,y)_
一个简单神经网络看起来如下图:
如之前提到,可以把许多Sigmoid单元堆叠起来形成一个神经网络。对于图中的节点,它包含了之前讲的两个步骤:
上图的神经网络对应的3个节点,首先计算第一层网络中的各个节点相关的数 _z[1]_ ,接着计算 _α[1]_ ,再计算下一层网络。
定义符号:
这些节点的集合被称为第 _m_ 层网络。这样可以保证 _[m]_ 不会和我们之前用来表示单个的训练样本的 _(i)_ (即我们使用表示第 _i_ 个训练样本)混淆。
计算过程如下:
接下来你需要使用另外一个线性方程对应的参数计算 _z[2]_ ,计算 _a[2]_ ,此时 _a[2]_ 就是整个神经网络最终的输出,用 _ŷ_ 表示网络的输出。
在逻辑回归中,通过直接计算 _z_ 得到结果 _a_ 。而神经网络中,需要反复的计算 _z_ 和 _a_ ,计算 _a_ 和 _z_ ,最后得到了最终的输出**loss function**。
此外,在逻辑回归中,有一些从后向前的计算用来计算导数 _da_ 、 _dz_ 。同样在神经网络中也有从后向前的计算,对于这个例子会先计算 _da[2]_ 、 _dz[2]_ ,计算出来之后,然后计算 _dW[2]_ 、 _db[2]_ ,然后是_da[1]_ 、_dW[1]_ 、 _db[1]_ ,从右到左反向计算,如下图所示:
首先看一个例子(见下图),本例中的神经网络只包含一个隐藏层。让我们先给此图的不同部分取一些名字。
* **输入层**:输入特征包括 _x1_ 、 _x2_ 、 _x3_ ,它们被竖直地堆叠起来,叫做神经网络的**输入层**。
* **隐藏层**:它包含了神经网络的输入;然后有另外一层我们称之为**隐藏层**(图中中间的四个结点)。
* **输出层**:最后一层只由一个结点构成,只有一个结点的层被称为**输出层**,它负责产生预测值。
**隐藏层的含义**:在一个神经网络中,当使用监督学习训练它的时候,训练集包含了输入 _x_ 也包含了目标输出 _y_ 。在训练过程中,隐藏层这些中间结点的准确值我们是不知道到的,也就是说你看不见它们在训练集中应具有的值。所以这也解释了词语隐藏层,只是表示你无法在训练中看到他们。
这里,也可**用符号 _a[0]_ 来表示输入层的值**。_a_ 是activation的首字母,表示激活的意思,它意味着网络中不同层的值会传递到它们后面的层中。输入层将 _x_ 传递给隐藏层,所以我们将输入层的激活值称为 _a[0]_ 。下一层即隐藏层也同样会产生一些激活值,我将其记作 _a[1]_。
具体地,这里的第一个单元我们将其表示为 _a[1]1_ ,第二个结点的值我们记为 _a[1]2_,以此类推。所以 _a[1]_ 是个四维的向量,在Python中是一个4x1的矩阵或一个大小为4的列向量:
最后输出层将产生某个数值 _a_ ,它只是一个单独的实数,所以的 _ŷ_ 将取为 _a[2]_ 。这与逻辑回归很相似,在逻辑回归中,_ŷ_ 直接等于 _a_ ,逻辑回归中只有一个输出层,所以没有用带方括号的上标。
在神经网络中,将使用这种带上标的形式来明确地指出这些值来自于哪一层。
* 有趣的是,在约定俗成的符号传统中,上面这个神经网络被认为是两层网络。原因是当我们计算网络的层数时,输入层是不算入总层数内。所以隐藏层是第一层,输出层是第二层。
* 第二个惯例是我们将输入层称为第0层,所以在技术上,这仍然是一个三层的神经网络,因为这里有输入层、隐藏层,还有输出层。但是在传统的符号使用中,如果你阅读研究论文或者在这门课中,你会看到人们将这个神经网络称为一个两层的神经网络,因为我们也不把输入层看作一个标准的层。
* 最后,我们看到的隐藏层以及最后的输出层是带参数的,这里的隐藏层将拥有两个参数 _W_ 和 _b_ ,将给它们加上上标 _[1]_ ( _W[1]_ , _b[1]_ ),表示这些参数是和第一层这个隐藏层有关系的。之后在这个例子中会看到 _W_ 是一个4x3的矩阵,而 _b_ 是一个4x1的向量,第一个数字4源自于我们有四个结点或隐藏层单元,然后数字3源自于这里有三个输入特征,我们之后会更加详细地讨论这些矩阵的维数。相似的输出层也有一些与之关联的参数 _W[2]_ 以及 _b[2]_ 。从维数上来看,它们的规模分别是1x4以及1x1。1x4是因为隐藏层有四个隐藏层单元而输出层只有一个单元。
现在你已经知道一个两层的神经网络什么样的了,即它是一个只有一个隐藏层的神经网络。
上一节中,我们介绍只有一个隐藏层的神经网络的结构与符号表示。在这节中我们了解神经网络的输出究竟是如何计算出来的。
首先,回顾下只有一个隐藏层的**简单两层神经网络结构**(如上节的图)。
这是神经网络的符号惯例。
#### 神经网络的计算
从之前提及的逻辑回归开始,如下图所示。用圆圈表示神经网络的计算单元,逻辑回归的计算有两个步骤,首先你按步骤计算出 _z_ ,然后在第二步中你以**Sigmoid**函数为激活函数计算 _z_ (得出 _a_ ),一个神经网络只是多次重复计算。
回到两层的神经网络,我们从隐藏层的第一个神经元开始计算,如上图第一个最上面的箭头所指。
从上图可以看出,输入与逻辑回归相似,这个神经元的计算与逻辑回归一样分为两步,小圆圈代表了计算的两个步骤。
* 第一步:计算 _z[1]1, z[1]1 = w[1]T1x + b[1]1_ 。
* 第二步:通过激活函数计算 _a[1]1, a[1]1 = σ(z[1]1)_ 。
隐藏层的第二个以及后面两个神经元的计算过程一样,只是符号表不同,最终分别得到 _a[1]2、a[1]3、a[1]4_ :
* _z[1]1 = w[1]T1 x + b[1]1, a[1]1 = σ(z[1]1)_
* _z[1]2 = w[1]T2 x + b[1]2, a[1]2 = σ(z[1]2)_
* _z[1]3 = w[1]T3 x + b[1]3, a[1]3 = σ(z[1]3)_
* _z[1]4 = w[1]T4 x + b[1]4, a[1]4 = σ(z[1]4)_
#### 向量化计算
如果你执行神经网络的程序,可以选择用for循环来做上述的计算,但是会很低效。
接下来我会介绍上述四个等式向量化。向量化的过程是将神经网络中的一层神经元参数纵向堆积起来,例如隐藏层中的 _w_ 纵向堆积起来变成一个 _(4, 3)_ 的矩阵,用符号 _W[1]_ 表示。
另一个看待这个的方法是我们有四个逻辑回归单元,且每一个逻辑回归单元都有相对应的参数——向量 _w_ ,把这四个向量堆积在一起,你会得出这4×3的矩阵。
因此,
* _z[n] = w[n]x + b[n]_
* _a[n] = σ(z[n])_
详细过程见下:
对于第一层,给予输入 _x_ ,得到 _a[1]_ , _x_ 可以表示为 _a[0]_ 。后一层可以写成类似的形式,得到 _a[2]_ , _ŷ = a[2]_。
如上图左半部分所示为神经网络,把网络左边部分盖住先忽略,那么最后的输出单元就相当于一个逻辑回归的计算单元。当有一个包含一层隐藏层的神经网络,你需要去实现以计算得到输出的是右边的四个等式,并且可以看成是一个向量化的计算过程,计算出隐藏层的四个逻辑回归单元和整个隐藏层的输出结果,如果编程实现需要的也只是这四行代码(上图右侧)。
逻辑回归是将各个训练样本组合成矩阵,对矩阵的各列进行计算。神经网络是通过对逻辑回归中的等式简单的变形,让神经网络计算出输出值。这种计算是所有的训练样本同时进行的,以下是实现它具体的步骤:
上一节中得到的四个等式,它们给出如何计算出 _z[1]_ , _a[1]_ , _z[2]_ , _a[2]_ 。
对于一个给定的输入特征向量 _X_ ,这四个等式可以计算出 _a[2]_ 等于 _ŷ_ 。但是这是针对于单一的训练样本。
如果有 _m_ 个训练样本,就需要重复这个过程:
* 用第一个训练样本 _x(1)_ 来计算出预测值 _ŷ(1)_ ,得到第一个训练样本上的结果。
* 用 _x(2)_ 来计算出预测值 _ŷ(2)_ ,循环往复,直至用 _x(m)_ 计算出 _ŷ(m)_ 。
* 用激活函数表示法,如上图左下所示,它写成 _a[2] (1)_ 、 _a[2] (2)_ 和 _a[2] (m)_ 。
**对于_a[2] (i)_ , _(i)_ 是指第 _i_ 个训练样本,_[2]_ 是指第二层。**
如果有一个非向量化形式的实现,要计算出它的预测值,对于所有训练样本,需要让 _i_ 从1到 _m_ 实现这四个等式:
* _z[1] (i) = W[1] (i) x(i) + b[1] (i)_
* _a[1] (i) = σ(z[1] (i))_
* _z[2] (i) = W[2] (i) a[1] (i) + b[2] (i)_
* _a[2] (i) = σ(z[2] (i))_
对于上面的这个方程中的 _(i)_ ,是所有依赖于训练样本的变量,即将 _(i)_ 添加到 _x_ ,_z_ 和 _a_ 。如果想计算 _m_ 个训练样本上的所有输出,就应该向量化整个计算,以简化这列。
接下来讲讲如何向量化这些:
前一张幻灯片中的 `for` 循环是来遍历所有个训练样本。定义矩阵 _X_ 等于训练样本,将它们组合成矩阵的各列,形成一个 _n_ 维或 _n_ 乘以 _m_ 维矩阵。
以此类推,从小写的向量 _x_ 到这个大写的矩阵 _X_ ,只是通过组合 _x_ 向量在矩阵的各列中。
* _z[1] (1)_ , _z[1] (2)_ 等等都是 _z[1] (m)_ 的列向量,将所有 _m_ 都组合在各列中,就的到矩阵 _Z[1]_ 。
* _a[1] (1)_ , _a[1] (2)_ ,……, _a[1] (m)_ 将其组合在矩阵各列中,如同从向量 _x_ 到矩阵 _X_ ,以及从向量 _z_ 到矩阵 _Z_ 一样,就能得到矩阵 _A[1]_ 。
* 对于 _Z[2]_ 和 _A[2]_ ,也是这样得到。
这种符号其中一个作用就是,可以通过训练样本来进行索引。
* 在垂直方向,这个垂直索引对应于神经网络中的不同节点。
* 例如,一个节点位于矩阵的最左上角对应于激活单元,它是位于第一个训练样本上的第一个隐藏单元。它的下一个值对应于第二个隐藏单元的激活值。它是位于第一个训练样本上的,以及第一个训练示例中第三个隐藏单元,等等。
* 从竖直上看,矩阵 _A_ 的不同的索引对应于不同的隐藏单元。
* 当水平扫描,将从第一个训练示例中从第一个隐藏的单元到第二个训练样本,第三个训练样本……直到节点对应于第一个隐藏单元的激活值,且这个隐藏单元是位于这 _m_ 个训练样本中的最终训练样本。
* 从水平上看,矩阵 _A_ 代表了各个训练样本。
对于矩阵 _Z,X_ 情况也类似。神经网络上通过在多样本情况下的向量化来使用这些等式。
在上一节中,学习到如何将多个训练样本横向堆叠成一个矩阵 _X_ ,然后就可以推导出神经网络中前向传播(**Forward propagation**)部分的向量化实现。
这一节继续介绍为什么上一节的公式就是将多个样本向量化的正确实现。
我们先手动对几个样本计算一下前向传播,看看有什么规律:
* _z[1] (1) = W[1] x(1) + b[1]_
* _z[1] (2) = W[1] x(2) + b[1]_
* _z[1] (3) = W[1] x(3) + b[1]_
这里,为了描述的简便,我们先忽略掉 _b[1]_ ,后面你将会看到利用Python的广播机制,可以很容易的将 _b[1]_ 加进来。
现在 _W[1]_ 是一个矩阵, _x(1), x(2), x(3)_ 都是列向量,矩阵乘以列向量得到列向量,下面将它们用图形直观的表示出来:
从这里我们也可以明白,为什么之前我们对单个样本的计算要写成 _z[1] (i) = 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]_ 是前向传播的第一步计算的正确向量化实现。类似的分析可以发现,前向传播的其它步也可以使用非常相似的逻辑,即如果将输入按列向量横向堆叠进矩阵,那么通过公式计算之后,也能得到成列堆叠的输出。
总结:由上述公式可以看出,使用向量化的方法,可以不需要显示循环,而直接通过矩阵运算从 _X_ 就可以计算出 _A[1]_ ,实际上 _X_ 可以记为 _A[0]_ 。使用同样的方法就可以由神经网络中的每一层的输入 _A[i-1]_ 计算输出 _A[i]_ 。这些方程有一定对称性,其中第一个方程也可以写成 _Z[1] = W[1]A[0] + b[1]_ ,还有这对方程形式其实很类似,只不过这里所有指标加了1。所以这样就显示出神经网络的不同层次,你知道大概每一步做的都是一样的,或者只不过同样的计算不断重复而已。这里的例子只是一个双层神经网络,在之后会讲更深层的神经网络,会看到随着网络的深度变大,基本上也还是重复这两步运算,只不过是比这里你看到的重复次数更多。
使用一个神经网络时,需要决定使用哪种激活函数用隐藏层上,哪种用在输出节点上。到目前为止,只介绍过`sigmoid`激活函数,但很多情况下其他激活函数效果会更好。
在神经网路前向传播的 _a[1] = σ(z[1])_ 和 _a[2] = σ(z[2])_ 这两步会使用到`sigmoid`函数。`sigmoid`函数在这被称为激活函数。其表达式写作:
不同的情况下,会使用不同的函数 _g(z[1])_ 。 _g_ 可以是除了`sigmoid`函数以外的非线性函数。
`tanh`函数或者双曲正切函数是总体上都优于`sigmoid`函数的激活函数。事实上,`tanh`函数是 `sigmoid` 的向下平移和伸缩后的结果。对它进行了变形后,穿过了 _(0,0)_ 点,并且值域介于+1和-1之间。
结果表明,如果在隐藏层上使用函数 _g(z[1]) = tanh(z[1])_ ,效果总是优于sigmoid函数。因为函数值域在-1和+1的激活函数,其均值是更接近零均值的。在训练一个算法模型时,如果使用tanh函数代替sigmoid函数中心化数据,使得数据的平均值更接近0而不是0.5.
因此,**很多研究者基本已经不用sigmoid激活函数了,tanh函数在几乎所有场合都优于sigmoid函数**。但在特殊的情况下,例如二分类的问题中,对于输出层,因为 _y_ 的值是0或1,所以想让 _ŷ_ 的数值介于0和1之间,而不是在-1和+1之间。所以需要使用sigmoid激活函数。这里的 _g(z[2])=σ(z[2])_ 。在这个例子里看到的是,对隐藏层使用tanh激活函数,输出层使用sigmoid函数。
**所以,在不同的神经网络层中,激活函数可以不同。** 为了表示不同的激活函数,在不同的层中,使用方括号上标来指出 _g_ 上标为 _[1]_ 的激活函数,可能会跟 _g_ 上标为 _[2]_ 不同。方括号上标 _[1]_ 代表隐藏层,方括号上标 _[2]_ 表示输出层。
sigmoid函数和tanh函数两者共同的缺点是:在 _z_ 特别大或者特别小的情况下,导数的梯度或者函数的斜率会变得特别小,最后就会接近于0,导致降低梯度下降的速度。
在机器学习**另一个很流行的函数是:修正线性单元的函数(ReLU)**,ReLU函数为 _a = max(0, z)_。只要 _z_ 是正值的情况下,ReLU的导数恒等于1,当 _z_ 是负值的时候,导数恒等于0。当使用 _z_ 的导数时, _z_ =0的导数是没有定义的。但是当编程实现的时候, _z_ 的取值刚好等于0.00000001,这个值相当小,所以,在实践中,不需要担心这个值, _z_ 是等于0的时候,假设一个导数是1或者0效果都可以。
这有一些**选择激活函数的经验法则**:
* 如果输出是0、1值(二分类问题),则输出层选择sigmoid函数,然后其它的所有单元都选择ReLU函数。
* ReLU是很多激活函数的默认选择,如果在隐藏层上不确定使用哪个激活函数,那么通常会使用ReLU激活函数。
* 有时,也会使用tanh激活函数,但Relu的一个优点是:当 _z_ 是负值的时候,导数等于0。
这里也有另一个版本的ReLU被称为**Leaky ReLU**。
* 当 _z_ 是负值时,这个函数的值不是等于0,而是轻微的倾斜。
* 这个函数通常比ReLU激活函数效果要好,尽管在实际中Leaky ReLU使用的并不多。
ReLU和LeakyReLU的优点是:
* 第一,在 _z_ 的区间变动很大的情况下,激活函数的导数或者激活函数的斜率都会远大于0,在程序实现就是一个**if-else**语句,而**sigmoid**函数需要进行浮点四则运算,在实践中,使用**ReLu**激活函数神经网络通常会比使用**sigmoid**或者**tanh**激活函数学习的更快。
* 第二,**sigmoid**和**tanh**函数的导数在正负饱和区的梯度都会接近于0,这会造成梯度弥散,而**Relu**和**Leaky ReLu**函数大于0部分都为常数,不会产生梯度弥散现象。(同时应该注意到的是,**Relu**进入负半区的时候,梯度为0,神经元此时不会被训练(因为对应的W不会再更新),产生所谓的稀疏性,而**Leaky ReLu**不会有这问题)。_z_ 在ReLu的梯度一半都是0,但是,有足够的隐藏层使得z值大于0,所以对大多数的训练数据来说学习过程仍然可以很快。
**概括一下不同激活函数的过程和结论:**
* sigmoid:除了输出层是一个二分类问题基本不会用它。
* tanh:tanh几乎适合所有场合。
* ReLU:最常用的默认函数,如果不确定用哪个激活函数,就使用ReLU或者Leaky ReLU。
对于Leaky Relu,一个例子是_a = max(0.01z, z)_,为什么常数是0.01?当然,可以选择不同的参数。
在编写神经网络的时候,你会有很多选择,例如隐藏层单元的个数、激活函数的选择、初始化权值等等。想得到一个可以适用所有场景的指导原则是挺困难的。你可以提前知道哪一种方法工业界用的多,哪一种用的少。但是,对于特定的神经网络的应用,因为其特殊性,是很难提前知道选择哪些效果更好。所以建议如果不确定哪一个激活函数效果更好,可以把它们都试试,然后在验证集或者开发数据集上进行评价。看哪一种表现的更好,就去使用它。为自己的神经网络的应用测试这些不同的选择,会在以后检验自己的神经网络或者评估算法的时候,看到不同的效果。如果仅仅遵守使用默认的ReLU激活函数,而不要用其他的激励函数,这可能对于你未来的问题不适用。
要让你的神经网络能够计算出有意义的函数,必须使用非线性激活函数,看如下的证明:
对于神经网络正向传播的方程,如果去掉函数 _g_ ,令 _a[1]=z[1]_ ,或者令 _g(z)=z_ ,这个有时被叫做线性激活函数(更学术点的名字是恒等激励函数,因为它们就是把输入值输出)。为了说明问题我们把 _a[2]=z[2]_ ,那么这个模型的输出 _y_ 或仅仅只是输入特征 _x_ 的线性组合。
如果我们改变前面的式子,令:
* _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 + W[2]b[1] + b[2]_
简化多项式得 _a[2] = z[2] = W'x + b'_,如果你是用线性激活函数或者叫恒等激励函数,那么神经网络只是把输入线性组合再输出。
稍后会谈到深度网络,有很多层的神经网络,很多隐藏层。事实证明,如果使用线性激活函数或者没有使用一个激活函数,那么无论你的神经网络有多少层一直在做的只是计算线性函数,所以不如直接去掉全部隐藏层。在简明案例中,事实证明如果你在隐藏层用线性激活函数,在输出层用sigmoid函数,那么这个模型的复杂度和没有任何隐藏层的标准Logistic回归是一样的。
在这里线性隐层一点用也没有,因为这两个线性函数的组合本身就是线性函数,所以除非你引入非线性,否则你无法计算更有意义的函数,即使你的网络层数再多也不行。
只有一
grace_robot
2025-02-04 00:30文档分析功能很实用,但对某些复杂表格的识别还需要改进。
frank_cv
2025-02-03 22:30Vision功能对图表理解特别准确,帮我省了很多工作。