学习笔记

第3章 神经网络

3.1 从感知机到神经网络

3.1.1 神经网络的例子
图 3-1中的网络一共由 3层神经元构成,但实质上只有 2层神经元有权重,因此将其称为“2层网络”。请注意,有的书也会根据构成网络的层数,把图 3-1的网络称为“3层网络”。本书将根据实质上拥有权重的层数(输入层、隐藏层、输出层的总数减去 1后的数量)来表示网络的名称。
3.1.2 复习感知机

3-2 中的感知机接收 x 1 x 2 两个输入信号,输出 y 。如果用数学式来表示图 3-2 中的感知机,则如式( 3 . 1 )所示。
b是被称为偏置的参数,用于控制神经元被激活的容易程度;而w1和w2是表示各个信号的权重的参数,用于控制各个信号的重要性。
顺便提一下,在图 3-2 的网络中,偏置 b并没有被画出来。 如果要明确 地表示出 b ,可以像图 3-3 那样做。图 3-3 中添加了权重为 b 的输入信号 1 。这 个感知机将 x 1 x 2 1 三个信号作为神经元的输入,将其和各自的权重相乘后, 传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。如果 这个总和超过 0 ,则输出 1 ,否则输出 0 。另外,由于偏置的输入信号一直是 1 所以为了区别于其他神经元,我们在图中把这个神经元整个涂成灰色。
现在将式( 3 . 1 )改写成更加简洁的形式。为了简化式( 3 . 1 ),我们用一个函数来表示这种分情况的动作(超过 0 则输出 1 ,否则输出 0 )。 引入新函数h(x) ,将式( 3 . 1 )改写成下面的式( 3 . 2 )和式( 3 . 3 )。
式( 3 . 2 )中,输入信号的总和会被函数 h ( x ) 转换,转换后的值就是输出 y 。然后,式( 3 . 3 )所表示的函数 h ( x ) ,在输入超过 0 时返回 1 ,否则返回 0 。因此,式( 3 . 1 )和式( 3 . 2 )、式( 3 . 3 )做的是相同的事情。
3.1.3 激活函数登场
刚才登场的 h x )函数会将输入信号的总和转换为输出信号,这种函数一般称为 激活函数(activation function) 。如“激活”一词所示,激活函数的作用在于决定如何来激活输入信号的总和。
现在来进一步改写式( 3 . 2 )。式( 3 . 2 )分两个阶段进行处理,先计算输入信号的加权总和,然后用激活函数转换这一总和。因此,如果将式( 3 . 2 )写得详细一点,则可以分成下面两个式子。
首先,式( 3 . 4 )计算加权输入信号和偏置的总和,记为 a 。然后,式( 3 . 5 )用 h () 函数将 a 转换为输出 y 。之前的神经元都是用一个○表示的,如果要在图中明确表示出式( 3 . 4 )和式( 3 . 5 ),则可以像图 3-4 这样做。
如图 3-4 所示,表示神经元的○中明确显示了激活函数的计算过程,即信号的加权总和为节点 a ,然后节点 a 被激活函数 h () 转换成节点 y 本书中,“神经元”和“节点”两个术语的含义相同 。这里,我们称 a y 为“节点”,其实它和之前所说的“神经元”含义相同。
通常如图 3-5 的左图所示,神经元用一个○表示。本书中,在可以明确神经网络的动作的情况下,将在图中明确显示激活函数的计算过程,如图 3 - 5的右图所示。
下面,我们将仔细介绍激活函数。激活函数是连接感知机和神经网络的桥梁。 A
本书在使用“感知机”一词时,没有严格统一它所指的算法。一般而言, “朴素感知机”是指单层网络, 指的是激活函数使用了 阶跃函数 的模型。“ 多层感知机”是指神经网络, 即使用 sigmoid
函数 (后述)等平滑的激活函数的多层网络。

3.2 激活函数

式( 3 . 3 )表示的激活函数以阈值为界,一旦输入超过阈值,就切换输出。神经网络中经常使用的一个激活函数就是式( 3 . 6 )表示的这样的函数称为“阶跃函数”。
因此,可以说感知机中使用了阶跃函数作为激活函数 。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。那么,如果感知机使用其他函数作为激活函数的话会怎么样呢? 实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。 下面我们就来介绍一下神经网络使用的激活函数。
阶跃函数是众多激活函数中的一种,感知机使用阶跃函数,不使用阶跃函数作为激活函数就不再是感知机而认为是神经网络。
3.2.1 sigmoid函数
神经网络中用 sigmoid函数作为激活函数,进行信号的转换,转换后的 信号被传送给下一个神经元。实际上,上一章介绍的感知机和接下来要介绍 的神经网络的主要区别就在于这个激活函数。其他方面,比如神经元的多层 连接的构造、信号的传递方法等,基本上和感知机是一样的。下面,让我们 通过和阶跃函数的比较来详细学习作为激活函数的 sigmoid 函数。
3.2.2 阶跃函数的实现
这里我们试着用 Python 画出阶跃函数的图(从视觉上确认函数的形状对理解函数而言很重要)。阶跃函数如式( 3 . 3 )所示,当输入超过 0 时,输出 1 ,否则输出 0 。可以像下面这样简单地实现阶跃函数。
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

