返回介绍

2.5 手写数字的数据集 MNIST

发布于 2024-01-27 20:58:55 字数 30628 浏览 0 评论 0 收藏 0

识别人的笔迹这个问题相对复杂,也非常模糊,因此这是一种检验人工智能的理想挑战。这不像进行大量数字相乘那样明确清晰。

让计算机准确区分图像中包含的内容,有时也称之为图像识别问题。科学家对这个问题进行了几十年的研究,直到最近,才取得了一些比较好的进展,如神经网络这样的方法则是构成这些飞跃的重要部分。

为了让你对图像识别究竟有多难有一个感性认识,举个例子,人类有时候对图像中包含的内容有不同意见。人们很容易对手写字符实际上是什么产生分歧意见,特别对于书写者非常匆忙或粗心大意时写下的手写字符,更是如此。看一看下面的手写数字,这是个4还是9?

人工智能研究者使用一套流行的手写数字图片来测试他们的最新思想和算法。这套图片众所周知,非常流行,这意味着我们很容易与其他研究者比较,检验我们最近关于图像识别的疯狂想法究竟有多优秀。

这个数据集称为手写数字的MNIST数据库。从受人尊敬的神经网络研究员Yann LeCun的网站http://yann.lecun.com/exdb/mnist/ ,可以得到这个数据集。

这个网页也列出了在学习和正确识别这些手写字符方面,这些新旧想法的表现如何。我们将会多次提到这个列表,看看比起专业人士我们的想法表现如何!MNIST数据库的格式不容易使用,因此其他人已经创建了相对简单的数据文件格式,参见 http://pjreddie.com/projects/mnist-in-csv/ ,这对我们非常有帮助。

这些文件称为CSV文件,这意味着纯文本中的每一个值都是由逗号分隔的。你可以轻松地在任何文本编辑器中查看这些数值,大部分的电子表格和数据分析软件也兼容CSV文件,它们是非常通用的标准。

这个网站提供了两个CSV文件:

· 训练集http://www.pjreddie.com/media/files/mnist_train.csv

· 测试集 http://www.pjreddie.com/media/files/mnist_test.csv

顾名思义,训练集是用来训练神经网络的60 000个标记样本集。标记是指输入与期望的输出匹配,也就是答案应该是多少。

可以使用较小的只有10 000个样本的测试集来测试我们的想法或算法工作的好坏程度。由于这也包含了正确的标记,因此可以观察神经网络是否得到正确的答案。

将训练和测试数据集分开的想法,是为了确保可以使用神经网络之前没有见过的数据进行再次测试。否则,我们就可以采用欺骗手段,让神经网络简单地记忆训练数据,得到一个完美、但是有欺骗性的得分。在整个机器学习领域,将测试数据与训练数据分开是一种很常见的想法。

让我们来一窥这些文件。下面显示的是加载到文本编辑器中的MNIST测试集的一部分。

哇!这看起来好像出事了!就像在20世纪80年代的电影中一样,计算机被黑客攻击了。

其实一切都很好。我们很容易看到,文本编辑器显示很长的文本,这些行由使用逗号分隔的数字组成。这些行非常长,以至于它们折行了好几次。对我们有帮助的是,这个文本编辑器在边缘显示了实际的行号,可以看到完整的4行数据以及第5行的一部分数据。

在文本中,这些记录或这些行的内容很容易理解:

· 第一个值是标签,即书写者实际希望表示的数字,如“7”或“9”。这是我们希望神经网络学习得到的正确答案。

· 随后的值,由逗号分隔,是手写体数字的像素值。像素数组的尺寸是28 乘以28,因此在标签后有784个值。如果想知道这是否有784个值,可以一个一个地数一下。

因此,第一个记录表示数字“5”,就是所显示的第一个值,这行文本的其余部分是某人的手写数字5的像素值。第二个记录表示数字“0”,第三个记录表示数字“4”,第四个记录表示“1”,第五个表示“9”。你可以从MNIST数据文件中挑选任一行,第一个数字告诉你接下来图像数据的标签是什么。

但是,从这个长达784个值的列表中,人们很难看出这些数字组成了某人手写数字5的图片。我们会将这些数字绘制为图像,让读者确认这784个值真的是手写数字的像素值。

在深入研究进行这样操作之前,我们应该下载MNIST数据集中的一个较小的子集。MNIST数据的数据文件是相当大的,而较小的子集意味着我们可以实验、尝试和开发代码,而不会由于大量的数据集而拖慢计算机的速度,因此小数据集还是大有裨益的。一旦确定了乐于使用的算法和代码,我们就可以使用完整的数据集。

以下是MNIST数据集中较小子集的链接,也是以CSV格式存储的:

· MNIST测试数据集中的10条记录——https://raw.githubusercontent.com/ makeyourownneuralnetwork/makeyourownneuralnetwork/master/mnist_dataset/ mnist_test_10.csv

· MNIST训练数据集中的100条记录——https://raw.githubusercontent.com/ makeyourownneuralnetwork/makeyourownneuralnetwork/master/mnist_dataset/ mnist_train_100.csv

如果浏览器显示的是数据而不是自动下载,可以使用“File → Save As ...”手动保存文件,或在浏览器上进行等效操作。

