MXNet手写体识别

在本教程中,我们将逐步介绍如何使用MNIST数据集构建手写的数字分类器.对于一个刚刚接触深度学习的朋友来说,这个练习可以说是“Hello World”.

Note:This blog without all the resources that are outside the original,reproduced please indicate the source.

官网:mxnet.incubator.apache.org,文中如有任何错漏的地方请谅解并指出,不胜感激.

MNIST是一个广泛使用的数据集,用于手写数字分类任务。它由70,000个标记为28x28像素的手写数字的灰度图像组成。数据集被分成60,000个训练图像和10,000个测试图像。它总共有10个类(每个10个数字对应一个)。目前的任务是用6万张训练图像训练一个模型,然后测试其在10,000个测试图像上的分类精度。

图一:来自MNIST数据集的示例图像

前提

完成教程,您需要:

- �安装好MXNet 0.10 以上版本
- 安装python以及Jupyter Notebook.
$ pip install requests jupyter

加载数据

在我们定义模型之前,让我们先获取MNIST数据集。

下面的源代码下载并将图像和相应的标签载入内存。

import mxnet as mx
mnist = mx.test_utils.get_mnist()

在运行上述源代码之后,整个MNIST数据集应该被完全加载到内存中。注意,对于大型数据集,预先加载整个数据集是不可行的,就像我们在这里做的那样。我们需要的是一种机制,让我们可以快速有效地从源头直接流数据。MXNet数据迭代器通过提供精确的数据来补偿这里。数据迭代器是我们将输入数据输入到MXNet训练算法中的一种机制,它们对于初始化和使用非常简单,并且对速度进行了优化。在训练过程中,我们通常会小批量的训练样本,并且在整个训练周期中,我们会最后多次处理每个训练实例。在本教程中,我们将配置数据迭代器来以100为批次的方式提供示例。请记住,每个示例都是一个28x28灰度图像和相应的标签。

图像批次通常用一个4-D(�四维)数组表示(行如:batch_size,num_channels,width,height)。对于MNIST数据集,由于图像是灰度的,所以只有一个颜色通道。另外,因为图像是28x28像素,所以每个图像的宽度和高度等于28。因此,输入应该是这样:(batch_size,1,28,28)。另一个重点关注的是输入样本的顺序。当提供训练样本时,我们不能连续地给样本提供相同的标签,这是非常重要的。这样做会减缓训练。数据迭代器通过随机打乱输入来解决这个问题。注意,我们只需要打乱训练数据。这个顺序对测试数据没有影响。

下面的源代码初始化了MNIST数据集的数据迭代器。请注意,我们初始化了两个迭代器:一个用于训练数据,一个用于测试数据。

batch_size = 100
train_iter = mx.io.NDArrayIter(mnist['train_data'], mnist['train_label'], batch_size, shuffle=True)
val_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)

训练

我们将介绍几个执行手写数字识别任务的方法。第一种方法利用传统的深度神经网络结构,称为多层感知器(MLP)。我们将讨论它的缺点,并以此为动机引入第二种更高级的方法,称为卷积神经网络(CNN),它已经被证明能够很好地处理图像分类任务。

多层感知器

第一个方法是利用多层感知器来解决这个问题。我们将使用MXNet的符号接口定义MLP。我们首先为输入数据创建一个place holder变量。在使用MLP时,我们需要将28x28的图像压缩成一个平面一维结构的784(28 * 28)原始像素值。在平坦的矢量中,像素值的顺序并不重要,只要我们在所有图像上都是一致的。

data = mx.sym.var('data')
# Flatten the data from 4-D shape into 2-D (batch_size, num_channel*width*height)
data = mx.sym.flatten(data=data)

您可能会有疑问,我们是否通过扁平化来丢弃有价值的信息。这确实是事实,在保留输入形状的问题上我们会在讨论卷积神经网络的时候进行详细的介绍。现在,我们将继续使用扁平的图像。

MLP包含几个完全连接层。一个完全连接层或简称FC层,是层中的每个神经元与前一层中的每个神经元相连接的地方。从线性代数的角度来看,FC层对n x m输入矩阵x进行仿射变换,并输出n x k大小的矩阵Y,其中kFC层中的神经元数。k也被称为隐藏的大小。输出Y是根据等式Y = WX + b计算的,FC层有两个可学习的参数,即mxk权重矩阵W和mx1偏差向量b