def step_function(x):
    y = x > 0
    return y.astype(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.xlabel("x")  # x轴的标签
plt.ylabel("y")  # y轴的标签
plt.title("step_function")

plt.show()
3.2.3 阶跃函数的图形

3.2.4 sigmoid函数的实现
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))    

X = np.arange(-5.0, 5.0, 0.1)
Y = sigmoid(X)
plt.plot(X, Y)
plt.ylim(-0.1, 1.1)

plt.xlabel("x")  # x轴的标签
plt.ylabel("y")  # y轴的标签
plt.title("sigmoid_function")
plt.show()

3.2.5 sigmoid函数和阶跃函数的比较
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))    

def step_function(x):
    return np.array(x > 0, dtype=int)

x = np.arange(-5.0, 5.0, 0.1)
y1 = sigmoid(x)
y2 = step_function(x)

plt.plot(x, y1)
plt.plot(x, y2, 'k--')
plt.ylim(-0.1, 1.1) #指定图中绘制的y轴的范围
plt.show()

3 - 8  阶跃函数与 sigmoid 函数(虚线是阶跃函数)
现在我们来比较一下 sigmoid 函数和阶跃函数,如图 3-8 所示。两者的不同点在哪里呢?又有哪些共同点呢?我们通过观察图 3-8 来思考一下。观察图 3-8 ,首先注意到的是 “平滑性”的不同 sigmoid 函数是一条平滑的曲线,输出随着输入发生连续性的变化。而阶跃函数以 0 为界,输出发生急剧性的变化。 sigmoid函数的平滑性对神经网络的学习具有重要意义。
另一个不同点是,相对于阶跃函数只能返回 0 1 sigmoid 函数可以返回 0 . 731 ... 0 . 880 ... 等实数(这一点和刚才的平滑性有关)。 也就是说,感知机中神经元之间流动的是0或1的二元信号,而神经网络中流动的是连续的实数值信号。
如果把这两个函数与水联系起来,则阶跃函数可以比作“竹筒敲石” A ,sigmoid 函数可以比作“水车”。阶跃函数就像竹筒敲石一样,只做是否传送水( 0 1 )两个动作,而 sigmoid 函数就像水车一样,根据流过来的水量相应地调整传送出去的水量。
接着说一下阶跃函数和 sigmoid 函数的共同性质。阶跃函数和 sigmoid函数虽然在平滑性上有差异,但是如果从宏观视角看图 3-8 ,可以发现它们 具有相似的形状 。实际上 两者的结构均是“输入小时,输出接近0(为0);随着输入增大,输出向1靠近(变成1) ”。也就是说,当输入信号为重要信息时,阶跃函数和 sigmoid 函数都会输出较大的值;当输入信号为不重要的信息时,两者都输出较小的值。还有一个共同点是,不管输入信号有多小,或者有多大,输出信号的值都在 0 1 之间。
3.2.6 非线性函数
阶跃函数和 sigmoid 函数还有其他共同点,就是两者均为 非线性函数 。sigmoid 函数是一条曲线,阶跃函数是一条像阶梯一样的折线,两者都属于非线性的函数。
神经网络的激活函数必须使用非线性函数 。换句话说,激活函数不能使用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神经网络的层数就没有意义了。
3.2.7 ReLU函数
在神经网络发展的历史上, sigmoid 函数很早就开始被使用了,而最近则主要使用 ReLU Rectified Linear Unit )函数。ReLU 函数在输入大于 0 时,直接输出该值;在输入小于等于 0 时,输出 0 (图 3-9 )。
ReLU 函数可以表示为下面的式 (3 . 7)
这里使用了NumPy的maximum函数。maximum函数会从输入的数值中选择较大的那个值进行输出。
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

def relu(x):
    return np.maximum(0, x)

x = np.arange(-6.0, 6.0, 0.1)
y = relu(x)

