1、基本概念
卷积神经网络(Convolutional Neural Networks, CNN)是机器视觉中应用最为广泛的算法,主要用于图像分类、识别,其识别率比肉眼更高。对于全连接神经网络而言,当输入特征值、中间隐藏层数量增加时,会造成参数的总数明显增多,从而导致运算速度极具下降以及过拟合的问题。因此需要使用更为合理的模型来减少参数的个数,即卷积神经网络。
过拟合问题是指当网络参数过多时,模型就会记住样本的特征值,从而在样本的预测上表现得很好,但是在测试数据上却表现得不好。
CNN的结构如上图所示,它由多层神经网络组成,除了输入、输出层之外,中间由多对卷积层与降采样层,每层由多个二维平面组成,每个平面又包括多个独立神经元。卷积层的目的是降低噪音、增强原信号特征以便于采集数据。降采样层是降低网格训练的参数数量。再经过若干次卷积、降采样后,经过全连接层、softmax层的处理再进行输出。
卷积操作实质上是矩阵加权求和的过程,例如左上图所示,将深蓝色3×3矩阵内的数值分别与红色的权值相乘再求和,得到的值12填入绿色矩阵的第一个位置,然后将矩阵窗口向右滑动一格,重复上面操作,得到绿色矩阵第二个值,以此类推填满绿色矩阵,这个过程就是卷积。红色的权值称为卷积核,得到的绿色结果图称为特征图。
从卷积的过程可以看出,每个输出特征只与其中的3×3输入特征有关,而不是像全连接一样连接每个输入,这个特性叫做局部连接。在卷积过程中,卷积核并没有发生变化,整张图共享一个3×3的权值,这叫做权值共享。由于以上两个特性,使得卷积神经网络的参数量大大下降。
0填充:从上面的卷积过程可以看出,原来5×5的图像经过卷积后变成了3×3,损失掉了很多图像信息。为了弥补损失,可以在图像外围用0填充一层信息,并从外围开始卷积,如下面左图所示,这样得到的图像就不会有大小损失。
多通道卷积:卷积的目的是对目标图片的特征进行人为提取,忽略其他特点,提取主要特点。例如用一个纵向的卷积核与图片运算,得到的图像会显示出明显的纵向线条,而忽略了其它横向的信息。为了使特征提取更充分,可 以添加多个卷积核以提取不同的特征,即多通道卷积。例如上图中分别对一幅图的RGB三个通道用不同卷积核进行卷积操作,将得到的结果再每个位置相加得到特征图。卷积结束后也可以对特征图加偏置,例如给原图加橘色偏置,使图片整体偏橘色。
降采样是通过减少矩阵的长和宽来降低参数的数量,例如一个12×12的网格,将其3×3的区域映射为1个网格,那么原来的12×12就被压缩为为4×4的网格了。降采样也叫池化操作(Pooling),最常用的是池化操作:计算图像一个区域上的某个特定特征的平均值或最大值的聚合操作叫做池化(pooling)。均值池化:对池化区域内的像素点取均值,这种方法得到的特征数据对背景信息更敏感。最大池化:对池化区域内所有像素点取最大值,这种方法得到的特征对纹理特征信息更加敏感。
步长(stride)表示卷积核在图片上移动的格数。通过步长的变换,可以得到不同尺寸的卷积输出结果,例如步长为2时,得到的就是2×2的结果。可以看到通过步长大于1的卷积操作也能达到降低参数维的目的,因此降采样层并不是必须的。
卷积输出大小=(输入大小-卷积核+padding)/stride+1
Tensor FLow中定义了许多卷积函数,放在在tensorflow/python/ops下的nn_impl.py和nn_ops.py文件中。例如二维卷积函数tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None)
- input:需要做卷积的输入数据。这是一个4维的张量([batch, in_height, in_width, in_channels])其中bath为每批样本个数,in_height、 in_width为图像长和宽,channels为图片通道数。要求类型为float32或float64其中之一。
- filter:卷积核。[filter_height, filter_width, in_channels, out_channels]前两个为长和宽,后两个为输入输出的通道数。
- strides:图像每一维的步长,是一个一维向量,长度为4
- padding:是否采用0填充边缘。当值为"SAME"时,表示边缘填充,适用于全尺寸操作;当为"VALID"时,表示边缘不填充。
- use_cudnn_on_gpu:bool类型,是否使用cudnn加速
- name:该操作的名称
- 返回值:返回一个tensor,即特征图
Tensor FLow中的池化函数定义在tensorflow/python/ops下的nn.py和gen_nn_ops.py文件中,其中最大池化:tf.nn.max_pool() 平均池化:tf.nn.avg_pool(value, ksize, strides, padding, name=None)
- value:需要池化的输入。一般池化层接在卷积层后面,所以输入通常是conv2d所输出的feature map,依然是4维的张量([batch, height, width, channels])。
- ksize:池化窗口的大小,由于一般不在batch和channel上做池化,所以ksize一般是[1,height, width,1],
- strides:图像每一维的步长,是一个一维向量,长度为4
- padding:是否0填充边缘
- name:该操作的名称
- 返回值:返回一个tensor
2、CIFAR-10图像识别
2.1、加载数据集
CIFAR-10是一个用于识别普适物体的小型数据集,它包含了10个类别的彩色RGB图片。其中包含五个批次的训练集数据,每批内含一万张32×32图片,还有一万张测试集图片。其介绍网址:https://www.cs.toronto.edu/~kriz/cifar.html
首先通过python代码从网上下载并解压cifar数据到指定文件夹,然后定义函数load_batch加载一批测试训练集数据,然后通过load_data()调用load_batch()读取所有批次的数据并拼接在一起,然后返回训练集和测试集数据。
其中测试数据有五个数据集,每个数据集有一万张32×32的RGB三通道数据,这些数据以一维的形式存储。所以当通过load_batch()读入数据后利用reshape()函数将其化为为(10000,3,32,32)的四维数组,之后再利用transpose()函数调整为(10000,32,32,3)的四维数据。
import urllib
import os
import tarfile
import numpy as np
import pickle as pk
#下载、解压CIFAR数据集
url='https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz'
file_path='D:/Temp/MachineLearning/data/cifar-10-python.tar.gz'
#如果目标文件不存在,则从指定url下载该文件
if not os.path.isfile(file_path):
urllib.request.urlretrieve(url,file_path)
#如果目录下不存在文件,则解压
if not os.path.exists('D:/Temp/MachineLearning/data/cifar-10-batches-py'):
tfile=tarfile.open('D:/Temp/MachineLearning/data/cifar-10-python.tar.gz','r:gz')
tfile.extractall('D:/Temp/MachineLearning/data/')
#载入数据
def load_batch(file): #读取一个批次的数据
with open(file,'rb') as f:
data_dict=pk.load(f,encoding='bytes')
images=data_dict[b'data']
labels=data_dict[b'labels']
#将一维图片数据调整为四维数组
images=images.reshape(10000,3,32,32)
#将(10000,3,32,32)调整参数数组维度为(10000,32,32,3)
images=images.transpose(0,2,3,1)
labels=np.array(labels)
return images,labels
def load_data(data_dir):
images_train=[]
labels_train=[]
for i in range(5):
file=os.path.join(data_dir,'data_batch_%d'%(i+1))
print('加载文件:',file)
#按批次读取训练集数据并拼接到图像和标签列表后,直到读入所有批次数据
images_batch,labels_batch=load_batch(file)
images_train.append(images_batch)
labels_train.append(labels_batch)
#将多个批次的数组统一为一个数组
Xtrain=np.concatenate(images_train)
Ytrain=np.concatenate(labels_train)
del images_batch,labels_batch
#加载测试集图像和标签
Xtest,Ytest=load_batch(os.path.join(data_dir,'test_batch'))
return Xtrain,Ytrain,Xtest,Ytest
data_dir='D:/Temp/MachineLearning/data/cifar-10-batches-py/'
Xtrain,Ytrain,Xtest,Ytest=load_data(data_dir)
#通过函数显示具体图片及其对应标签
%matplotlib inline
import matplotlib.pyplot as plt
#定义标签对应的类别
label_dict={0:'airplane',1:'automobile',2:'bird',3:'cat',4:'deer',
5:'dog',6:'frog',7:'horse',8:'ship',9:'trunk'}
#定义图片显示函数,从index开始显示num个图片,images为图像资源,labels为标签,prediction为预测值
def show_img(images,labels,prediction,index,num=10):
#获取整张图片资源并设置大小
figure=plt.gcf()
figure.set_size_inches(12,6)
for i in range(num):
#绘制每个子图图像
sub_img=plt.subplot(2,5,i+1)
sub_img.imshow(images[index],cmap='binary')
#显示子图标题,序号+标签,如果有预测,也显示
title=str(i)+':'+label_dict[labels[index]]
if len(prediction)>0:
title+='vs'+label_dict[labels[index]]
sub_img.set_title(title,fontsize=10)
index+=1
plt.show()
show_img(Xtrain,Ytrain,[],10)
2.2、数据预处理
训练集由五万个32×32个像素点组成,每个像素点包含三个数字分别代表RGB三个色彩通道,其值介于0~255之间,因此首先需要将数据标准化,即将每个色彩值除以255,化为0~1之间的值。
接着需要把数据的标签值化为独热编码。预处理如下:
#数据预处理
Xtrain=Xtrain.astype('float32')/255.0 #数字标准化
Xtest=Xtest.astype('float32')/255.0
from sklearn.preprocessing import OneHotEncoder #独热编码
encoder=OneHotEncoder(sparse=False)
one_format=[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]
encoder.fit(one_format)
Ytrain=Ytrain.reshape(-1,1) #数组化为一维包含一个元素的二维数组,-1代表二维的数量自适应
Ytrain=encoder.transform(Ytrain)
Ytest=Ytest.reshape(-1,1)
Ytest=encoder.transform(Ytest)
print(Ytest[0])
输出Ytest[0]的OneHot编码为:[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
2.3、定义网络结构
卷积网络输入层输入的是32×32的3通道数据,通过第一个卷积层输出32×32个32通道的数据。通过采用多个卷积核进行不同角度的卷积操作,使得通道数量增加,但不改变图像的大小(32×32)。接着进行第一个池化层,池化不改变通道数,而是缩减图片大小,输出16×16的32通道数据。
同理经过第二个卷积层,输出16×16的64通道数据,之后再经过第二个池化层,输出8×8的64通道数据。
将第二个池化层的输出数据输入全连接层,对应4096个一维向量(8×8×64=4096),在该层定义128个神经元。之后将数据输出到输出层,输出层有10个神经元,对应输出10类图片。
#共享函数定义
import tensorflow as tf
#根据不同的shape生成权值变量,并利用截断正态分布随机赋初值
def weight(shape):
return tf.Variable(tf.truncated_normal(shape,stddev=0.1))
#根据不同shape生成初始值为0.1的偏置值
def bias(shape):
return tf.Variable(tf.constant(0.1,shape=shape),name='b')
#定义卷积层操作,参数分别为:输入x,W
def convolute(x,W):
return tf.nn.conv2d(x,W,strides=[1,1,1,1],padding='SAME') #步长为1,0填充
#定义最大池化函数,步长为2
def max_pool(in_images):
return tf.nn.max_pool(in_images,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
x=tf.placeholder('float',shape=[None,32,32,3])
y=tf.placeholder('float',shape=[None,10])
#第一个卷积层,卷积核3×3,输入通道3,输出32
with tf.name_scope('conv1'):
W1=weight([3,3,3,32])
b1=bias([32])
c1=tf.nn.relu(convolute(x,W1)+b1)
#第一池化层
p1=max_pool(c1)
#第二卷积层,卷积核3×3,输入通道32,输出64
with tf.name_scope('conv2'):
W2=weight([3,3,32,64])
b2=bias([64])
c2=tf.nn.relu(convolute(p1,W2)+b2)
#第二池化层
p2=max_pool(c2)
#全连接层
w3=weight([4096,128])
b3=bias([128])
fcl=tf.reshape(p2,[-1,4096]) #将第二个池化层的输出重构为4096个一维向量
h=tf.nn.relu(tf.matmul(fcl,w3)+b3)
drop_res=tf.nn.dropout(h,keep_prob=0.8) #防止过拟合,随机丢掉一部分神经元
#输出层
w4=weight([128,10])
b4=bias([10])
pred=tf.nn.softmax(tf.matmul(drop_res,w4)+b4)
2.4、训练模型
与普通的模型训练相同,首先需要定义模型的损失函数、优化器、准确率,其次对训练的超参数进行设置。在每一轮训练中,分批次读入数据进行训练,在每轮训练结束后求出损失与准确率并打印。
值得注意的是在使用卷积函数进行训练时十分消耗cpu资源,有可能一次无法训练完成很多轮数据,此时可以进行断点续训,即将训练前几轮的数据tf.train.Saver保存到ckpt_dir目录下,下次开始训练前先读取检查点文件到session,然后继续训练。
#定义损失函数、优化器、准确率
loss_function=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred,labels=y))
optimizer=tf.train.AdamOptimizer(learning_rate=0.0001).minimize(loss_function)
correct_prediction=tf.equal(tf.argmax(pred,1),tf.argmax(y,1))
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
#设置超参数
train_epochs=25
batch_size=50
total_batch=int(len(Xtrain)/batch_size)
epoch=tf.Variable(0,name='epoch',trainable=False) #用于保存断点训练轮数的变量
epoch_list=[];accuracy_list=[];loss_list=[]; #用于暂存数据的列表
ss=tf.Session()
ss.run(tf.global_variables_initializer())
#断点续训
ckpt_dir='D:/Temp/MachineLearning/ModelSaving/CIFAR_10/'
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir)
#保存节点时过滤Adam有关变量
vl = [v for v in tf.global_variables() if "Adam" not in v.name]
saver=tf.train.Saver(var_list=vl)
tf.reset_default_graph() #重置计算图与节点
#读取最新的检查点文件到session
ckpt=tf.train.latest_checkpoint(ckpt_dir)
if ckpt!=None:
saver.restore(ss,ckpt)
start=ss.run(epoch) #读取当前的训练轮数epoch
print('开始第%d轮训练'%(start+1))
#手动定义批数据返回函数
def get_batch(number,batch_size):
return Xtrain[number*batch_size:(number+1)*batch_size],\
Ytrain[number*batch_size:(number+1)*batch_size]
#开始多轮训练
for ep in range(start,train_epochs):
for i in range(total_batch):
bx,by=get_batch(i,batch_size)
ss.run(optimizer,feed_dict={x:bx,y:by})
loss,acc=ss.run([loss_function,accuracy],feed_dict={x:bx,y:by})
epoch_list.append(ep+1)
loss_list.append(loss)
accuracy_list.append(acc)
print('第%2d轮训练,损失=%.6f,准确率=%f'%(ss.run(epoch)+1,loss,acc))
#保存检查点
saver.save(ss,ckpt_dir+'CIFAR10_cnn_model.ckpt',global_step=ep+1)
ss.run(epoch.assign(ep+1))
ss.close()
在断点续训时会遇到报错:Key Variable/Adam not found in checkpoint,这是由于Adam优化器的参数在不同轮次变量保存出现无法读取,可以在保存变量时过滤掉Adam相关变量。
遇到的第二个报错为:Key Variable_4 not found in checkpoint,可以在读取检查点之前通过tf.reset_default_graph()重置计算图与节点。
程序的运行结果如下,先进行了三轮训练,停止之后从第四轮开始继续训练: