Fashion MNIST with Tensorflow + CNN

Fashion MNIST 是德国一家时尚公司提供的数据集,包含十个品类的七万中商品。其数据格式,图片尺寸,数据集大小都保持和手写数字 MNIST 一模一样,完全可以起到替代的作用,而且可以提升挑战难度和算法优化的空间。

之前已经尝试过不用 Tensorflow 自行实现梯度下降(手工打造神经网络: 透视分析),现在来看看用 Tensorflow 在 Fashion MNIST 数据集的尝试。关于 Tensorflow 的安装可以看我这篇文章: CUDA+cuDNN+Tensorflow-GPU Install

image

Tensorflow 官方教程有两篇以MNIST为例的资料, 入门篇跳过直接看深度卷积神经网络的实现 - Deep MNIST for Experts,官方教程中的代码不是那么好懂,可以像庖丁解牛一样把教程拆解开来一点点理解。

我们首先来看看CNN的三个重要组成部分: 卷积,池化和全连接层。

卷积
卷积可以通过从输入的一小块数据中抽取图像的特征,并保留像素间的空间关系。美图秀秀的各种特效和滤镜其实就可以看做是卷积操作的实例。

假设我们有一个5 x 5 的原始图像,它的像素值仅为 0 或者 1
image.png

再设定一个 3 x 3 的矩阵作为卷积核
image.png

卷积核也叫滤波器,通过在图像上滑动滤波器并计算点乘得到矩阵叫做卷积特征或者特征图。卷积特征由深度,步长和填充三个参数决定。如果卷积核尺寸过大,会导致提取图像的特征过于复杂,尺寸过小,难以表示有用的特征。实际应用中一般选取5x5或者7x7的最佳。卷积核是奇数的,这样就有了中心和半径的概念,也可以保证特征图的输入尺寸与输出尺寸一致。

让卷积核在原始图像上滑动,在每个位置上计算对应元素的乘积,并把乘积的和作为最后的结果,得到输出矩阵(粉色)中的每一个元素的值。
图 7

池化
空间池化的主要目的是降维,在保持原有空间特征的基础上最大限度将数组的维度变小。空间池化有下面几种方式:最大化、平均化、加和等等,如最大池化就是在每个2x2的空间邻域取出最大值。

image.png
我们可以对原始图片应用多个滤波器再分别池化,如下图所示:
image.png
有时候我会想,难道 Adobe 就是世界上最早应用卷积神经网络来处理图片的公司吗?

全连接
全连接层的目的是将卷积和池化层的输出的高级特征把输入图像基于训练数据集进行分类。对于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 楼里打扫

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容