plt.plot(x, y,'b')
plt.ylim(-1, 5)
plt.xlabel("x")  # x轴的标签
plt.ylabel("y")  # y轴的标签
plt.title("relu function")
plt.show()

3-9 ReLU函数

3.3 多维数组的运算

3.3.1 多维数组
简单地讲,多维数组就是“数字的集合”,数字排成一列的集合、排成长方形的集合、排成三维状或者(更加一般化的) N 维状的集合都称为多维数组。
import numpy as np
A = np.array([1, 2, 3, 4])
print('A = ', A)
b = np.ndim(A)
print('b = ',b)
c = A.shape
print('c = ',c)
print(A.shape[0])
A =  [1 2 3 4]
b =  1
c =  (4,)
4
如上所示,数组的维数可以通过 np.dim() 函数获得。此外,数组的形状可以通过实例变量 shape 获得。在上面的例子中, A 是一维数组,由 4 个元素构成。注意,这里的 A.shape 的结果是个元组( tuple )。这是因为一维数组的情况下也要返回和多维数组的情况下一致的结果。例如,二维数组时返回的是元组 (4,3) ,三维数组时返回的是元组 (4,3,2) ,因此一维数组时也同样以元组的形式返回结果。
3.3.2 矩阵乘法

A B 都 是 2 × 2 的 矩 阵,它 们 的 乘 积 可 以 通 过 NumPy 的np.dot() 函数计算(乘积也称为点积)。 np.dot() 接收两个 NumPy 数组作为参数,并返回数组的乘积。这里 要注意的是,np.dot(A, B)和np.dot(B, A)的值可能不一样 。和一般的运算( + * 等)不同,矩阵的乘积运算中,操作数( A 、B )的顺序不同,结果也会不同。
在多维数组的乘积运算中,必须使两个矩阵中的对应维度的元素个数一致,这一点很重要。
3.3.3 神经网络的内积

>>> X = np.array([1, 2])
>>> X.shape
(2,)
>>> W = np.array([[1, 3, 5], [2, 4, 6]])
>>> print(W)
[[1 3 5]
 [2 4 6]]
>>> W.shape
(2, 3)
>>> Y = np.dot(X, W)
>>> print(Y)
[ 5 11 17]
如上所示, 使用np.dot(多维数组的点积),可以一次性计算出Y 的结果 。这意味着,即便 Y 的元素个数为 100 1000 ,也可以通过一次运算就计算出结果!如果不使用 np.dot ,就必须单独计算 Y 的每一个元素(或者说必须使用 for 语句),非常麻烦。 因此,通过矩阵的乘积一次性完成计算的技巧,在实现的层面上可以说是非常重要的。

3.4 3层神经网络的实现

3.4.1 符号确认

3.4.2 各层间信号传递的实现

此外,如果使用矩阵的乘法运算,则可以将第 1 层的加权和表示成下面的式( 3 . 9 )。
下面我们用 NumPy 多维数组来实现式( 3 . 9 ),这里将输入信号、权重、偏置设置成任意值。
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)
A1 = np.dot(X, W1) + B1
这个运算和上一节进行的运算是一样的。 W1是2 × 3的数组,X是元素个 数为2的一维数组。 这里, W1 X 的对应维度的元素个数也保持了一致。
接下来,我们观察第 1层中激活函数的计算过程。如果把这个计算过程用图来表示的话,则如图 3-18 所示。
如图 3-18 所示,隐藏层的加权和(加权信号和偏置的总和)用 a表示,被 激活函数转换后的信号用 z 表示。此外,图中 h ()表示激活函数,这里我们 使用的是 sigmoid 函数。用 Python 来实现,代码如下所示。
def sigmoid(x):
    return 1/(1+np.exp(-x))

Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0.57444252, 0.66818777, 0.75026011]
这个 sigmoid() 函数就是之前定义的那个函数。它会接收 NumPy数组, 并返回元素个数相同的 NumPy 数组。
下面,我们来实现第 1 层到第 2 层的信号传递(图 3-19 )。
B2 = np.array([0.1, 0.2])
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
print(Z1.shape) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
print("A2 = ", A2) # A2 =  [0.51615984 1.21402696]

