TensorFlow实战-TensorFlow实现卷积神经网络

在图像中,我们很难根据认为我理解提取出有效而丰富的特征。在深度学习出现之前,我们必须借助SIFT,HoG等算法提取有良好区分性的特征,再集合SVM等进行图像识别。但是SIFT算法错误率常年难以突破,卷积神经网络提取的特征则可以达到更好的效果。CNN最大特点在于卷积的权值共享结构,可以大幅减少神经网络的参数量,防止过拟合的同时又降低了神经网络模型的复杂度。
在卷积神经网络中,第一各卷积层会直接接受图像像素级的输入,每一个卷积操作只处理一小块图像,进行卷积变化后再传入后面的网络,每一层卷积(或者说滤波器)都可以提取数据中最有效的特征。再进行组合抽象形成更高阶的特征。
一般的卷积神经网络由多个卷积层构成,每个卷积层通常有如下几个操作:
(1)图像会通过多个不同的卷积核的滤波,并加偏置(bias),提取出局部特征,每一个卷积核会映射出一个新的2D图像;
(2)将前面卷积核的滤波输出结果,进行非线性的激活函数处理。目前常用ReLu,以前sigmoid最多;
(3)对激活函数的结果再进行池化操作(即降采样,比如将2x2的图片降为1x1的图片),目前一般是使用最大池化,保留最显著特征,并提升模型的畸变容忍能力
权限共享降低模型复杂度,减轻过拟合并且降低计算量
通过局部链接,可以明显降低参数量,但是仍然偏多,但是使用卷积核可以大量降低
卷积的好处是,不管图片尺寸如何,我们需要训练的权重数量只跟卷积核大小,卷积核数量有关,我们可以使用非常少的参数快速处理任意大小的图片
每一层卷积层提取的特征,在后面的层中都会抽象组合成更高阶的特征。而且多层抽象的卷积网络表达能力更强,效率更高,相比只使用一个隐含层提取全部高阶特征,反而可以节省大量的参数
最后总结一下,卷积神经网络的要点就是局部连接,权值共享和池化层中的降采样。
下面使用TensorFlow实现一个简单的卷积网络,首先载入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()

tf.InteractiveSession():它能让你在运行图的时候,插入一些计算图,这些计算图是由某些操作(operations)构成的。这对于工作在交互式环境中的人们来说非常便利,比如使用IPython。
tf.Session():需要在启动session之前构建整个计算图,然后启动该计算图。
意思就是在我们使用tf.InteractiveSession()来构建会话的时候,我们可以先构建一个session然后再定义操作(operation),如果我们使用tf.Session()来构建会话我们需要在会话构建之前定义好全部的操作(operation)然后再构建会话。
接下来创建所需要的权重和偏置。先定义好初始化函数以便重复使用。我们需要给权重制造一些随机噪声来打破完全对称,比如截断的正态分布噪声,标准差设为0.1,同时因为我们使用ReLU,也给偏置增加一些小的正值(0.1)用来避免死亡节点(dead neurons):

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)

下面创建卷积层和池化层函数。conv2d是TensorFlow的2维卷积函数,x是输入,W是卷积参数。例如[5,5,1,32]代表5x5的卷积核尺寸,1个channel(灰色),32个卷积核,也就是这个卷积核会提取多少类特征,strides代表步长,都是1代表不会遗漏地划过每一个点。Padding代表边界处理方式,SAME代表给边界加上Padding让卷积的输出和输入保持同样的尺寸。ksze指滑动窗口尺寸:

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。因为卷积神经网络会利用空间信息,因此需要把一维输入结果转化为二维,即1x784到28x28。[-1,28,28,1]中,-1代表样本数量不固定,1代表颜色通道数量。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种特征:

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)

然后使用reshape函数对第二个卷积层的输出tenso进行变形,转成一维向量,然后连接全连接层,隐含节点为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层,训练时,随机丢弃一部分节点的数据来减轻过拟合,预测时则保留全部数据来追求最好的预测性能:

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,共进行2w次训练迭代,参与训练的样本数量总共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}))

最后跑了很久。。。应该是我超极本的原因吧

