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
2
3
4
5
# 调包
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split

虽然TensorFlow可以调用自带的数据集API来读取MNIST数据,但依然严格按照Kaggle提供的数据进行实践。Kaggle上的练习提供了42000个带标签的训练集样本和28000个测试集样本。每个样本具有784个特征。我们的模型接收具有4个维度的输入,第一维表示样本量,第二维和第三维表示图像的尺寸,第四维表示颜色通道数。我们需要将其转换为需要的数据类型,并进行标准化。最后,分割训练集和验证集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

y = train['label'].values
train.drop(['label'], axis=1, inplace=True)

# 标准化
train = train / 255.
test = test / 255.

# 转换为合适的shape
train = train.values.reshape(-1,28,28,1)
test = test.values.reshape(-1,28,28,1)

# 对y进行one-hot编码
def one_hot_encoding(y, C):
return np.eye(C)[y.reshape(-1)]

y = one_hot_encoding(y, 10)

# 拆分训练集和验证集
X_train, X_test, y_train, y_test = train_test_split(train, y, test_size=0.1)

卷积神经网络经过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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 定义超参数
LEARNING_RATE = 1e-3 #初始学习率
KEEP_PROB = 1.0 #Dropout保留神经元的比例
BATCH_SIZE = 128 #一个batch的大小
EPOCHS = 200 #整个数据集迭代次数

# 定义占位符,shape[0]设为None便于自定batch的大小
X = tf.placeholder(tf.float32, shape=[None, 28, 28, 1])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

# 封装接下来会重复使用的代码
# 随机初始化权重
def weight(shape):
return tf.Variable(tf.truncated_normal(shape, mean=0, stddev=1))

# 常量初始化偏置项
def bias(shape):
return tf.Variable(tf.constant(0.1, shape=shape))

# 卷积层
def conv(X, W, strides=1):
return tf.nn.conv2d(X, W, strides=[1, strides, strides, 1], padding='SAME')

# 池化层
def pool(X, k=2, strides=2):
return tf.nn.max_pool(X, ksize=[1, k, k, 1], strides=[1, strides, strides, 1], padding='SAME')

# 激活函数
def relu(X, b):
return tf.nn.relu(tf.nn.bias_add(X, b))

# 全连接层
def dense(X, W, b):
return relu(tf.matmul(X, W), b)

层1:卷积层

第一层卷积层采用5×5的卷积核,深度为32,步长为1,使用全0填充使输出图像的维度与输入相同(padding='SAME')。输入数据的尺寸为(BATCH_SIZE×28×28×1),其中BATCH_SIZE为预先设置好的超参数。在大多数深度学习任务中,通常会使用mini-batch梯度下降,能够保证内存不溢出并提高收敛速度。由于MNIST是一个很小的数据集,因此将整个数据集用于一次迭代也是可行的。

卷积后,图像的尺寸为28×28×32。

1
2
3
W1 = weight([5, 5, 1, 32])
b1 = bias([32])
CONV1 = relu(conv(X, W1), b1)

层2:池化层

第二层池化层采用2×2的最大池化,步长为2,使用全0填充。池化处理后图像的尺寸为14×14,深度为32。

1
POOL2 = pool(CONV1)

层3:卷积层

第三层卷积层采用5×5的卷积核,深度为64,步长为1,使用全0填充。卷积后,图像的尺寸为14×14×64。

1
2
3
W2 = weight([5, 5, 32, 64])
b2 = bias([64])
CONV3 = relu(conv(POOL2, W2), b2)

层4:池化层

第四层池化层采用2×2的最大池化,步长为2,使用全0填充。池化处理后图像的尺寸为7×7,深度为64。

1
POOL4 = pool(CONV3)

层5:全连接层+Dropout

该层为传统神经网络结构,首先需要将上一层输出的矩阵扁平化,再使用ReLU激活函数。然后使用Dropout稀疏化矩阵并且防止过拟合。keep_prob参数表示保留的神经元数目,例如该参数设置为0.7即表示有30%的神经元被抑制。

1
2
3
4
5
6
7
8
9
# 展开
Flatten = tf.reshape(POOL4,[-1,7*7*64])

W3 = weight([7*7*64,1024])
b3 = bias([1024])
FC5 = dense(Flatten, W3, b3)

# Dropout
Dropout = tf.nn.dropout(FC5, keep_prob=KEEP_PROB)

层6:全连接层

该层经过Softmax函数即可输出样本属于各个类别的概率,但由于损失函数需未经Softmax的输出值,这里暂时先不进行Softmax操作。

1
2
3
W4 = weight([1024,10])
b4 = bias([10])
FC6 = tf.add(tf.matmul(Dropout, W4),b4)

训练及评估模型

对于多分类任务,通常使用交叉熵损失函数,TensorFlow有softmax_cross_entropy_with_logitssparse_softmax_cross_entropy_with_logits两种封装好的损失函数,前者适用于one-hot编码后的标签输入,后者适用于原始标签输入。两者都会先对输入数据进行一层softmax,因此这里的logits需要接收未经过softmax层的上一层的输出。

1
2
# 定义交叉熵损失函数
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=FC6))

然后定义优化器,这里选择Adam优化器,参数一般只需设置初始学习率即可,其他保持默认设置。优化目标即为最小化损失函数。

1
2
3
# 定义优化器与优化目标
optimizer = tf.train.AdamOptimizer(learning_rate=LEARNING_RATE)
train_step = optimizer.minimize(cross_entropy)

然后使用准确率作为评估指标。

1
2
3
# 定义评估指标
correct_prediction = tf.equal(tf.argmax(FC6, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

使用tf.Session会话机制开始训练模型。设置每10个epoch输出一次在验证集上的准确率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
with tf.Session() as sess:
# 全局初始化参数
tf.global_variables_initializer().run()
# mini-batch
epoch = 1
while epoch <= EPOCHS:
for i in range(len(X_train) // BATCH_SIZE + 1):
start = i*BATCH_SIZE
end = min((i+1)*BATCH_SIZE, len(X_train))
train_step.run(feed_dict={X: X_train[start:end], y_: y_train[start:end]})
# 每过10个epoch,输出一次当前的验证集精度
if not epoch % 10:
dev_accuracy = accuracy.eval(feed_dict={X:X_test, y_:y_test})
print("Epoch %d, validation accuracy: %g"%(epoch, dev_accuracy))
epoch += 1

经过200个epoch的迭代,在验证集上的准确率达到了99.119%。想要进一步提高模型的性能,可以扩大训练集,例如使用数据增强对图片进行轻微的变形等,更直接的办法是把整个数据集作为训练集而不采用验证集。接下来用整个训练集重新训练模型,并在测试集上预测,最后上传至Kaggle查看得分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
output = tf.nn.softmax(FC6)
label = np.zeros(test.shape[0])
pred = tf.argmax(output, 1)
with tf.Session() as sess:
tf.global_variables_initializer().run()
epoch = 1
while epoch <= EPOCHS:
for i in range(len(train) // BATCH_SIZE + 1):
start = i*BATCH_SIZE
end = min((i+1)*BATCH_SIZE, len(train))
train_step.run(feed_dict={X: train[start:end], y_: y[start:end]})
epoch += 1

for j in range(len(test) // BATCH_SIZE + 1):
start = j*BATCH_SIZE
end = min((j+1)*BATCH_SIZE, len(test))
label[start:end] = pred.eval(feed_dict={X: test[start:end]})

得到了测试集上的预测,然后导出预测的数据。

1
2
3
4
5
label = label.astype('int')
ImageId = np.arange(1, test.shape[0]+1)

submission = pd.DataFrame({'ImageId': ImageId, 'Label': label})
submission.to_csv("MNISTSubmission.csv", index=False)

在测试集上取得了0.98742的准确率。

总结

  • 这次实现的是最简单的卷积神经网络,更深、更加实用的CNN包括Inception、ResNet等有待进一步学习。
  • 相比于易于使用的Keras,TensorFlow最大的优势是能够自定义网络(虽然本文没有体现)。