第三章——神经网络
上一章我们学习了感知机。关于感知机,既有好消息,也有坏消息。好消息是,即便对于复杂的函数,感知机也隐含着能够表示它的可能性。上一章已经介绍过,即便是计算机进行的复杂处理,感知机(理论上)也可以将其表示出来。坏消息是,设定权重的工作,即确定合适的、能符合预期的输入与输出的权重,现在还是由人工进行的。上一章中,我们结合与门、或门的真值表人工决定了合适的权重。神经网络的出现就是为了解决刚才的坏消息。具体地讲,神经网络的一个重要性质是它可以自动地从数据中学习到合适的权重参数。本章中,我们会先介绍神经网络的概要,然后重点关注神经网络进行识别时的处理。在下一章中,我们将了解如何从数据中学习权重参数。
-
从感知机到神经网络
(1)神经网络的例子
神经网络分为输入层,中间层和输出层。中间层有时候也称为隐藏层,“隐藏”一词的意思是,隐藏层的神经元(和输入层、输出层不同)肉眼看不见。另外,本书中把输入层到输出层依次称为第0层、第1层、第2层(层号之所以从0开始,是为了方便后面基于Python进行实现)。
h(x)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)。如“激活”一词所示,激活函数的作用在于决定如何来激活输入信号的总和。
阶跃函数:一旦输入超过阈值,就切换输出。这样的函数称为“阶跃函数”。
如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。
-
激活函数
(1)sigmoid函数:h(x)=1/1+exp(-x)
其中exp(-x)表示e的-x次方,e为纳皮尔常数(自然数)。
(2)阶跃函数的实现
# 画一个阶跃函数图像 # 第一种实现 简单、易于理解,但 是参数x只能接受实数(浮点数),而不允许参数取Numpy数组 def step_function_1(x): if x > 0: return 1 else: return 0 # 第二种实现 def step_function_2(x): y = x > 0 return y.astype(np.int32) x = np.array([-1.0, 1.0, 2.0]) print(x) print(x > 0) # astype()方法通过参数指定期望的类型 ,这个例子中是np.int32型 # python中将布尔型转换为int型后,True会转换为1,False会转换为0 print(step_function_2(x))
可以用astype()方法转换NumPy数组的类型。astype()方法通过参数指定期望的类型,这个例子中是np.int型。Python中将布尔型转换为int型后,True会转换为1,False会转换为0。以上就是阶跃函数的实现中所用到的NumPy的“技巧”。
(3)阶跃函数的图形
# 阶跃函数的图形 def step_function_3(x): return np.array(x > 0, dtype=np.int32) x = np.arange(-5.0, 5.0, 0.1) y = step_function_3(x) plt.plot(x, y) plt.ylim(-0.1, 1.1) # 指定y轴的范围 plt.show()
(4)sigmoid函数的实现
# 阶跃函数的图形 def step_function_3(x): return np.array(x > 0, dtype=np.int32) x = np.arange(-5.0, 5.0, 0.1) y = step_function_3(x) plt.plot(x, y) plt.ylim(-0.1, 1.1) # 指定y轴的范围 plt.show()
(5)sigmoid函数和阶跃函数的比较
sigmoid函数是一条平滑的曲线,输出随着输入发生连续性的变化。而阶跃函数以0为界,输出发生急剧性的变化。sigmoid函数的平滑性对神经网络的学习具有重要意义。
另一个不同点是,相对于阶跃函数只能返回0或1,sigmoid函数可以返回0.731 ...、0.880 ...等实数(这一点和刚才的平滑性有关)。也就是说,感知机中神经元之间流动的是0或1的二元信号,而神经网络中流动的是连续的实数值信号。
接着说一下阶跃函数和sigmoid函数的共同性质。阶跃函数和sigmoid函数虽然在平滑性上有差异,但是如果从宏观视角看图3-8,可以发现它们具有相似的形状。实际上,两者的结构均是“输入小时,输出接近0(为0);随着输入增大,输出向1靠近(变成1)”。也就是说,当输入信号为重要信息时,阶跃函数和sigmoid函数都会输出较大的值;当输入信号为不重要的信息时,两者都输出较小的值。还有一个共同点是,不管输入信号有多小,或者有多大,输出信号的值都在0到1之间。
(6)非线性函数
神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神经网络的层数就没有意义了。 线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”。
使用线性函数时,无法发挥多层网络带来的优势。因此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
(7)ReLU函数
到目前为止,我们介绍了作为激活函数的阶跃函数和sigmoid函数。在神经网络发展的历史上,sigmoid函数很早就开始被使用了,而最近则主要使用ReLU函数(Rectified Linear Unit,线性整流函数)。
ReLU函数可以表示为h(x)=max(x,0)
# ReLU函数 def rely(x): return np.maximum(0, x) x = np.arange(-5.0, 5.0) y = rely(x) plt.plot(x, y) plt.show()
-
多维数组的运算
如果掌握了NumPy多维数组的运算,就可以高效地实现神经网络。
(1)多维数组
简单地讲,多维数组就是“数字的集合”,数字排成一列的集合、排成长方形的集合、排成三维状或者(更加一般化的)N维状的集合都称为多维数组。
# 多维数组 A = np.array([1, 2, 3, 4]) print(A) print(np.ndim(A)) # 获得A的维度 # shape属性的每一个元素表示在每一维的大小是多少 print(A.shape) # 获得A的形状,这里不管数组是多少维,都会返回一个元组 # 获得A在第0+1=1维的大小(python的索引从0开始) print(A.shape[0])
如上所示,数组的维数可以通过np.dim()函数获得。此外,数组的形状可以通过实例变量shape获得。在上面的例子中,A是一维数组,由4个元素构成。注意,这里的A.shape的结果是个元组(tuple)。
(2)矩阵乘法
矩阵的乘积是通过左边矩阵的行(横向)和右边矩阵的列(纵向)以对应元素的方式相乘后再求和而得到的。并且,运算的结果保存为新的多维数组的元素。
# 矩阵乘法 A = np.array([[1, 2], [3, 4], [5, 6]]) # 3行2列 B = np.array([[1, 2, 3, 4], [5, 6, 7, 8]]) # 2行4列 # 矩阵A的第1维度的元素个数(列数)必须和矩阵B的第0维的元素个数(行数)相等,否则报错(矩阵乘法无意义) C = np.dot(A, B) print(C) # 测试 A = np.array([[1, 2, 3], [4, 5, 6]]) # 2行3列 B = np.array([1, 2, 3]) # 1行2列 C = np.dot(A, B) # 在numpy中只有“一维”这个概念,一维的数组既可以是“行向量”,也可以是“列向量” # 所以这里的A和B可以相乘,并且结果为[14, 32],是一维的,它到底是“横着的”还是“竖着的”并不重要 print(C)
这 里,A 和 B 都 是 2 × 2 的 矩 阵,它 们 的 乘 积 可 以 通 过 NumPy 的np.dot()函数计算(乘积也称为点积)。np.dot()接收两个NumPy数组作为参数,并返回数组的乘积(运算规则同线性代数里的矩阵乘法)。这里需要注意的是矩阵的形状(shape)。具体地讲,矩阵A的第1维的元素个数(列数)必须和矩阵B的第0维的元素个数(行数)相等。
(3)神经网络的内积(点积)
# 神经网络的内积 # 一个简单的神经网络,有2个输入和3个输出 x = np.array([1, 2]) w = np.array([[1, 3, 5], [2, 4, 6]]) y = np.dot(x, w) print(y)
-
3层神经网络的实现
在代码实现方面,巧妙地使用上一节介绍的NumPy数组,可以用很少的代码完成神经网络的前向处理。
# 3层神经网络的实现 # 从第0层(输入层)到第1层 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]) A1 = np.dot(X, W1) + B1 # 加权和 Z1 = sigmoid(A1) # 用激活函数转换后的信号 print(A1) print(Z1) # 从第1层到第2层 W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) B2 = np.array([0.1, 0.2]) A2 = np.dot(Z1, W2) + B2 Z2 = sigmoid(A2) print(A2) print(Z2) # 从第2层到输出层(第3层) W3 = np.array([[0.1, 0.3], [0.2, 0.4]]) B3 = np.array([0.1, 0.2]) A3 = np.dot(Z2, W3) + B3 # 恒等函数,为了和之前的流程保持一致 def identity_function(x): return x Y = identity_function(A3) # Y = A3 print(Y) # 代码整理 def init_network(): network = {'w1': np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]), 'b1': np.array([0.1, 0.2, 0.3]), 'w2': np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]), 'b2': np.array([0.1, 0.2]), 'w3': np.array([[0.1, 0.3], [0.2, 0.4]]), 'b3': np.array([0.1, 0.2])} return network # forward表示前向,之后会有backward后向的处理 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)
输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用 sigmoid函数,多元分类问题可以使用 softmax函数。关于输出层的激活函数,我们将在下一节详细介绍。
-
输出层的设计
神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数,分类问题用softmax函数。
机器学习的问题大致可以分为分类问题和回归问题。分类问题是数据属于哪一个类别的问题。比如,区分图像中的人是男性还是女性的问题就是分类问题。而回归问题是根据某个输入预测一个(连续的)数值的问题。
(1)恒等函数和softmax函数
恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直接输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出。
分类问题中使用的softmax函数可以用下面的式表示
exp(x)是表示e的x次方的指数函数(e是纳皮尔常数,也叫自然数)
# 用python实现softmax函数 def softmax(a): exp_a = np.exp(a) # e的指数函数 sum_exp_a = np.sum(exp_a) # 指数函数求和 y = exp_a / sum_exp_a # softmax函数的值 return y # 利用指数函数的性质进行计算 a = np.array([1010, 1000, 990]) c = np.max(a) # c=1010 print(softmax(a)) print(softmax(a - c))
上面的softmax函数的实现虽然正确描述了式(3.10),但在计算机的运算上有一定的缺陷。这个缺陷就是溢出问题。可以利用指数函数的性质,对上面的函数进行优化
# 对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
(2)softmax函数的性质
softmax函数的输出是0.0到1.0之间的实数。并且,softmax函数的输出值的总和是1。输出总和为1是softmax函数的一个重要性质。正因为有了这个性质,我们才可以把softmax函数的输出解释为“概率”。
一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且,即便使用softmax函数,输出值最大的神经元的位置也不会变。因此,神经网络在进行分类时,输出层的softmax函数可以省略。在实际的问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的softmax函数一般会被省略。
求解机器学习问题的步骤可以分为“学习” (也称为训练,为了强调算法从数据中学习模型)和“推理”两个阶段。首先,在学习阶段进行模型的学习(指使用训练数据、自动调整参数的过程),然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的 softmax函数。在输出层使用 softmax函数是因为它和神经网络的学习有关系(详细内容请参考下一章)。
-
手写数字识别
这里我们来进行手写数字图像的分类。假设学习已经全部结束,我们使用学习到的参数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向传播(forward propagation)
(1)MNIST数据集
这里使用的数据集是MNIST手写数字图像集。MNIST是机器学习领域最有名的数据集之一,被应用于从简单的实验到发表的论文研究等各种场合。实际上,在阅读图像识别或机器学习的论文时,MNIST数据集经常作为实验用的数据出现。
MNIST数据集的一般使用方法是,先用训练图像进行学习,再用学习到的模型度量能在多大程度上对测试图像进行正确的分类。
# coding: utf-8 import sys import os from PIL import Image import numpy as np 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,) # python有pickle这个便利的功能,可以将程序运行中的对象保存为文件 # 如果加载保存过的pickle文件,可以立刻复原之前程序运行中的对象。用于读入mnist数据集的load_mnist()函数内部也使用了pickle功能。利用pickle功能,可以高效地完成mnist数据的准备工作 def img_show(img): pil_img = Image.fromarray(np.uint8(img)) pil_img.show() img = x_train[0] label = t_train[0] print(label) print(img.shape) img = img.reshape(28, 28) print(img.shape) img_show(img) # 这里需要注意的是,flatten=True时读入的图像是以一列(一维)Numpy数组的形式保存的 # 因此显示图像时,需要把它变为原来的28像素×28像素的形状,可以通过reshape()方法的参数指定期望的形状,更改Numpy数组的形状 # 此外,还需要把保存为Numpy数组的图像数据转换为PIL用的数据对象,这个转换处理由Image.fromarray()来完成
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表示。
(2)神经网络的推理处理
import sys, os import pickle from dataset.mnist import load_mnist from module3 import sigmoid, softmax import numpy as np sys.path.append(os.pardir) 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 def init_network(): with open("sample_weight.pkl", 'rb') as f: network = pickle.load(f) print("pkl file was successfully opened") 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 i in range(len(x)): y = predict(network, x[i]) p = np.argmax(y) # 获取概率最高的索引 if p == t[i]: accuracy_cnt += 1 print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
init_network()会读入保存在pickle文件sample_weight.pkl中的学习到的权重参数。这个文件中以字典变量的形式保存了权重和偏置参数。
在这个例子中,我们把load_mnist函数的参数normalize设置成了True。将normalize设置成True后,函数内部会进行转换,将图像的各个像素值除以255,使得数据的值在0.0~1.0的范围内。像这样把数据限定到某个范围内的处理称为正规化(normalization)。此外,对神经网络的输入数据进行某种既定的转换称为预处理(pre-processing)。这里,作为对输入图像的一种预处理,我们进行了正规化。
预处理在神经网络(深度学习)中非常实用,其有效性已在提高识别性能和学习的效率等众多实验中得到证明。在刚才的例子中,作为一种预处理,我们将各个像素值除以 255,进行了简单的正规化。实际上,很多预处理都会考虑到数据的整体分布。比如,利用数据整体的均值或标准差,移动数据,使数据整体以 0为中心分布,或者进行正规化,把数据的延展控制在一定范围内。除此之外,还有将数据整体的分布形状均匀化的方法,即数据白化(whitening)等。
(3)批处理
使用python解释器,输出刚才的神经网络的各层的权重的形状
x, t = get_data() W1, W2, W3 = network['W1'], network['W2'], network['W3'] print(x.shape) # (10000, 784) print(x[0].shape) # (784,) print(W1.shape) # (784, 50) print(W2.shape) # (50, 100) print(W3.shape) # (100, 10)
现在我们来考虑打包输入多张图像的情形。比如,我们想用predict()函数一次性打包处理100张图像。为此,可以把x的形状改为100 × 784,将100张图像打包作为输入数据。输入数据的形状为 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)))
本章所学的内容
• 神经网络中的激活函数使用平滑变化的sigmoid函数或ReLU函数。 • 通过巧妙地使用NumPy多维数组,可以高效地实现神经网络。 • 机器学习的问题大体上可以分为回归问题和分类问题。 • 关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中一般用softmax函数。 • 分类问题中,输出层的神经元的数量设置为要分类的类别数。 • 输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速的运算。