将数据文件保存到方便操作的位置。我将数据文件保存在名为“mnist_ dataset”的文件夹中,这个文件夹就在IPython的Notebook文件旁边,如下面的屏幕截图所示。如果IPython的Notebook文件和数据文件散落在计算机的各个地方,那就会变得很混乱。

在使用数据做任何事情之前,比如绘图或使用数据训练神经网络,我们需要找到一种方式来用Python代码得到这些数据。

在Python中,打开文件并获取其中的内容是一件非常容易的事情。先来演示一下这个操作,然后再做出解释。看看下面的代码:

data_file
 = open("mnist_dataset/mnist_train_100.csv", 'r') 
data_list 
= data_file
.readlines() 
data_file
.close() 

这里只有3行代码。让我们一一进行讨论。

第一行使用open()函数打开一个文件。传递给函数的第一个参数是文件的名称。其实,这不仅仅是文件名“mnist_train_100.csv”,这是整个路径,其中包括了文件所在的目录。第二个参数是可选的,它只是告诉Python我们希望如何处理文件。“r”告诉Python以只读的方式而不是可写的方式打开文件。这样可以避免任何更改数据或删除数据的意外。如果试图写入文件、修改文件,Python将阻止并生成一条错误消息。

变量data_file是什么?open()函数创建了此文件的一个文件句柄、一个引用,我们将这个句柄分配给命名为data_file的变量。现在已经打开了文件,任何进一步的操作,如读取文件,都将通过句柄完成。

下一行代码很简单。使用与文件句柄data_file相关的readlines()函数,将文件中的所有行读入变量data _list。这个变量包含了一个列表,列表中的一项是表示文件中一行的字符串。在列表中,可以跳到特定的条目,类似地,也可以跳到文件中特定的行,这样这个列表就有用多了。因此data_list [0] 是第一条记录,data_list [9]是第十条记录,以此类推。

顺便说一句,由于readlines()会将整个文件读取到内存中,因此你可能会听到别人告诉你不要使用这种方法。他们会告诉你,一次读取一行,对这行进行所需要进行的操作,然后移动到下一行。他们都没有错,不要将整个文件读入内存中,而是一次在一行上工作,这更有效率。但是,我们的文件不是很大,如果使用readlines(),那么代码相对容易一些,对我们而言,在学习Python时简单和清晰是很重要的。

最后一行代码关闭文件。在用完如文件这样的资源后,关闭和清理文件是一种很好的做法。如果不这样做,文件依然开着,这可能会造成问题。什么问题呢?有些程序可能不希望写入处在打开状态的文件,以免导致不一致。这就像是两个人试图在同一张纸上写信!有时候,计算机可能会锁定文件,防止发生这种冲突。如果使用完文件不清理,那么你就有一堆锁定的文件。最起码应该关闭文件,让计算机释放用于保存文件的部分内存。

创建一个新的空的Notebook,试试这段代码,当打印出列表的元素时,观察发生了什么。下图显示了这种操作的结果。

可以看到,列表的长度为100 。Python的len()函数告诉我们列表的大小。

还可以看到第一条记录data_list [0]的内容。第一个数字是“5”,这是标签,并且其余的784个数字是构成图像像素的颜色值。如果你仔细观察,可以发现这些颜色值似乎介于0和255之间。

你可能希望看看其他记录,看看在其他记录中是否也是这样的。你会发现,颜色值确实落到了0到255的范围内。

先前,我们确实看到了如何使用imshow()函数绘制数字矩形数组。在这里,我们要做的事情是相同的,但是需要将使用逗号分隔的数字列表转换成合适的数组。要达到这个目标,需要进行以下的步骤:

· 将由逗号分隔,长的文本字符串值,拆分成单个值,在逗号处进行分割。

· 忽略第一个值,这是标签,将剩余的28 × 28 = 784个值转换成28列28行的数组。

· 绘制数组!

同样,先演示可以执行这个任务的简单Python代码,然后讨论代码,最后更详细地解释所发生的事情,这是最简单的学习方式。

首先,一定不要忘记导入Python扩展库,这将有助于我们使用数组以及进行绘图:

import numpy 
import matplotlib.pyplot 
%matplotlib inline

看看下面3行代码。变量已经着色了,这样更容易理解在何处使用何种数据。

all_values 
= data_list 
[0].split(',')
image_array
 = numpy.asfarray( all_values
 [1:]).reshape((28,28))
matplotlib.pyplot.imshow( image_array 
, cmap='Greys',
interpolation=’None’)

第一行代码接受了刚才打印出来的data_list [0],这是第一条记录,根据逗号,将这一长串进行拆分。split()函数就是执行这项任务的,其中有一个参数告诉函数根据哪个符号进行拆分。在这个例子中,这个符号为逗号。得到的结果将放到all_values中。可以将这个变量打印出来,检查这确实是Python中长列表的值。

有几件事情发生在同一行代码中,因此下一行代码看起来相对复杂。让我们从核心开始解释。核心是all_values列表,但是这次使用了方括号[1:],表示采用除了列表中的第一个元素以外的所有值,也就是忽略第一个标签值,只要剩下的784个值。numpy.asfarray()是一个numpy函数,这个函数将文本字符串转换成实数,并创建这些数字的数组。

