本文将介绍如何使用MNIST数据集构建手写数字分类器,并逐步提升训练模型的精度
一:模型构建篇
1.数据集介绍:
MNIST是一个广泛使用的数据集,用于手写数字分类任务。它由70,000个标记为28x28像素的手写数字的灰度图像组成。数据集被分成60,000个训练图像和10,000个测试图像。它总共有10个类(每个10个数字对应一个)。目前的任务是使用6w张训练图像训练一个模型,然后测试其在1w个图像上的分类精度。
2.加载数据集:
import mxnet as mx
mnist = mx.test_utils.get_mnist()
在运行上述源代码之后,整个MNIST数据集会被完全加载到内存中。共四个压缩包,每个训练集和测试集包括两个lables和images。
3.数据处理:
图像输入通常用一个四维数组表示(例如: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)
4.模型训练
下面的代码定义了一个LeNet的卷积神经网络架构。用tanh激活来代替神经元的sigmoid激活
data = mx.sym.var('data')
# 第一层卷积
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)) #池化层1
# 第二层卷积
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)) #池化层2
# 第一层全连接
flatten = mx.sym.flatten(data=pool2)
fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=500)
tanh3 = mx.sym.Activation(data=fc1, act_type="tanh")
# 第二层全连接
fc2 = mx.sym.FullyConnected(data=tanh3, num_hidden=10)
# softmax loss
lenet = mx.sym.SoftmaxOutput(data=fc2, name='softmax')
那么,很容易观察到这里使用了两个卷积层(conv1,conv2),两个池化层(pool1,pool2)和两个全连接层(fc1,fc2),并使用tanh作为激活函数,最终输入一个softmax层。
这里引入卷积层,池化层的概念:
1)卷积层
单个卷积层由一个或多个过滤器组成,每个过滤器(filter)都扮演特征检测器的角色。
如上图所示,我们输入1个28 * 28层数为1的图片,经过20个5 * 5层数为1的filter扫描后得到一个20 * 24 * 24的卷积层。(这里,大家可以自行查阅为何filter的层数需要与输入层保持相等)
2) 激活函数
首先需要明白一个道理,激活函数是可以写出表达式的函数,并不是抽象存在的。
在每个卷积层之后,通常会立即应用一个激活层。其目的是给一个在卷积层中刚经过线性计算操作的系统引入非线性特征。这一层会增加模型乃至整个神经网络的非线性特征,而且不会影响卷积层的感受野。
这里以tanh和Relu两种激活函数为例,简单介绍激活函数的作用和效果等
(第二部分需要将原始代码使用的tanh改为Relu做为对比试验)
tanh函数图像:
如图所示,上图为tanh的函数图像
优点 :输出以0为中心,比sigmoid函数训练时收敛更快。
缺点:仍然是饱和函数,没有解决梯度消失问题。
ReLU函数图像
如图所示,上图为ReLU的函数图像
优点 :解决了部分梯度消失问题,训练收敛速度相比tanh更快
缺点:当x<0时,出现梯度消失问题。
#这里可参见 Geoffrey Hinton(即深度学习之父)的论文:
Rectified Linear Units Improve Restricted Boltzmann Machines
3)池化层
在CNN的每一个conv(+激活)层之后都要包含池层,池化层有多种选择,常见有平均池化(average pooling)和L2-norm 池化,这里以常用的最大池化(max-pool)为例进行讲解。
这一层背后的直观推理是:一旦我们知道了原始输入(这里会有一个高激活值)中一个特定的特征,它与其它特征的相对位置就比它的绝对位置更重要。可想而知,这一层大幅减小了输入卷的空间维度。这到达了两个主要目的。第一个是权重参数的数目减少到了75%,因此降低了计算成本。第二是它可以控制过拟合(overfitting)。这个术语是指一个模型与训练样本太过匹配了,以至于用于验证和检测组时无法产生出好的结果。出现过拟合的表现是一个模型在训练集能达到 100% 或 99% 的准确度,而在测试数据上却只有50%。
5.结果分析:
lenet_model = mx.mod.Module(symbol=lenet, context=mx.cpu()) #使用gpu训练,请将cpu改为gpu
lenet_model.fit(train_iter,
eval_data=val_iter,
optimizer='sgd',
optimizer_params={'learning_rate':0.1}, #学习速率=0.1
eval_metric='acc',
batch_end_callback = mx.callback.Speedometer(batch_size, 100),
num_epoch=50)
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)
acc = mx.metric.Accuracy()
lenet_model.score(test_iter, acc)
print(acc)
最终得到的精确度:
意味着这个模型可以正确预测82%以上的图像,可见结果并不是很理想。
下一模块我将详细描述如何提升预测精确度。
二:模型优化篇
1.使用合适的激活函数
将原始代码使用的tanh改成Relu,修改过后的代码如下:
# 第一全连通层及其激活函数
fc1 = mx.sym.FullyConnected(data=data, num_hidden=128)
act1 = mx.sym.Activation(data=fc1, act_type="relu")
# 第二完全连通层及其激活函数
fc2 = mx.sym.FullyConnected(data=act1, num_hidden = 64)
act2 = mx.sym.Activation(data=fc2, act_type="relu")`在这里插入代码片`
fc3 = mx.sym.FullyConnected(data=act2, num_hidden = 32)
act3 = mx.sym.Activation(data=fc3, act_type="relu")
训练50次后得到的结果如下(修改num_epoch=50):
2.调整learning_rate
learning_rate | accuracy |
---|---|
0.05 | 0.9409 |
0.1 | 0.9527 |
0.15 | 0.9943 |
0.2 | 0.9874 |
上表可以看出,调整学习率,训练精度将产生相应的变化,各位可以尝试测试多种学习速率。
—END—