print("Z2 = ", Z2) # Z2 =  [0.62624937 0.7710107 ]
除了第 1 层的输出( Z1 )变成了第 2层的输入这一点以外,这个实现和刚 才的代码完全相同。由此可知,通过使用 NumPy数组,可以将层到层的信 号传递过程简单地写出来。
A2 = np.dot(Z1, W2) + B2
最后是第 2 层到输出层的信号传递(图 3-20 )。输出层的实现也和之前的
实现基本相同。不过,最后的激活函数和之前的隐藏层有所不同。
def identity_function(x):
 return x
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3) # 或者Y = A3
这里我们定义了 identity_function()函数(也称为“恒等函数”) ,并将其作为输出层的激活函数。恒等函数会将输入按原样输出,因此,这个例子中没有必要特意定义 identity_function() 。这里这样实现只是为了和之前的流程保持统一。另外,图 3-20 中, 输出层的激活函数用σ()表示,不同于隐
藏层的激活函数h()(σ读作sigma)
输出层所用的激活函数,要根据求解问题的性质决定。 一般地,回归问题可以使用恒等函数,二元分类问题可以使用 sigmoid函数多元分类问题可以使用 softmax函数 。关于输出层的激活函数,我们将在下一节详细介绍。
3.4.3 代码实现小结
import numpy as np


def sigmoid(x):
    return 1 / (1 + np.exp(-x))


def identity_function(x):
    return x


def init_network():
    network = {}
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    return network


def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = identity_function(a3)
    return y


network = init_network()

x = np.array([1.0, 0.5])

y = forward(network, x)
print(y)  # [ 0.31682708 0.69627909]
这里 定义了init_network()和forward()函数 init_network() 函数会进行权重和偏置的初始化,并将它们保存在字典变量 network 中。这个字典变量 network 中保存了每一层所需的参数(权重和偏置)。 forward()函数中则封装了将输入信号转换为输出信号的处理过程
另外,这里出现了 forward(前向 )一词,它表示的是从输入到输出方向的传递处理。后面在进行神经网络的训练时,我们将介绍 后向(backward,从输出到输入方向) 的处理。
至此,神经网络的前向处理的实现就完成了。通过巧妙地使用 NumPy多维数组,我们高效地实现了神经网络。

3.5 输出层的设计

神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数,分类问题用softmax函数。
机器学习的问题大致可以分为分类问题和回归问题。分类问题是数据属于哪一个类别的问题。比如,区分图像中的人是男性还是女性的问题就是分类问题。而回归问题是根据某个输入预测一个(连续的)数值的问题。比如,根据一个人的图像预测这个人的体重的问题就是回归问题(类似“57.4kg”这样的预测)。
3.5.1 恒等函数和 softmax函数
恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直接输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出。
另外,将恒等函数的处理过程用之前的神经网络图来表示的话,则如图3-21所示。和前面介绍的隐藏层的激活函数一样,恒等函数进行的转换处理可以用一根箭头来表示。
分类问题中使用的 softmax函数 可以用下面的式( 3 . 10 )表示。
exp( x ) 是表示 e^{x} 的指数函数( e 是纳皮尔常数 2 . 7182 ... )。式( 3 . 10)表示假设输出层共有 n 个神经元,计算第 k 个神经元的输出 y_{k} 。如式( 3 . 10)所示,softmax 函数的分子是输入信号 a_{k} 的指数函数,分母是所有输入信号的指数函数的和。
用图表示 softmax 函数的话,如图 3-22 所示。图 3-22 中, softmax函数的输出通过箭头与所有的输入信号相连。这是因为,从式( 3 . 10)可以看出,输出层的各个神经元都受到所有输入信号的影响。
现在我们来实现 softmax 函数。在这个过程中,我们将使用 Python解释器逐一确认结果。
>>> a = np.array([0.3, 2.9, 4.0])
>>>
>>> exp_a = np.exp(a) # 指数函数
>>> print(exp_a)
[ 1.34985881 18.17414537 54.59815003]
>>>
>>> sum_exp_a = np.sum(exp_a) # 指数函数的和
>>> print(sum_exp_a)
74.1221542102
>>>
>>> y = exp_a / sum_exp_a
>>> print(y)
[ 0.01821127 0.24519181 0.73659691]
这个 Python 实现是完全依照式( 3 . 10 )进行的,所以不需要特别的解释。
考虑到后面还要使用 softmax 函数,这里我们把它定义成如下的 Python 函数。
def softmax(a):
     exp_a = np.exp(a)
     sum_exp_a = np.sum(exp_a)
     y = exp_a / sum_exp_a
     return y

