Kaggle机器学习实战(6)——MNIST(下)
之前已经使用TensorFlow的高层封装Keras运行过一次CNN,这次直接使用TensorFlow复现经典的LeNet-5来完成MNIST手写数字识别,顺便学习一下TensorFlow的基本使用方法。
LeNet简介
LeNet出自Yann LeCun于1998年发表的经典论文《Gradient-Based Learning Applied to Document Recognition》,他首次使用卷积神经网络进行手写数字识别,并达到了惊人的99.2%的准确率。他在文章中详细阐述了卷积核以及降采样的用处,并因此被认为是卷积神经网络之父。由于当时受制于数据量与算力,卷积神经网络没能得到更进一步的发展,直到2012年的AlexNet横空出世,才让人们对CNN的认识达到了空前的高度。
该网络共有7层构成(不包括输入层):
- 输入层为经过处理的32×32×1的手写数字图像。
- 第1层为使用大小为5×5、深度为6的卷积层,不使用填充,步长为1,经过卷积处理的图像的边长变为((32-5)/1+1=28)。该层共有(5×5+1)×6=156)个参数。
- 第2层为使用2×2的平均池化层(降采样层),经过处理的图像的边长变为14。并用Sigmoid函数去线性化。
- 第3层为使用大小为5×5、深度为16的卷积层,但是每个卷积核与上一层的多个特征图谱相连接。经过卷积处理的图像的边长变为((14-5)/1+1=10)。该层共有1516个参数。
- 第4层为池化层,经过处理的图像的边长变为5。
- 第5层为使用大小为5×5、深度为120的卷积层,经过卷积处理的图像的边长变为((5-5)/1+1=1),由于这一步实际上将图像展开为一维向量,可以视为全连接层。共有48120个参数。
- 第6层为全连接层,共有84个节点,使用Sigmoid激活函数。共有((120+1)×84=10164)个参数。
- 第7层为输出层,使用RBF函数,可认为输出的是输入图像与各数字ASCII编码图的相似度,越接近于0表示与该标准图像越接近。
原论文中附有网络结构的图解,结合图片能对该网络有更好的认识。
现在结合Kaggle上的入门训练《Digit Recognizer》并使用TensorFlow加深对卷积神经网络的认识。本文参考了一篇kernel:《TensorFlow deep NN》,以及《TensorFlow实战Google深度学习框架》、《TensorFlow技术解析与实战》两本书。
使用TensorFlow构建卷积神经网络
TensorFlow是目前运用最多的深度学习框架,每个人工智能学者当熟练运用之。而本次最简单的CNN只用到几个简单的API。更进一步的操作包括TensorBoard、RNN等有时间会进一步学习。
1 | # 调包 |
虽然TensorFlow可以调用自带的数据集API来读取MNIST数据,但依然严格按照Kaggle提供的数据进行实践。Kaggle上的练习提供了42000个带标签的训练集样本和28000个测试集样本。每个样本具有784个特征。我们的模型接收具有4个维度的输入,第一维表示样本量,第二维和第三维表示图像的尺寸,第四维表示颜色通道数。我们需要将其转换为需要的数据类型,并进行标准化。最后,分割训练集和验证集。
1 | train = pd.read_csv('train.csv') |
卷积神经网络经过20年的发展,相较1998年的LeNet,现常用的CNN已有较大变化:
- 输入层图片大小从32×32×1改为28×28×1,直接使用标准MNIST数据;
- 使用最大池化代替平均池化;
- 激活函数从Sigmoid换成了ReLU,后者是目前最常用的激活函数;
- 在全连接层之间添加了一层Dropout,可以防止过拟合;
- 最后的多分类输出层使用Softmax函数。
本次练习采用的模型为:CONV->MAX_POOL->CONV->MAX_POOL->FC->Dropout->FC->Softmax,具有7层深度的结构在能得到较高的准确率的同时,也能在一块普通GPU上很快跑完。
1 | # 定义超参数 |
层1:卷积层
第一层卷积层采用5×5的卷积核,深度为32,步长为1,使用全0填充使输出图像的维度与输入相同(padding='SAME'
)。输入数据的尺寸为(BATCH_SIZE
×28×28×1),其中BATCH_SIZE
为预先设置好的超参数。在大多数深度学习任务中,通常会使用mini-batch梯度下降,能够保证内存不溢出并提高收敛速度。由于MNIST是一个很小的数据集,因此将整个数据集用于一次迭代也是可行的。
卷积后,图像的尺寸为28×28×32。
1 | W1 = weight([5, 5, 1, 32]) |
层2:池化层
第二层池化层采用2×2的最大池化,步长为2,使用全0填充。池化处理后图像的尺寸为14×14,深度为32。
1 | POOL2 = pool(CONV1) |
层3:卷积层
第三层卷积层采用5×5的卷积核,深度为64,步长为1,使用全0填充。卷积后,图像的尺寸为14×14×64。
1 | W2 = weight([5, 5, 32, 64]) |
层4:池化层
第四层池化层采用2×2的最大池化,步长为2,使用全0填充。池化处理后图像的尺寸为7×7,深度为64。
1 | POOL4 = pool(CONV3) |
层5:全连接层+Dropout
该层为传统神经网络结构,首先需要将上一层输出的矩阵扁平化,再使用ReLU激活函数。然后使用Dropout稀疏化矩阵并且防止过拟合。keep_prob
参数表示保留的神经元数目,例如该参数设置为0.7即表示有30%的神经元被抑制。
1 | # 展开 |
层6:全连接层
该层经过Softmax函数即可输出样本属于各个类别的概率,但由于损失函数需未经Softmax的输出值,这里暂时先不进行Softmax操作。
1 | W4 = weight([1024,10]) |
训练及评估模型
对于多分类任务,通常使用交叉熵损失函数,TensorFlow有softmax_cross_entropy_with_logits
和sparse_softmax_cross_entropy_with_logits
两种封装好的损失函数,前者适用于one-hot编码后的标签输入,后者适用于原始标签输入。两者都会先对输入数据进行一层softmax,因此这里的logits
需要接收未经过softmax层的上一层的输出。
1 | # 定义交叉熵损失函数 |
然后定义优化器,这里选择Adam优化器,参数一般只需设置初始学习率即可,其他保持默认设置。优化目标即为最小化损失函数。
1 | # 定义优化器与优化目标 |
然后使用准确率作为评估指标。
1 | # 定义评估指标 |
使用tf.Session
会话机制开始训练模型。设置每10个epoch输出一次在验证集上的准确率。
1 | with tf.Session() as sess: |
经过200个epoch的迭代,在验证集上的准确率达到了99.119%。想要进一步提高模型的性能,可以扩大训练集,例如使用数据增强对图片进行轻微的变形等,更直接的办法是把整个数据集作为训练集而不采用验证集。接下来用整个训练集重新训练模型,并在测试集上预测,最后上传至Kaggle查看得分。
1 | output = tf.nn.softmax(FC6) |
得到了测试集上的预测,然后导出预测的数据。
1 | label = label.astype('int') |
在测试集上取得了0.98742的准确率。
总结
- 这次实现的是最简单的卷积神经网络,更深、更加实用的CNN包括Inception、ResNet等有待进一步学习。
- 相比于易于使用的Keras,TensorFlow最大的优势是能够自定义网络(虽然本文没有体现)。