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、池化層
和卷積操作一樣,也是用filter對原圖進行操作,超引數是filter的大小f和步長s。一般 f=2,s=2,效果相當於高度和寬度各縮減一半。也有 f=3,s=2的情況。沒有需要學習的引數。目的:縮減模型的大小,提高計算速度,提高所提取特徵的魯棒性。
Max pooling:直觀解釋:只要filter的區域內提取到某個特徵,它都會被保留在輸出裡。
Average pooling:filter內取平均。
Max pooling是最常用的。對於特別深的網路,也許會用average pooling來處理很多通道的資料(比如7*7*1000,整個空間內求平均值得到1*1*1000)。也可以根據自己的意願做padding,則多一個超引數p,但padding的操作非常非常少用。一般池化層filter的通道數為1,對輸入的每個通道單獨處理,所以輸入有幾個通道,輸出也是幾個通道。
3、神經網路演算法相關特性
3.1、優點
(1)可以高效提取特徵。
當我們面對一個分類任務時,傳統的機器學習演算法,一般要首先明確feature和label,然後拿資料取“喂”訓練模型並儲存,最後測試模型的準確性。這就需要我們確定好特徵,當特徵數目很少就無法精確進行分類而引起欠擬合;當特徵數目很多,又會在分類過程中太過於看重某個特徵引起分類錯誤,產生過擬合。而神經網路則不需要做大量的特徵工程,可以直接把資料“灌”進去而讓其自身訓練,自我“修正”,即可達到預期效果。
(2)資料格式更加簡易
利用傳統的機器學習解決分類問題時,資料不能直接“灌”進去的,需要對資料進行一些處理,譬如量綱的歸一化,格式的轉化等等,然而在神經網路裡卻不需要額外對資料做過多的處理。
(3) 引數數目的少量性
同樣在面對一個分類問題時,利用傳統機器學習SVM來做的話,需要涉及核函式,懲罰因子,鬆弛變數等等引數,而這些不同的引數組合會對模型效果產生不一樣的影響,想要迅速而又準確的得到最適合模型的引數,需要對相關理論知識有深入研究,但對於一個基本的三層神經網路來說(輸入-隱含-輸出),只需要初始化時給每一個神經元上隨機的賦予一個權重w和偏置項b,在訓練過程中,這兩個引數就會不斷的修正,調整到最優質,使模型的誤差最小。所以從這個角度來看,我們的工作效率會更佳。
3.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實戰》(黃文堅 唐源 著)(電子工業出版社)。