目录
神经网络的一个重要性质是它可以自动地从数据中学习到合适的权重参数。不像感知机那样,设定权重的工作,即确定合适的、能符合预期的输入与输出的权重,是由人工进行的。
1. 从感知机到神经网络
1.1 神经网络的例子
用图来表示神经网络的话,如图①所示。我们把最左边的一列称为输入层,最右边的一列称为输出层,中间的一列称为中间层。中间层有时也称为隐藏层。“隐藏”一词的意思是,隐藏层的神经元(和输入层、输出层不同)肉眼看不见。另外,书中把输入层到输出层依次称为第0层、第1层、第2层(层号之所以从0开始,是为了方便后面基于Python进行实现)。图①中,第0层对应输入层,第1层对应中间层,第2层对应输出层。
1.2 复习感知机
不同于之前的神经网络,这次我们在图中明确地表示出b,可以像图②那样做。图②中添加了权重为b的输入信号1。这个感知机将x1、x2、1三个信号作为神经元的输入,将其和各自的权重相乘后,传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。
现在将式①改写成更加简洁的形式。为了简化式①,我们用一个函数来表示这种分情况的动作(超过0则输出1,否则输出0)。引入新函数h(x),将式①改写成下面的式②和式③。
式②中,输入信号的总和会被函数h(x)转换,转换后的值就是输出y。然后,式③所表示的函数h(x),在输入超过0时返回1,否则返回0。因此,式①和式②、式③做的是相同的事情。
1.3 激活函数登场
刚才登场的h(x)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)。如“激活”一词所示,激活函数的作用在于决定如何来激活输入信号的总和。
现在来进一步改写式②。式②分两个阶段进行处理,先计算输入信号的加权总和,然后用激活函数转换这一总和。
首先计算加权输入信号和偏置的总和,记为a。然后用h()函数将a转换为输出y。
如图所示,表示神经元的○中明确显示了激活函数的计算过程,即信号的加权总和为节点a,然后节点a被激活函数h()转换成节点y。(“节点”=“神经元”)
2 激活函数
式③表示的激活函数以阈值为界,一旦输入超过阈值,就切换输出。这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。那么,如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。
2.1 sigmoid函数
神经网络中经常使用的一个激活函数就是式(3.6)表示的sigmoid函数(sigmoid function)。
神经网络中用sigmoid函数作为激活函数,进行信号的转换,转换后的信号被传送给下一个神经元。实际上,上一章介绍的感知机和接下来要介绍的神经网络的主要区别就在于这个激活函数。其他方面,比如神经元的多层连接的构造、信号的传递方法等,基本上和感知机是一样的。下面,让我们通过和阶跃函数的比较来详细学习作为激活函数的sigmoid函数。
2.2 阶跃函数的实现
阶跃函数如式③所示,当输入超过0时,输出1,否则输出0。可以像下面这样简单地实现阶跃函数。
def step_function(x):
if x > 0:
return 1
else:
return 0
这个实现简单、易于理解,但是参数x只能接受实数(浮点数)。也就是说,允许形如step_function(3.0)的调用,但不允许参数取NumPy数组,例如step_function(np.array([1.0, 2.0]))。为了便于后面的操作,我们把它修改为支持NumPy数组的实现。为此,可以考虑下述实现。
def step_function(x):
y = x > 0
return y.astype(np.int)
上述函数的内容只有两行。由于使用了NumPy中的“技巧”,可能会有点难理解。下面我们通过Python解释器的例子来看一下这里用了什么技巧。下面这个例子中准备了NumPy数组x,并对这个NumPy数组进行了不等号运算。
>>> import numpy as np
>>> x = np.array([-1.0, 1.0, 2.0])
>>> x
array([-1., 1., 2.])
>>> y = x > 0
>>> y
array([False, True, True], dtype=bool)
对NumPy数组进行不等号运算后,数组的各个元素都会进行不等号运算,生成一个布尔型数组。这里,数组x中大于0的元素被转换为True,小于等于0的元素被转换为False,从而生成一个新的数组y。数组y是一个布尔型数组,但是我们想要的阶跃函数是会输出int型的0或1的函数。因此,需要把数组y的元素类型从布尔型转换为int型。
>>> y = y.astype(np.int)
>>> y
array([0, 1, 1])
如上所示,可以用astype()方法转换NumPy数组的类型。astype()方法通过参数指定期望的类型,这个例子中是np.int型。Python中将布尔型转换为int型后,True会转换为1,False会转换为0。以上就是阶跃函数的实现中所用到的NumPy的“技巧”。
2.3 阶跃函数的图形
下面我们就用图来表示上面定义的阶跃函数,为此需要使用matplotlib库。
import numpy as np
import matplotlib.pylab as plt
def step_function(x):
return np.array(x > 0, dtype=np.int)
x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定y轴的范围
plt.show()
2.4 sigmoid函数的实现
def sigmoid(x):
return 1 / (1 + np.exp(-x))
这里,np.exp(-x)对应exp(−x)。
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定y轴的范围
plt.show()
2.5 sigmoid函数和阶跃函数的比较
相对于阶跃函数只能返回0或1,sigmoid函数可以返回0.731 . . .、0.880 . . .等实数(这一点和刚才的平滑性有关)。也就是说,感知机中神经元之间流动的是0或1的二元信号,而神经网络中流动的是连续的实数值信号。
接着说一下阶跃函数和sigmoid函数的共同性质。阶跃函数和sigmoid函数虽然在平滑性上有差异,但是如果从宏观视角看,可以发现它们具有相似的形状。实际上,两者的结构均是“输入小时,输出接近0(为0);随着输入增大,输出向1靠近(变成1)”。也就是说,当输入信号为重要信息时,阶跃函数和sigmoid函数都会输出较大的值;当输入信号为不重要的信息时,两者都输出较小的值。还有一个共同点是,不管输入信号有多小,或者有多大,输出信号的值都在0到1之间。
2.6 非线性函数
阶跃函数和sigmoid函数还有其他共同点,就是两者均为非线性函数。sigmoid函数是一条曲线,阶跃函数是一条像阶梯一样的折线,两者都属于非线性的函数。
神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神经网络的层数就没有意义了。
线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”。为了具体地(稍微直观地)理解这一点,我们来思考下面这个简单的例子。这里我们考虑把线性函数 h(x) = cx 作为激活函数,把y(x) = h(h(h(x)))的运算对应3层神经网络A。这个运算会进行y(x) = c × c × c × x的乘法运算,但是同样的处理可以由y(x) = ax(注意,a = c 3)这一次乘法运算(即没有隐藏层的神经网络)来表示。如本例所示,使用线性函数时,无法发挥多层网络带来的优势。因此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
2.7 ReLU函数
def relu(x):
return np.maximum(0, x)
3 多维数组的运算
如果掌握了NumPy多维数组的运算,就可以高效地实现神经网络。
3.1 多维数组
>>> import numpy as np
>>> A = np.array([1, 2, 3, 4])
>>> print(A)
[1 2 3 4]
>>> np.ndim(A)
1
>>> A.shape
(4,)
>>> A.shape[0]
4
数组的维数可以通过np.dim()函数获得。此外,数组的形状可以通过实例变量shape获得。在上面的例子中,A是一维数组,由4个元素构成。注意,这里的A.shape的结果是个元组(tuple)。这是因为一维数组的情况下也要返回和多维数组的情况下一致的结果。例如,二维数组时返回的是元组(4,3),三维数组时返回的是元组(4,3,2),因此一维数组时也同样以元组的形式返回结果。
>>> B = np.array([[1,2], [3,4], [5,6]])
>>> print(B)
[[1 2]
[3 4]
[5 6]]
>>> np.ndim(B)
2
>>> B.shape
(3, 2)
这里生成了一个3 × 2的数组B。3 × 2的数组表示第一个维度(从外往里数第一个中括号里面的内容)有3个元素,第二个维度有2个元素。另外,第一个维度对应第0维,第二个维度对应第1维(Python的索引从0开始)。二维数组也称为矩阵(matrix)。
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
在写文章的过程中我发觉我更多只是在复制搬运书本的内容,而这样的工作既累又不见得有所收获,而且失去了作为学习笔记的便捷、轻量,所以下面的内容我将选取我认为值得重点记忆的知识内容(不是说书本里的其他知识不值得记忆,只是每个人的记忆点不同,非常推荐大家都去看原书,对小白很有帮助)而不是根据书本的逻辑顺序一点点做笔记,所以内容可能会有点跳跃。
一般地,回归问题可以使用恒等函数,二元分类问题可以使用sigmoid函数,多元分类问题可以使用softmax函数。
恒等函数
softmax激活函数
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
上面的softmax函数的实现虽然正确描述了式①,但在计算机的运算上有一定的缺陷。这个缺陷就是溢出问题。softmax函数的实现中要进行指数函数的运算,但是此时指数函数的值很容易变得非常大。比如,e^10的值会超过20000,e100会变成一个后面有40多个0的超大值,e1000的结果会返回一个表示无穷大的inf。如果在这些超大值之间进行除法运算,结果会出现“不确定”的情况。
首先,式②在分子和分母上都乘上C这个任意的常数(因为同时对分母和分子乘以相同的常数,所以计算结果不变)。然后,把这个C移动到指数函数(exp)中,记为log C。最后,把log C替换为另一个符号C’。式②说明,在进行softmax的指数函数的运算时,加上(或者减去)某个常数并不会改变运算的结果。这里的C’可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。我们来看一个具体的例子。
>>> a = np.array([1010, 1000, 990])
>>> np.exp(a) / np.sum(np.exp(a)) # softmax函数的运算
array([ nan, nan, nan]) # 没有被正确计算
>>>
>>> c = np.max(a) # 1010
>>> a - c
array([ 0, -10, -20])
>>>
>>> np.exp(a - c) / np.sum(np.exp(a - c))
array([ 9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
如该例所示,通过减去输入信号中的最大值(上例中的c),我们发现原本为nan(not a number,不确定)的地方,现在被正确计算了。综上,我们可以像下面这样实现softmax函数。
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
使用softmax()函数,可以按如下方式计算神经网络的输出。
>>> a = np.array([0.3, 2.9, 4.0])
>>> y = softmax(a)
>>> print(y)
[ 0.01821127 0.24519181 0.73659691]
>>> np.sum(y)
1.0
softmax函数的输出值的总和是1。输出总和为1是softmax函数的一个重要性质。正因为有了这个性质,我们才可以把softmax函数的输出解释为“概率”。
比如,上面的例子可以解释成y[0]的概率是0.018(1.8 %),y[1]的概率是0.245(24.5 %),y[2]的概率是0.737(73.7 %)。从概率的结果来看,可以说“因为第2个元素的概率最高,所以答案是第2个类别”。而且,还可以回答“有74 %的概率是第2个类别,有25 %的概率是第1个类别,有1 %的概率是第0个类别”
这里需要注意的是,即便使用了softmax函数,各个元素之间的大小关系也不会改变。这是因为指数函数(y = exp(x))是单调递增函数。实际上,上例中a的各元素的大小关系和y的各元素的大小关系并没有改变。比如,a的最大值是第2个元素,y的最大值也仍是第2个元素。一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且,即便使用softmax函数,输出值最大的神经元的位置也不会变。因此,神经网络在进行分类时,输出层的softmax函数可以省略。在实际的问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的softmax函数一般会被省略。
求解机器学习问题的步骤可以分为“学习” 和“推理”两个阶段。首先,在学习阶段进行模型的学习,然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的softmax函数。在输出层使用softmax函数是因为它和神经网络的学习有关系(这个推理处理也称为神经网络的前向传播(forward propagation))。