a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
print('y = ', y) # y =  [0.01821127 0.24519181 0.73659691]
3.5.2 实现 softmax函数时的注意事项
上面的 softmax 函数的实现虽然正确描述了式( 3 . 10 ),但在计算机的运算上有一定的缺陷。这个 缺陷就是溢出问题 softmax 函数的实现中要进行指数函数的运算,但是此时指数函数的值很容易变得非常大。比如, e^{10} 的值会超过 20000 e^{100} 会变成一个后面有 40 多个 0 超大值, e^{1000} 的结果会返回一个表示无穷大的 inf 。如果在这些超大值之间进行除法运算,结果会出现“不确定”的情况。
计算机处理“数”时,数值必须在 4字节或 8字节的有限数据宽度内。这意味着数存在有效位数,也就是说,可以表示的数值范围是有限的。因此,会出现超大值无法表示的问题。这个问题称为 溢出 ,在进行计算机的运算时必须(常常)注意。
softmax 函数的实现可以像式( 3 . 11 )这样进行改进。
首先,式( 3 . 11 )在分子和分母上都乘上 C 这个任意的常数(因为同时对分母和分子乘以相同的常数,所以计算结果不变)。然后,把这个 C 移动到指数函数( exp )中,记为 log C 。最后,把 log C 替换为另一个符号 C^{'} 。式( 3 . 11 )说明,在进行 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 函数。其中用到了 max()函数
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
3.5.3 softmax函数的特征

使用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函数的输出是0.0到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函数,我们可以用概率的(统计的)方法处理问题。

这里需要注意的是,即便使用了softmax函数,各个元素之间的大小关系也不会改变。这是因为指数函数(y = exp(x))是单调递增函数。实际上,上例中a的各元素的大小关系和y的各元素的大小关系并没有改变。比如,a的最大值是第2个元素,y的最大值也仍是第2个元素。

一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且,即便使用softmax函数,输出值最大的神经元的位置也不会变。因此,神经网络在进行分类时,输出层的softmax函数可以省略。在实际的问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的softmax函数一般会被省略。

求解机器学习问题的步骤可以分为“学习”和“推理”两个阶段。首先,在学习阶段进行模型的学习,然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的 softmax函数。在输出层使用 softmax函数是因为它和神经网络的学习有关系(详细内容请参考下一章)。

3.5.4 输出层的神经元数量

输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测是图中的数字0到9中的哪一个的问题(10类别分类问题),可以像图3-23这样将输出层的神经元设定为10个。

如图3-23所示,在这个例子中,输出层的神经元从上往下依次对应数字0, 1, ..., 9。此外,图中输出层的神经元的值用不同的灰度表示。这个例子中神经元y2颜色最深,输出的值最大。这表明这个神经网络预测的是y2对应的类别,也就是“2”。

3.6 手写数字识别

介绍完神经网络的结构之后,现在我们来试着解决实际问题。这里我们来进行手写数字图像的分类。假设学习已经全部结束,我们使用学习到的参数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向传播(forward propagation)

和求解机器学习问题的步骤(分成学习和推理两个阶段进行)一样,使用神经网络解决问题时,也需要首先使用训练数据(学习数据)进行权重参数的学习;进行推理时,使用刚才学习到的参数,对输入数据进行分类。

3.6.1 MNIST数据集

这里使用的数据集是MNIST手写数字图像集。MNIST是机器学习领域最有名的数据集之一,被应用于从简单的实验到发表的论文研究等各种场合。实际上,在阅读图像识别或机器学习的论文时,MNIST数据集经常作为实验用的数据出现。

MNIST数据集是由0到9的数字图像构成的(图3-24)。训练图像有6万张,测试图像有1万张,这些图像可以用于学习和推理。MNIST数据集的一般使用方法是,先用训练图像进行学习,再用学习到的模型度量能在多大程度上对测试图像进行正确的分类

MNIST的图像数据是28像素 × 28像素的灰度图像(1通道),各个像素的取值在0到255之间。每个图像数据都相应地标有“7”“2”“1”等标签。

以下是Python脚本mnist.py,该脚本支持从下载MNIST数据集到将这些数据转换成NumPy数组等处理(mnist.py在dataset目录下)。使用mnist.py时,当前目录必须是ch01、ch02、ch03、…、ch08目录中的一个。使用mnist.py中的load_mnist()函数,就可以按下述方式轻松读入MNIST数据。

import sys, os 
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定 
from dataset.mnist import load_mnist 
# 第一次调用会花费几分钟 …… 
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, 
normalize=False) 
# 输出各个数据的形状 
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,) 
print(x_test.shape) # (10000, 784) 
print(t_test.shape) # (10000,)

首先,为了导入父目录中的文件,进行相应的设定。然后,导入dataset/mnist.py中的 load_mnist函数。最后,使用 load_mnist函数,读入MNIST数据集。第一次调用load_mnist函数时,因为要下载MNIST数据集,所以需要接入网络。第2次及以后的调用只需读入保存在本地的文件(pickle文件)即可,因此处理所需的时间非常短。

