在圖像中,我們很難根據(jù)認(rèn)為我理解提取出有效而豐富的特征。在深度學(xué)習(xí)出現(xiàn)之前,我們必須借助SIFT,HoG等算法提取有良好區(qū)分性的特征,再集合SVM等進(jìn)行圖像識(shí)別。但是SIFT算法錯(cuò)誤率常年難以突破,卷積神經(jīng)網(wǎng)絡(luò)提取的特征則可以達(dá)到更好的效果。CNN最大特點(diǎn)在于卷積的權(quán)值共享結(jié)構(gòu),可以大幅減少神經(jīng)網(wǎng)絡(luò)的參數(shù)量,防止過(guò)擬合的同時(shí)又降低了神經(jīng)網(wǎng)絡(luò)模型的復(fù)雜度。
在卷積神經(jīng)網(wǎng)絡(luò)中,第一各卷積層會(huì)直接接受圖像像素級(jí)的輸入,每一個(gè)卷積操作只處理一小塊圖像,進(jìn)行卷積變化后再傳入后面的網(wǎng)絡(luò),每一層卷積(或者說(shuō)濾波器)都可以提取數(shù)據(jù)中最有效的特征。再進(jìn)行組合抽象形成更高階的特征。
一般的卷積神經(jīng)網(wǎng)絡(luò)由多個(gè)卷積層構(gòu)成,每個(gè)卷積層通常有如下幾個(gè)操作:
(1)圖像會(huì)通過(guò)多個(gè)不同的卷積核的濾波,并加偏置(bias),提取出局部特征,每一個(gè)卷積核會(huì)映射出一個(gè)新的2D圖像;
(2)將前面卷積核的濾波輸出結(jié)果,進(jìn)行非線性的激活函數(shù)處理。目前常用ReLu,以前sigmoid最多;
(3)對(duì)激活函數(shù)的結(jié)果再進(jìn)行池化操作(即降采樣,比如將2x2的圖片降為1x1的圖片),目前一般是使用最大池化,保留最顯著特征,并提升模型的畸變?nèi)萑棠芰?br>
權(quán)限共享降低模型復(fù)雜度,減輕過(guò)擬合并且降低計(jì)算量
通過(guò)局部鏈接,可以明顯降低參數(shù)量,但是仍然偏多,但是使用卷積核可以大量降低
卷積的好處是,不管圖片尺寸如何,我們需要訓(xùn)練的權(quán)重?cái)?shù)量只跟卷積核大小,卷積核數(shù)量有關(guān),我們可以使用非常少的參數(shù)快速處理任意大小的圖片
每一層卷積層提取的特征,在后面的層中都會(huì)抽象組合成更高階的特征。而且多層抽象的卷積網(wǎng)絡(luò)表達(dá)能力更強(qiáng),效率更高,相比只使用一個(gè)隱含層提取全部高階特征,反而可以節(jié)省大量的參數(shù)
最后總結(jié)一下,卷積神經(jīng)網(wǎng)絡(luò)的要點(diǎn)就是局部連接,權(quán)值共享和池化層中的降采樣。
下面使用TensorFlow實(shí)現(xiàn)一個(gè)簡(jiǎn)單的卷積網(wǎng)絡(luò),首先載入MNIST數(shù)據(jù)集,并創(chuàng)建默認(rèn)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():它能讓你在運(yùn)行圖的時(shí)候,插入一些計(jì)算圖,這些計(jì)算圖是由某些操作(operations)構(gòu)成的。這對(duì)于工作在交互式環(huán)境中的人們來(lái)說(shuō)非常便利,比如使用IPython。
tf.Session():需要在啟動(dòng)session之前構(gòu)建整個(gè)計(jì)算圖,然后啟動(dòng)該計(jì)算圖。
意思就是在我們使用tf.InteractiveSession()來(lái)構(gòu)建會(huì)話的時(shí)候,我們可以先構(gòu)建一個(gè)session然后再定義操作(operation),如果我們使用tf.Session()來(lái)構(gòu)建會(huì)話我們需要在會(huì)話構(gòu)建之前定義好全部的操作(operation)然后再構(gòu)建會(huì)話。
接下來(lái)創(chuàng)建所需要的權(quán)重和偏置。先定義好初始化函數(shù)以便重復(fù)使用。我們需要給權(quán)重制造一些隨機(jī)噪聲來(lái)打破完全對(duì)稱(chēng),比如截?cái)嗟恼龖B(tài)分布噪聲,標(biāo)準(zhǔn)差設(shè)為0.1,同時(shí)因?yàn)槲覀兪褂肦eLU,也給偏置增加一些小的正值(0.1)用來(lái)避免死亡節(jié)點(diǎn)(dead neurons):
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)#返回元素服從截?cái)嗾龖B(tài)分布
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
下面創(chuàng)建卷積層和池化層函數(shù)。conv2d是TensorFlow的2維卷積函數(shù),x是輸入,W是卷積參數(shù)。例如[5,5,1,32]代表5x5的卷積核尺寸,1個(gè)channel(灰色),32個(gè)卷積核,也就是這個(gè)卷積核會(huì)提取多少類(lèi)特征,strides代表步長(zhǎng),都是1代表不會(huì)遺漏地劃過(guò)每一個(gè)點(diǎn)。Padding代表邊界處理方式,SAME代表給邊界加上Padding讓卷積的輸出和輸入保持同樣的尺寸。ksze指滑動(dòng)窗口尺寸:
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_是真實(shí)的label。因?yàn)榫矸e神經(jīng)網(wǎng)絡(luò)會(huì)利用空間信息,因此需要把一維輸入結(jié)果轉(zhuǎn)化為二維,即1x784到28x28。[-1,28,28,1]中,-1代表樣本數(shù)量不固定,1代表顏色通道數(shù)量。tf.reshape是變形函數(shù):
x = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])
x_image = tf.reshape(x, [-1,28,28,1])
接下來(lái)我們定義第一個(gè)卷積層。我們先用前面寫(xiě)好的函數(shù)進(jìn)行初始化,包括weights和bias。這里的[5,5,1,32]代表卷積核尺寸為5x5,1個(gè)顏色通道,32個(gè)不同的卷積核。然后使用conv2d函數(shù)進(jìn)行卷積操作進(jìn)行卷積操作,并加上偏置,接下來(lái)使用ReLU進(jìn)行非線性處理。最后使用最大池化函數(shù)max_pool-2x2對(duì)卷積輸出結(jié)果進(jìn)行池化操作:
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)
現(xiàn)在定義第二個(gè)卷積層,不同的是會(huì)提取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函數(shù)對(duì)第二個(gè)卷積層的輸出tenso進(jìn)行變形,轉(zhuǎn)成一維向量,然后連接全連接層,隱含節(jié)點(diǎn)為1024,并使用ReLU激活函數(shù):
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)
為了減輕過(guò)擬合,下面使用一個(gè)Dropout層,訓(xùn)練時(shí),隨機(jī)丟棄一部分節(jié)點(diǎn)的數(shù)據(jù)來(lái)減輕過(guò)擬合,預(yù)測(cè)時(shí)則保留全部數(shù)據(jù)來(lái)追求最好的預(yù)測(cè)性能:
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
最后我們將Dropout層的輸出連接一個(gè)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)
我們定義損失函數(shù)為cross entropy,和之前一樣,但是優(yōu)化器使用Adam,并給與一個(gè)比較小的學(xué)習(xí)速率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)
再定義準(zhǔn)確率操作:
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
下面開(kāi)始訓(xùn)練。首先依然是初始化所有參數(shù),設(shè)置訓(xùn)練時(shí)Dropout的keep_prob比率為0.5。然后使用大小為50的mini-batch,共進(jìn)行2w次訓(xùn)練迭代,參與訓(xùn)練的樣本數(shù)量總共100w。其中每100次訓(xùn)練,我們會(huì)對(duì)準(zhǔn)確率進(jìn)行評(píng)測(cè) (評(píng)測(cè)時(shí)keep_prob設(shè)為1),用以實(shí)時(shí)監(jiān)測(cè)模型的性能:
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})
全部訓(xùn)練完后,我們?cè)跍y(cè)試集上進(jìn)行全面的測(cè)試,得到整體的分類(lèi)準(zhǔn)確率:
print("test accuracy %g"%accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
最后跑了很久。。。應(yīng)該是我超極本的原因吧
然后實(shí)現(xiàn)一個(gè)進(jìn)階的卷機(jī)網(wǎng)絡(luò)
這個(gè)卷積神經(jīng)網(wǎng)絡(luò)中,我們使用了以下技巧:
(1)對(duì)weights進(jìn)行了L2的正則化;
(2)對(duì)圖片進(jìn)行翻轉(zhuǎn)、隨機(jī)剪裁等數(shù)據(jù)增強(qiáng),制造更多的樣本;
(3)在每個(gè)卷積-最大池化層后面使用了LRN層,增強(qiáng)了模型的泛化能力
先載入常用庫(kù)和數(shù)據(jù)集的默認(rèn)路徑:
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'
正則化即特征的權(quán)重也會(huì)成為模型的損失函數(shù)的一部分。可以理解為,為了使用某個(gè)特征,我們需要付出loss的代價(jià),除非這個(gè)特征非常有效,否則就會(huì)被loss上的增加覆蓋效果(奧卡姆剃刀)。
def variable_with_weight_loss(shape, stddev, wl):
var = tf.Variable(tf.truncated_normal(shape, stddev=stddev))#截?cái)嗾龖B(tài)分布
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
然后解壓展開(kāi)數(shù)據(jù)集到指定位置:
cifar10.maybe_download_and_extract()
產(chǎn)生需要使用的數(shù)據(jù),包括特征及其對(duì)應(yīng)的label,返回已經(jīng)封裝好的Tensor,每次執(zhí)行都會(huì)生成一個(gè)batch_size的數(shù)量的樣本。但是對(duì)圖片進(jìn)行了增強(qiáng)(課本87頁(yè))。所以需要耗費(fèi)大量的CPU時(shí)間,因此distorted_input使用了16個(gè)獨(dú)立的線程加速任務(wù):
images_train, labels_train = cifar10_input.distorted_inputs(data_dir=data_dir,
batch_size=batch_size)
再生成測(cè)試數(shù)據(jù),不需要對(duì)圖片進(jìn)行處理,不過(guò)要剪裁圖片正中間24x24大小的區(qū)塊,并進(jìn)行數(shù)據(jù)標(biāo)準(zhǔn)化操作:
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])
下面創(chuàng)建第一個(gè)卷積層。卷積核大小5x5,顏色通道3,,6個(gè)。標(biāo)準(zhǔn)差0.05。尺寸和步長(zhǎng)不一致,增加數(shù)據(jù)的豐富性。在之后,使用tf.nn.lrn函數(shù),即LRN對(duì)結(jié)果進(jìn)行處理。LRN層模仿了生物神經(jīng)網(wǎng)絡(luò)的“側(cè)抑制”機(jī)制,對(duì)局部神經(jīng)元的活動(dòng)創(chuàng)建競(jìng)爭(zhēng)環(huán)境,使得其中響應(yīng)比較大的值變得相對(duì)更大,并抑制其他反饋較小的神經(jīng)元,增強(qiáng)了模型的泛化能力。LRN對(duì)ReLU這種沒(méi)有上限邊界的激活函數(shù)會(huì)比較有用,因?yàn)樗鼤?huì)從附件的多個(gè)卷積核的響應(yīng)(response)中挑選比較大的反饋,但不適用于Sigmoid這種有固定邊界并且能夠抑制過(guò)大值的激活函數(shù):
weight1 = variable_with_weight_loss(shape=[5, 5, 3, 64], stddev=5e-2, wl=0.0)#創(chuàng)建卷積核函數(shù)并初始化
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)
現(xiàn)在來(lái)創(chuàng)建第二個(gè)卷積層。這里的步驟和第一步很像,區(qū)別如下:輸入通道調(diào)整為64;bias初始化為0.1,而不是0;調(diào)整最大池化層和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')
在兩個(gè)卷積層后,將使用一個(gè)全連接層,這里先reshape第二個(gè)卷積層的輸出結(jié)果。我們希望這個(gè)全連接層不要過(guò)擬合,所以設(shè)了非0的weight loss值0.04,讓這一層所有參數(shù)都被L2正則所約束:
reshape = tf.reshape(pool2, [batch_size, -1])
dim = reshape.get_shape()[1].value#獲取扁平化后的長(zhǎng)度
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)
接下來(lái)的這個(gè)全連接層和前一層很像,只不過(guò)其隱含層節(jié)點(diǎn)數(shù)下降了一半,其他參數(shù)不變:
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)
下面是最后一層,依然先創(chuàng)建這一層的weight,其正態(tài)分布標(biāo)準(zhǔn)差是上一層隱含節(jié)點(diǎn)數(shù)的倒數(shù),并且不計(jì)入L2正則。因?yàn)閟oftmax主要是為了計(jì)算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部分的構(gòu)建,接下來(lái)計(jì)算CNN的loss。這里依然使用cross entropy,需要注意的是我們把softmax的計(jì)算和cross entropy的計(jì)算合在一起了。使用tf.reduce_mean對(duì)cross entropy計(jì)算均值,再使用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節(jié)點(diǎn)和label_holder傳入loss函數(shù)獲得最終的loss:
loss = loss(logits, label_holder)
優(yōu)化器:
train_op = tf.train.AdamOptimizer(1e-3).minimize(loss) #0.72
返回分?jǐn)?shù)自高的那一類(lèi):
top_k_op = tf.nn.in_top_k(logits, label_holder, 1)
創(chuàng)建默認(rèn)session,初始化全部模型參數(shù):
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()
啟動(dòng)圖片增強(qiáng)線程:
tf.train.start_queue_runners()
下面開(kāi)始訓(xùn)練過(guò)程。每隔10步計(jì)算當(dāng)前l(fā)oss、每秒能訓(xùn)練的樣本數(shù)量,以及訓(xùn)練一個(gè)batch數(shù)據(jù)所花費(fèi)的時(shí)間:
###
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))
接下來(lái)測(cè)試數(shù)據(jù)集。使用固定的batch_size,然后一個(gè)batch一個(gè)batch地輸入測(cè)試數(shù)據(jù):
###
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
最后打印準(zhǔn)確率的評(píng)測(cè)結(jié)果計(jì)算并打印出來(lái):
precision = true_count / total_sample_count
print('precision @ 1 = %.3f' % precision)
最終結(jié)果大致73%。持續(xù)增加max_step,可以期望準(zhǔn)確率逐漸增加。如果max_steps比較大,推薦使用學(xué)習(xí)速率衰減(decay)的SGD進(jìn)行訓(xùn)練,這樣訓(xùn)練過(guò)程的準(zhǔn)確率會(huì)大致接近86%。
數(shù)據(jù)增強(qiáng)(Data Augmenation)在外面的訓(xùn)練作用很大,它可以給單幅畫(huà)增加多個(gè)副本,提高圖片的利用率,防止過(guò)擬合。
從本章的例子可以發(fā)現(xiàn),卷積層一般需要和一個(gè)池化層連接。卷積層最后幾個(gè)全連接層的作用是輸出分類(lèi)結(jié)果,前面的卷積層主要是特征提取工作,直到最后全連接層更復(fù)雜,訓(xùn)練全連接層基本是進(jìn)行一些矩陣運(yùn)算,而目前卷積層的訓(xùn)練基本依賴(lài)于cuDNN的實(shí)現(xiàn)。