《机器学习:算法视角(原书第2版)》 —3.3.5 具体实现
3.3.5 具体实现
把算法转换成相应的代码很容易,我们需要设计一些数据结构来保存变量,然后编写并测试程序。数据结构通常对机器学习算法来说是非常基础的,这里我们需要一个数组存放输入,一个数组存放权重,另外还有两个存放输出和目标。前面讨论提供给神经网络的数据时,我们使用了术语输入向量(input vector)。向量是提供给感知器的一列的值,每一个值对应着网络中的一个节点。当我们编写计算机代码的时候,把这些值存入一个数组是比较合适的。然而,我们不能只提供给神经网络一个数据点,它需要大量的数据。因此,我们通常把输入数据安排在一个二维数组里,其中的每一行代表一个数据点。在一个像C或Java的语言中,你可以通过写一个循环访问数组的每一行来提供输入,然后内部嵌套一个循环访问输入的每一个节点,计算当前输入向量的输出值。
若想用Python语言书写(附录A将对Python语言做简要介绍),经过对数组inputs中的一系列数据点nData的训练之后,再现阶段的代码如下所示(本书的网站上可以找到这段代码):
然而,Python的数值计算库NumPy提供了另外一种方法,它可以方便地计算数组或是矩阵的乘积(MATLAB和R也有这样的功能)。这意味着我们可以编写包含更少循环的代码,这增加了易读性,同时减少了代码量。但是在第一次使用的时候,可能会有些难以理解。为了理解它,我们需要一点数学知识,那就是矩阵的概念。在计算机术语中,矩阵就是二维数组。我们可以用一个矩阵来表示网络权重的集合,只需要定义一个有m+1行(输入节点的个数加上偏置节点的个数1)、n列(神经元的个数)的np.array即可。现在,位于矩阵(i,j)处的元素存储的是连接输入i和神经元j的权重,这在我们上面的代码中也出现过。
以这种方式思考的好处在于矩阵或是向量的乘法是定义明确的。你很可能在高中或是其他地方已经见过矩阵乘法的定义,但以防万一,我们还是要说明一下,若要使两个矩阵能够相乘,我们需要它们的内维(inner dimension)相等。这表明如果我们有A和B两个矩阵,A的大小是m×n的,那么B的大小必须是n×p,p可以是任意自然数。n被称作内维,因为当我们在乘法式子中写出矩阵的大小时,会得到(m×n)×(n×p)。
现在我们能够计算AB(但未必能够计算BA,因为如果那样的话,上面的式子将变成(n×p)×(m×n),这要求m=p)。乘法的计算过程是这样的,首先选出矩阵B的第1列,逆时针旋转90°,使它从1列变成1行,然后把这一行的每个元素与矩阵A第一行的每一个对应的元素相乘,再把所得的积相加,得到结果矩阵的第一个元素。接下来选出矩阵B的第2列,同样逆时针旋转90°,把每个元素与A第一行的对应元素相乘,然后把所得的积相加,得到结果矩阵第1行的第2个元素,以此类推即可。下面给出一个例子:345
2038(3.15)NumPy可以为我们做这样的乘法,只需要使用np.dot()函数即可(从数学角度来看,这真是一个奇怪的名字,不过不必放在心上)。所以我们可以用如下的方法计算上式(这里的>>>代表Python的命令行,我们在这里键入公式,答案在下方由Python解释器给出):
np.array()函数定义NumPy中的数组,这里得到的实际上是一个矩阵,数组的每一个元素本身也是数组:每一行都是一个独立的数组,就像你可以看到的方括号嵌套在方括号中一样。请注意,我们可以把二维数组写在一行代码之中,只需要把数组的每一行用逗号隔开即可,但是当NumPy输出一个矩阵的时候,它会把矩阵的每一行单独写在一行代码之中,这可以使我们看得更加清楚。
这些似乎距离感知器还有很长的距离,但我保证,我们马上就会到达那里。我们可以把输入向量存储在一个大小为N×m的二维数组中,这里N是输入向量的个数,m是每个向量中输入的个数。权重数组的大小为m×n,因此它们可以相乘。如果这样做的话,输出将是一个N×n的矩阵,里面存储着每一个神经元对每一个输入向量计算得到的和。现在我们只需根据这些和来计算神经元的激活状态。这里,NumPy为我们提供了另一个有用的函数where(condition,x,y)(condition是一个逻辑上的条件,x和y是具体的数值),它返回一个矩阵,对于矩阵中的每一个元素,当condition为真的时候,值为x,当condition为假的时候,值为y。对上面的矩阵a利用这个函数:
据此,我们可以得到这样的结论,感知器再现阶段函数的整个代码可以写在如下的两行之中:
训练部分并不会更难。你应该注意到训练算法的第一部分与再现阶段的计算是一样的,因此我们可以把它们放在同一个函数之中(我把它取名为pcnfwd,因为它包含了在网络中前向(forward)传播来得到输出的过程)。然后我们只需要计算权重的更新。权重存放在一个m×n的矩阵中,激活状态和目标都存放在N×m的矩阵中,输入存放在N×m的矩阵中。因此,为了计算矩阵的乘法np.dot(inputs,targets-activations),我们需要对输入矩阵进行转置,使它变成m×N。这可以通过函数np.transpose()来实现,它交换矩阵的行和列。对上面的矩阵a用此函数,可以得到:
知道这些之后,我们可以把整个网络的权重更新写在一行中,其中eta代表学习速率η:
假设你事先就能够确定输入矩阵的大小是正确的(函数np.shape()可以得到数组每一维的元素个数,可能会对你有所帮助),那么你唯一需要做的就是在输入向量中为偏置节点添加额外的-1,然后决定把每一个权重的初始值设为多少。添加偏置节点这件事可以通过函数np.concatenate()来实现,它可以定义一个一维数组,里面所有元素都为-1,然后把它添加到输入数组上。如下所示(请注意,这里的nData与上文中提到的N是等价的):
我们需要做的最后一件事就是给每个权重赋初值。我们可以把它们都设为0,并且算法也会得到正确的结果。然而,一般我们把初值设为小的随机数,其中的原因将会在4.2.2节中讨论。同样,NumPy利用内置的随机数生成器可以很好地完成这件事(nin与nout分别对应m和n):
至此,我们已经研究了所有需要的代码片段,把它们合并起来应该不是什么难事。完整的程序可以从本书的网站上得到,标题为pcn.py。注意,这里提供的是算法的另一个版本——批量(batch)版本:所有输入批量进入算法,之后计算误差并更新权重。这与第一个算法所采用的顺序(sequential)版本的程序有所不同。
看程序如何运行是一件有趣的事情,这就是我们下面将要做的,从先前手工计算演示过的逻辑OR的例子开始。
得到OR的数据是很容易的,运行代码需要使用文件名(pcn)来导入它,再调用函数pcntrain。下文打印的内容包含了数组定义和函数调用的语句,以及该程序某一次特定的运行里,在循环5次的过程中权重的输出。权重的初始值设为随机数(请注意,在下文的这次运行中,第一轮循环过后,权重就不再发生变化,不过每次运行的结果都会有所不同)。
我们在四个数据点(0,0)、(1,0)、(0,1)和(1,1)上训练感知器。然而,我们也可以给出类似(0.8,0.8)这样的输入,并且预计可以从神经网络中得到输出。很显然,从逻辑函数的角度来看,这样做没有任何意义,不过至少我们使用神经网络做的大部分的事情会比这个更有吸引力。 图3-6 OR函数感知器的决策边界图3-6给出了决策边界(decision boundary),它告诉我们当对输入进行分类的时候,输出的结果什么时候从圆形变成了十字形。至于这里的决策边界为什么是一条直线,我们将在3.4节予以说明。
在返回权重之前,上面的感知器算法首先打印了训练输入的输出。你同样可以使用神经网络来预测其他输入值的输出,只需使用函数pcnfwd即可。不过,在这种情况下需手工添加-1,使用如下代码:
我们可以使用2.2节中描述的方法对测试数据(test data)结果计算训练算法的准确性。
我们现在对于数据集的学习已经达到了神经网络在1969年达到的阶段。随后,研究员Minsky 和Papert出版了一本名为《感知器》的书。该书的目的是通过讨论感知器的学习能力刺激神经网络的研究,并展示网络能够学习和不能学习的内容。不幸的是,这本书的另一个影响是:它有效地遏制了神经网络大约20年的研究进展。为了了解原因,我们需要了解感知器的不同学习方式。
- 点赞
- 收藏
- 关注作者
评论(0)