用来读入MNIST图像的文件在本书提供的源代码的dataset目录下。并且,我们假定了这个MNIST数据集只能从ch01、ch02、ch03、…、ch08目录中使用,因此,使用时需要从父目录(dataset目录)中导入文件,为此需要添加sys.path.append(os.pardir)语句。

load_mnist函数以“(训练图像 ,训练标签 ),(测试图像,测试标签 )”的形式返回读入的MNIST数据。此外,还可以像load_mnist(normalize=True,flatten=True, one_hot_label=False) 这 样,设 置 3 个 参 数。第 1 个参数normalize设置是否将输入图像正规化为0.0~1.0的值。如果将该参数设置为False,则输入图像的像素会保持原来的0~255。第2个参数flatten设置是否展开输入图像(变成一维数组)。如果将该参数设置为False,则输入图像为1 × 28 × 28的三维数组;若设置为True,则输入图像会保存为由784个元素构成的一维数组。第3个参数one_hot_label设置是否将标签保存为onehot表示(one-hot representation)。one-hot表示是仅正确解标签为1,其余皆为0的数组,就像[0,0,1,0,0,0,0,0,0,0]这样。当one_hot_label为False时,只是像7、2这样简单保存正确解标签;当one_hot_label为True时,标签则保存为one-hot表示。

Python有 pickle这个便利的功能。这个功能可以将程序运行中的对象保存为文件。如果加载保存过的 pickle文件,可以立刻复原之前程序运行中的对象。用于读入MNIST数据集的load_mnist()函数内部也使用了 pickle功能(在第 2次及以后读入时)。利用 pickle功能,可以高效地完成MNIST数据的准备工作。

也可以使用tensorflow框架下的自带数据集加载:

from tensorflow.keras.datasets import mnist

# 加载数据集(返回四个变量:训练数据、标签、测试数据、测试标签)
(x_train, t_train), (x_test, t_test) = mnist.load_data()
# 可能需要几分钟时间
# 输出各个数据的形状
print(x_train.shape)  # (60000, 784)
print(t_train.shape)  # (60000,)
print(x_test.shape)  # (10000, 784)
print(t_test.shape)  # (10000,)

现在,我们试着显示MNIST图像,同时也确认一下数据。图像的显示使用PIL(Python Image Library)模块。执行下述代码后,训练图像的第一张就会显示出来,如图3-25所示(源代码在ch03/mnist_show.py中)。

dataset 是文件夹, mnist 是文件
load_mnist 是函数
意思是从 mnist 文件中导入 load_mnist函数
import numpy as np
from PIL import Image
from tensorflow.keras.datasets import mnist


def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()


# 加载数据集(返回四个变量:训练数据、标签、测试数据、测试标签)
(x_train, t_train), (x_test, t_test) = mnist.load_data()
img = x_train[0]
label = t_train[0]
print(label)  # 5
print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 把图像的形状变成原来的尺寸
print(img.shape)  # (28, 28)
img_show(img)

mnist显示优化:

# 鱼书第三章 mnist_show.py的优化
import sys
from tensorflow.keras.datasets import mnist
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical

def visualize_image(image, label):
    """专业图像可视化函数"""
    plt.figure(figsize=(6, 6))
    plt.imshow(image.reshape(28, 28), cmap='gray', vmin=0, vmax=1)
    plt.title(f"Digit: {label}", fontsize=14, color='blue')
    plt.colorbar(label='Intensity', ticks=[0, 0.5, 1], orientation='horizontal')
    plt.show()


def main():
    """主程序入口"""
    # 加载数据
    (x_train, y_train), (x_test, y_test) = mnist.load_data()

    # 可视化第一个样本
    first_image = x_train[0]
    first_label = y_train[0]
    print(f"\n正在显示第1个训练样本:")
    print(f"标签: {first_label}")
    print(f"图像形状: {first_image.shape}\n")

    visualize_image(first_image, first_label)

    # 数据预处理(可选)
    x_train = x_train.astype('float32') / 255.0
    y_train = to_categorical(y_train)


if __name__ == "__main__":
    main()

这里需要注意的是,flatten=True时读入的图像是以一列(一维)NumPy数组的形式保存的。因此,显示图像时,需要把它变为原来的28像素 × 28像素的形状。可以通过reshape()方法的参数指定期望的形状,更改NumPy数组的形状。此外,还需要把保存为NumPy数组的图像数据转换为PIL用的数据对象,这个转换处理由Image.fromarray()来完成

3.6.2 神经网络的推理处理

