TensorFlow 实战之实现卷积神经网络的实例讲解

本文根据最近学习TensorFlow书籍网络文章的情况,特将一些学习心得做了总结,详情如下.如有不当之处,请各位大拿多多指点,在此谢过。

一、相关性概念

1、卷积神经网络(ConvolutionNeural Network,CNN)

19世纪60年代科学家最早提出感受野(ReceptiveField)。当时通过对猫视觉皮层细胞研究,科学家发现每一个视觉神经元只会处理一小块区域的视觉图像,即感受野。20世纪80年代,日本科学家提出神经认知机(Neocognitron)的概念,被视为卷积神经网络最初的实现原型。神经认知机中包含两类神经元:S-cells和C-cells。S-cells用来抽取特征,对应我们现在主流卷积神经网络中的卷积核滤波操作;C-cells用来抗形变,对应现在的激活函数、最大池化(Max-Pooling)等操作。

一般情况下,卷积神经网络由多个卷积层构成,每个卷积层通常会进行如下操作:

(1) 图像通过多个不同的卷积核的滤波,并加偏置(bias),提取出局部特征,每一个卷积核会映射出一个新的2D图像。

(2) 将前面卷积核的滤波处理结果,进行非线性的激活函数处理。目前最常见的是使用ReLU函数,之前Sigmoid函数应用较多。

(3)多激活函数处理的结果再进行池化操作(即降采样,例如:将4*4的图片降为1*1的图片),一般会使用最大池化,保留最显著特征,并提升模型畸变容忍能力。

这几个步骤就构成了最常见的卷积层,也可以再加上一个LRN(LocalResponse Normalization,局部响应归一化层)层,现在非常流行的Trick还有BatchNormalization等。

2、池化层

3、卷积核尺寸

4、神经网络算法相关特性

4.1、优点

(1)可以高效提取特征。

当我们面对一个分类任务时,传统的机器学习算法,一般要首先明确feature和label,然后拿数据取“喂”训练模型并保存,最后测试模型的准确性。这就需要我们确定好特征,当特征数目很少就无法精确进行分类而引起欠拟合;当特征数目很多,又会在分类过程中太过于看重某个特征引起分类错误,产生过拟合。而神经网络则不需要做大量的特征工程,可以直接把数据“灌”进去而让其自身训练,自我“修正”,即可达到预期效果。   (2)数据格式更加简易

利用传统的机器学习解决分类问题时,数据不能直接“灌”进去的,需要对数据进行一些处理,譬如量纲的归一化,格式的转化等等,然而在神经网络里却不需要额外对数据做过多的处理。

(3) 参数数目的少量性

同样在面对一个分类问题时,利用传统机器学习SVM来做的话,需要涉及核函数,惩罚因子,松弛变量等等参数,而这些不同的参数组合会对模型效果产生不一样的影响,想要迅速而又准确的得到最适合模型的参数,需要对相关理论知识有深入研究,但对于一个基本的三层神经网络来说(输入-隐含-输出),只需要初始化时给每一个神经元上随机的赋予一个权重w和偏置项b,在训练过程中,这两个参数就会不断的修正,调整到最优质,使模型的误差最小。所以从这个角度来看,我们的工作效率会更佳。 4.2、缺点

如果我们加深我网络层,每一个网络层都增加神经元数量,则参数的个数将是M*N(m为网络层数,N为每层神经元个数),这样一来参数很多,引起模型复杂化,就更加不好调参,进而会更加容易导致过拟合。另外,从神经网络的反向传播的过程来看,梯度在反向传播时,不断的迭代会导致梯度越来越小,引起梯度趋近于0(梯度消失),梯度消失就使得权值无法更新,这个神经元的存在就毫无意义,很难导致收敛。尤其是在图像领域,直接使用最基本的神经网络,是不合理的。

二、卷积神经网络基本原理

1、基本阐述

现在有一图像,其尺寸大小是1000像素*1000像素且设定为黑白图像,也就是只有一个颜色通道,则一张图片就要100万个像素点,输入数据维度也是100万维。如果连接的现在隐含层大小也是同样大小(100万个隐含节点),最后将产生100万*100万即一亿万个连接。仅仅一个全连接(FullConnected Layer),就有一万亿连接的权重需要去训练,目前看,显然是不划算不现实。

通过局部连接(LocalConnect)方法优化解决:由于每一个感受野只接受一小块区域的信号,且这一小块区域内的像素是互相关联的,每一个神经元不需要接收全部像素点的信息,只需要接收局部的像素点作为输入,而后将所有这些神经元收到的局部信息综合起来,就可以得到全局信息。假设局部感受野大小是10*10,即每个隐含节点只与10*10个像素点相连,现在只需要10*100万即1亿个连接。

