MXNet官方文档中文版教程(7):手写数字识别(Handwritten Digit Recognition)
在本教程中,我们将逐步介绍如何使用MNIST 数据集构建手写数字分类器。 对于深度学习新手来说,这个练习可以说是和“Hello World”等同的。
MNIST 是广泛使用的用于手写数字分类任务的数据集。它由70,000个有标记的,28x28分辨率的手写数字图像组成。数据集分为6万个训练图像和10,000个测试图像。共有10个类别(10个数字每个一类)。目前的任务是使用60,000个训练图像训练模型,并随后在10,000张测试图像上测试其分类准确率。
前提条件
为了完成以下教程,我们需要:
- MXNet:安装教程
- Jupyter Notebook and Python Requests.
pip install jupyter requests
- 1
加载数据
在我们定义模型之前,我们先来获取MNIST 数据集。
以下源代码下载并将图像和相应的标签加载到内存中。
import mxnet as mx
mnist = mx.test_utils.get_mnist()
- 1
- 2
运行上述源代码后,整个MNIST数据集应该被完全加载到内存中。 请注意,对于大型数据集,先预加载整个数据集是不可行的。我们需要一种可以直接从源头快速有效地传输数据的机制。 MXNet数据迭代器提供了解决方案。 数据迭代器是一种将数据输入到MXNet训练算法中,能够非常简单的初始化和使用,并针对速度进行了优化的机制。 在训练时,我们通常以小批量处理训练样本,并且在整个训练生命周期中将多次处理每个训练样本。 在本教程中,我们将配置数据迭代器以100进行批量提供样本。请记住,每个样本都是一个28x28的灰度图像及其对应的标签。
图像批次通常由4维数组表示,其形状为(batch_size,num_channels,width,height)。 对于MNIST数据集,由于图像是灰度级,所以只有一个颜色通道。 此外,图像是28×28像素,因此每个图像的宽度和高度等于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)
- 1
- 2
- 3
- 4
训练
我们将介绍实现手写数字识别任务的几种方法。 第一种方法利用传统的称为多层感知(MLP)的深度神经网络结构。 我们将讨论其缺点,并引入第二种更先进的方法,称为卷积神经网络(CNN),它已被证明在图像分类任务中工作得很好。
多层感知
第一种方法利用多层感知器来解决这个问题。 我们将使用MXNet的符号交互定义MLP。 我们首先为输入数据创建一个占位符变量。 当使用MLP时,我们需要将我们的28x28图像平展为784(28 * 28)原始像素值的平面1维结构。 只要我们在所有图像中都这样做,平面向量中像素值的顺序就不重要了。
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)
- 1
- 2
- 3
人们可能会想知道平展是否会丢失有价值的信息。 这确实是真的,当我们谈论保持输入形状不变的卷积神经网络时,我们将会更多地讨论这个问题。现在,我们将继续使用平展的图像。
MLP包含几个全连接层。 全连接层(简称FC层)中每个神经元都与前一层中的每个神经元相连。 从线性代数的角度来说,FC层对n×m的输入矩阵X做仿射变换,并且输出大小为n×k的矩阵Y,其中k是FC层中的神经元的个数。 k也被称为隐藏层大小。根据等式Y=WX+bY=WX+b 计算输出Y。 FC层具有两个可学习的参数,即m×k的权重矩阵W和m×1的偏置向量b。
在MLP中,大多数FC层的输出被送到**函数中,该函数进行非线性操作。 这一步是至关重要的,它使神经网络能够对线性不可分的输入进行分类。 常用的**函数是 sigmoid,tanh和ReLU。 在这个例子中,我们将使用具有多个所需属性的ReLU**函数,这也是通常的默认选择。
以下代码定义两个全连接层,每个层分别为128个和64个神经元。此外,这些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")
- 1
- 2
- 3
- 4
- 5
- 6
- 7
最后一个全连接层的隐藏大小通常等于数据集中的输出类别个数。该层的**功能将是softmax函数。 Softmax层将其输入映射到每一类输出的概率分数。 在训练阶段,损失函数计算网络预测的概率分布(softmax输出)与标签给出的真实概率分布之间的交叉熵。
以下源代码定义了大小为10的最后一层全连接层。10代表的是数字的个数。该层的输出被送到SoftMaxOutput 层,其一次性就算softmax和交叉熵 loss。 请注意,仅在训练时才计算损失函数。
# 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')
- 1
- 2
- 3
- 4
定义了数据迭代器和神经网络之后,我们可以开始训练。这里我们将使用MXNet中的module 功能,它提供了一个用于在预定义网络上训练和推理的高层抽象。module 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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
预测
训练完成后,我们可以通过在测试数据上预测来评估训练好的模型。 以下代码对每张测试图像计算预测概率得分。 prob[i][j]是第i个测试图像的第j个输出类的概率。
test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
prob = mlp_model.predict(test_iter)
assert prob.shape == (10000, 10)
- 1
- 2
- 3
由于数据集对所有测试图像也有标签,因此可以按以下方式计算准确率度量:
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
- 1
- 2
- 3
- 4
- 5
- 6
如果一切顺利,我们应该看到准确率大约为0.96,这意味着我们可以准确预测96%的测试图像中的数字。这是一个很好的结果。但是正如我们将在本教程的下一部分中看到的那样,我们可以做的比这更好。
卷积神经网络
首先,我们简要介绍了MLP的一个缺点,我们之前说需要舍弃输入图像的原始形状并将其平展为一个向量,然后才能将其作为MLP第一个全连接层的输入。实际上这是一个重要的问题,因为我们没有利用到沿水平轴和垂直轴方向,图像中的像素所具有的天然空间相关性。卷积神经网络(CNN)旨在通过使用更结构化的权重表示来解决这个问题。 它没有平展图像并进行简单的矩阵乘法,而是采用一个或多个卷积层,每个卷积层在输入图像上做2维卷积运算。
单个卷积层包括一个或多个滤波器,每个滤波器都起到特征检测器的作用。在训练时,CNN学习适用于这些滤波器的合理的表示(参数)。与MLP类似,卷积层的输出进行非线性变换。除了卷积层之外,CNN的另一个关键方面是池化层。 池化层用于使CNN保持转平移不变性:一个数字即使向左/向右/向上/向下移动几个像素,它仍然保持不变。池化层将n×m的区域降维成单个值,以降低网络对空间位置的敏感性。CNN中每个卷积层(+**层)之后始终有池化层。
以下代码定义了称为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')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
现在我们用和之前相同的超参数训练LeNet。请注意,如果可以用GPU,我们建议使用它,因为LeNet比以前的多层感知器更复杂,计算更多,GPU可以大大加快了计算。为此,我们只需要将mx.cpu() 更改为mx.gpu(),其余操作将由MXNet完成。和之前一样,我们将在十个批次后停止训练。
# 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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
预测
最后,我们将使用经过训练的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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
如果一切顺利,我们应该看到使用LeNet进行预测的准确率更高。 使用CNN,我们应该能够在测试图像上达到98%的准确率。
总结
在本教程中,我们已经学会了如何使用MXNet来解决标准的计算机视觉问题:手写数字图像分类。 你已经看到如何使用MXNet快速轻松地构建,训练和评估MLP和CNN等模型。