2.4 感知机的局限性
感知机可以实现与门,与非门,或门三种逻辑电路,现在我们来考虑以下“异或门”(XOR gate)。
2.4.1 异或门
异或门也称为逻辑异或电路。它仅当x1或x2任意一方为1时,才会输出1(“异或”时拒绝其他的意思)
实际上,前面介绍的感知机是实现不了异或门的。如果将或门进行图像化,就会发现1和0是可以利用一根直线分割出来的:
图中○表示0,△表示1。可当变成异或门的时候图就变成这样了:
此时就无法利用一根直线去分割0和1了,这也是感知机的局限性。
2.4.2 线性与非线性
异或门的图像无法用一条直线去分割0和1,但如果将“直线”这个限制去掉,就可以实现了:
像上图中利用曲线分割0和1是感知机无法表示出来的。因此由曲线分割而成的空间称为非线性空间,由直线分割而成的空间称为线性空间。这两个术语都是在机器学习领域很常见的。
2.5 多层感知机
虽然感知机无法表示异或门,但感知机可以通过“叠加层”来表示它。
2.5.1 已有门电路的组合
异或门制作方法繁多,其中之一便是组合之前做好的与门,与非门,或门进行配置。这里与门,与非门,或门我们分别用AND,NAND,OR来表示:
与非门前端的〇表示反转输出的意思。将与门,与非门,或门带入到下图中即可实现异或门:
异或门——x1和x2是输入信号,s1和s2表示与非门和或门的输出,y表示输出信号。x1和x2是与非门和或门的输入,而与非门和或门的输出则是与门的输入。随后即可得出异或门的真值表:
2.5.2 异或门的实现
下面尝试使用Python和之前定义的AND函数,NAND函数,OR函数来实现异或门:
#异或门
def XOR(x1,x2):
s1 = NAND(x1,x2) #与非门
s2 = OR(x1,x2) #或门
y = AND(s1,s2) #与门
return y
print(XOR(0,0)) ###0
print(XOR(1,0)) ###1
print(XOR(0,1)) ###1
print(XOR(1,1)) ###0
试着用感知机的表示方法(神经元)来表示异或门:
图中可以看出异或门是2层感知机,而与门,与非门,或门都是单层感知机。叠加了多层的感知机就是多层感知机。
2.6 从与非门到计算机
多层感知机可以实现比之前见到的电路更复杂的电路,因此只要一直堆叠感知机的层,理论上就可以实现几乎任何东西。就比如可以用多层感知机的与非门来实现计算机,因为计算机也是有输入有输出的东西,这很符合感知机的逻辑。
2.7 小结
本章我们学习了感知机及其构造以及它的一些算法。这章是下章《神经网络》的基础,因此本章内容特别重要。
第三章 神经网络
3.1 从感知机到神经网络
神经网路与感知机有许多共同点,这里,我们主要以二者的差异为中心来介绍神经网络的结构。
3.1.1 神经网络的例子
下图就是用图来表示的神经网络。最左边的那一列称为“输入层”,最右边的那一列是“输出层”,中间的一列是“中间层”,又称“隐藏层”(与前二者不同,中间层的神经元是肉眼看不见的)。另外,为了方便基于Python进行实现神经网络,从输入层到输出层依次称为“第0层”,“第1层”,···,“第n层”。
神经网络的形状类似于感知机,其连接方式也和感知机没有任何差异。
3.1.2 复习感知机
上图感知机接收x1和x2两个输入信号,输出y。用数学公式来表示图中的感知机则是:
b称为偏置的参数,用于控制神经元被激活的容易程度;w1和w2表示各个信号的权重的参数,用于控制各个信号的重要性。如果将偏置也在图中表示出来则是:
现在将上面的数学式进行简化,因此引入新函数h(x)。将上式改写成下面的两种式子:
式1
式2
式1中输入信号的总和会被函数h(x)转换,转换后的值就是输出值y。式2所表示的函数h(x)在输入超过0时则返回1,反之则返回0。因此上面三式做的都是相同的事情。
3.1.3 激活函数登场
刚才的h(x)函数会将输入信号的总和转换成输出信号,这种函数一般称为“激活函数”(activation function),它是连接感知机和神经网络的桥梁。其作用在于决定如何来激活输入信号的总和。
现在进一步将式1改写:
3式
之前神经元都是用〇来表示的,如果在图中明确表示式3,则是:
上图的神经元中明确显示出了激活函数的计算过程,即信号的加权总和为节点(与“神经元”的含义相同)a,然后节点a被激活函数h()转换成节点y作为输出值。上图简单表示之后则可以变成:
3.2 激活函数
式2表示的激活函数以阈值为界,一旦输入超过阈值,机会切换输出。这样的函数称为“阶跃函数”。因此可以说感知机在众多激活函数中使用了阶跃函数。如果感知机使用其他激活函数,则就可以进入神经网络的世界了。接下来我们来介绍以下神经网络使用的激活函数。
3.2.1 sigmoid函数
神经网络中常使用的一个激活函数,其表达式为:
式4
式4中的exp(-x)表示e-x的意思。Sigmoid函数看上去很复杂,实际上它跟普通函数一样,只要给输入,就会输出某个值。如h(1.0)=0.731···,h(2.0)=0.880···。
3.2.2 阶跃函数的实现
式2
这里我们试着用Python画出阶跃函数的图。阶跃函数如式2所示,>0则输出1,≤0则输出0:
def step_function(x1)
if x1 > 0:
return 1
else:
return 0
这个实现的很简单,到那时参数x只接受实数(浮点数),不能接受NumPy数组。因此为了便于后面的操作,我们将它改为支持NumPy数组的实现:
#阶跃函数
def step_function(x1):
y = np.array(x1) #将参数x1转为数组
y = y>0 #判断元素>或<=0
return y.astype(int) #利用numpy中的astype()方法将布尔值转为int(True=1,False=0)
print(step_function(np.array([-1,2,0]))) ###[0 1 1]
上图其中astype()类型转换方法是NumPy中专属的“技巧”。
3.2.3 阶跃函数的图形
绘出图形需要用到matplotlib库。
#阶跃函数
def step_function(x1):
y = np.array(x1)
y = y>0
return y.astype(int)
print(step_function(np.array([-1,2,0])))
x1 = np.arange(-5,5,0.1) #生成一个-5到5,步数为0.1的数组
y1 = step_function(x1) #将数组x传入函数中
plt.plot(x1,y1,linestyle='dashed',label='y=step_function')
plt.ylim(-0.1,1.1)
plt.show()
运行结果:
通过图像我们可以看出阶跃函数的输出值从0 切换到1(或者从1到0)呈阶梯式变化,这也是它名字的由来。
3.2.4 sigmoid函数的实现
式4
用Python可以这样实现sigmoid函数:
def sigmoid(x2):
return 1/(1+np.exp(-x2))
print(sigmoid(np.array([-1,1,2]))) ###[0.26894142 0.73105858 0.88079708]
下面我们把sigmoid函数画出来。代码中我们可以看到和刚才的阶跃函数几乎是一样的,唯一不同之处在于输出y:
#sigmoid函数
def sigmoid(x2):
return 1/(1+np.exp(-x2))
x2 = np.arange(-5,5,0.1)
y2 = sigmoid(x2)
plt.plot(x2,y2,label='y=sigmoid')
plt.ylim(-0.1,1.1)
plt.legend()
plt.show()
运行结果:
3.2.5 sigmoid函数和阶跃函数的比较
阶跃函数与sigmoid函数的图形对比:
二者之间sigmoid函数要相对阶跃函数更加平滑一点,这对神经网络的学习具有重要意义。正因为阶跃函数只能返回0或1,而sigmoid函数可以返回0.731···,0.880···等实数,可以得出结论感知机中神经元之间流动的是0或1的二元信号,而神经网络中流动的是连续的实数值信号。
Sigmoid函数和阶跃函数共同性质之处在于二者的结构均为“输入小时输出接近或为0,随着输入增大,输出会向1靠近或变成1”。无论输入的值有多大/小,输出的值都会在0-1之间。
3.2.6 非线性函数
Sigmoid函数和阶跃函数还有其他的共同点,就是二者均为非线性函数,因二者的图都不是一条笔直的线。线性函数是一条笔直线的函数,反之即是非线性函数。
神经网络的激活函数必须使用非线性函数。因为线性函数无论加多少层神经网络都会变成没有意义的。为了理解,这里请思考一下下面的例子:我们把线性函数h(x)=cx作为激活函数,把y(x)=h(h(h(x)))的运算对应3层神经网络。这个运算会进行y(x)=c·c·c·x的乘法运算,但是同样的处理可以由y(x)=ax(a=c3)这一次乘法运算来表示。如本例所示,使用线性函数时,无法发挥多层网络带来的优势,因此为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
3.2.7 ReLU函数
sigmoid函数在神经网络的发展史上很早就开始使用了,但最近则主要是ReLU(Rectified Linear Unit)函数。
ReLU函数在输入>0时会直接输出该值;输入<=0时则输出0:
ReLU函数的时间也很简单:
def ReLU(x3):
return np.maximum(0,x3) #实现>0直接输出数值,<=0则输出0
print(ReLU(np.array([-1,-2,3,6]))) ###[0 0 3 6]
运行结果:
3.3 多维数组的运算
如果掌握了NumPy多维数组的运算,就可以高效地实现神经网络,因此本章将介绍NumPy多维数组的运算。
3.3.1 多维数组
多维数组就是“数字的集合”,排成一列及以上的都是多维数组。接下来就用NumPy生成一个多维数组:
import numpy as np
a = np.array([1,2,3,4,5])
print(a) ###[1 2 3 4 5]
print(np.ndim(a)) #输出数组的维度 ###1
print(a.shape) #输出数组是几行几列的 ###(5,)
print(a.shape[0]) ###5
这里需要注意.shape()输出的结果是元组(tuple)类型,这是为了与多维数组结果的一致性。接下来生成一个二维数组:
b = np.array([[1,2],
[3,4],
[5,6]])
print(b) ###[[1 2] [3 4] [5 6]]
print(np.ndim(b)) ###2
print(b.shape) ###(3,2)
这里生成了一个3×2的数组b。3×2表示第一个维度有3个元素,第二个维度有2个元素。另外,第一个维度对应第0维,第二个维度对应第1维(Python的索引从0开始)。二维数组也成为矩阵(matrix),数组的横向称为行(row),纵向称为列(column)。
3.3.2 矩阵乘法
NumPy中矩阵的乘法与线性代数中的矩阵乘法相同,都是如下图表示的那样:
接下来我们用Python来实现以下上面的运算:
A = np.array([[1,2],[3,4]])
B = np.array([[9,8],[7,6]])
print(A.shape) ###(2,2)
print(B.shape) ###(2,2)
print(np.dot(A,B)) ###[[23 20] [55 48]]
这里的乘积用的是NumPy中的np.dot()函数计算的。np.dot()接收到两个参数之后并返回数组的乘积,这里要注意的是np.dot(A,B)的结果和np.dot(B,A)的结果是不一样的,也就是说np.dot()函数有顺序性。
除此之外,其他形状的矩阵也可以用这个方法进行计算:
A0 = np.array([[1,2,3],[4,5,6]])
B0 = np.array([[9,8],[7,6],[5,4]])
print(A0.shape) ###(2,3)
print(B0.shape) ###(3,2)
print(np.dot(A0,B0)) ###[[38 32] [101 86]]
这里需要注意乘积的两个矩阵需满足“行=对方的列,列=对方的行”。如上面的例子,矩阵A是一个2×3的矩阵,B是3×2的矩阵。A的行=B的列,A的列=B的行,所以才可以进行乘积的运算。如果这两个值不相等,就会报错。其背后的逻辑是这样的:
就算是多维数组对一维数组进行乘积,也会遵循上图的原则:
c = np.array([[1,2],[3,4],[5,6]])
d = np.array([1,2])
print(np.dot(c,d)) ###[5 11 17]
3.3.3 神经网络的内积
下面我们将使用NumPy矩阵来实现神经网络。我们以下图中的简单神经网络为对象。这个神经网络省略了偏置和激活函数,只有权重。
实现该神经网络时需要注意X,W,Y的形状。特别是X和W的对应维度的元素格式是否一致。
X0 = np.array([1,2])
print(X0.shape) ###(2,)
print(X0) ###[1 2]
W0 = np.array([[1,2,3],[4,5,6]])
print(W0.shape) ###(2,3)
print(W0) ###[[1 2 3] [4 5 6]]
Y0 = np.dot(X0,W0)
print(Y0) ###[9 12 15]
如上所示,使用np.dot(多维数组的点积)可以一次性计算出Y的结果,不管Y有多大。如果不使用np.dot(),就必须单独计算Y的每一个元素,因此通过矩阵的乘积一次性完成计算的技巧在实现层面上是非常重要的。
3.4 3层神经网络的实现
接下来我们以下图的3层神经网络为对象,来实现从输入到输出的书里。代码方面巧妙地使用NumPy的多维数组:
图中从左到右分别是输入层(第0层)有两个神经元,第1个隐藏层(第1层)有三个神经元,第2个隐藏层(第2层)有两个神经元,输出层(第3层)有两个神经元。
3.4.1 符号确认
本节的重点是神经网络的运算可以作为矩阵运算打包进行。因为神经网络各层的运算时通过矩阵的乘法运算打包进行的,因此即便忘了具体符号的规则也不影响后面的内容。
在介绍神经网络中的处理之前,我们先从定义符号开始。
上图中凸显了从输入层x2到后一层神经元a1(1)的权重以及符号的意思。图中我们可以明白权重的右下角时按照“后一层的索引号,前一层的索引号”的顺序排列的。
3.4.2 各层间信号传递的实现
现在先看一下从输入层到第一层的第一个神经元的信号传递过程:
上图中增加了表示偏置的神经元“1”.为了确认前面的内容,现在用数学式表示a1(1):
式5
如果使用矩阵的乘法运算,则可以将第一层的加权和表示成下面的式:
式6
其中
A(1)=(a1(1),a2(1),a3(1))
X=(x1,x2)
B(1)=(b1(1),b2(1),b3(1))
下面用NumPy多维数组来实现式6,这里将输入信号,权重,偏置设置成任意值:
X = np.array([4,5])
B = np.array([6,7,8])
W = np.array([[9,10,11],[12,13,14]])
print(X.shape) ###(2,)
print(B.shape) ###(3,)
print(np.dot(X,W)+B ###[102 112 122]
如果将这个运算过程用图来表示的话就是下面这个样子:
如图所示,隐藏层的加权和(加权信号和偏置的总和)用a表示,被激活函数转换后用z表示。这里的激活函数使用sigmoid函数。用Python实现就是下面这个样子:
X = np.array([4,5])
B = np.array([6,7,8])
W = np.array([[9,10,11],[12,13,14]])
A = np.dpt(X,W)+B
def sigmoid(A):
return 1/(1+np.exp(A))
Z = sigmoid(np.array([-1,1,2])
print(A) ###[102 112 122]
print(Z) ###[0.73105858 0.26894142 0.11920292]
这样第一层的输出Z就变成了第二层的输入。接着第三层的实现与之前一模一样:
最后第二层的输出变成了 第三层的输入。输出层的实现与之前基本相同,就是激活函数和之前的隐藏层有所不同:
def identity_function(x): #可有可无,仅为了与一二层的格式保持一致
return x
W3 = np.array([[1,2],[3,4]])
B3 = np.array([5,6])
A3 = np.dot(Z2,W2)+B3
Y = identity_function(A3) #或者Y=A3
这里为了与前几层的格式保持一致,所以定义了一个identity_function()函数(“恒等函数”)。第2层到输出层的图像表示是这样的:
输出层的激活函数不同于隐藏层,用σ()表示(σ读作sigma)。
输出层的激活函数(下章细讲)要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用sigmoid函数,多元分类问题可以使用softmax函数。
3.4.3 代码实现小结
至此,我们已经介绍完了三层神经网络的实现,现在我们将之前的代码进行整理。这里,按照神经网络的实现惯例,将权重记为W1,其他的都用小写字母表示:
import numpy as np
class Nueral_Networks:
#对权重和偏置进行初始化
def __init__(self):
self.network = {} #将权重和偏置存入network字典中
self.network['W1'] = np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]])
self.network['W2'] = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]])
self.network['W3'] = np.array([[0.1,0.3],[0.2,0.4]])
self.network['b1'] = np.array([0.1,0.2,0.3])
self.network['b2'] = np.array([0.1,0.2])
self.network['b3'] = np.array([0.1,0.2])
#将输入信号转为输出信号
def forward(self,x):
W1,W2,W3 = self.network['W1'],self.network['W2'],self.network['W3']
b1,b2,b3 = self.network['b1'],self.network['b2'],self.network['b3']
a1 = np.dot(x,W1)+b1
z1 = 1/(1+np.exp(-a1)) #用sigmoid函数运算出self.z1
a2 = np.dot(z1,W2)+b2
z2 = 1/(1+np.exp(-a2)) #用sigmoid函数运算出self.z2
self.a3 = np.dot(z2,W3)+b3
#输出层(输出y)
def identity_function(self):
y = self.a3
print(y)
x = Nueral_Networks()
x.forward(np.array([1,0.5]))
x.identity_function() ###[0.31682708 0.69627909]
这里我们通过巧妙地运用NumPy多维数组实现了神经网络前向(forward)处理。以后的学习中将会学习到后向。