现在隐含层每一个节点都与10*10的像素相连,即每一个隐含层节点都拥有100个参数。假设我们的局部连接方式是卷积操作,即默认每一个隐含节点的参数都完全一样,这样参数从1亿降为100。不管图像大小是多大,一律都是这个10*10=100个参数,即卷积核尺寸,显然卷积核对缩小参数数量贡献非常大、意义非凡。因此,此时,我们不需要再担心有多少隐含节点或者图片多大,参数量只跟卷积核的大小有关,即所谓的权值共享。

总结:卷积神经网络要素是局部连接(LocalConnection)、权值共享(WeightSharing)和池化层(Pooling)中的降采样(Down-Sampling)。其中,局部连接和权值共享降低了参数量,训练复杂度被大大下降、过拟合被减轻。同时,权值共享还赋予了卷积网络对平移的容忍性,而池化层降采样则进一步降低了输出参数量,并赋予模型对轻度形变的容忍性,提供了模型的泛化能力。

2、LeNet5

1994年,大名鼎鼎的LeNet5诞生,作为最早的深层卷积神经网络之一,推动了深度学习的发展。自1998年开始,在多次成功迭代之后,由Yann LeCun完成的开拓性成果被命名为LeNet5。LeCun认为,可训练参数的卷积层是一种利用少量参数在图像的多个位置上提取相似特征的有效方式,这和直接把每个像素作为多层神经网络的输入不一样。像素不应该被使用在输入层,因为图像具有很强的空间相关性,而使用图像中独立的像素直接作为输入则利用不到这些相关性。笔者认为,这些内容比较重要。

在当时,LeNet5的特性如下:

(1)每个卷积层包含三个部分:卷积、池化和非线性激活函数;

(2)使用卷积提取空间特征;

(3)降采样(Subsample)的平均池化层(AveragePooling);

(4)双曲正切(Tanh)或S型(Sigmoid)的激活函数;

(5)MLP作为最后的分类器;

(6)层与层之间的稀疏性连接减少计算复杂度。

三、TensorFlow 实现简单的卷积网络

1、简要说明

这里使用的数据集依然是MNIST,使用两个卷积层加一个全连接层构建一个简单但非常有代表性的卷积神经网络,预计准确率约为99.2%左右。

2、实现过程

#载入MNIST数据集,创建默认的Interactive Session。
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
sess = tf.InteractiveSession()
 
#定义初始化函数,以便重复使用创建权重、偏置、卷积层、池化层。
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)
 
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。
#由于卷积神经网络会使用到空间结构信息,所以,需要将1D的输入向量转为2D图片结构,即从1*784的形式转换为原始的28*28结构。
#因为只有一个颜色通道,所以最终尺寸为[-1,28,28,1],其中‘-1'代表样本数量不固定,'1'代表颜色通道数量。
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]代表卷积核尺寸为5**5,1个颜色通道,32个不同的卷积核。
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.
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
 
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层,其用法是通过一个placeholder传入keep_prob比率来控制。
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
 
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优化器。
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))
 
#开始训练过程。
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}))

3、执行结果

Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
step 0, training accuracy 0.1
step 100, training accuracy 0.82
step 200, training accuracy 0.9
step 300, training accuracy 0.9
step 400, training accuracy 0.96
step 500, training accuracy 0.9
step 600, training accuracy 0.94
step 700, training accuracy 0.96
step 800, training accuracy 0.94
step 900, training accuracy 0.98
.  .  .  .  .
 
step 19600, training accuracy 1
step 19700, training accuracy 1
step 19800, training accuracy 1
step 19900, training accuracy 1
test accuracy 0.9929

4、模型分析

CNN模型的最终准确率约为99.2%,基本上可以满足对手写数字识别准确率的初步要求。相比于之前的MLP2%的错误率,CNN下降了60%左右。这里的性能提升主要在于更佳的网络设计,也就是卷积网络对图像特征的提取和抽象能力。依靠卷积核的权值共享,CNN的参数数量没有爆炸,降低计算量的同时也减去了过拟合,所以,整个模型的性能会有较大的提升。

四、TensorFlow实现进阶的卷积网络

1、基本介绍

这里使用CIFAR-10数据集,包含60,000张32*32的彩色图像,其中训练集50,000张,测试10,000张,一共标注为10类,分别为airplane、automobile、bird、cat、deer、dog、frog、horse、shhip、truck,,每一类图片6000张,其中没有任何重叠情况发生,例如:automobile 只包括小型汽车,truck只包括卡车,不会出现一张图片展现两类物体的现象。