等等——将文本字符串转换为数字,这是什么意思?嗯,文件是以文本的形式读取的,每一行或每一条记录依然是文本。由逗号分割每一行得到的仍然是文本片段。文本可以是单词“apple”“orange123”或“567”。文本字符串“567”与数字567不同。因此,即使文本看起来像数字,我们也需要将文本字符串转换为数字。最后一项.reshape((28,28))可以确保数字列表每28个元素折返一次,形成28乘28的方形矩阵。所得到的28 乘28的数组名为image_array。唷!这么多事发生在一行的代码中。

第三行代码非常简单,就是使用imshow()函数绘出image_array。

这一次,选择灰度调色板——cmap=“Greys(灰度)”,以更好地显示手写字符。

下图显示了这段代码的结果:

可以看到绘制的图像是5,这就是标签所表示的预期数字。如果转而选择下一条记录data_list [1],而这条记录的标签为0,就可以得到下面的图片。

可以很容易地分辨出手写数字确实是0。

2.5.1 准备MNIST训练数据

我们已经知道如何获取和折开MNIST数据文件中的数据,从而理解并可视化这些数据。我们要使用此数据训练神经网络,但是我们需要想想,在将数据抛给神经网络之前如何准备数据。

我们先前看到,如果输入数据和输出值,形状正好适合,这样它们就可以待在网络节点激活函数的舒适区域内,那么神经网络的工作会更出色。

我们需要做的第一件事情是将输入颜色值从较大的0到255的范围,缩放至较小的0.01 到 1.0的范围。我们刻意选择0.01作为范围最低点,是为了避免先前观察到的0值输入最终会人为地造成权重更新失败。我们没有选择0.99作为输入的上限值,是因为不需要避免输入1.0会造成这个问题。我们只需要避免输出值为1.0。

将在0到255范围内的原始输入值除以255,就可以得到0到1范围的输入值。

然后,需要将所得到的输入乘以0.99,把它们的范围变成0.0 到0.99。接下来,加上0.01,将这些值整体偏移到所需的范围0.01到1.00。下面的Python代码演示了这些操作:

scaled_input = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
print(scaled_input)

输出确认,这些值当前的范围为0.01到0.99。

我们已经通过缩放和移位让MNIST数据准备就绪,可以输入神经网络进行训练和查询了。

现在,我们需要思考神经网络的输出。先前,我们看到输出值应该匹配激活函数可以输出值的范围。我们使用的逻辑函数不能输出如-2.0或 255这样的数字,能输出的范围为0.0到1.0,事实上不能达到0.0或1.0,这是逻辑函数的极限值,逻辑函数仅接近这两个极限,但不能真正到达那里。因此,看起来在训练时必须调整目标值。

但是,实际上,我们要问自己一个更深层次的问题。输出应该是什么样子的?这应该是图片答案吗?这意味着有28×28 = 784个输出节点。

如果退后一步,想想要求神经网络做什么,我们会意识到,要求神经网络对图像进行分类,分配正确的标签。这些标签是0到9共10个数字中的一个。这意味着神经网络应该有10个输出层节点,每个节点对应一个可能的答案或标签。如果答案是“0”,输出层第一个节点激发,而其余的输出节点则保持抑制状态。如果答案是“9”,输出层的最后节点会激发,而其余的输出节点则保持抑制状态。下图详细阐释了这个方案,并显示了一些示例输出。

第一个示例是神经网络认为它看到的是数字“5”。可以看到,从输出层出现的最大信号来自于标签为5的节点。由于从标签0开始,因此这是第六个节点。这很容易吧。其余的输出节点产生的信号非常小,接近于零。舍入误差可能导致零输出,但事实上,要记住激活函数不会产生实际为零的输出。

下一个示例演示了如果神经网络认为它看到了手写的“0”将会发生的事情。同样,目前最大输出来自于第一个输出节点,对应的标签为“0”。

最后一个示例更有趣。这里,神经网络的最大输出信号来自于最后一个节点,对应标签“9”。然而,在标签为“4”的节点处,它得到了中等大小的输出。通常,我们会使用最大信号为答案,但是,可以看看网络为何会认为答案可能是“4”。也许笔迹使得它难以确定?神经网络中确实会发生这种不确定性,我们不应该把它看作是一件坏事,而应该将其视为有用的见解,即另一个答案也可能满足条件。

这真是太棒了!现在,我们需要把这些想法转换成目标数组,用于神经网络的训练。

如果训练样本的标签为“5”,那么需要创建输出节点的目标数组,其中除了对应于标签“5”的节点,其他所有节点的值应该都很小,这个数组看起来可能如[0,0,0,0,0,1,0,0,0,0]。

事实上,我们已经明白了,试图让神经网络生成0和1的输出,对于激活函数而言是不可能的,这会导致大的权重和饱和网络,因此需要重新调整这些数字。我们将使用值0.01和0.99来代替0和1,这样标签为“5”的目标输出数组为[0.01, 0.01, 0.01, 0.01, 0.01, 0.99, 0.01, 0.01, 0.01, 0.01]。

仔细观察下列的Python代码,这些代码构建了目标矩阵:

#output nodes is 10 (example)
onodes = 10
targets = numpy.zeros(onodes) + 0.01
targets[int(all_values[0])] = 0.99

不算注释,第一行代码只是将输出节点的数量设置为10。在这个示例中,这是正确的,因为有10个标签。

第二行代码只是使用方便的numpy函数numpy.zeros(),创建用零填充的数组。这个函数的第一个参数是希望的数组大小和形状。此处,我们只希望得到一个简单的、长度为onodes的数组,onodes为最终输出层的节点数量。我们加上了0.01,解决刚才谈到的0输入造成的问题。