下面,我们对这个MNIST数据集实现神经网络的推理处理。神经网络的输入层有784个神经元,输出层有10个神经元。输入层的784这个数字来源于图像大小的28 × 28 = 784,输出层的10这个数字来源于10类别分类(数字0到9,共10类别)。此外,这个神经网络有2个隐藏层,第1个隐藏层有50个神经元,第2个隐藏层有100个神经元。这个50和100可以设置为任何值。下面我们先定义get_data()、init_network()、predict()这3个函数(代码在ch03/neuralnet_mnist.py中)。

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import pickle

#从dataset/mnist文件夹导入load_minst函数
from dataset.mnist import load_mnist 
from common.functions import sigmoid, softmax 
#从common/functions文件夹导入sigmoid和softmax函数


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test

#用init_network()读入保存在pickle文件sample_weight.pkl中的学习到的权重参数A
def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()
accuracy_cnt = 0
#用for语句逐一取出保存在x中的图像数据,用predict()函数进行分类
for i in range(len(x)):  
    y = predict(network, x[i])
    p= np.argmax(y)      # 用np.argmax(x)函数取出数组中的最大值的索引
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
#Accuracy:0.9352
init_network() 会读入保存在 pickle 文件 sample_weight.pkl 中的学习到的权重参数 A 。这个文件中以字典变量的形式保存了权重和偏置参数。剩余的 2个函数,和前面介绍的代码实现基本相同,无需再解释。现在,我们用这 3个函数来实现神经网络的推理处理。然后,评价它的 识别精度 accuracy ),即能在多大程度上正确分类。
首先获得 MNIST 数据集,生成网络。接着,用 for 语句逐一取出保存在 x 中的图像数据,用 predict() 函数进行分类。 predict() 函数以 NumPy 数组的形式输出各个标签对应的概率。比如输出 [0.1, 0.3, 0.2, ..., 0.04] 的数组,该数组表示“ 0 ”的概率为 0 . 1 ,“ 1 ”的概率为 0 . 3 ,等等。然后,我们取出这个概率列表中的最大值的索引(第几个元素的概率最高),作为预测结果。可以用 np.argmax(x) 函数取出数组中的最大值的索引, np.argmax(x)将获取被赋给参数x的数组中的最大值元素的索引 。最后,比较神经网络所预测的答案和正确解标签,将回答正确的概率作为识别精度。
执行上面的代码后,会显示“ Accuracy:0 . 9352 ”。这表示有 93 . 52 % 的数据被正确分类了。目前我们的目标是运行学习到的神经网络,所以不讨论识别精度本身,不过 以后我们会花精力在神经网络的结构和学习方法上,思考如何进一步提高这个精度 。实际上,我们打算把精度提高到 99 % 以上。
另外,在这个例子中,我们把 load_mnist 函数的参数 normalize 设置成了True 。将 normalize 设置成 True 后,函数内部会进行转换,将图像的各个像素值除以 255 ,使得数据的值在 0 . 0 1 . 0 的范围内。 像这样把数据限定到某个范围内的处理称为正规化(normalization )。此外,对神经网络的输入数据进行某种既定的转换称为 预处理(pre-processing )。这里,作为对输入图像的一种预处理,我们进行了正规化。
预处理在神经网络(深度学习)中非常实用 ,其有效性已在提高识别性能和学习的效率等众多实验中得到证明。在刚才的例子中,作为一种预处理,我们将各个像素值除以 255,进行了简单的正规化。实际上,很多预处理都会考虑到数据的整体分布。比如, 利用数据整体的均值或标准差,移动数据,使数据整体以 0为中心分布,或者进行正规化,把数据的延展控制在一定范围内 。除此之外,还有将数据整体的分布形状均匀化的方法,即数据 白化 (whitening)等。
3.6.3 批处理
以上就是处理 MNIST 数据集的神经网络的实现,现在我们来关注输入数据和权重参数的“形状”。再看一下刚才的代码实现。
下面我们使用 Python解释器 ,输出刚才的神经网络的各层的权重的形状。
>>> x, _ = get_data()
>>> network = init_network()
>>> W1, W2, W3 = network['W1'], network['W2'], network['W3']
>>>
>>> x.shape
(10000, 784)
>>> x[0].shape
(784,)
>>> W1.shape
(784, 50)
>>> W2.shape
(50, 100)
>>> W3.shape
(100, 10)
我们通过上述结果来确认一下多维数组的对应维度的元素个数是否一致(省略了偏置)。用图表示的话,如图 3-26 所示。可以发现,多维数组的对应维度的元素个数确实是一致的。此外,我们还可以确认最终的结果是输出了元素个数为 10 的一维数组。
从整体的处理流程来看,图 3-26 中, 输入一个由784个元素(原本是一个28 × 28的二维数组) 构成的一维数组后, 输出一个有10个元素的一维数组 。这是只输入一张图像数据时的处理流程。
现在我们来考虑 打包输入多张图像的情形 。比如,我们想用 predict()函数一次性打包处理 100 张图像。为此,可以 x的形状改为100 × 784 ,将100 张图像打包作为输入数据。用图表示的话,如图 3-27 所示。

如图 3-27 所示,输入数据的形状为 100 × 784输出数据的形状为100 × 10。这表示输入的100张图像的结果被一次性输出了。比如,x[0]和y[0]中保存了第0张图像及其推理结果,x[1]y[1]中保存了第1张图像及其推理结果,等等。

这种打包式的输入数据称为 批(batch) 。批有“捆”的意思,图像就如同纸币一样扎成一捆。
批处理对计算机的运算大有利处,可以大幅缩短每张图像的处理时间。 那么为什么批处理可以缩短处理时间呢?这是因为大多数处理数值计算的库都进行了能够高效处理大型数组运算的最优化。并且, 在神经网络的运算中,当数据传送成为瓶颈时,批处理可以减轻数据总线的负荷 (严格地讲,相对于数据读入,可以将更多的时间用在计算上)。也就是说,批处理一次性计算大型数组要比分开逐步计算各个小型数组速度更快。
下面我们进行基于批处理的代码实现。这里用粗体显示与之前的实现的不同之处。
x, t = get_data()
network = init_network()
batch_size = 100 # 批数量
accuracy_cnt = 0
for i in range(0, len(x), batch_size) :
x_batch = x[i:i+batch_size]
y_batch = predict(network, x_batch)
p = np.argmax(y_batch, axis=1)
accuracy_cnt += np.sum(p == t[i:i+batch_size])
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
我们来逐个解释粗体的代码部分。首先是 range() 函数。 range()函数若指定为range(start, end) ,则会生成一个由 start end-1 之间的整数构成的列表。若像 range(start, end, step)这样指定3个整数,则生成的列表中的下一个元素会增加step指定的值 。我们来看一个例子。
>>> list( range(0, 10) )
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list( range(0, 10, 3) )
[0, 3, 6, 9]
range() 函数生成的列表的基础上, 通过x[i:i+batch_size]从输入数据中抽出批数据。x[i:i+batch_n]会取出从第i个到第i+batch_n个之间的数据。 本例中是像 x[0:100] x[100:200] ……这样,从头开始以 100 为单位将数据提取为批数据。
然后, 通过argmax()获取值最大的元素的索引 。不过这里需要注意的是,我们给定了参数 axis=1 。这指定了在 100 × 10 的数组中,沿着第 1 维方向(以第 1 维为轴)找到值最大的元素的索引(第 0 维对应第 1 个维度)A 。这里也来看一个例子。
>>> x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6],
... [0.2, 0.5, 0.3], [0.8, 0.1, 0.1]])
>>> y = np.argmax(x, axis=1)
>>> print(y)
[1 2 1 0]
最后,我们比较一下以批为单位进行分类的结果和实际的答案。为此,需要在 NumPy 数组之间使用比较运算符( == )生成由 True/False 构成的布尔型数组,并计算 True 的个数。我们通过下面的例子进行确认。
>>> y = np.array([1, 2, 1, 0])
>>> t = np.array([1, 2, 0, 0])
>>> print(y==t)
[True True False True]
>>> np.sum(y==t)
3
至此,基于批处理的代码实现就介绍完了。 使用批处理,可以实现高速且高效的运算 。下一章介绍神经网络的学习时,我们将把图像数据作为打包的批数据进行学习,届时也将进行和这里的批处理一样的代码实现。
3.7 小结
本章介绍了神经网络的前向传播。本章介绍的神经网络和上一章的感知机在信号的按层传递这一点上是相同的,但是, 向下一个神经元发送信号时,改变信号的激活函数有很大差异 。神经网络中使用的是平滑变化的 sigmoid函数,而感知机中使用的是信号急剧变化的阶跃函数。这个差异对于神经网络的学习非常重要,我们将在下一章介绍。

本章所学的内容

• 神经网络中的激活函数使用平滑变化的sigmoid函数或ReLU函数。

• 通过巧妙地使用NumPy多维数组,可以高效地实现神经网络。

• 机器学习的问题大体上可以分为回归问题和分类问题。

• 关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中一般用softmax函数。

• 分类问题中,输出层的神经元的数量设置为要分类的类别数。

• 输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速的运算。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