2、实现过程

#载入数据
import cifar10,cifar10_input
import tensorflow as tf
import numpy as np
import time
 
max_steps = 3000 #训练轮数
batch_size = 128
data_dir = '/tmp/cifar10_data/cifar-10-batches-bin'#下载数据默认路径。
 
 
def variable_with_weight_loss(shape, stddev, wl):
 var = tf.Variable(tf.truncated_normal(shape, stddev=stddev))
 if wl is not None:
  weight_loss = tf.multiply(tf.nn.l2_loss(var), wl, name='weight_loss')
  tf.add_to_collection('losses', weight_loss)
 return var
 
#计算CNN的损失。
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') 
 
cifar10.maybe_download_and_extract()
 
images_train, labels_train = cifar10_input.distorted_inputs(data_dir=data_dir,
               batch_size=batch_size)
 
images_test, labels_test = cifar10_input.inputs(eval_data=True,
            data_dir=data_dir,
            batch_size=batch_size)            
 
image_holder = tf.placeholder(tf.float32, [batch_size, 24, 24, 3])
label_holder = tf.placeholder(tf.int32, [batch_size])
 
#创建第一个卷积层。
weight1 = variable_with_weight_loss(shape=[5, 5, 3, 64], stddev=5e-2, wl=0.0)
kernel1 = tf.nn.conv2d(image_holder, weight1, [1, 1, 1, 1], padding='SAME')
bias1 = tf.Variable(tf.constant(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)
 
#创建第二个卷积层。
weight2 = variable_with_weight_loss(shape=[5, 5, 64, 64], stddev=5e-2, wl=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')
#使用一个全连接层,先把第二个卷积层的输出结果flatten,将每个样本都变成一维向量。
reshape = tf.reshape(pool2, [batch_size, -1])
dim = reshape.get_shape()[1].value
weight3 = variable_with_weight_loss(shape=[dim, 384], stddev=0.04, wl=0.004)
bias3 = tf.Variable(tf.constant(0.1, shape=[384]))
local3 = tf.nn.relu(tf.matmul(reshape, weight3) + bias3)
#下面这个使用的全连接层,其隐含节点数下降了一半。
weight4 = variable_with_weight_loss(shape=[384, 192], stddev=0.04, wl=0.004)
bias4 = tf.Variable(tf.constant(0.1, shape=[192]))          
local4 = tf.nn.relu(tf.matmul(local3, weight4) + bias4)
 
weight5 = variable_with_weight_loss(shape=[192, 10], stddev=1/192.0, wl=0.0)
bias5 = tf.Variable(tf.constant(0.0, shape=[10]))
logits = tf.add(tf.matmul(local4, weight5), bias5)
loss = loss(logits, label_holder)
train_op = tf.train.AdamOptimizer(1e-3).minimize(loss) #0.72
top_k_op = tf.nn.in_top_k(logits, label_holder, 1)
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()
tf.train.start_queue_runners()
#正式开始训练。
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 step % 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))
  
#评测模型在测试集上的准确率。
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)

3、执行结果

由于笔者试了几次CIFAR-10模块,最后一步失败,所以没能正确显示,后续找机会再试试,但以笔者的初步判断,在CIFAR-10安装成功的情况下,执行结果应该是没问题的。感兴趣的朋友可以再看看,这里就不贴出相关结果了,望各位网友理解。

4、模型分析

在CIFAR-10数据集上,通过一个短时间小迭代次数的训练,可以达到约73%的准确率,后续若增加max_steps,期望准确率会逐渐增加。若max_steps比较大的化,建议使用学习速率衰减(decay)的SGD来进行训练,其训练过程中准确率的峰值会较高,约86%,因为这其中的L2正则和LRN层的使用均提升了模型准确率和模型的泛化性能。

五、小结

卷积网络最后的几个全连接层的作用是输出分类结果,前面的卷积层主要做特征提取工作,直到最后的全连接层才开始对特征进行组合分配,并进行分类。

卷积层一般需要和一个池化层连接,二者组合是做图像识别时的一个标准组件。卷积层的训练相对于全连接层更复杂,训练全连接层基本是进行一个些矩阵乘法运算,而目前卷积层的训练基本上依赖于cuDNN实现,其中的算法也相对复杂,甚至会涉及到傅里叶变换。

参考资料 主要参考资料《TensorFlow实战》(黄文坚 唐源 著)(电子工业出版社)。

相关推荐