下一行代码获得了MNIST数据集记录中的第一个元素,也就是训练目标标签,将其从字符串形式转换为整数形式。请记住,从源文件读取的记录是文本字符串,而不是数字。一旦转换完成,我们使用目标标签,将目标列表的正确元素设置为0.99。标签“0”将转换为整数0,这与标签对应的targets []中的索引是一致的,因此这看起来非常整洁。类似地,标签“9”将转换为整数9,targets [9]确实是此数组的最后一个元素。

下面展示了这种工作方式的一个示例:

太好了,现在,我们已经明白了如何准备用于训练和查询的输入数据以及如何准备用于训练的输出数据。

让我们更新Python代码,使其包括这些操作。下面代码是迄今为止开发的代码。这些代码在以下GitHub链接中可以得到,但是,随着添加越来越多的代码,代码会逐渐演变:

· https://github.com/makeyourownneuralnetwork/makeyourownneuralnetwork/ blob/master/ part2_neural_network_mnist_data.ipynb

也可以在以下的链接中找到开发代码,通过这个链接,可以看到以前的版本:

· https://github.com/makeyourownneuralnetwork/makeyourownneuralnetwork/ commits/master/part2_neural_network_mnist_data.ipynb

# python notebook for Make Your Own Neural Network
# code for a 3-layer neural network, and code for learning the MNIST
dataset
# (c) Tariq Rashid, 2016
# license is GPLv2
import numpy
# scipy.special for the sigmoid function expit()
import scipy.special
# library for plotting arrays
import matplotlib.pyplot
# ensure the plots are inside this notebook, not an external window
%matplotlib inline
# neural network class definition
class neuralNetwork:
  # initialise the neural network
  def __init__(self, inputnodes, hiddennodes, outputnodes,
learningrate):
    # set number of nodes in each input, hidden, output layer
    self.inodes = inputnodes
    self.hnodes = hiddennodes
    self.onodes = outputnodes
    # link weight matrices, wih and who
    # weights inside the arrays are w_i_j, where link is from
node i to node j in the next layer
    # w11 w21
    # w12 w22 etc
    self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5),
(self.hnodes, self.inodes))
    self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5),
(self.onodes, self.hnodes))
    # learning rate
    self.lr = learningrate
    # activation function is the sigmoid function
    self.activation_function = lambda x: scipy.special.expit(x)
    pass
  # train the neural network
  def train(self, inputs_list, targets_list):
    # convert inputs list to 2d array
    inputs = numpy.array(inputs_list, ndmin=2).T
    targets = numpy.array(targets_list, ndmin=2).T
    # calculate signals into hidden layer
    hidden_inputs = numpy.dot(self.wih, inputs)
    # calculate the signals emerging from hidden layer
    hidden_outputs = self.activation_function(hidden_inputs)
    # calculate signals into final output layer
    final_inputs = numpy.dot(self.who, hidden_outputs)
    # calculate the signals emerging from final output layer
    final_outputs = self.activation_function(final_inputs)
    # output layer error is the (target - actual)
    output_errors = targets - final_outputs
    # hidden layer error is the output_errors, split by weights,
recombined at hidden nodes
    hidden_errors = numpy.dot(self.who.T, output_errors)
    # update the weights for the links between the hidden and
output layers
    self.who += self.lr * numpy.dot((output_errors *
final_outputs * (1.0 - final_outputs)),
numpy.transpose(hidden_outputs))
    # update the weights for the links between the input and