在MLP中,大多数FC层的输出都被输入到一个激活函数中,适用于非线性元素。这一步是至关重要的,它给神经网络提供了分类输入的能力,而这些输入不是线性可分的。激活函数的常见选择是sigmoidtanhrecUNK linear unit(ReLU)。在这个示例中,我们将使用具有多个理想属性的ReLU激活函数,通常他被认为是默认选项。

下面的代码声明了两个完全连接层,每个层有128个和64个神经元。此外,这些FC层被夹在ReLU激活层之间,每个层负责执行在FC层输出上执行元素的ReLU转换。

# The first fully-connected layer and the corresponding activation function
fc1  = mx.sym.FullyConnected(data=data, num_hidden=128)
act1 = mx.sym.Activation(data=fc1, act_type="relu")

# The second fully-connected layer and the corresponding activation function
fc2  = mx.sym.FullyConnected(data=act1, num_hidden = 64)
act2 = mx.sym.Activation(data=fc2, act_type="relu")

最后一个完全连接的层通常有其隐藏的大小,等于数据集中的输出类的数量。这个层的激活函数将是softmax函数。Softmax层将其输入映射为每一类输出的概率分数。在训练阶段,一个损失函数计算网络预测的概率分布(softmax output)与标签给出的真实概率分布之间的交叉熵。

下面的源代码声明了最终全连接的10级的层。顺便说一下,10是数字的总数。该层的输出被输入到一个软maxoutput层,在一个过程中执行软max和交叉熵损失计算。请注意,损失计算只在训练期间发生。

# MNIST has 10 classes
fc3  = mx.sym.FullyConnected(data=act2, num_hidden=10)
# Softmax with cross entropy loss
mlp  = mx.sym.SoftmaxOutput(data=fc3, name='softmax')
MXNet手写体识别_第1张图片
图二:MNIST的MLP网络架构

现在,数据迭代器和神经网络都被定义了,我们可以开始训练了。在这里,我们将使用MXNet中的模块特性,它为在预定义网络上运行训练和推理提供高级抽象。模块API允许用户指定适当的参数来控制培训的进展。

下面的源代码初始化一个模块来训练我们上面定义的MLP网络。对于我们的培训,我们将使用随机梯度下降(SGD)优化器。特别地,我们将使用迷你批处理SGD。标准的SGD进程一次只处理一个示例。在实践中,这是非常缓慢的,一个可以通过小批量的例子来加快进程。在这种情况下,我们的批量大小为100,这是一个合理的选择。我们在这里选择的另一个参数是学习速率,它控制优化器获取解决方案所需的步骤大小。我们会选择一个0.1的学习率,也是一个合理的选择。诸如批量大小和学习速率等设置通常被称为超参数。我们给予他们的价值观会对他们的训练有很大的影响。为了本教程的目的,我们将从一些合理和安全的值开始。在其他教程中,我们将讨论如何为最佳模型性能寻找超参数的组合。

通常情况下,一个运行训练直到收敛,这意味着我们从训练数据中学习了一组很好的模型参数(权重+偏差)。为了本教程的目的,我们将运行10次训练并停止。�一次训练是整个列车数据的一个完整的传递。

import logging
logging.getLogger().setLevel(logging.DEBUG)  # logging to stdout
# create a trainable module on CPU
mlp_model = mx.mod.Module(symbol=mlp, context=mx.cpu())
mlp_model.fit(train_iter,  # train data
              eval_data=val_iter,  # validation data
              optimizer='sgd',  # use SGD to train
              optimizer_params={'learning_rate':0.1},  # use fixed learning rate
              eval_metric='acc',  # report accuracy during training
              batch_end_callback = mx.callback.Speedometer(batch_size, 100), # output progress for each 100 data batches
              num_epoch=10)  # train for at most 10 dataset passes
              

预测

在上述培训完成后,我们可以通过对测试数据的预测来评估培训的模型。下面的源代码计算每个测试图像的预测概率得分。prob[i][j]是第i个测试图像包含j - th输出类的概率。

test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
prob = mlp_model.predict(test_iter)
assert prob.shape == (10000, 10)

由于数据集也有所有的测试图像的标签,我们可以计算精度指标如下:

