这次的实验仍旧是图像识别,使用的数据集是CIFAR-10,其中包含10个类别的RGB彩色图片:飞机、汽车、鸟类、猫、鹿、狗、蛙类、马、船和卡车,其中训练图片共50000张,测试图片共10000张。实验来自于《21个项目玩转深度学习:基于TensorFlow的实践详解》一书中的实验,在这里做一篇学习笔记,笔者不才,欢迎斧正!
项目文件如下: 。其中,
一、下载CIFAR-10数据
运行cifar10_download.py代码,成功将数据集下载到目录中。
可以看到数据集中数据文件名及用途:
二、Tensorflow的数据读取机制
读取数据集必须考虑的一个问题:GPU/CPU因为I/O而空闲的问题
简单地将图片数据集从内存中读进CPU或GPU中进行计算,读取的时间便意味在降低内存的效率。为了解决这个问题,方法即将读入数据和计算分别放在两个线程中,先将数据读入内存队列中。读取线程负责读入数据到内存队列,另一个线程则负责计算,计算需要数据时直接从内存队列中取即可。
Tensorflow使用“文件名队列+内存队列”双队列的形式读入文件,更好的管理epoch。
对于一个数据集来讲,运行一个epoch就是将这个数据集中的图片全部计算一遍。运行一个epoch,就在文件名队列中把A、B、C各放入一次,并在之后标注队列结束。
程序运行后,内存先读取A,再依次读取B、C。
由于系统检测到“结束”,则自动抛出异常,外部捕捉到这个异常后就可以结束程序了。这就是Tensorflow中读取数据的基本机制。如果要运行两个epoch,那么只要在文件名队列中将A、B、C依次放入两次再标记结束就可以了。
在Tensorflow中创建文件名队列和内存队列
使用tf.train.string_input_producer函数。这个函数有两个参数,一个是num_epoch是,即epoch数目;另一个是shuffle,指在一个epoch内文件的顺序是否被打乱。若设置shuffle=False,则每个epoch内,数据仍然按照A、B、C的顺序进入文件名队列,顺序不变。反之,则在一个epoch内数据的前后顺序就会被打乱。在Tensorflow中内存队列不需要自己建立,只需要使用reader读取数据就可以了。
在使用tf.train.string_input_producer创建文件名队列之后,整个系统还处于停滞状态,就是说文件名其实并没有真正加入到队列中。此时若开始计算,内存队列为空,计算单元就会一直阻塞。使用tf.train.start_queue_runners之后,才会启动填充队列的线程,这时系统就不在停滞了。
在下面的代码中,我们读取三张照片的5个epoch,并把读取的结果重新存到read文件夹中。
# coding:utf-8
import os
if not os.path.exists('read'):
os.makedirs('read/')
# 导入TensorFlow
import tensorflow as tf
# 新建一个Session
with tf.Session() as sess:
# 我们要读三幅图片A.jpg, B.jpg, C.jpg
filename = ['A.jpg', 'B.jpg', 'C.jpg']
# string_input_producer会产生一个文件名队列
filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)
# reader从文件名队列中读数据。对应的方法是reader.read
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
# tf.train.string_input_producer定义了一个epoch变量,要对它进行初始化
tf.local_variables_initializer().run()
# 使用start_queue_runners之后,才会开始填充队列
threads = tf.train.start_queue_runners(sess=sess)
i = 0
while True:
i += 1
# 获取图片数据并保存
image_data = sess.run(value)
with open('read/test_%d.jpg' % i, 'wb') as f:
f.write(image_data)
# 程序最后会抛出一个OutOfRangeError,这是epoch跑完,队列关闭的标志
其中,我们使用“filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)” 来建立一个运行5个epoch的文件名队列,并使用reader读取,reader每次读取一张照片并保存。
运行代码后,程序最后会抛出OutOfRangeError异常,这就是epoch跑完队列关闭的标志,得到read文件夹中的图片正好是按顺序的5个epoch。下图是shuffle=False时的结果:
下面我们再来将CIFAR-10数据集保存成图片形式。运行的代码是cifar10_extract.py。Cifar数据集包括10000个样本,每个样本有3073个字节,第一个字节标签,剩下的3072个字节是图像数据。样本和样本之间无多余的字节分割,因此这几个二进制文件的大小是30730000字节。
- 第一步,用tf.train.string_input_producer建立队列。
- 第二步,用reader.read读取数据。注意:读取以文件格式存放的图片是tf.WholeFileReader(),但是Cifar数据是一个文件中包含多个以字节存储的样本,因此使用tf.FixedLengthRecordReaer()
- 第三步,调用tf.train.start_queue_runners
- 最后通过sess.run()取出图片结果
三、利用TensorFlow训练CIFAR-10识别模型
(壹)数据增强
原理部分:
一般来说在深度学习中,数据的总量越多,训练得到的模型效果会越好。在图像任务中,对输入的图像进行简单的平移、
缩放、颜色变换,并不会影响图像类别。因此可以依据此增强训练样本的个数。
数据增强(Data Augmentation)方法是指利用平移、缩放、颜色变换,人工增大训练集样本个数,使得模型训练的效果更好。
常见的图像数据增强方法:
- 平移
- 旋转
- 翻转:水平翻转或上下翻转图像
- 裁剪:在原有图像上裁剪出一块
- 缩放
- 颜色变换:对图像的RGB颜色空间进行一些变换
- 噪声扰动:给图像加入一些人工生成的噪声
这些数据增强方法不会改变图像原有标签。
TensorFlow中数据增强的实现
代码为:
# Randomly crop a [height, width] section of the image.
distorted_image = tf.random_crop(reshaped_image, [height, width, 3])
# Randomly flip the image horizontally.
distorted_image = tf.image.random_flip_left_right(distorted_image)
# Because these operations are not commutative, consider randomizing
# the order their operation.
distorted_image = tf.image.random_brightness(distorted_image,max_delta=63)
distorted_image = tf.image.random_contrast(distorted_image,lower=0.2, upper=1.8)
进行的操作分别是:
- 随机裁剪
- 对裁剪后的小块进行水平反转
- 对得到的图片进行亮度和对比度的随机训练
(贰)Cifar-10识别模型
该模型通过三个模块来构造训练图,最大限度得提高代码复用率:
- 模型输入:读取数据集中的图像并进行预处理
- 模型预测:进行统计计算
- 模型训练:计算损失、计算梯度、进行变量更新、保存最终结果
(一)模型输入
在模型输入部分将图像的加载和变换过程放入16个线程中,以减慢训练过程。
其中,在distorted_inputs()函数中采取对图像随机左右翻转、变换图像亮度、变换图像对比度等操作。该函数的核心代码为:
# Randomly crop a [height, width] section of the image.
distorted_image = tf.random_crop(reshaped_image, [height, width, 3])
# Randomly flip the image horizontally.
distorted_image = tf.image.random_flip_left_right(distorted_image)
# Because these operations are not commutative, consider randomizing
# the order their operation.
distorted_image = tf.image.random_brightness(distorted_image,
max_delta=63)
distorted_image = tf.image.random_contrast(distorted_image,
lower=0.2, upper=1.8)
# Subtract off the mean and divide by the variance of the pixels.
float_image = tf.image.per_image_standardization(distorted_image)
- tf.random_crop:为图片随机裁剪
- tf.image.random_flip_left_right:随机左右翻转
- tf.image.random_brightness:随机亮度变化
- tf.image.random_contrast:随机对比度变化
- tf.image.per_image_standardization:减去均值像素,并除以像素方差(图片标准化)
(二)模型预测
预测流程由inference() 构造。这个函数会添加必要的操作步骤用于计算预测值的logits。
1、构建第一个卷积层
卷积层:
- 输入:images
- 卷积核:宽度为5×5 、通道为3、共64个
- 步长:1×1
- padding:SAME
- 偏置项:通过函数生成的初始化为全0的64维向量
- W*x+b:通过函数tf.nn.bias_add(conv,biases)实现
- relu:使用ReLu激活函数完成修正线性激活
池化层:
- 输入:conv1
- 池化窗口大小:3×3
- 滑动步长:2×2
- padding:SAME
局部响应归一化
- 原因:
- 公式
i:代表下标,你要计算像素值的下标,从0计算起
j:平方累加索引,代表从j~i的像素值平方求和
x,y:像素的位置,公式中用不到
a:代表feature map里面的 i 对应像素的具体值
N:每个feature map里面最内层向量的列数
k:超参数,由原型中的bias指定
α:超参数,由原型中的alpha指定
n/2:超参数,由原型中的deepth_radius指定
β:超参数,由原型中的belta指定
2、构建第二个卷积层
卷积层
- 输入:norm1
- 卷积核:宽度为5×5、通道数为64、共64个
- 步长:1×1
- padding:SAME
- 偏置项:初始化全为0.1的64维向量
- W*A+b:通过函数tf.nn.bias_add(conv,biases)实现
- 修正线性激活函数:ReLu
池化层:
- 输入:norm2
- 池化窗口大小:3×3
- 滑动步长:2×2
- padding:SAME
3、基于修正线性激活的全连接层
- 原因:前面的卷积和池化相当于做了特征工程,而全连接层则负责特征加权,简单来说全连接的目的即对特征高度提纯,方便交给最后的分类器或者回归。
- 本质: 由一个特征空间变换到另一个特征空间
- 为了提高CNN网络性能,在这里全连接层每个神经元的激励函数都采用ReLu函数
4、基于修正线性激活的全连接层
5、softmax逻辑回归输出分类结果
现在避免全连接的方法是全局平均值法,即将最后一层卷积的输出结果(featuremap)求平均值。
(三)模型训练
训练可进行N维分类的网络常用多项式逻辑回归,即softmax回归。Softmax 回归在网络的输出层上附加了一个softmax nonlinearity,并且计算归一化的预测值和label的1-hot encoding的交叉熵。在正则化过程中,我们会对所有学习变量应用权重衰减损失。模型的目标函数是求交叉熵损失和所有权重衰减项的和,loss()
函数的返回值就是这个值。
在TensorBoard中可以查看这个值的变化:
使用标准的梯度下降算法训练的模型,其学习率随时间以指数形式衰减(本实验只让服务器跑了十万多次,衰减程度不是很明显):
训练速度的变化:
(叁)测试模型效果
本实验跑了3000个训练样本,用记事本打开checkpoint发现:
说明model_checkpoint_path表示最新的模型是model.ckpt-112632。112632即为第112632步的模型。后面5个all_model_checkpoint_paths表示所有存储下来的5个模型和它们的步数。