hidden layers
    self.wih += self.lr * numpy.dot((hidden_errors *
hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
    pass
  # query the neural network
  def query(self, inputs_list):
    # convert inputs list to 2d array
    inputs = numpy.array(inputs_list, ndmin=2).T
    # calculate signals into hidden layer
    hidden_inputs = numpy.dot(self.wih, inputs)
    # calculate the signals emerging from hidden layer
    hidden_outputs = self.activation_function(hidden_inputs)
    # calculate signals into final output layer
    final_inputs = numpy.dot(self.who, hidden_outputs)
    # calculate the signals emerging from final output layer
    final_outputs = self.activation_function(final_inputs)
    return final_outputs
# number of input, hidden and output nodes
input_nodes = 784
hidden_nodes = 100
output_nodes = 10
# learning rate is 0.3
learning_rate = 0.3
# create instance of neural network
n = neuralNetwork(input_nodes,hidden_nodes,output_nodes,
learning_rate)
# load the mnist training data CSV file into a list
training_data_file = open("mnist_dataset/mnist_train_100.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# train the neural network
# go through all records in the training data set
for record in training_data_list:
  # split the record by the ',' commas
  all_values = record.split(',')
  # scale and shift the inputs
  inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
  # create the target output values (all 0.01, except the desired
label which is 0.99)
  targets = numpy.zeros(output_nodes) + 0.01
  # all_values[0] is the target label for this record
  targets[int(all_values[0])] = 0.99
  n.train(inputs, targets)
  pass

在代码顶部导入了绘图库,添加了一些代码,设置输入层、隐藏层和输出层的大小,读取相对较小的MNIST训练数据集,然后使用这些记录训练神经网络。

为什么选择784个输入节点呢?请记住,这是28×28的结果,即组成手写数字图像的像素个数。

选择使用100个隐藏层节点并不是通过使用科学的方法得到的。我们认为,神经网络应该可以发现在输入中的特征或模式,这些模式或特征可以使用比输入本身更简短的形式表达,因此没有选择比784大的数字。通过选择使用比输入节点的数量小的值,强制网络尝试总结输入的主要特点。但是,如果选择太少的隐藏层节点,那么就限制了网络的能力,使网络难以找到足够的特征或模式,也就会剥夺神经网络表达其对MNIST数据理解的能力。给定的输出层需要10个标签,对应于10个输出层节点,因此,选择100这个中间值作为中间隐藏层的节点数量,似乎有点道理。

这里应该强调一点。对于一个问题,应该选择多少个隐藏层节点,并不存在一个最佳方法。同时,我们也没有最佳方法选择需要几层隐藏层。就目前而言,最好的办法是进行实验,直到找到适合你要解决的问题的一个数字。

2.5.2 测试网络

现在,我们至少已经使用了一个较小的100条记录的子集来训练网络,我们希望测试训练效果如何。使用称为测试数据集的第二个数据集来测试神经网络。

首先需要获得测试记录,这与用于获取训练数据的Python代码非常相似。

# load the mnist test data CSV file into a list
test_data_file = open("mnist_dataset/mnist_test_10.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()

像以前一样,这些数据具有相同的结构,我们以同样的方式解压了这些数据。

在创建循环使用所有测试记录进行测试之前,先看看如果手动运行一个测试会发生什么。下图显示从测试数据集中取出第一条记录,查询当前已得到训练的神经网络。

可以看到,测试数据集的第一条记录具有标签“7”。这是当我们查询这条记录时,我们希望神经网络给出的回答。

绘制像素值,使数据变成图像,我们确认该手写数字的确为“7”。

查询已得到训练的网络,生成了对应每个输出节点所输出的一串数字。你很快就会发现,其中一个输出值比其他输出值大很多,且对应于标签“7”。由于第一个元素对应于标签“0”,因此这就是第8个元素。

成功了!

这是一个需要细细品味的时刻。我们在本书中进行的辛勤工作都有了价值!

我们训练了神经网络,让神经网络告诉我们图片中所代表的数字是什么。请记住,神经网络之前没有见过那张图片,它不是训练数据集的一部分。因此,神经网络能够正确区分它从来没有见过的手写字符。这真是让人印象深刻啊!

只需几行简单的Python,我们就已经创建了一个神经网络,这个神经网络可以执行许多人认为是具备人工智能的事情——它学会了识别人的笔迹图片。

更令人称奇的是,我们只是使用完整的训练数据集的一个小子集对神经网络进行了训练。请记住,训练数据集有60 000条记录,我们只训练了100条记录。我曾经认为这不能成功!让我们扯满篷帆,继续前进,编写代码来看看神经网络对数据集的其余记录有何表现。我们可以记录分数,这样迟些时候,再看看改进神经网络学习能力的想法是否能够成功,同时也可以比较一下其他神经网络的表现如何。

最简单的方式就是察看下面的代码,并根据这些代码进行讨论:

# test the neural network
# scorecard for how well the network performs, initially empty
scorecard = []
# go through all the records in the test data set
for record in test_data_list:
  # split the record by the ',' commas
  all_values = record.split(',')
  # correct answer is first value
  correct_label = int(all_values[0])
  print(correct_label, "correct label")
  # scale and shift the inputs
  inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
  # query the network
  outputs = n.query(inputs)
  # the index of the highest value corresponds to the label
  label = numpy.argmax(outputs)
  print(label, "network's answer")
  # append correct or incorrect to list
  if (label == correct_label):
    # network's answer matches correct answer, add 1 to
scorecard
    scorecard.append(1)
  else:
    # network's answer doesn't match correct answer, add 0 to
scorecard
    scorecard.append(0)
    pass
  pass

循环可以使用测试数据集中的所有记录进行测试,在跳进这个循环之前,创建一个空的列表,称为计分卡(scorecard),这个记分卡在测试每条记录之后都会进行更新。

可以看到,在循环内部,我们所做的与先前所做的一样,根据逗号拆分文本记录,分离出数值。记下第一个数字,这是正确答案。然后,重新调整剩下的值,让它们适合用于查询神经网络。

我们将来自神经网络的回答保存在名为outputs的变量中。

接下来是非常有趣的一点。我们知道具有最大值的输出节点是网络认为的答案。这个节点的索引,也就是节点的位置,与标签对应。闲话少说,也就是第一个元素对应于标签“0”,第五元素对应于标签“4”,以此类推。幸运的是,有一个便利的numpy函数numpy.argmax()可以发现数组中的最大值,并告诉我们它的位置。你可以在线阅读关于这个函数的文档。如果这个函数返回0,我们知道网络认为答案是零,以此类推。

代码的最后一部分将标签与已知的正确标签进行比较。如果它们是相同的,那么在计分卡上附加一个“1”,否则附加“0”。

在代码中,我已经包含了一些有用的print()指令,这样就可以看到正确的标签和预测的标签。下图显示了代码运行的结果,同时打印出了计分卡。

这次有点失败!可以看到,这有相当多的不匹配标签。最后的计分卡显示,在10个测试记录中神经网络只答对了6个,也就是只得了60分。不过,考虑到使用的训练集很小,这实际上并不是太糟糕。

让我们编写一段代码,将测试成绩作为分数并打印出来,结束程序。

# calculate the performance score, the fraction of correct answers
scorecard_array = numpy.asarray(scorecard)
print ("performance = ", scorecard_array.sum() /
scorecard_array.size)

这是一个简单的计算,得到了正确答案的分数。这段代码将计分卡上“1”的条目相加,除以计分卡的条目总数,即这个计分卡的大小。来看看这段代码生成的结果。

正如我们预期的,这段代码生成了分数0.6,即60%的准确率。

2.5.3 使用完整数据集进行训练和测试

让我们将这些已开发的测试网络性能的新代码,添加到主要程序中。

此时改变文件名,这样就可以指向具有60 000条记录的完整的训练数据集,以及具有10 000条记录的测试数据集。先前,我们将这些文件保存为mnist_dataset / mnist_train.csv和mnist_dataset / mnist_test.csv。现在,我们要认真对待了!请记住,你可以访问GitHub获取Python的Notebook文件:

· https://github.com/makeyourownneuralnetwork/makeyourownneuralnetwork/ blob/master/part2_neural_network_mnist_data.ipynb

在GitHub上,也可以得到历史代码,这样就可以看到代码的开发过程:

· https://github.com/makeyourownneuralnetwork/makeyourownneuralnetwork/ commits/ma ster/part2_neural_network_mnist_data.ipynb

使用60 000个训练样本训练简单的3层神经网络,然后使用10 000条记录对网络进行测试,得到的总表现分数为0.9473。这个表现简直太棒了,几乎是95%的准确率!

这个略低于95%的准确性,可以与记录在http://yann.lecun.com/exdb/mnist/ 网页的行业标准媲美。我们可以看到,比起一些历史基准,这个准确率还是略胜一筹的,这里列出的最简单的神经网络方法所表现的准确率为95.3%,而我们的神经网络的性能大致相当。

这一点也不糟糕。我们应该感到高兴,第一次尝试的简单神经网络就实现了研究者所开发的专业神经网络的性能。

顺便说一句,计算60 000个训练样本,每个样本的计算都需要进行一组784个输入节点、经过100个隐藏层节点的前馈计算,同时还要进行误差反馈和权重更新,即使对于一台快速的现代家用计算机而言,这一切也需要花上一段时间,这一点都不令人吃惊。我的新笔记本计算机花了约2分钟时间完成了训练循环。你的计算机应该也差不多。

2.5.4 一些改进:调整学习率

我们的第一个神经网络,只使用简单的思路、简单的Python代码,就可以在MNIST数据集上获得准确率为95%的性能分数,这已经很不错了。如果你希望就此打住,也完全可以理解的。

但是,让我们看看是否可以进行一些简单的改进。

可以尝试的第一个改进是调整学习率。先前没有真正使用不同的值进行实验,就将它设置为0.3了。

试一下将学习率翻倍,设置为0.6,看看提高学习率对整个网络的学习能力是否有益。如果此时运行代码,会得到0.9047性能得分。这比以前更糟。因此,看起来好像大的学习率导致了在梯度下降过程中有一些来回跳动和超调。

使用0.1的学习率再试一次。这次,性能有所改善,得到了0.9523分。在性能上,这与网站上列出的具有1 000个隐藏层节点的神经网络类似。我们“以少胜多”了。

如果继续设置一个更小的0.01学习率,会发生什么情况?性能没有变得更好,得分为0.9241。因此,似乎过小的学习率也是有害的。

由于限制了梯度下降发生的速度,使用的步长太小了,因此对性能造成了损害,这个结果也是有道理的。

下图画出了这些结果。我们应该多次进行了这些实验,减小随机性以及在梯度下降过程中不好的路径带来的影响,只有这样的方法才是科学的,但是这依然能够有助于我们明白一个总体思路,那就是对于学习率存在一个甜蜜点。

上图表明,学习率在0.1和0.3之间可能会有较好的表现,因此,尝试0.2的学习率,得到0.9537的性能得分。比起0.1或0.3,这个表现确实好了一些。我们可以绘制图表,对所发生的事情得到一种较好的认识,在其他情况下,你也应该考虑这种方法——和一串数字相比,图表有助于更好地理解!因此,我们将坚持使用0.2的学习率,这看起来似乎是MNIST数据集和神经网络的甜蜜点。

顺便说一句,由于代码运行的整个过程有一点随机,因此,如果你自己运行这段代码,成绩会略有不同。你的初始随机权重可能不同于我的初始随机权重,因此你的代码与我的代码所使用的梯度下降路线有所不同。

2.5.5 一些改进:多次运行

接下来可以做的改进,是使用数据集,重复多次进行训练。

有些人把训练一次称为一个世代。因此,具有10个世代的训练,意味着使用整个训练数据集运行程序10次。为什么要这么做呢?特别是,如果这次计算机花的时间增加到10或20甚至30分钟呢?这是值得的,原因是通过提供更多爬下斜坡的机会,有助于在梯度下降过程中进行权重更新。

试一下使用2个世代。由于现在我们在训练代码外围添加了额外的循环,因此代码稍有改变。下面的代码显示了外围循环,将代码着色有助于看到发生了什么。

# train the neural network
# epochs is the number of times the training data set is used for
training
epochs = 2
  
for e in range(epochs):
  # go through all records in the training data set
  for record in training_data_list:
    # split the record by the ',' commas
    all_values = record.split(',')
    # scale and shift the inputs
    inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) +
0.01
    # create the target output values (all 0.01, except the
desired label which is 0.99)
    targets = numpy.zeros(output_nodes) + 0.01
    # all_values[0] is the target label for this record
    targets[int(all_values[0])] = 0.99
    n.train(inputs, targets)
    pass
  pass

使用2个世代神经网络所得到的性能得分为0.9579,比只有1个世代的神经网络有所改进。

就像调整学习率一样,让我们使用几个不同的世代进行实验并绘图,以可视化这些效果。直觉告诉我们,所做的训练越多,所得到的性能越好。有人可能会注意到,太多的训练实际上会过犹不及,这是由于网络过度拟合训练数据,因此网络在先前没有见到过的新数据上表现不佳。不仅是神经网络,在各种类型的机器学习中,这种过度拟合也是需要注意的。

发生的事情如下所示:

结果呈现出不可预测性。在大约5或7个世代时,有一个甜蜜点。在此之后,性能会下降,这可能是过度拟合的效果。性能在6个世代的情况下下降,这可能是运行中出了问题,导致网络在梯度下降过程中被卡在了一个局部的最小值中。事实上,由于没有对每个数据点进行多次实验,无法减小随机过程的影响,因此我们已经预见到结果会有各种变化。这就是为什么保留了6个世代这个奇怪的点,这是为了提醒我们,神经网络的学习过程其核心是随机过程,有时候工作得不错,有时候工作得很糟。

另一个可能的原因是,在较大数目的世代情况下,学习率可能设置过高了。继续这个实验,将学习率从0.2减小到0.1,看看会发生什么情况。

在7个世代的情况下,峰值性能高达0.9628或96.28%。

下图显示了在学习率为0.1情况下,得到的新性能与前一幅图叠加的情况。

可以看到,在更多世代的情况下,减小学习率确实能够得到更好的性能。0.9689的峰值表示误差率接近3%,这可以与Yann LeCun网站上的神经网络标准相媲美了。

直观上,如果你打算使用更长的时间(多个世代)探索梯度下降,那么你可以承受采用较短的步长(学习率),并且在总体上可以找到更好的路径,这是有道理的。确实,对于MNIST学习任务,我们的神经网络的甜蜜点看起来是5个世代。请再次记住,我们在使用一种相当不科学的方式来进行实验。要正确、科学地做到这一点,就必须为每个学习率和世代组合进行多次实验,尽量减少在梯度下降过程中随机性的影响。

2.5.6 改变网络形状

我们还没有尝试过改变神经网络的形状,也许应该更早尝试这件事。让我们试着改变中间隐藏层节点的数目。一直以来,我们将它们设置为100!

在尝试使用不同数目的隐藏层节点进行实验之前,让我们思考一下,如果这样做可能会发生什么情况。隐藏层是发生学习过程的层次。请记住,输入节点只需引入输入信号,输出节点只要送出神经网络的答案,是隐藏层(可以多层)进行学习,将输入转变为答案。这是学习发生的场所。事实上,隐藏层节点前后的链接权重具有学习能力。

如果隐藏层节点太少,比如说3个,那么你可以想象,这不可能有足够的空间让网络学习任何知识,并将所有输入转换为正确的输出。这就像要5座车去载10个人。你不可能将那么多人塞进去。计算机科学家称这种限制为学习容量。虽然学习能力不可能超过学习容量,但是可以通过改变车辆或网络形状来增加容量。

如果有10 000个隐藏层节点,会发生什么情况呢?虽然我们不会缺少学习容量,但是由于目前有太多的路径供学习选择,因此可能难以训练网络。这也许需要使用10 000个世代来训练这样的网络。

让我们进行一些实验,看看会发生什么情况。

可以看到,比起较多的隐藏层节点,隐藏层节点数量少,其效果不是很理想,这是我们预期的结果。但是,只有5个隐藏层节点的神经网络,其性能得分就可以达到0.7001,鉴于只给了如此少的学习场所,而网络仍有70%的正确率,这已经相当惊人了。

请记住,迄今为止,程序运行的是100个隐藏层节点。只用10个隐藏层节点,网络就得到了0.8998的准确性,这同样让人侧目。只使用我们曾经用过的节点数目的1/10,网络的性能就跳到90%。只使用如此少的隐藏层节点或学习场所,神经网络就能够得到如此好的结果。这也证明了神经网络的力量。这一点值得我们赞赏。

随着增加隐藏层节点的数量,结果有所改善,但是不显著。由于增加一个隐藏层节点意味着增加了到前后层的每个节点的新网络链接,这一切都会产生额外较多的计算,因此训练网络所用的时间也显著增加了!因此,必须在可容忍的运行时间内选择某个数目的隐藏层节点。

对于我的计算机而言,这个数字是200个节点。你的计算机可能会相对较快或相对较慢。

我们还创造了准确度的新纪录,使用200个节点,得分0.9751。使用500个节点,运行较长的时间,我们的神经网络得到了0.9762分。相比于Yann LeCun的网站上列出的基准,这是相当不错的成绩了。

回过头去,看看以前的图,可以发现,通过改变网络形状,先前约95%的准确度这个“冥顽不灵”的极限已经被打破了。

2.5.7 大功告成

回顾这项工作,我们只用先前介绍的简单概念以及简单的Python代码,创建了一个神经网络。

没有任何多余花哨、神奇的数学,神经网络就已经表现得如此出众,相比于学者和研究人员所编写的神经网络,这个神经网络的表现也是可圈可点。

本书的第3章内容更有趣,即使你还未探讨过这些想法,请不要犹豫,使用已经写出的神经网络,进一步去实验,尝试不同数量的隐藏层节点或不同的调整比例,甚至使用不同的激活函数,看看会发生什么情况。

2.5.8 最终代码

为了防止不能访问GitHub上的代码,同时出于方便参考的原因,我们把代码副本在此列出,下面是最终代码。

# python notebook for Make Your Own Neural Network
# code for a 3-layer neural network, and code for learning the MNIST
dataset
# (c) Tariq Rashid, 2016
# license is GPLv2
import numpy
# scipy.special for the sigmoid function expit()
import scipy.special
# library for plotting arrays
import matplotlib.pyplot
# ensure the plots are inside this notebook, not an external window
%matplotlib inline
# neural network class definition
class neuralNetwork :

  # initialise the neural network
  def __init__(self, inputnodes, hiddennodes, outputnodes,
learningrate) :
    # set number of nodes in each input, hidden, output layer
    self.inodes = inputnodes
    self.hnodes = hiddennodes
    self.onodes = outputnodes
    # link weight matrices, wih and who
    # weights inside the arrays are w_i_j, where link is from
node i to node j in the next layer
    # w11 w21
    # w12 w22 etc
    self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5),