test_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
# predict accuracy of mlp
acc = mx.metric.Accuracy()
mlp_model.score(test_iter, acc)
print(acc)
assert acc.get()[1] > 0.96

如果一切顺利,我们应该看到一个大约0.96的精度值,这意味着我们能够准确地预测96%的测试图像中的数字。这是一个很好的结果。但正如我们将在本教程的下一部分中看到的,我们可以做得更好。

卷积神经网络

上文中,我们简要介绍了MLP的一个缺点,我们说到我们需要丢弃输入图像的原始形状,并将它作为一个矢量来压平,然后我们可以把它作为输入到MLP的第一个完全连接的层。这是一个很重要的问题,因为我们没有利用图像中的像素在水平和垂直轴上具有自然空间相关的事实。一个卷积神经网络(CNN)旨在通过使用更结构化的权重表示来解决这个问题。它没有把图像压平,而是做一个简单的矩阵乘法,而是使用一个或多个卷积层,每个层在输入图像上执行二维的卷积。

单个卷积层由一个或多个过滤器组成,每个过滤器都扮演特性检测器的角色。在训练过程中,CNN学习了这些过滤器的适当表示(参数)。类似于MLP,卷积层的输出通过应用非线性转换。除了卷积层之外,CNN的另一个关键方面是汇聚层。一个汇聚层可以使CNN的平移不变:即使在向左/右/向上移动一些像素时,数字仍然保持不变。池层将n个x m补丁减少为单个值,以使网络对空间位置不敏感。在CNN的每一个conv(+激活)层之后都要包含池层。

下面的代码定义了一个称为LeNet的卷积神经网络架构。LeNet是一个很受欢迎的网络,它可以很好地处理数字分类任务。我们将使用与原来的LeNet实现稍微不同的版本,用tanh激活来代替神经元的sigmoid激活

data = mx.sym.var('data')
# first conv layer
conv1 = mx.sym.Convolution(data=data, kernel=(5,5), num_filter=20)
tanh1 = mx.sym.Activation(data=conv1, act_type="tanh")
pool1 = mx.sym.Pooling(data=tanh1, pool_type="max", kernel=(2,2), stride=(2,2))
# second conv layer
conv2 = mx.sym.Convolution(data=pool1, kernel=(5,5), num_filter=50)
tanh2 = mx.sym.Activation(data=conv2, act_type="tanh")
pool2 = mx.sym.Pooling(data=tanh2, pool_type="max", kernel=(2,2), stride=(2,2))
# first fullc layer
flatten = mx.sym.flatten(data=pool2)
fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=500)
tanh3 = mx.sym.Activation(data=fc1, act_type="tanh")
# second fullc
fc2 = mx.sym.FullyConnected(data=tanh3, num_hidden=10)
# softmax loss
lenet = mx.sym.SoftmaxOutput(data=fc2, name='softmax')
MXNet手写体识别_第2张图片
图三

现在我们用同样的超参数来训练LeNet。注意,如果GPU可用,我们建议使用它。这大大加快了计算速度,因为LeNet比之前的多层感知器更加复杂和计算更密集。为此,我们只需要将mx. cpu()改为mx.gpu(),而MXNet则负责其余部分。就像以前一样,我们将在10次训练之后停止训练。

# create a trainable module on GPU 0
lenet_model = mx.mod.Module(symbol=lenet, context=mx.cpu())
# train with the same
lenet_model.fit(train_iter,
                eval_data=val_iter,
                optimizer='sgd',
                optimizer_params={'learning_rate':0.1},
                eval_metric='acc',
                batch_end_callback = mx.callback.Speedometer(batch_size, 100),
                num_epoch=10)                

预测

最后,我们将使用经过训练的LeNet模型来生成对测试数据的预测。

test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
prob = lenet_model.predict(test_iter)
test_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
# predict accuracy for lenet
acc = mx.metric.Accuracy()
lenet_model.score(test_iter, acc)
print(acc)
assert acc.get()[1] > 0.98

如果一切顺利,我们应该会看到使用LeNet的预测更准确。在CNN,我们应该能够正确预测98%的测试图像。

小结

在本教程中,我们学习了如何使用MXNet来解决一个标准的计算机视觉问题:将手写数字的图像分类。您已经了解了如何快速、轻松地构建、训练和评估诸如MLPCNN等模型,并使用MXNet

你可能感兴趣的:(MXNet手写体识别)