CIFAR-10与FC
数据集cifar-10包括约70000张图片,每张图片为32X32X3的格式,总共分为10各类别的数据。
实现对于cifar数据集的分类,仅仅利用fc层是不现实的,以一个500个神经元的隐含层为例,共需要32X32X3X500约150万个参数,但是如此简单的结构肯定是无法达到所需的识别能力,所以总体需要的参数量可能是上亿甚至更多。
过多的参数往往会带来两个问题:第一是计算量过大,可能会超过计算机的承载能力,并带来极长的训练时间;第二是过多参数会引起过拟合。
为解决上述问题,将采用卷积神经网络进行cifar-10的识别。
CNN
卷积神经网络,其整体结构与之前所述的单层神经网络大体一致,都有输入层、fc层以及输出层,不同的是在cnn中又加入了卷积层、降采样层以及随机失活层等。下文对于额外的层进行进一步的描述。
1.卷积层
卷积在信号处理中,是用于增强信号与消除噪声;在对于图像的处理上,一个卷积相当于一个模板,对图像处理进行滑动与点积的计算,最终得到一个特征图(feature map);卷积的模板性体现在其本质在于对图像进行某一种特征的提取,图像对于此特征的近似程度就是最终卷积的值,也就是特征图。
卷积层最大的两个优点就是局部连接与参数共享。
局部连接是对应着fc层的全连接而言的,在卷积层处理数据时,不会对图像的每一个点进行处理,而是以一定的大小区块划分整图,处理每一区块得到的卷积结果即为最终的特征图;局部连接划分的区块取决于卷积核大小以及步长与是否补零,而对于一个卷积核或者神经元来说,其参数量取决于卷积核自身的尺寸而与输入的数据尺寸无关。
参数共享,就是指一个卷积核或者说神经元,对一张图像做滑动卷积时,其内部的值是不变的;这在物理意义上表示为一个卷积核就是一个模板,处理图像时就是利用这一模板提取相似性结果,所以模板固定也就是参数固定。
2.降采样层
降采样即实现数据的压缩,进而减少参数量,降低计算量,防止过拟合;一般采用池化的方式进行降采样,但是也有网络使用步长大于一的卷积层实现降采样,所以注意池化只是降采样的一种形式而非唯一形式。
池化就是对于卷积得到的feature map进行尺寸缩减,主要分为最大池化与平均池化,分别可以达到抽取主要特征与抽取平均特征的目的,池化就是要尽量保存原有特征,并达到降采样的效果。
3.随机失活层
即dropout层,主要是在非输出层的fc层之后使用,目的也是为了提高模型的泛化能力,防止过拟合;其思想是将每个神经元当作一个开关,开关的开与关具有一定的随机性;利用这一机制,在每次训练时只有部分神经元打开,参与训练,得到一组权重参数,或者说是当前开关形式下的模型,最终将所有的模型组合,形成一个最终的模型,即为训练的结果;利用上述训练过程,每次不完全训练,参数量较小,并且最终融合的模型相当于多角度描述特征,所以有更好的泛化能力。
TF中的卷积相关函数
1.二维卷积
tf.nn.conv2d(input,filter,strides,padding,name=None)
# input 是输入数据,其格式是[batch, width, height, channel]这样的四维格式,并且batch常写作-1
要求输入数据应为浮点类型,即tf.float32或tf.float64
# filter 是卷积层,格式为 [filter_width, filter_height,channel]的三维格式,其中前两项表示本层每 个神经元的尺寸,最后的参数表示本层有多少个神经元,一个神经元得到一个特征图,最终得到的特征图数就是本层的神经元数目,所以这个参数也叫做输出维度。
# stride 是步长,即一个卷积核载图像上做滑动点积时,每次滑动的步长;其格式为[1,2,2,1]类型,即4维格式,每一个参数表示在不同维度上的步长,且一般首末两个参数为1.
# padding 即补零,有VAlID与SAME两个可选参数,表示是否进行补零来保证卷积前后尺寸是否变化
2.池化
tf.nn.average_pool(value,ksize,stride,padding,name=None)
tf.nn.max_pooling(同上)
# value 即卷积之后的值,所以格式也是[batch,width,height,channel]
# ksize 即池化窗口大小,因为不会在batch与channel上做池化,所以其格式为[1,2,2,1]的4维格式但是首末值为1.
#stride padding的意义与二维卷积一致
TF载入CIFAR-10数据集
import urllib.request
import os
import tarfile
import pickle as p
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import OneHotEncoder
import tensorflow as tf
from time import time
url = 'http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz' # 下载链接
filepath = 'data/cifar-10-python.tar.gz' # 下载好的数据集应在的路径
# 下载数据集
if not os.path.isfile(filepath):
result = urllib.request.urlretrieve(url, filepath)
print('download: ', result)
else:
print('Data file already exist')
# 解压数据集
if not os.path.exists("data/cifar-10-batches-py"):
tfile = tarfile.open('data/cifar-10-python.tar.gz', 'r:gz')
result = tfile.extractall('data/')
print('Extracted to ./data/cifar-10-batches-py/')
else:
print('Directory already exist')
# 批次读取数据 的函数
def load_CIFAR_batch(filename):
with open(filename,'rb') as f:
data_dict = p.load(f,encoding='bytes')
images = data_dict[b'data']
labels = data_dict[b'labels']
images = images.reshape(10000, 3, 32, 32)
images = images.transpose(0, 2, 3, 1)
labels = np.array(labels)
return images, labels
# 完整读取数据的函数,通过多次调用批次读取数据的函数实现
def load_CIFAR_data(data_dir):
images_train =[]
labels_train = []
for i in range(5):
f = os.path.join(data_dir, 'data_batch_%d'%(i+1))
print('loading', f)
image_batch, label_batch = load_CIFAR_batch(f)
images_train.append(image_batch)
labels_train.append(label_batch)
Xtrain = np.concatenate(images_train)
Ytrian = np.concatenate(labels_train)
del image_batch, label_batch
Xtest, Ytest = load_CIFAR_batch(os.path.join(data_dir, 'test_batch'))
return Xtrain, Ytrian, Xtest, Ytest
data_dir = 'data/cifar-10-batches-py' # 解压抽取数据集后的路径
Xtrain, Ytrain, Xtest, Ytest = load_CIFAR_data(data_dir) # 获取训练集测试集相关数据与标签
对数据进行预处理
# 对数据进行预处理
# 对图像进行数值标准化,注意标准化之前要先类型转化为浮点型
Xtrain_normalize = Xtrain.astype('float32')/255.0
Xtest_normalize = Xtest.astype('float32')/255.0
# 标签进行独热编码转换
encoder = OneHotEncoder(sparse=False)
yy = [[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]]
encoder.fit(yy)
Ytrain_reshape = Ytrain.reshape(-1, 1)
Ytrain_onehot = encoder.transform(Ytrain_reshape) # 50000,1->50000,10
Ytest_reshape = Ytest.reshape(-1, 1)
Ytest_onehot = encoder.transform(Ytest_reshape)
# 使用独热编码是要用于对预测结果和标签值的类欧氏距离,来表征预测的偏差程度
定义网络构造辅助函数
# 定义 权重 构造函数 使用截断随机初始化
# 输入为权重的尺寸,即卷积核的尺寸与通道数
def weight(shape):
return tf.Variable(tf.truncated_normal(shape, stddev=0.1), name="W")
# 定义 偏置 构造函数
# 偏置的输入是一个以一维数组,大小等于卷积核数目
def bias(shape):
return tf.Variable(tf.constant(0.1, shape=shape), name="b")
# 定义 2维卷积 构造函数,卷积前后尺寸不变
# 输入为图像或特征图 与 权重
# 输出 再加上偏置 就得到卷积结果
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
# 定义 最大池化 构造函数,池化不改变通道数,但是将图像的长宽各减一半
# 输入为卷积结果
def max_pool_2X2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME
构造网络
# 定义网络结构
# 使用了tf的命名空间
# 输入层
with tf.name_scope("input_layer"):
# cifar数据集的图像格式为32X32X3,批次为待定参数,用于批次读取数据
x = tf.placeholder(tf.float32, [None, 32, 32, 3], name="x")
# 第一层
with tf.name_scope("conv_1"):
w1 = weight([3, 3, 3, 32]) # 尺寸为3X3的卷积核,输入通道是3,输出是32
b1 = bias([32]) # 维度与输出通道数目一致即可
conv_1 = conv2d(x, w1)+b1 # 进行卷积计算并加上偏置
conv_1 = tf.nn.relu(conv_1) # 对卷积结果进行激活 输出为 batch,32,32,32
# print(conv_1.shape)
# 第一池化层
with tf.name_scope("pool1"):
pool1 = max_pool_2X2(conv_1) # 对第一层激活结果池化,不改变通道数,
# 改变尺寸为 batch,16,16,32
# print(pool1.shape)
# 第二层
with tf.name_scope("conv_2"):
w2 = weight([3, 3, 32, 64]) # 3X3的卷积核,输入通道是32,输出是64
b2 = bias([64]) # 维度与输出通道一致即可
conv_2 = conv2d(pool1, w2)+b2 # 进行卷积计算,注意是对第一池化层数据进行卷积
conv_2 = tf.nn.relu(conv_2) # 对卷积结果进行激活 输出为 batch,16,16,64
# print(conv_2.shape)
# 第二池化层
with tf.name_scope("pool2"):
pool2 = max_pool_2X2(conv_2) # 对第二层激活结果池化,不改变通道数,
# 改变尺寸为 batch,8,8,64
# 全连接层 # 注意卷积层向全连接层连接时要先做扁平化
with tf.name_scope("fc"):
flat = tf.reshape(pool2, [-1, 4096]) # pool2结果是四维的,在传输给全连接层时,要先做扁平化
w3 = weight([4096, 128]) # 输入维度为4096,输出维度为128,或者说有128个神经元在本fc层
b3 = bias([128]) # 偏置与输出维度一致
# 4096 = 8X8X64,即将pool2得到的feature map数据,合并为一个维度
h = tf.nn.relu(tf.matmul(flat, w3)+b3) # fc层使用矩阵乘法,并使用relu激活
h_dropout = tf.nn.dropout(h, keep_prob=0.8) # 引入随机失活,防止过拟合
# 输出层
with tf.name_scope("output"):
w4 = weight([128, 10]) # 输出维度即为分类数目
b4 = bias([10]) # 偏置与输出维度一致
# 输出层使用矩阵乘法,并使用softmax激活
preb = tf.nn.softmax(tf.matmul(h_dropout, w4)+b4) # -1,10
超参与优化器、损失函数的设定
# 构建优化器与损失函数
with tf.name_scope("optimizer"):
y = tf.placeholder(tf.float32, [None, 10], name="label")
loss_function = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=preb, labels=y))
optimizer = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(loss_function)
# 构建准确率,实际上是对于一个批次下所有准确度的均值
with tf.name_scope("evaluation"):
correct_prediction = tf.equal(tf.argmax(preb, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 设置超参
train_epochs = 2500 # 总的训练轮数
batch_size = 50 # 但批次训练数目
total_batch = int(len(Xtrain)/batch_size) # 一轮训练多少单批次
epoch_list = [] # 论数集合
accuracy_list = [] # 准确度集合
loss_list = [] # 损失值集合
epoch = tf.Variable(0, name="epoch", trainable=False) # 定义一个不可训练的变量,用于统计训练轮数,以及后续断点续训
start_time = time() # 计时开始
启动会话设置断点续训
# 启动会话
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# 设置日志目录
ckpt_dir = 'CIFAR_log/'
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir) # 没有这个目录,将自动创建
# 生成存储对象,用于后续存储模型
saver = tf.train.Saver(max_to_keep=1)
# 构造断点训练
ckpt = tf.train.latest_checkpoint(ckpt_dir) # 尝试从日志目录读取存储的模型
if ckpt!= None:
saver.restore(sess, ckpt)
print("载入成功")
else:
print("载入失败,开始训练")
开训练
# 迭代训练
# 首先定义批量获取训练数据的函数,返回值是经过归一化的图像与独热后的标签
def get_train_batch(number, batch_size):
return Xtrain_normalize[number*batch_size:(number+1)*batch_size], Ytrain_onehot[number*batch_size:(number+1)*batch_size]
# 逐个轮数,逐个批次进行数据训练
for ep in range(start,train_epochs):
for i in range(total_batch):
batch_x, batch_y = get_train_batch(i, batch_size)
sess.run(optimizer, feed_dict={x: batch_x, y: batch_y})
if i % 100 == 0:
print("Step{}".format(i), "finished")
# 每轮训练完成,统计损失与准确率
loss, acc = sess.run([loss_function, accuracy], feed_dict={x: batch_x, y: batch_y})
epoch_list.append(ep+1) # 统计轮数
loss_list.append(loss) # 统计损失
accuracy_list.append(acc) # 统计准确率
print("Train epoch: ", "%2d" % (sess.run(epoch)+1), "Loss: ","{:.6f}".format(loss),"Accuracy: ", acc)
saver.save(sess, ckpt_dir+"CIFAR_cnn_model.ckpt", global_step=ep+1) # 每轮保存模型
sess.run(epoch.assign(ep+1)) # 更新轮数计数
duration = time()-start_time
print("Train duration: ", duration) 统计时长