(self.hnodes, self.inodes))
    self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5),
(self.onodes, self.hnodes))
    # learning rate
    self.lr = learningrate
    # activation function is the sigmoid function
    self.activation_function = lambda x: scipy.special.expit(x)
    pass
  # train the neural network
  def train(self, inputs_list, targets_list) :
    # convert inputs list to 2d array
    inputs = numpy.array(inputs_list, ndmin=2).T
    targets = numpy.array(targets_list, ndmin=2).T
    # calculate signals into hidden layer
    hidden_inputs = numpy.dot(self.wih, inputs)
    # calculate the signals emerging from hidden layer
    hidden_outputs = self.activation_function(hidden_inputs)
    # calculate signals into final output layer
    final_inputs = numpy.dot(self.who, hidden_outputs)
    # calculate the signals emerging from final output layer
    final_outputs = self.activation_function(final_inputs)
    # output layer error is the (target - actual)
    output_errors = targets - final_outputs
    # hidden layer error is the output_errors, split by weights,
recombined at hidden nodes
    hidden_errors = numpy.dot(self.who.T, output_errors)
    # update the weights for the links between the hidden and
output layers
    self.who += self.lr * numpy.dot((output_errors *
final_outputs * (1.0 - final_outputs)),
numpy.transpose(hidden_outputs))
    # update the weights for the links between the input and
hidden layers
    self.wih += self.lr * numpy.dot((hidden_errors *
hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
    pass
  # query the neural network
  def query(self, inputs_list) :
    # convert inputs list to 2d array
    inputs = numpy.array(inputs_list, ndmin=2).T
    # calculate signals into hidden layer
    hidden_inputs = numpy.dot(self.wih, inputs)
    # calculate the signals emerging from hidden layer
    hidden_outputs = self.activation_function(hidden_inputs)
    # calculate signals into final output layer
    final_inputs = numpy.dot(self.who, hidden_outputs)
    # calculate the signals emerging from final output layer
    final_outputs = self.activation_function(final_inputs)
    return final_outputs
# number of input, hidden and output nodes
input_nodes = 784
hidden_nodes = 200
output_nodes = 10
# learning rate
learning_rate = 0.1

# create instance of neural network
n = neuralNetwork(input_nodes,hidden_nodes,output_nodes,
learning_rate)
# load the mnist training data CSV file into a list
training_data_file = open("mnist_dataset/mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# train the neural network
# epochs is the number of times the training data set is used for
training
epochs = 5
for e in range(epochs):
  # go through all records in the training data set
  for record in training_data_list:
    # split the record by the ',' commas
    all_values = record.split(',')
    # scale and shift the inputs
    inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) +
0.01
    # create the target output values (all 0.01, except the
desired label which is 0.99)
    targets = numpy.zeros(output_nodes) + 0.01
    # all_values[0] is the target label for this record
    targets[int(all_values[0])] = 0.99
    n.train(inputs, targets)
    pass
  pass
# load the mnist test data CSV file into a list
test_data_file = open("mnist_dataset/mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# test the neural network
# scorecard for how well the network performs, initially empty
scorecard = []
# go through all the records in the test data set
for record in test_data_list:
  # split the record by the ',' commas
  all_values = record.split(',')
  # correct answer is first value
  correct_label = int(all_values[0])
  # scale and shift the inputs
  inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
  # query the network
  outputs = n.query(inputs)
  # the index of the highest value corresponds to the label
  label = numpy.argmax(outputs)
  # append correct or incorrect to list
  if (label == correct_label):
    # network's answer matches correct answer, add 1 to
scorecard
    scorecard.append(1)
  else:
    # network's answer doesn't match correct answer, add 0 to
scorecard
    scorecard.append(0)
    pass
  pass
# calculate the performance score, the fraction of correct answers
scorecard_array = numpy.asarray(scorecard)
print ("performance = ", scorecard_array.sum() /
scorecard_array.size)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文