卷積神經網絡(Convolutional?Neural?Network,CNN)初是為解決圖像識別等問題設計的,當然其現在的應用不僅限于圖像和視頻,也可用于時間序列信號,比如音頻信號、文本數據等。在早期的圖像識別研究中,大的挑戰是如何組織特征,因為圖像數據不像其他類型的數據那樣可以通過人工理解來提取特征。在股票預測等模型中,我們可以從原始數據中提取過往的交易價格波動、市盈率、市凈率、盈利增長等金融因子,這即是特征工程。但是在圖像中,我們很難根據人為理解提取出有效而豐富的特征。在深度學習出現之前,我們必須借助SIFT、HoG等算法提取具有良好區分性的特征,再集合SVM等機器學習算法進行圖像識別。如圖5-1所示,SIFT對一定程度內的縮放、平移、旋轉、視角改變、亮度調整等畸變,都具有不變性,是當時重要的圖像特征提取方法之一??梢哉f,在之前只能依靠SIFT等特征提取算法才能勉強進行可靠的圖像識別。
然而SIFT這類算法提取的特征還是有局限性的,在ImageNet?ILSVRC比賽的好結果的錯誤率也有26%以上,而且常年難以產生突破。卷積神經網絡提取的特征則可以達到更好的效果,同時它不需要將特征提取和分類訓練兩個過程分開,它在訓練時就自動提取了有效的特征。CNN作為一個深度學習架構被提出的初訴求,是降低對圖像數據預處理的要求,以及避免復雜的特征工程。CNN可以直接使用圖像的原始像素作為輸入,而不必先使用SIFT等算法提取特征,減輕了使用傳統算法如SVM時必需要做的大量重復、煩瑣的數據預處理工作。和SIFT等算法類似,CNN訓練的模型同樣對縮放、平移、旋轉等畸變具有不變性,有著很強的泛化性。CNN的大特點在于卷積的權值共享結構,可以大幅減少神經網絡的參數量,防止過擬合的同時又降低了神經網絡模型的復雜度。CNN的權值共享其實也很像早期的延時神經網絡(TDNN),只不過后者是在時間這一個維度上進行權值共享,降低了學習時間序列信號的復雜度。
卷積神經網絡的概念早出自19世紀60年代科學家提出的感受野(Receptive?Field)。當時科學家通過對貓的視覺皮層細胞研究發現,每一個視覺神經元只會處理一小塊區域的視覺圖像,即感受野。到了20世紀80年代,日本科學家提出神經認知機(Neocognitron)的概念,可以算作是卷積網絡初的實現原型。神經認知機中包含兩類神經元,用來抽取特征的S-cells,還有用來抗形變的C-cells,其中S-cells對應我們現在主流卷積神經網絡中的卷積核濾波操作,而C-cells則對應激活函數、大池化(Max-Pooling)等操作。同時,CNN也是首個成功地進行多層訓練的網絡結構,即前面章節提到的LeCun的LeNet5,而全連接的網絡因為參數過多及梯度彌散等問題,在早期很難順利地進行多層的訓練。卷積神經網絡可以利用空間結構關系減少需要學習的參數量,從而提高反向傳播算法的訓練效率。在卷積神經網絡中,個卷積層會直接接受圖像像素級的輸入,每一個卷積操作只處理一小塊圖像,進行卷積變化后再傳到后面的網絡,每一層卷積(也可以說是濾波器)都會提取數據中有效的特征。這種方法可以提取到圖像中基礎的特征,比如不同方向的邊或者拐角,而后再進行組合和抽象形成更高階的特征,因此CNN可以應對各種情況,理論上具有對圖像縮放、平移和旋轉的不變性。
一般的卷積神經網絡由多個卷積層構成,每個卷積層中通常會進行如下幾個操作。
這幾個步驟就構成了常見的卷積層,當然也可以再加上一個LRN(Local?Response?Normalization,局部響應歸一化層)層,目前非常流行的Trick還有Batch?Normalization等。
一個卷積層中可以有多個不同的卷積核,而每一個卷積核都對應一個濾波后映射出的新圖像,同一個新圖像中每一個像素都來自完全相同的卷積核,這就是卷積核的權值共享。那我們為什么要共享卷積核的權值參數呢?答案很簡單,降低模型復雜度,減輕過擬合并降低計算量。舉個例子,如圖5-2所示,如果我們的圖像尺寸是1000像素×1000像素,并且假定是黑白圖像,即只有一個顏色通道,那么一張圖片就有100萬個像素點,輸入數據的維度也是100萬。接下來,如果連接一個相同大小的隱含層(100萬個隱含節點),那么將產生100萬×100萬=一萬億個連接。僅僅一個全連接層(Fully?Connected?Layer),就有一萬億連接的權重要去訓練,這已經超出了普通硬件的計算能力。我們必須減少需要訓練的權重數量,一是降低計算的復雜度,二是過多的連接會導致嚴重的過擬合,減少連接數可以提升模型的泛化性。
圖像在空間上是有組織結構的,每一個像素點在空間上和周圍的像素點實際上是有緊密聯系的,但是和太遙遠的像素點就不一定有什么關聯了。這就是前面提到的人的視覺感受野的概念,每一個感受野只接受一小塊區域的信號。這一小塊區域內的像素是互相關聯的,每一個神經元不需要接收全部像素點的信息,只需要接收局部的像素點作為輸入,而后將所有這些神經元收到的局部信息綜合起來就可以得到全局的信息。這樣就可以將之前的全連接的模式修改為局部連接,之前隱含層的每一個隱含節點都和全部像素相連,現在我們只需要將每一個隱含節點連接到局部的像素節點。假設局部感受野大小是10×10,即每個隱含節點只與10×10個像素點相連,那么現在就只需要10×10×100萬=1億個連接,相比之前的1萬億縮小了10000倍。
上面我們通過局部連接(Locally?Connect)的方法,將連接數從1萬億降低到1億,但仍然偏多,需要繼續降低參數量。現在隱含層每一個節點都與10×10的像素相連,也就是每一個隱含節點都擁有100個參數。假設我們的局部連接方式是卷積操作,即默認每一個隱含節點的參數都完全一樣,那我們的參數不再是1億,而是100。不論圖像有多大,都是這10×10=100個參數,即卷積核的尺寸,這就是卷積對縮小參數量的貢獻。我們不需要再擔心有多少隱含節點或者圖片有多大,參數量只跟卷積核的大小有關,這也就是所謂的權值共享。但是如果我們只有一個卷積核,我們就只能提取一種卷積核濾波的結果,即只能提取一種圖片特征,這不是我們期望的結果。好在圖像中基本的特征很少,我們可以增加卷積核的數量來多提取一些特征。圖像中的基本特征無非就是點和邊,無論多么復雜的圖像都是點和邊組合而成的。人眼識別物體的方式也是從點和邊開始的,視覺神經元接受光信號后,每一個神經元只接受一個區域的信號,并提取出點和邊的特征,然后將點和邊的信號傳遞給后面一層的神經元,再接著組合成高階特征,比如三角形、正方形、直線、拐角等,再繼續抽象組合,得到眼睛、鼻子和嘴等五官,后再將五官組合成一張臉,完成匹配識別。因此我們的問題就很好解決了,只要我們提供的卷積核數量足夠多,能提取出各種方向的邊或各種形態的點,就可以讓卷積層抽象出有效而豐富的高階特征。每一個卷積核濾波得到的圖像就是一類特征的映射,即一個Feature?Map。一般來說,我們使用100個卷積核放在個卷積層就已經很充足了。那這樣的話,如圖5-3所示,我們的參數量就是100×100=1萬個,相比之前的1億又縮小了10000倍。因此,依靠卷積,我們就可以高效地訓練局部連接的神經網絡了。卷積的好處是,不管圖片尺寸如何,我們需要訓練的權值數量只跟卷積核大小、卷積核數量有關,我們可以使用非常少的參數量處理任意大小的圖片。每一個卷積層提取的特征,在后面的層中都會抽象組合成更高階的特征。而且多層抽象的卷積網絡表達能力更強,效率更高,相比只使用一個隱含層提取全部高階特征,反而可以節省大量的參數。當然,我們需要注意的是,雖然需要訓練的參數量下降了,但是隱含節點的數量并沒有下降,隱含節點的數量只跟卷積的步長有關。如果步長為1,那么隱含節點的數量和輸入的圖像像素數量一致;如果步長為5,那么每5×5的像素才需要一個隱含節點,我們隱含節點的數量就是輸入像素數量的1/25。
我們再總結一下,卷積神經網絡的要點就是局部連接(Local?Connection)、權值共享(Weight?Sharing)和池化層(Pooling)中的降采樣(Down-Sampling)。其中,局部連接和權值共享降低了參數量,使訓練復雜度大大下降,并減輕了過擬合。同時權值共享還賦予了卷積網絡對平移的容忍性,而池化層降采樣則進一步降低了輸出參數量,并賦予模型對輕度形變的容忍性,提高了模型的泛化能力。卷積神經網絡相比傳統的機器學習算法,無須手工提取特征,也不需要使用諸如SIFT之類的特征提取算法,可以在訓練中自動完成特征的提取和抽象,并同時進行模式分類,大大降低了應用圖像識別的難度;相比一般的神經網絡,CNN在結構上和圖片的空間結構更為貼近,都是2D的有聯系的結構,并且CNN的卷積連接方式和人的視覺神經處理光信號的方式類似。
大名鼎鼎的LeNet5?誕生于1994年,是早的深層卷積神經網絡之一,并且推動了深度學習的發展。從1988年開始,在多次成功的迭代后,這項由Yann?LeCun完成的開拓性成果被命名為LeNet5。LeCun認為,可訓練參數的卷積層是一種用少量參數在圖像的多個位置上提取相似特征的有效方式,這和直接把每個像素作為多層神經網絡的輸入不同。像素不應該被使用在輸入層,因為圖像具有很強的空間相關性,而使用圖像中獨立的像素直接作為輸入則利用不到這些相關性。
LeNet5當時的特性有如下幾點。
LeNet5中的諸多特性現在依然在state-of-the-art卷積神經網絡中使用,可以說LeNet5是奠定了現代卷積神經網絡的基石之作。Lenet-5的結構如圖5-4所示。它的輸入圖像為32×32的灰度值圖像,后面有三個卷積層,一個全連接層和一個高斯連接層。它的個卷積層C1包含6個卷積核,卷積核尺寸為5×5,即總共(5×5+1)×6=156個參數,括號中的1代表1個bias,后面是一個2×2的平均池化層S2用來進行降采樣,再之后是一個Sigmoid激活函數用來進行非線性處理。而后是第二個卷積層C3,同樣卷積核尺寸是5×5,這里使用了16個卷積核,對應16個Feature?Map。需要注意的是,這里的16個Feature?Map不是全部連接到前面的6個Feature?Map的輸出的,有些只連接了其中的幾個Feature?Map,這樣增加了模型的多樣性。下面的第二個池化層S4和個池化層S2一致,都是2×2的降采樣。接下來的第三個卷積層C5有120個卷積核,卷積大小同樣為5×5,因為輸入圖像的大小剛好也是5×5,因此構成了全連接,也可以算作全連接層。F6層是一個全連接層,擁有84個隱含節點,激活函數為Sigmoid。LeNet-5后一層由歐式徑向基函數(Euclidean?Radial?Basis?Function)單元組成,它輸出后的分類結果。
本節將講解如何使用TensorFlow實現一個簡單的卷積神經網絡,使用的數據集依然是MNIST,預期可以達到99.2%左右的準確率。本節將使用兩個卷積層加一個全連接層構建一個簡單但是非常有代表性的卷積神經網絡,讀者應該能通過這個例子掌握設計卷積神經網絡的要點。
首先載入MNIST數據集,并創建默認的Interactive?Session。本節代碼主要來自TensorFlow的開源實現。
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()
接下來要實現的這個卷積神經網絡會有很多的權重和偏置需要創建,因此我們先定義好初始化函數以便重復使用。我們需要給權重制造一些隨機的噪聲來打破完全對稱,比如截斷的正態分布噪聲,標準差設為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)
卷積層、池化層也是接下來要重復使用的,因此也為他們分別定義創建函數。這里的tf.nn.conv2d是TensorFlow中的2維卷積函數,參數中x是輸入,W是卷積的參數,比如[5,5,1,32]:前面兩個數字代表卷積核的尺寸;第三個數字代表有多少個channel。因為我們只有灰度單色,所以是1,如果是彩色的RGB圖片,這里應該是3。后一個數字代表卷積核的數量,也就是這個卷積層會提取多少類的特征。Strides代表卷積模板移動的步長,都是1代表會不遺漏地劃過圖片的每一個點。Padding代表邊界的處理方式,這里的SAME代表給邊界加上Padding讓卷積的輸出和輸入保持同樣(SAME)的尺寸。tf.nn.max_pool是TensorFlow中的大池化函數,我們這里使用2×2的大池化,即將一個2×2的像素塊降為1×1的像素。大池化會保留原始像素塊中灰度值高的那一個像素,即保留顯著的特征。因為希望整體上縮小圖片尺寸,因此池化層的strides也設為橫豎兩個方向以2為步長。如果步長還是1,那么我們會得到一個尺寸不變的圖片。
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代表顏色通道數量。這里我們使用的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]代表卷積核尺寸為5×5,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種特征。
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)
因為前面經歷了兩次步長為2×2的大池化,所以邊長已經只有1/4了,圖片尺寸由28×28變成了7×7。而第二個卷積層的卷積核數量為64,其輸出的tensor尺寸即為7×7×64。我們使用tf.reshape函數對第二個卷積層的輸出tensor進行變形,將其轉成1D的向量,然后連接一個全連接層,隱含節點為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的用法第4章已經講過,是通過一個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)
再繼續定義評測準確率的操作,這里和第3章、第4章一樣。
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次訓練迭代,參與訓練的樣本數量總共為100萬。其中每100次訓練,我們會對準確率進行一次評測(評測時keep_prob設為1),用以實時監測模型的性能。
tf.global_variables_initializer().run()
for?i in range(20000):
????batch?=?mnist.train.next_batch(50)
????if?i0?== 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}))
后,這個CNN模型可以得到的準確率約為99.2%,基本可以滿足對手寫數字識別準確率的要求。相比之前MLP的2%錯誤率,CNN的錯誤率下降了大約60%。這其中主要的性能提升都來自于更的網絡設計,即卷積網絡對圖像特征的提取和抽象能力。依靠卷積核的權值共享,CNN的參數量并沒有爆炸,降低計算量的同時也減輕了過擬合,因此整個模型的性能有較大的提升。本節我們只實現了一個簡單的卷積神經網絡,沒有復雜的Trick。接下來,我們將實現一個稍微復雜一些的卷積網絡,而簡單的MNIST數據集已經不適合用來評測其性能,我們將使用CIFAR-10數據集進行訓練,這也是深度學習可以大幅領先其他模型的一個數據集。
本節使用的數據集是CIFAR-10,這是一個經典的數據集,包含60000張32×32的彩色圖像,其中訓練集50000張,測試集10000張。CIFAR-10如同其名字,一共標注為10類,每一類圖片6000張。這10類分別是airplane、automobile、bird、cat、deer、dog、frog、horse、ship和truck,其中沒有任何重疊的情況,比如automobile只包括小型汽車,truck只包括卡車,也不會在一張圖片中同時出現兩類物體。它還有一個兄弟版本CIFAR-100,其中標注了100類。這兩個數據集是前面章節提到的深度學習之父Geoffrey?Hinton和他的兩名學生Alex?Krizhevsky和Vinod?Nair收集的,圖片來源于80?million?tiny?images這個數據集,Hinton等人對其進行了篩選和標注。CIFAR-10數據集非常通用,經常出現在各大會議的論文中用來進行性能對比,也曾出現在Kaggle競賽而為大家所知。圖5-5所示為這個數據集的一些示例。
許多論文中都在這個數據集上進行了測試,目前state-of-the-art的工作已經可以達到3.5%的錯誤率了,但是需要訓練很久,即使在GPU上也需要十幾個小時。CIFAR-10數據集上詳細的Benchmark和排名在classification?datasets?results上(http://rodrigob.github.io/are_we_there_yet/build/classification_datasets_results.html)。據深度學習三巨頭之一LeCun說,現有的卷積神經網絡已經可以對CIFAR-10進行很好的學習,這個數據集的問題已經解決了。本節中實現的卷積神經網絡沒有那么復雜(根據Alex描述的cuda-convnet模型做了些許修改得到),在只使用3000個batch(每個batch包含128個樣本)時,可以達到73%左右的正確率。模型在GTX?1080單顯卡上大概只需要幾十秒的訓練時間,如果在CPU上訓練則會慢很多。如果使用100k個batch,并結合學習速度的decay(即每隔一段時間將學習速率下降一個比率),正確率高可以到86%左右。模型中需要訓練的參數約為100萬個,而預測時需要進行的四則運算總量在2000萬次左右。在這個卷積神經網絡模型中,我們使用了一些新的技巧。
我們首先下載TensorFlow?Models庫,以便使用其中提供CIFAR-10數據的類。
git clone https://github.com/tensorflow/models.git cd?models/tutorials/image/cifar10
然后我們載入一些常用庫,比如NumPy和time,并載入TensorFlow?Models中自動下載、讀取CIFAR-10數據的類。本節代碼主要來自TensorFlow的開源實現。
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'
這里定義初始化weight的函數,和之前一樣依然使用tf.truncated_normal截斷的正態分布來初始化權重。但是這里會給weight加一個L2的loss,相當于做了一個L2的正則化處理。在機器學習中,不管是分類還是回歸任務,都可能因特征過多而導致過擬合,一般可以通過減少特征或者懲罰不重要特征的權重來緩解這個問題。但是通常我們并不知道該懲罰哪些特征的權重,而正則化就是幫助我們懲罰特征權重的,即特征的權重也會成為模型的損失函數的一部分。可以理解為,為了使用某個特征,我們需要付出loss的代價,除非這個特征非常有效,否則就會被loss上的增加覆蓋效果。這樣我們就可以篩選出有效的特征,減少特征權重防止過擬合。這也即是奧卡姆剃刀法則,越簡單的東西越有效。一般來說,L1正則會制造稀疏的特征,大部分無用特征的權重會被置為0,而L2正則會讓特征的權重不過大,使得特征的權重比較平均。我們使用wl控制L2?loss的大小,使用tf.nn.l2_loss函數計算weight的L2?loss,再使用tf.multiply讓L2?loss乘以wl,得到后的weight?loss。接著,我們使用tf.add_to_collection把weight?loss統一存到一個collection,這個collection名為“losses”,它會在后面計算神經網絡的總體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')
????????tf.add_to_collection('losses',?weight_loss) return var
下面使用cifar10類下載數據集,并解壓、展開到其默認位置。
cifar10.maybe_download_and_extract()
再使用cifar10_input類中的distorted_inputs函數產生訓練需要使用的數據,包括特征及其對應的label,這里返回的是已經封裝好的tensor,每次執行都會生成一個batch_size的數量的樣本。需要注意的是我們對數據進行了Data?Augmentation(數據增強)。具體的實現細節,讀者可以查看cifar10_input.distorted_inputs函數,其中的數據增強操作包括隨機的水平翻轉(tf.image.random_flip_left_right)、隨機剪切一塊24×24大小的圖片(tf.random_crop)、設置隨機的亮度和對比度(tf.image.random_brightness、tf.image.random_contrast),以及對數據進行標準化tf.image.per_image_whitening(對數據減去均值,除以方差,保證數據零均值,方差為1)。通過這些操作,我們可以獲得更多的樣本(帶噪聲的),原來的一張圖片樣本可以變為多張圖片,相當于擴大樣本量,對提高準確率非常有幫助。需要注意的是,我們對圖像進行數據增強的操作需要耗費大量CPU時間,因此distorted_inputs使用了16個獨立的線程來加速任務,函數內部會產生線程池,在需要使用時會通過TensorFlow?queue進行調度。
images_train,?labels_train?=?cifar10_input.distorted_inputs(
?????????????????????????????????data_dir=data_dir,?batch_size=batch_size)
我們再使用cifar10_input.inputs函數生成測試數據,這里不需要進行太多處理,不需要對圖片進行翻轉或修改亮度、對比度,不過需要裁剪圖片正中間的24×24大小的區塊,并進行數據標準化操作。
images_test,?labels_test?=?cifar10_input.inputs(eval_data=True,
????????????????????????????????????????????????data_dir=data_dir,
????????????????????????????????????????????????batch_size=batch_size)
這里創建輸入數據的placeholder,包括特征和label。在設定placeholder的數據尺寸時需要注意,因為batch_size在之后定義網絡結構時被用到了,所以數據尺寸中的個值即樣本條數需要被預先設定,而不能像以前一樣可以設為None。而數據尺寸中的圖片尺寸為24×24,即是裁剪后的大小,而顏色通道數則設為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函數創建卷積核的參數并進行初始化。個卷積層使用5×5的卷積核大小,3個顏色通道,64個卷積核,同時設置weight初始化函數的標準差為0.05。我們不對個卷積層的weight進行L2的正則,因此wl(weight?loss)這一項設為0。下面使用tf.nn.conv2d函數對輸入數據image_holder進行卷積操作,這里的步長stride均設為1,padding模式為SAME。把這層的bias全部初始化為0,再將卷積的結果加上bias,后使用一個ReLU激活函數進行非線性化。在ReLU激活函數之后,我們使用一個尺寸為3×3且步長為2×2的大池化層處理數據,注意這里大池化的尺寸和步長不一致,這樣可以增加數據的豐富性。再之后,我們使用tf.nn.lrn函數,即LRN對結果進行處理。LRN早見于Alex那篇用CNN參加ImageNet比賽的論文,Alex在論文中解釋LRN層模仿了生物神經系統的“側抑制”機制,對局部神經元的活動創建競爭環境,使得其中響應比較大的值變得相對更大,并抑制其他反饋較小的神經元,增強了模型的泛化能力。Alex在ImageNet數據集上的實驗表明,使用LRN后CNN在Top1的錯誤率可以降低1.4%,因此在其經典的AlexNet中使用了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(即輸出64個通道),所以本層卷積核尺寸的第三個維度即輸入的通道數也需要調整為64;還有一個需要注意的地方是這里的bias值全部初始化為0.1,而不是0。后,我們調換了大池化層和LRN層的順序,先進行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')
在兩個卷積層之后,將使用一個全連接層,這里需要先把前面兩個卷積層的輸出結果全部flatten,使用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,?wl=0.004)
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.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輸出后結果,這是因為我們把softmax的操作放在了計算loss的部分。我們不需要對inference的輸出進行softmax處理就可以獲得終分類結果(直接比較inference輸出的各類的數值大小即可),計算softmax主要是為了計算loss,因此softmax操作整合到后面是比較合適的。
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的部分。梳理整個網絡結構可以得到表5-1。從上到下,依次是整個卷積神經網絡從輸入到輸出的流程??梢杂^察到,其實設計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_placeholder傳入loss函數獲得終的loss。
loss = loss(logits,?label_holder)
優化器依然選擇Adam?Optimizer,學習速率設為1e-3。
train_op?=?tf.train.AdamOptimizer(1e-3).minimize(loss)
使用tf.nn.in_top_k函數求輸出結果中top?k的準確率,默認使用top?1,也就是輸出分數高的那一類的準確率。
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數據所花費的時間,這樣就可以比較方便地監控整個訓練過程。在GTX?1080上,每秒鐘可以訓練大約1800個樣本,如果batch_size為128,則每個batch大約需要0.066s。損失loss在一開始大約為4.6,在經過了3000步訓練后會下降到1.0附近。
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))
接下來評測模型在測試集上的準確率。測試集一共有10000個樣本,但是需要注意的是,我們依然要像訓練時那樣使用固定的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層的使用都對模型準確率有提升作用,他們都可以從某些方面提升模型的泛化性。
數據增強(Data?Augmentation)在我們的訓練中作用很大,它可以給單幅圖增加多個副本,提高圖片的利用率,防止對某一張圖片結構的學習過擬合。這剛好是利用了圖片數據本身的性質,圖片的冗余信息量比較大,因此可以制造不同的噪聲并讓圖片依然可以被識別出來。如果神經網絡可以克服這些噪聲并準確識別,那么它的泛化性必然會很好。數據增強大大增加了樣本量,而數據量的大小恰恰是深度學習看重的,深度學習可以在圖像識別上領先其他算法的一大因素就是它對海量數據的利用效率非常高。用其他算法,可能在數據量大到一定程度時,準確率就不再上升了,而深度學習只要提供足夠多的樣本,準確率基本可以持續提升,所以說它是適合大數據的算法。如圖5-6所示,傳統的機器學習算法在獲取了一定量的數據后,準確率上升曲線就接近瓶頸,而神經網絡則可以持續上升到更高的準確率才接近瓶頸。規模越大越復雜的神經網絡模型,可以達到的準確率水平越高,但是也相應地需要更多的數據才能訓練好,在數據量小時反而容易過擬合。我們可以看到Large?NN在數據量小的時候,并不比常規算法好,直到數據量持續擴大才慢慢超越了常規算法、Small?NN和Medium?NN,并在后達到了一個非常高的準確率。根據Alex在cuda-convnet上的測試結果,如果不對CIFAR-10數據使用數據增強,那么錯誤率低可以下降到17%;使用數據增強后,錯誤率可以下降到11%左右,模型性能的提升非常顯著。
從本章的例子中可以發現,卷積層一般需要和一個池化層連接,卷積加池化的組合目前已經是做圖像識別時的一個標準組件了。卷積網絡后的幾個全連接層的作用是輸出分類結果,前面的卷積層主要做特征提取的工作,直到后的全連接層才開始對特征進行組合匹配,并進行分類。卷積層的訓練相對于全連接層更復雜,訓練全連接層基本是進行一些矩陣乘法運算,而目前卷積層的訓練基本依賴于cuDNN的實現(另有nervana公司的neon也占有一席之地)。其中的算法相對復雜,有些方法(比如Facebook開源的算法)還會涉及傅里葉變換。同時,卷積層的使用有很多Trick,除了本章提到的方法,實際上有很多方法可以防止CNN過擬合,加快收斂速度或者提高泛化性,這些會在后續章節中講解。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。