卷积神经网络(Convolutional Neural Network,CNN),一般卷积神经网路由多个卷积层构成,每个卷积层中通常会进行如下几个操作:
(1)图像通过多个不同的卷积核的滤波,并加偏置(bias),提取出局部特征,每一个卷积核会映射出一个新的2D图像。
(2)将前面卷积核的滤波输出结果,进行非线性的激活函数处理,目前最常见的是ReLu函数,而以前Sigmoid函数用的比较多。
(3)对激活函数的结果再进行池化操作(即降采样6,比如2x2的图片降维1x1的图片),目前一般是使用最大池化,保留最显著的特征,并提升模型的畸变容忍能力。
一个卷积层中可以有多个不同的卷积核,而每一个卷积核都对应一个滤波后映射出的新图像,同一个新图像中每一个像素都来自完全相同的卷积核,这就是卷积核的权值共享。卷积核的大小,一般为3X3,5X5,7X7,1x1。卷积操作是局部连接方式,比如整个图像是1000x1000像素,卷积核是10x10,那图像的维度就是100,一个卷积核对应一个隐藏节点,隐藏层节点就是1W个。如果只有一个卷积核,就只能提取一种卷积核滤波的结果,即只能提出一种图片特征。我么可以增加卷积核的数量多提取一些特征。每一个卷积核滤波得到的图像就是一类特征的映射,即一个Feature Map.
卷积的好处是,不管图片尺寸如何,我们需要训练的权值数量只跟卷积核大小、卷积核数量有关,我们可以使用非常少的参数量处理任意大小的图片。隐藏节点的数量只跟卷积的步长有关,如果步长为1,那么隐藏节点的数量和输入的图像的像素数量一致;如果步长为5,那么每5X5的像素才需要一个隐藏节点,我们隐藏节点的数量就是输入像素数量的1/25.
总结一下,卷积神经网络的要点就是局部连接、权值共享和池化层中的降采样。其中局部连接和权值共享降低了参数量,使训练复杂度大大下降,并减轻了过拟合。同时,权值共享还赋予了卷积网络对平移的容忍性,而池化曾降采样则进一步降低了输出参数量,并赋予模型对轻度形变的容忍性,提高了模型的泛化能力。
======================================================================================
Tensorflow实现简单的卷积神经网络
这个简单的卷积神经网络使用两个卷积层加一个全连接层构建一个简单但是非常有代表性的卷积神经网络。
首先载入MNIST数据集,并创建默认的interactive Session
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
mnist=input_data.read_data_sets('/tmp/data',one_hot=True)
sess=tf.InteractiveSession()
接下来是实现这个卷积神经网络会有很多权重和偏置需要创建,因此我们先定义好初始化函数以便重复使用。我们需要给权重制造一些随机的噪声来打破完全对称,比如截断的正太分布噪声,标准差为0.1.同时因为我们使用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)
卷积层、池化层也是接下来重复使用的,也为他们分别定义创建函数。这里的tf.nn.conv2d是tensorflow中的2维卷积函数,参数中x是输入,
w是卷积参数,比如[5,5,1,32]:前面两个数字代表卷积核的尺寸;第三个数字代表有多少个channel。因为我们只有灰度单色,所以是1,如果是彩色的RGB图片,这里应该是3;最后一个数字代表卷积核的数量,也就是这个卷积层会提取多少类的特征(多少个不同的卷积核)。
strides代表卷积模板移动的步长,都是1代表会不遗漏地划过图片的每一个点。strides[0]和strides[3]的两个1是默认值,中间两个1代表padding时在x方向运动一步,y方向运动一步。
Padding代表边界的处理方式,这里SAME代表边界加上Padding让卷积的输出和输入保持同样的尺寸。
tf.nn.max_pool是Tensorflow中最大池化函数,这里使用2x2的最大池化,即讲一个2X2的像素块降维1x1的像素。最大池化会保留原始像素块中灰度值最高的哪一个像素,即保留最显著的特征。池化的核函数大小为2X2.
因为希望整体上缩小图片尺寸,因此池化层的strides也设为横竖两个方向以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')
在正式设计卷积神经网络结构之前,先定义输入的placeholder,x是特征,y_是真实的label。因为卷积神经网络利用到空间结构信息,因袭需要将1维的输入向量转为2维的图片结构,即从1X784->28X28,同时因为只有一个颜色通道,故最终尺寸为[-1,28,28,1],前面的-1代表样本数量不固定,最后的1代表颜色通道数量。这里使用的tensor变形函数是tf.reshape
x=tf.placeholder(tf.float32,[None,784])
y_=tf.placeholder(tf.float32,[None,10])
x_image=tf.reshape(x,[-1,28,28,1])
接下来定义我们的第一个卷积层。我们先使用前面写好的函数进行参数初始化,包括weights和bias。这里的[5,5,1,32],代表卷积核尺寸为5X5,1个颜色通道,32个不同的卷积核。然后使用conv2d函数进行卷积操作,并加上偏置,接着再使用ReLu激活函数进行非线性处理。最后,使用最大池化函数max_pool_2x2对卷积的输出结果进行池化操作。
w_conv1=weight_variable([5,5,1,32])
b_conv1=bias_variable([32])
h_conv1=tf.nn.relu(conv2d(x_image,w_conv1)+b_conv1)
h_pool1=max_pool_2X2(h_conv1)
现在定义第二个卷积层,这个卷积层基本和第一个卷积层一样,唯一的不同是卷积核的数量编程了64,也就是说这一层的卷积会提取64中 特征。上一层的卷积核数量为32(即输出32个通道),所以本卷积和尺寸的第三个维度即输入的通道数也需要调整为32
w_conv2=weight_variable([5,5,32,64])
b_conv2=bias_variable([64])
h_conv2=tf.relu(conv2d(h_pool1,w_conv2)+b_conv2)
h_pool2=max_pool_2X2(h_conv2)
因为前面经历了两次步长为2x2的最大池化,所以边长已经只有1/4了,图片的尺寸由28x28变成了7x7.而第二个卷积层的卷积核数量为64,其输出的tensor尺寸即为7x7x64。我们使用tf.reshape函数对第二个卷积层的输出tensor进行变形,将其转成1维的向量,然后连接一个全连接层,隐层节点数为1024,并使用ReLu函数。
w_fc1=weight_variable([7*7*64,1024])
b_fc1=bias_variable([1024])
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)
为了减轻过拟合,下面使用一个Dropout层,Dropout,通过一个placeholder传入keep_prob比率来控制。在训练的时候,我们随机丢弃一部分节点的数据来减轻过拟合,预测时则保留全部数据来追求最好的预测性能。
keep_prob=tf.placeholder(tf.float32)
h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)
最后我们将Dropout层的输出连接一个softmax层,得到最后的概率输出。
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)
在来定义损失函数为cross_entropy,优化器为adam,并给一个比较小的学习速率1e-4
cross_entropy=tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_conv),reduction_indices=[1]))
train_step=tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
再继续定义评测准确率的操作
correct_prediction=tf.equal(tf.argmax(y_conv,1),tf.argmax(y_,1))
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
下面开始训练过程。首先依然是初始化所有参数,设置训练时dropout的keep_prob比率为0.5,然后使用大小为50的mini-batch,共进行20000次迭代,参与训练的样本数量总共为100W,其中每100次训练,我们会对准确率进行一次评测(评测时keep_prob设为1),用以实时监测模型的性能。
tf.global_variables_initializer().run()
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})
全部训练完成后,我们再最终的测试集上进行全面测试,得到整体的分类准确率。========99.16%
=====================================================================================
Tensorflow实现进阶的卷积神经网络
使用的数据集是CIFAR-10
先下载tensorflow models库,以便使用其中提供的CIFAR-10的数据类
git clone https://github.com/tensorflow/models.git
cd models/tutorials/image/cifar10
然后我们载入一些常用库,并载入Tensorflow models中自动下载。读取CIFAR-10数据的类。
import cifar10,cifar10_input
import tensorflow as tf
import numpy as np
import time
接着定义batch_size、训练轮数max_steps,以及下载CIFAR-10数据的默认路径。
max_steps=3000
batch_size=128
data_dir='/tmp/cifar10_data/cifar-10-batches-bin'
定义初始化的权重函数,和之前一样使用tf.truncated_normal阶段的整他分布来初始化权重。但是这里我们给weight加一个L2的loss,相当于一个L2的正则化处理。
在机器学习中,因特征过多而导致过拟合,一般可以通过减少特征或者惩罚不重要特征的权重来缓解这个问题。但是通常我们并不知道该惩罚哪些特征的权重,而正则化就是版主我们惩罚特征权重的,即特征的权重也会成为模型损失函数的一部分。可以理解为,为了使用某个特征,我们需要付出loss的代价,除非这个特征非常有效,否则就会被loss上的增加覆盖效果。这样我们就可以筛选出最有效的特征,减少特征权重防止过拟合。
一般来说L1正则会制造稀疏的特征,大部分无用特征的权重会被置为0,而L2正则会让特征的权重不过大,使得特征的权重比较平均。
我们使用w1控制L2 loss的大小,使tf.nn.l2_loss函数计算weight的L2 loss,在使用tf.multiply 让L2 loss乘以w1,得到最后的weight loss。接着我们使用tf.add_to_collection 把weight loss统一存到一个collection,让这个'collection'名为‘losses’,它会在后面计算神经网络的总体loss时被用上。
def variable_with_weight_loss(shape,stddev,w1):
var=tf.Variable(tf.truncated_normal(shape,stddev=stddev))
if w1 is not None:
weight_loss=tf.multiply(tf.nn.l2_loss(var),w1,name='weight_loss')
tf.add_to_collection('losses',weight_loss)
return var
下面使用cifar10类下载数据集,并解压,展开到其默认位置。
cifar10.maybe_download_and_extract()
再使用cifar10_inputs类中的distorted_inputs函数产生训练需要使用的数据,包括特征及其对应的label,这里返回的是已经封装好的tensor,每次执行都会生成一个batch_size的数量的样本。
需要注意的是我们队数据进行了data augmentation(数据增强)。具体细节可查看cifar10_input.distorted_inputs函数,其中数据增强操作包括随机的水平翻转(tf.image.random_flip_left_right)、随机剪切一块24x24代销的图片(tf.random_crop)、设置随机的亮度和对比度(tf.image.random_brightness,tf.image.random_contrast),以及对数据进行标准化tf.image.per_image_whitening(对数据减去均值,除以方差,保证数据0均值,方差为1)。通过这些操作,我们可以获得更多的样本(带噪声的),原来的一张图片样本可以变为多张图片,相当于扩大样本量,对提高准确率非常有帮助。需要注意的是,我们对图像进行数据增强的操作需要耗费大量的CPU时间,因此distorted_inputs使用了16个独立的县城来加速任务,函数内部会产生线程池,在需要使用时会通过tensorflow queue进行调度。
images_train.labels_train=cifar10_input.distorted_inputs(data_dir=data_dir,batch_size=batch_size)
我们再使用cifar10_inputs.inputs函数生成测试数据,这里不需要进行太多处理,不需要对图片进行翻转或修改亮度、对比度,不过需要裁剪图片正中间的24x24大小的区块,并进行数据标准化操作。
images_test,labels_test=cifar10_input.inputs(eval_data=True,data_dir=data_dir,batch-size=batch_size)
接下来创建输入数据的placeholder,包括特征和label。在设定placeholder的数据尺寸时需要注意,因为batch_size在之后定义网路结构时被用到了,所以数据尺寸中的第一个值即为样本条数需要被预先设定,而不能像以前一样可以设为None.而数据尺寸中的图片尺寸为24x24,即是裁剪后的大小,而颜色通过数设为3,代表图片是彩色有RGB三条通道。
image_holder=tf.placeholder(tf.float32,[batch_size,24,24,3])
label_holder=tf.placeholder(tf.int32,[batch_size])
准备工作做好,开始创建第一个卷积层。使用之前创建好的variable_with_weight_loss函数创建卷积核的参数并进行初始化。第一个卷积层使用5x5的卷积核大小,3个颜色通道,64个卷积核,同时设置weight初始化函数的标准差为0.05。我们部队第一个卷积层的weight进行L2的正则,因此w1(weight loss)这一项设为0,下面使用tf.nn.conv2d函数对输入数据image_holder进行卷积操作,这里的步长stride设为1,padding 模式为SAME。把这层的bias全部初始化为0,再将卷积的结果加上bias,最后使用一个ReLu激活函数进行非线性化。在relu后,我们使用一个尺寸为3x3,且步长为2x2的最大池化层处理数据,注意这里最大池化的尺寸和步长不一致,这样可以增加数据的丰富性。再之后,我们使用tf.nn.lrn函数,即LRN对结果进行处理。(LRN模仿了生物神经系统的‘侧抑制’机制,对局部神经元的活动创建竞争环境,是的其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。)
LRN对relu这种没有上限边界的激活函数会比较有用,因为它会从附近的多个卷积核的响应中挑选比价打的反馈,但不适合sigmoid这种有固定边界并且能抑制过大值的激活函数。
weight1=variable_with_weight_loss(shape=[5,5,3,64],stddev=5e-2,w1=0.0)
kernel1=tf.nn.conv2d(image_holder,weight1,[1,1,1,1],padding='SAME')
bias1=tf.Variable(tf.constent(0.0,shape=[64]))
conv1=tf.nn.relu(tf.nn.bias_add(kernel1,bias1))
pool1=tf.nn.max_pool(conv1,ksize=[1,3,3,1],strides=[1,2,2,1],padding='SAME')
norm1=tf.nn.lrn(pool1,4,bias=1.0,alpha=0.001/9.0,beta=0.75)
现在来创建第二个卷积层,这里的步骤和第一步很像,区别如下:上一层的卷积核数量为64(即输出64个通道),所以本卷积和尺寸的第三个维度即输入的通道数也需要调整为64。还有一个需要注意的地方是这里的bias值全部出初始化为0.1,而不是0,最后我们调换了最大池化层和LRN层的顺序,先进性LRN层处理,在使用最大池化层。
weight2=variable_with_weight_loss(shape=[5,5,64,64],stddev=5e-2,w1=0.0)
kernel2=tf.nn.conv2d(norm1,weight2,[1,1,1,1],padding='SAME')
bias2=tf.Variable(tf.constant(0.1,shape=[64]))
conv2=tf.nn.relu(tf.nn.bias_add(kernel2,bias2))
norm2=tf.nn.lrn(conv2,4,bias=1.0,alpha=0.001/9.0,beta=0.75)
pool2=tf.nn.max_pool(norm2,ksize=[1,3,3,1],strides=[1,2,2,1],padding='SAME')
在两个卷积层之后,将使用一个全连接层,这里先要把第二个卷积层的输出结果flattern,使用tf.reshape函数将每个样本都变成一维向量。我们使用get_shape函数,获取数据扁平化之后的长度。接着使用variable_with_weight_loss函数对全连接层的weight进行初始化,这里隐含节点数为384,正太分布的标准差设为0.04,bias的值也初始化为0.1。需要注意的是我们希望这个全连接层不要过拟合,因此设了一个非零的weight loss值为0.04,让这一层的所有参数都被L2正则所约束。最后我们依然使用relu激活函数进行非线性化。
reshape=tf.reshape(pool2,[batch_size,-1])
dim=reshape.get_shape()[1].value
weight3=variable_with_weight_loss(shape=[dim,384],stddev=0.04,w1=0.04)
bias3=tf.Variable(tf.constant(0.1,shape=[384]))
local3=tf.nn.relu(tf.matmul(reshape,weight3)+bias3)
接下来这个全连接层和前一层很像,只不过其隐含节点数下降了一半,只有192个,其他的超参数保持不变。
weight4=variable_with_weight_loss(shape=[384,192],stddev=0.004,w1=0.004)
bias4=tf.Variable(tf.constant(0.1,shape=[192]))
local4=tf.nn.relu(tf.matmul(local3,weight4)+bias4)
下面是最后一层,依然先床架这一层的weight,其正太分布标准差设为上一个隐含层的节点数的倒数,并且不计入L2的正则。需要注意的是,这里不像之前那样使用softmax输出最后的结果,这是因为我们把softmax的操作放在了计算loss部分。我们不需要对inference的输出进行softmax处理就可以获得最终分类结果(直接比较inference输出的各类的数值大小即可),计算softmax主要是为了计算Loss,因此softmax操作整合到后面是比较合适的。
weight5=variable_with_weight_loss(shape=[192,10],stddev=1/192.0,w1=0.0)
bias5=tf.Variable(tf.constant(0.0,shape=[10]))
logits=tf.add(tf.matmul(local4,weight5),bias5)
卷积神经网络结构表
layer名称 描述
conv1 卷积层和Relu激活函数
pool1 最大池化
norm1 LRN
conv2 卷积层和relu激活函数
norm2 LRN
pool2 最大池化
local3 全连接层和relu激活函数
local4 全连接层和relu函数
logits 模型inference的输出结果
可以观察到,其实设计CNN主要就是安排卷积层、池化层、全连接层的分布和顺序,以及其中超参数的设置、trick的使用等。设计性能良好的CNN是有一定规律可寻的,但是想要针对某个问题设计最合适的网络结构,是需要大量时间摸索的。
完成了模型inference部分的构建,接下来计算cnn的loss。这里依然使用cross entropy,需要注意的是我们把softmax的计算和cross-entropy loss 计算何在一起了,即tf.nn.sparse_softmax_cross_entropy_with_logits。这里使用tf.reduce_mean,对cross entropy 计算均值,再使用tf.add_to_collection把cross entropy 的loss添加到整体losses的collection中。最后,使用tf.add_n将整体的losses的collection中的loss全部求和,得到最终的Loss,其中包括cross entropy loss ,还有两个全连接层中的weight 的L2 loss.
def loss(logits,labels):
labels=tf.cast(labels,tf.int64)
cross_entropy=tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,labels=labels,name='cross_entropy_per_example')
cross_entropy_mean=tf.reduce_mean(cross_entropy,name='cross_entropy')
tf.add_to_collection('losses',cross_entropy_mean)
return tf.add_n(tf.get_collection('losses'),name='total_loss')
接着将logits 节点和label_holder 传入loss函数获得最终的loss。
loss=loss(logits,label_holder)
优化器选择Adan optimizer,学习速率设为1e-3
train_op=tf.train.AdamOptimizer(1e-3).minimize(loss)
使用tf.nn.in_top_k函数求输出结果中top k 的准确率,默认使用top1,也就是输出分数最高的哪一类的准确率。
top_k_op=tf.nn.in_top_k(logits,label_holder,1)
使用tf.InteractiveSession创建默认的session,接着初始化全部模型参数
sess=tf.InteractiveSession()
tf.global_variables_initializer().run()
这一步是启动前面提到的图片数据增强的线程队列,这里一共使用了16个县城来进行加速。注意,如果这里不启动县城,那么后续的inference及训练的操作都是无法开始的
tf.train.start_queue_runners()
现在正是开始训练。在每一个step的训练过程中,我们需要先使用session的run方法执行images_train,labels_train的计算,获得一个batch的训练数据,再将这个batch的数据传入train_op和loss计算。我们记录每一个step花费的时间,每隔10个step会计算并展示当前的loss、每秒能训练的样本数量,以及训练一个batch数据所花费的时间,这样就可以比较方便地监控整个训练过程。
for step in range(max_steps):
start_time=time.time()
image_batch,label_batch=sess.run([images_train,labels_train])
_,loss_value=sess.run([train_op,loss],feed_dict={image_holder:image_batch,label_holder:label_batch})
duration=time.time()-start_time
if stap%10==0:
examples_per_sec=batch_size/duration
sec_per_batch=float(duration)
format_str=('step %d,loss=%.2f (%.1f examples/sec;%.3f sec/batch)')
print format_str % (step,loss_value,examples_per_sec,sec_per_batch)
接下来评测模型在测试集上的准确率。需要注意的是,我们依然要像训练时那样使用固定的batch_size,然后一个batch一个batch地输入测试数据。我们先计算一共要多少个batch才能将全部样本评测完。同时,在每一个step中使用session的run方法获取images_test,labels_test的batch,再执行top_k_op计算模型在这个batch的top 1上预测正确地样本数,最后汇总所有预测正确地结果,求得全部测试样本中预测正确的数量。
num_examples=10000
import math
num_iter=int(math.ceil(num_examples/batch_size))
true_count=0
total_sample_count=num_iter*batch_size
step=0
while step<num_iter:
image_batch,label_batch=sess.run([images_test,labels_test])
predictions=sess.run([top_k_op],feed_dict={image_holder:image_batch,label_holder:label_batch})
true_count+=np.sum(predictions)
step+=1
最后将准确率的评测结果计算并打印出来
precision=true_count/total_sample_count
print 'precision @1=%.3f' % precision
最终,在cifar-10数据集上,通过一个算时间小迭代次数的训练,可以达到大致73%准确率。持续增加max_steps,可以期望准确率逐渐增加。如果max_steps比较大,则推荐使用学习速率衰减(decay)的SGD进行训练,这样训练过程中能达到的准确率峰值会比较高,大致接近86%,而其中L2正则以及LRN层的使用都对模型准确率有所提升。