Fashion MNIST with Tensorflow +
Fashion MNIST 是德国一家时尚公司提供的数据集,包含十个品类的七万中商品。其数据格式,图片尺寸,数据集大小都保持和手写数字 MNIST 一模一样,完全可以起到替代的作用,而且可以提升挑战难度和算法优化的空间。
之前已经尝试过不用 Tensorflow 自行实现梯度下降(手工打造神经网络: 透视分析),现在来看看用 Tensorflow 在 Fashion MNIST 数据集的尝试。关于 Tensorflow 的安装可以看我这篇文章: CUDA+cuDNN+Tensorflow-GPU Install
imageTensorflow 官方教程有两篇以MNIST为例的资料, 入门篇跳过直接看深度卷积神经网络的实现 - Deep MNIST for Experts,官方教程中的代码不是那么好懂,可以像庖丁解牛一样把教程拆解开来一点点理解。
我们首先来看看CNN的三个重要组成部分: 卷积,池化和全连接层。
卷积
卷积可以通过从输入的一小块数据中抽取图像的特征,并保留像素间的空间关系。美图秀秀的各种特效和滤镜其实就可以看做是卷积操作的实例。
卷积核也叫滤波器,通过在图像上滑动滤波器并计算点乘得到矩阵叫做卷积特征或者特征图。卷积特征由深度,步长和填充三个参数决定。如果卷积核尺寸过大,会导致提取图像的特征过于复杂,尺寸过小,难以表示有用的特征。实际应用中一般选取5x5或者7x7的最佳。卷积核是奇数的,这样就有了中心和半径的概念,也可以保证特征图的输入尺寸与输出尺寸一致。
让卷积核在原始图像上滑动,在每个位置上计算对应元素的乘积,并把乘积的和作为最后的结果,得到输出矩阵(粉色)中的每一个元素的值。 图 7池化
空间池化的主要目的是降维,在保持原有空间特征的基础上最大限度将数组的维度变小。空间池化有下面几种方式:最大化、平均化、加和等等,如最大池化就是在每个2x2的空间邻域取出最大值。
全连接
全连接层的目的是将卷积和池化层的输出的高级特征把输入图像基于训练数据集进行分类。对于MNIST这样的多分类任务来说当然还是选择 softmax 好了。
实施步骤
Step 0: 获取数据,设置占位符,初始化变量并启动 Session
这段简单代码就不解释了
mnist = input_data.read_data_sets("d:/dev/fashion-mnist/", one_hot=True)
sess = tf.InteractiveSession()
# 原始图片的shape为28x28, 单通道
x = tf.placeholder("float", shape=[None, 784])
# 穿戴类的10个分类
y_ = tf.placeholder("float", shape=[None, 10])
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
sess.run(tf.initialize_all_variables())
Step 1: 初始化所有的滤波器,设置随机权重
权重矩阵的形状和以前一样也是784x10, 但官方用到了 truncated_normal 来初始化权重,因为模型需要创建很多权重,在初始化时最好加入少量的噪声来打破对称性以及避免0梯度。
这个函数从截断的正态分布中输出随机值,截断的逻辑是当随机数与平均值的标准偏差大于两倍。
tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
下面写段小代码对"截断的正态分布"做一个可视化展示。
import tensorflow as tf
import matplotlib.pyplot as plt
A = tf.truncated_normal([10000, 10])
with tf.Session() as sess:
a = sess.run(A)
plt.hist(a, 100, (-3, 3));
image.png
对于ReLU神经元,比较好的做法是用一个较小的正数来初始化偏置项,以避免神经元节点输出恒为零,这里用到的初始值是0.1
因为权重和偏置会设置多次,所以定义了两个函数复用
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
Step 2:以一张训练图像作为输入,通过前向传播过程(卷积,ReLU 和池化操作,以及全连接层的前向传播),找到各个类的输出概率。
卷积和池化操作会调用多次,所以也定义了两个函数复用:
卷积函数的四个参数分别是训练图像,卷积核,步长,填充。卷积核具有[filter_height, filter_width, in_channels, out_channels]这样的 shape,具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数], 它的第三维就是训练图形的第四维; 步长设为1; padding='SAME' 表示卷积核扫描的时候可以停留在图像边缘,在矩阵周边会补一圈零,所以当步长为1时生成尺寸不变,如果这个参数为'VALID'则生成窄卷积,结果比原始图片小。
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
池化函数的四个参数分别是池化输入,池化窗大小,步长,填充。池化层一般在卷积层后面,这里的输入就是卷积输出的特征图,具有[batch, height, width, channels]这样的shape; 池化窗大小是四维向量[1, height, width, 1],batch和channels一般缺省为1;步长设为2,即将原尺寸的长和款各除以2;填充参数和卷积函数一样。
# 求最大值池化,长宽缩小一半
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
2.1 第一次卷积和池化操作
# 卷积核为5x5矩阵,in_channels 1, out_channels 32[提取32个特征]
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
# 原始图片转为4维tensor满足卷积和池化要求
x_image = tf.reshape(x, [-1,28,28,1])
# 卷积后RELU, output size 28x28x32
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
# 池化后 output size 14x14x32
h_pool1 = max_pool_2x2(h_conv1)
2.2 第二次卷积和池化操作
#卷积核为5x5矩阵,in_channels 32, out_channels 64[提取64个特征]
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
# 卷积后RELU, output size 14x14x64
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
# 池化后 output size 7x7x64
h_pool2 = max_pool_2x2(h_conv2)
2.3 全连接层前向传播
# 隐藏层1024个神经元
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
# 4维张量转2维张量,第一维是样本数,第二维是神经元个数3136个
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
# 随机关闭一些神经元防止过拟
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
# 从1024个神经元映射到10个神经元
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
Dropout 预防过拟合,随机将x矩阵中一部分元素变为零,剩下的变成原值的 “1/keep_prob” 倍。
tf.nn.dropout(x, keep_prob, noise_shape=None, seed=None, name=None)
Step 3: 在输出层计算总误差,执行计算图
这里的损失函数是目标类别和预测类别之间的交叉熵,训练优化器用的是AdamOptimizer,在代码之后会讲。
# 计算交叉熵损失
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
# 创建优化器
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
#计算准确率, tf.argmax函数 在 label 中找出数值最大的那个元素的下标
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
sess.run(tf.initialize_all_variables())
Tensorflow 中所有的优化器实现都是基于tf.train.Optimizer这个基类的,常用的有以下这些实现
GradientDescentOptimizer
AdagradOptimizer
AdagradDAOptimizer
MomentumOptimizer
AdamOptimizer
FtrlOptimizer
RMSPropOptimizer
先挑两个来简单描述,要了解细节可以翻墙看这篇文章 Optimizing gradient descent,对各种优化器的实现原理和性能详细说明。
tf.train.GradientDescentOptimizer
将梯度下降算法进行了封装,tensorflow里的实现应该是随机下降SGD,构造函数只要给个学习率就行了。
tf.train.GradientDescentOptimizer.__init__(learning_rate, use_locking=False,name=’GradientDescent’)
tf.train.AdamOptimizer
寻找全局最优点的优化算法,引入了二次方梯度校正。相比于基础SGD算法不容易陷于局部优点且速度更快。
tf.train.AdamOptimizer.__init__(learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-08, use_locking=False, name=’Adam’)
Step 4: 开始训练
循环两万次,每次加载50个样本,每循环100次打印一次结果
for i in range(20000):
batch = mnist.train.next_batch(50)
if (i%100 == 0):
train_accuracy = accuracy.eval(feed_dict={
x:batch[0], y_: batch[1], keep_prob: 1.0})
print ("step %d, training accuracy %g"%(i, train_accuracy))
# 运行训练模型
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
print ("test accuracy %g"%accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
代码到这里就结束了,以上所有步骤可视化展示如下: (这里依然借用数字MNIST的例子)
image.png
最后看下训练结果,经过两万次循环后的准确率是91%,如果是数字MNIST达到99%不是难事,对于Fashion-MNIST则挑战大很多,但是调参以后也应该要达到95%的水平。这段代码里其实有很多参数可以调整,权重的初始值,不同的激活函数,卷积核大小,池化窗口大小,优化器的选择,卷积和池化的次数,dropout比例,隐藏层的个数,神经元的个数等等,不过这东西就像玄学一样大家都是凭感觉去试试,所以有人说训练的过程就是50%时间用来调参,49%时间用来对抗过拟/欠拟,最后1%的时间用来修改网上拷贝来的代码 :)
image.png
下一次我会继续写如何调参优化提升训练的准确率,如果能达到95%水平的话。
References:
An Intuitive Explanation of Convolutional Neural Networks - by ujjwalkarn
Deep MNIST for Experts
【TensorFlow】tf.nn.max_pool实现池化操作 - by xf__mao
【TensorFlow】tf.nn.conv2d是怎样实现卷积的?- by 楼里打扫