然后实现一个进阶的卷机网络
这个卷积神经网络中,我们使用了以下技巧:
(1)对weights进行了L2的正则化;
(2)对图片进行翻转、随机剪裁等数据增强,制造更多的样本;
(3)在每个卷积-最大池化层后面使用了LRN层,增强了模型的泛化能力
先载入常用库和数据集的默认路径:

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'

正则化即特征的权重也会成为模型的损失函数的一部分。可以理解为,为了使用某个特征,我们需要付出loss的代价,除非这个特征非常有效,否则就会被loss上的增加覆盖效果(奥卡姆剃刀)。

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')#加入l2loss,相乘
        tf.add_to_collection('losses', weight_loss)
    return var

然后解压展开数据集到指定位置:

cifar10.maybe_download_and_extract()

产生需要使用的数据,包括特征及其对应的label,返回已经封装好的Tensor,每次执行都会生成一个batch_size的数量的样本。但是对图片进行了增强(课本87页)。所以需要耗费大量的CPU时间,因此distorted_input使用了16个独立的线程加速任务:

images_train, labels_train = cifar10_input.distorted_inputs(data_dir=data_dir,
                                                            batch_size=batch_size)

再生成测试数据,不需要对图片进行处理,不过要剪裁图片正中间24x24大小的区块,并进行数据标准化操作:

images_test, labels_test = cifar10_input.inputs(eval_data=True,
                                                data_dir=data_dir,
                                                batch_size=batch_size) 

输入特征和label:

image_holder = tf.placeholder(tf.float32, [batch_size, 24, 24, 3])
label_holder = tf.placeholder(tf.int32, [batch_size])

下面创建第一个卷积层。卷积核大小5x5,颜色通道3,,6个。标准差0.05。尺寸和步长不一致,增加数据的丰富性。在之后,使用tf.nn.lrn函数,即LRN对结果进行处理。LRN层模仿了生物神经网络的“侧抑制”机制,对局部神经元的活动创建竞争环境,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。LRN对ReLU这种没有上限边界的激活函数会比较有用,因为它会从附件的多个卷积核的响应(response)中挑选比较大的反馈,但不适用于Sigmoid这种有固定边界并且能够抑制过大值的激活函数:

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)

现在来创建第二个卷积层。这里的步骤和第一步很像,区别如下:输入通道调整为64;bias初始化为0.1,而不是0;调整最大池化层和LRN层的顺序:

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')

在两个卷积层后,将使用一个全连接层,这里先reshape第二个卷积层的输出结果。我们希望这个全连接层不要过拟合,所以设了非0的weight loss值0.04,让这一层所有参数都被L2正则所约束:

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)

下面是最后一层,依然先创建这一层的weight,其正态分布标准差是上一层隐含节点数的倒数,并且不计入L2正则。因为softmax主要是为了计算loss,因此整合到后面比较合适:

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)

完成模型的inference部分的构建,接下来计算CNN的loss。这里依然使用cross entropy,需要注意的是我们把softmax的计算和cross entropy的计算合在一起了。使用tf.reduce_mean对cross entropy计算均值,再使用tf.add_to_collection把cross entropy的loss添加到整体losses的collection中。最后,全部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)

优化器:

train_op = tf.train.AdamOptimizer(1e-3).minimize(loss) #0.72

返回分数自高的那一类:

top_k_op = tf.nn.in_top_k(logits, label_holder, 1)

创建默认session,初始化全部模型参数:

sess = tf.InteractiveSession()
tf.global_variables_initializer().run()

启动图片增强线程:

tf.train.start_queue_runners()

下面开始训练过程。每隔10步计算当前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 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))

接下来测试数据集。使用固定的batch_size,然后一个batch一个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)

最终结果大致73%。持续增加max_step,可以期望准确率逐渐增加。如果max_steps比较大,推荐使用学习速率衰减(decay)的SGD进行训练,这样训练过程的准确率会大致接近86%。
数据增强(Data Augmenation)在外面的训练作用很大,它可以给单幅画增加多个副本,提高图片的利用率,防止过拟合。
从本章的例子可以发现,卷积层一般需要和一个池化层连接。卷积层最后几个全连接层的作用是输出分类结果,前面的卷积层主要是特征提取工作,直到最后全连接层更复杂,训练全连接层基本是进行一些矩阵运算,而目前卷积层的训练基本依赖于cuDNN的实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容