tensorflow學習筆記——圖像識別與卷積神經網絡

  不管是以前學習的MNIST數據集仍是Cifar數據集,相比真實環境下的圖像識別問題,有兩個最大的問題,一是現實生活中的圖片分辨率要遠高於32*32,並且圖像的分辨率也不會是固定的。二是現實生活中的物體類別不少,不管是10種仍是100種都遠遠不夠,並且一張圖片中不會只出現一個種類的物體。爲了更加貼近真實環境下的圖像識別問題,由李飛飛教授帶頭整理的ImageNet很大程度上解決了這個問題。node

  ImageNet是一個基於WordNet的大型圖像數據庫,在ImageNet中,將近1500萬圖片被關聯到了WorldNet的大約20000個名詞同義詞集上,目前每個與ImageNet相關的WordNet同義詞集都表明了現實世界中的一個實體,能夠被認爲是分類問題的一個類別。在ImageNet的圖片中,一張圖片可能出現多個同義詞集所表明的實體。python

  下面主要使用的是ILSVRC2012圖像分類數據集,ILSVRC2012圖像分類數據集的任務和Cifar數據集是基本一致的,也是識別圖像中的主要物體。ILSVRC2012圖像分類數據集包含了來自1000個類別的120萬張圖片,其中每張圖片數據且只屬於一個類別。由於ILSVRC2012圖像分類數據集的圖片是直接從互聯網上爬取獲得的,因此圖片的大小從幾千字節到幾百萬字節不等。git

卷積神經網絡簡介

  爲了將只包含全鏈接層的神經網絡和卷積神經網絡,循環神經網絡區分開,咱們將只包含全鏈接層的神經網絡稱之爲全鏈接神經網絡。下面先學習卷積神經網絡與全鏈接神經網絡的差別,並介紹組成一個卷積神經網絡的基本網絡結構,下圖顯示了全鏈接神經網絡與卷積神經網絡的結構對比圖。正則表達式

  上圖顯示的全鏈接神經網絡結構和卷積神經網絡的結構直觀上差別比較大,可是實際上他們的總體架構是很是類似的。從上圖中能夠看出,卷積神經網絡也是經過一層一層的節點組織起來的。和全鏈接神經網絡同樣,卷積神經網絡中的每個節點都是一個神經元。在全鏈接神經網絡中,每相鄰兩層之間的節點都是有邊相連,因而通常會將每層全鏈接層中的節點組織成一列,這樣方便顯示鏈接結構。而對於卷積神經網絡,相鄰兩次之間只有部分節點相連,爲了展現每一層神經元的維度,通常會將每一層卷積層的節點組織成一個三維矩陣。算法

  除告終構類似,卷積神經網絡的輸入輸出以及訓練流程與全鏈接神經網絡也基本一致。以圖形分類爲例,卷積神經網絡的輸入層就是圖像的原始像素,而輸出層中的每個節點表明了不一樣類別的可信度。這和全鏈接神經網絡的輸入輸出是一致的。卷積神經網絡和全鏈接神經網絡的惟一區別就在於神經網絡中相鄰兩次的鏈接方式。數據庫

  下面咱們瞭解一下爲何全鏈接神經網絡沒法很好地處理圖像數據。api

  使用全鏈接神經網絡處理圖像的最大問題在於全鏈接層的參數太多。對於MNIST數據,每一張圖片的大小是28*28*1,其中28*28是圖片的大小,*1表示圖像是黑白的,只有一個色彩通道。假設第一層隱藏層的節點數爲500個,那麼一個全鏈接層的神經網絡將有28*28*500+500=392500 個參數。當圖片更大時,好比在Cifar-10數據集中,圖片的大小爲32*32*3,其中32*32表示圖片的大小,*3表示圖片是經過紅綠藍三個色彩通道(channel)表示的。這樣輸入層就是3072個節點,若是第一次全鏈接層仍然是500個節點,那麼這一層全鏈接神經網絡將有32*32*3*500+500=150萬個參數(大約)。參數增多除了致使計算速度減慢,還很容易致使過擬合問題。因此須要一個更合理的神經網絡結構來有效的減小神經網絡中參數個數。而卷積神經網絡就能夠達到這個目的。數組

  下面給出了一個更加具體的神經網絡架構圖:緩存

  在卷積神經網絡的前幾層中,每一層的節點都被組織成一個三維矩陣。好比處理Cifar-10數據集中的圖片時,能夠將輸入層組織成一個32*32*3的三維矩陣。上圖的虛線部分展現了卷積神經網絡的一個鏈接示意圖,從圖中能夠看出卷積神經網絡中前幾層中每個節點只和上一層中部分的節點相連。網絡

  下面給出一個卷積神經網絡主要由如下五種結構組成:

  1,輸入層。輸入層是整個神經網絡的輸入,在處理圖像的卷積神經網絡中,它通常表明了一張圖片的像素矩陣。好比在上圖中最左側的三維矩陣就是能夠表明一張圖片。其中三維矩陣的長和寬表明了圖像的大小,而三維矩陣的深度表明了圖像的色彩通道(channel)。好比黑白圖片的深度爲1,而在RGB色彩模式下,圖像的深度爲3。從輸入層開始,卷積神經網絡經過不一樣的神經網絡結構將上一層的三維矩陣轉化爲下一層的三維矩陣,直到最後的全鏈接層。

  2,卷積層。從名字就能夠看出,卷積層是一個卷積神經網絡中最爲重要的部分,和傳統全鏈接層不一樣,卷積層是一個卷積神經網絡中最爲重要的部分,和傳統全鏈接層不一樣,卷積層中每個節點的輸入只是上一層神經網絡的一小塊,這個小塊經常使用的大小有3*3或者5*5.卷積層試圖將神經網絡中的每一小塊進行更加深刻的分析從而獲得抽象程度更高的特徵。通常來講,經過卷積層處理過的節點矩陣會變得更深,因此上圖能夠看到通過卷積層以後的節點矩陣的深度會增長。

  3,池化層(Pooling)。池化層神經網絡不會改變三維矩陣的深度,可是它能夠縮小矩陣的大小。池化操做能夠認爲是將一張分辨率較高的圖片轉化爲分辨率較低的圖片。經過池化層,能夠進一步縮小最後全鏈接層中節點的個數,從而達到減小整個神經網絡中參數的目的。

  4,全鏈接層,通過多輪卷積層和池化層的處理以後,在卷積神經網絡的最後通常會是由1到2個全鏈接層來給出最後的分類結果。通過幾輪卷積層和池化層的處理以後,能夠認爲圖像中的信息以及抽象成了信息含量更高的特徵。咱們能夠將卷積層和池化層看出自動圖像特徵提取的過程。在特徵提取完成以後,讓然須要使用全鏈接層來完成分類任務。

  5,Softmax層,Softmax層主要用於分類問題,經過Softmax層,能夠獲得當前樣例屬於不一樣種類的機率分佈狀況。

卷積神經網絡經常使用結構——卷積層

  本小節將詳細介紹卷積層的結構以及前向傳播的算法,下圖顯示了卷積層神經網絡結構中最爲重要的部分,這個部分被稱之爲過濾器(filter)或者內核(kernel)。由於TensorFlow文檔中將這個結構稱爲過濾器(filter),因此咱們本文就稱爲過濾器。如圖所示,過濾器能夠將當前層神經網絡的一個子節點矩陣轉化爲下一層神經網絡上的一個單位節點矩陣。單位節點矩陣指的是一個長和寬都是1,可是深度不限的節點矩陣。

  在一個卷積層中,過濾器所處理的節點矩陣的長和寬都是由人工指定的,這個節點矩陣的尺寸也被稱之爲過濾器的尺寸。經常使用的過濾器尺寸有3*3或者5*5。由於過濾器處理的矩陣深度和當前層神經網絡節點矩陣的深度是一致的,因此雖然節點矩陣是三維的,但過濾器的尺寸只須要指定兩個維度。過濾器中另一個須要人工指定的設置是處理獲得的單位節點矩陣的深度,這個設置稱爲過濾器的深度。注意過濾器的尺寸指的是一個過濾器輸入節點矩陣的大小。而深度指的就是輸出單位節點矩陣的深度。如圖所示,左側小矩陣的尺寸爲過濾器的尺寸,而右側單位矩陣的深度爲過濾器的深度。

  如圖所示,過濾器的前向傳播過程就是經過左側小矩陣中的節點計算出右側單位矩陣中的節點的過程。爲了直觀的解釋過濾器的前向傳播過程。下面給出一個樣例,在這個樣例中將展現如何經過過濾器將一個2*2*3的節點矩陣轉化爲一個1*1*5的單位節點矩陣,一個過濾器的前向傳播過程和全鏈接層類似,它總共須要 2*2*3*5+5=65個參數,其中最後的+5爲偏置項參數的個數,假設使用 來表示對於輸出單位節點矩陣中的第 i 個節點,過濾器輸入節點 (x, y ,z)的權重,使用 bi   表示第 i 個輸出節點對應的偏置項參數,那麼單位矩陣中的第 i 個節點的取值爲 g(i) 爲:

  其中,爲過濾器中節點(x, y ,z)的取值,f 爲激活函數,下圖展現了在給定 a , w0 和 b0 的狀況下,使用ReLU做爲激活函數時 g(0) 的計算過程。在圖中給出了 a 和 w0 的取值,這裏經過三個二維矩陣來表示一個三維矩陣的取值,其中每個二維矩陣表示三維矩陣中在某一個深度上的取值。圖中 • 符號表示點積,也就是矩陣中對應元素乘積的和,下圖右側顯示了 g(0) 的計算過程,若是給出 w1到 w4 和 b1 到 b4 ,那麼也能夠相似地計算出 g(1)到 g(4) 的取值。若是將 a 和 wi 組織成兩個向量,那麼一個過濾器的計算過程徹底能夠通向量乘積完成。

  上面的樣例已經學習了在卷積層中計算一個過濾器的前向傳播過程。卷積層結構的前向傳播就是經過將一個過濾器從神經網絡當前層的左上角移動到右下角,而且在移動中計算每個對應的單位矩陣獲得的。下圖展現了卷積層結構前向傳播的過程。爲了更好的可視化過濾器的移動過程,圖中使用的節點矩陣深度都是1。在圖中展現了在3*3矩陣上使用2*2過濾器的卷積前向傳播過程,在這個過程當中,首先將這個過濾器用於左上角子矩陣,而後移動到左下角矩陣,再到右上角矩陣,最後到右下角矩陣。過濾器每移動一次,能夠計算獲得一個值(當深度爲 k 時會計算出 k 個值)。將這些數值拼成一個新的矩陣,就完成了卷積層前向傳播的過程。圖中右側顯示了過濾器在移動過程當中計算獲得的結果與新矩陣中節點的對應關係。

  當過濾器的大小不爲1*1時,卷積層前向傳播獲得的矩陣的尺寸要小於當前層矩陣的尺寸。如上圖所示,當前層矩陣的大小爲3*3,而經過卷積層前向傳播算法以後,獲得的矩陣大小爲2*2。爲了不尺寸的變化,能夠在當前層矩陣的邊界上加入全0填充(zero-padding)。這樣可使得卷積層前向傳播結果矩陣的大小和當前層矩陣保持一致。

  下圖顯示了使用全0填充後卷積層前向傳播過程示意圖,從圖中能夠看出,加入一層全0填充後,獲得的結構矩陣大小就爲3*3了。

  除了使用全0填充,還能夠經過設置過濾器移動的步長來調整結果矩陣的大小。在上圖中,過濾器每次都只移動一格,下圖顯示了當移動步長爲2且使用全0填充時,卷積層前向傳播的過程。

  從上圖能夠看出,當長和寬的步長均爲2時,過濾器每隔2步計算一次結果,因此獲得的結果矩陣的長和寬也就只有原來的一半。下面的公式給出了在同時使用全0填充時結果矩陣的大小:

  其中outheight 表示輸出層矩陣的長度,它等於輸入層矩陣長度除以長度方向上的步長的向上取整值。相似的,outheight 表示輸出層矩陣的寬度,它等於輸入層矩陣寬度除以寬度方向上的步長的向上取整值。若是不使用全0填充,下面的公式給出告終果矩陣的大小:

  在上面,只有移動過濾器的方式,沒有涉及到過濾器中的參數如何設定,因此在這些圖片中結果矩陣中並無填上具體的值。在卷積神經網絡中,每個卷積層中使用的過濾器中的參數都是同樣的。這是卷積神經網絡一個很是重要的性質。從直觀上當即額,共享過濾器的參數可使得圖像上的內容不受位置的影響。以MNIST手寫體數字識別爲例,不管數字「1」出如今左上角仍是右下角,圖片的種類都是不變的。由於在左上角和右下角使用的過濾器參數相同,因此經過卷積層以後不管數字在圖像上的那個位置,獲得的結果都同樣。

  共享每個卷積層中過濾器中的參數能夠巨幅減小神經網絡上的參數。以Cifar-10問題爲例,輸入層矩陣的維度是32*32*3.假設第一層卷積使用尺寸爲5*5,深度爲16的過濾器,那麼這個卷積層的參數個數爲5*5*3*16+16=1216 個。上面提到過,使用500個隱藏層節點的全鏈接層將有1.5百萬個參數。相比之下,卷積層的參數個數要遠遠小於全鏈接層。並且卷積層的參數個數和圖片的大小無關,它之和過濾器的尺寸,深度以及前檔層節點矩陣的深度有關。這使得卷積神經網絡能夠很好的擴展到更大的圖像數據上。

  結合過濾器的使用方法和參數共享機制,下圖給出了使用全0填充,步長爲2的卷積層前向傳播的計算流程。

  下圖給出了過濾器上權重以及偏置項的取值,經過圖中所示的計算方法,能夠獲得每個格子的具體取值。下面公式給出了左上角格子取值的計算方法,其餘格子能夠依次類推。

  TensorFlow對卷積神經網絡提供了很是好的支持,下面程序實現了一個卷積層的前向傳播過程,從下面代碼能夠看出,經過TensorFlow實現卷積層是很是方便的。

#_*_coding:utf-8_*_
import tensorflow as tf

# 經過 tf.get_variable 的方式建立過濾器的權重變量和偏置項變量
# 卷積層的參數個數只和過濾器的尺寸,深度以及當前層節點矩陣的深度有關
# 因此這裏聲明的參數變量是一個四位矩陣,前面兩個維度表明了過濾器的尺寸
# 第三個維度表示當前層的深度,第四個維度表示過濾器的深度
filter_weight = tf.get_variable(
    'weights', [5, 5, 3, 16],
    initializer=tf.truncated_normal_initializer(stddev=0.1)
)
# 和卷積層的權重相似,當前層矩陣上不一樣位置的偏置項也是共享的,因此總共有下一層深度個不一樣的偏置項
# 下面樣例中16爲過濾器的深度,也是神經網絡中下一層節點矩陣的深度
biases = tf.get_variable(
    'biases', [16],
    initializer=tf.truncated_normal_initializer(0.1)
)

# tf.nn.conv2d 提供了一個很是方便的函數來實現卷積層前向傳播的算法
# 這個函數的第一個輸入爲當前層的節點矩陣。注意這個矩陣是一個思惟矩陣
# 後面三個維度對應一個節點矩陣,第一個對應一個輸入batch
# 好比在輸入層,input[0, :, :, :]表示第一張圖片 input[1. :, :, :]表示第二章圖片
# tf.nn.conv2d 第二個參數提供了卷積層的權重,第三個參數爲不一樣維度上的步長
# 雖然第三個參數提供的是一個長度爲4的數組,可是第一維和最後一維的數字要求必定是1
# 這是由於卷積層的步長只對矩陣的長和寬有效,最後一個參數是填充(padding)的方法
# TensorFlow中提供SAME 或者VALID 兩種選擇,SAME 表示填充全0 VALID表示不添加
conv = tf.nn.conv2d(
    input, filter_weight, strides=[1, 1, 1, 1], padding='SAME'
)

# tf.nn.bias_add 提供了一個方便的函數給每個節點加上偏置項
# 注意這裏不能直接使用加法,由於矩陣上不一樣位置上的節點都須要加上一樣的偏置項
# 雖然下一層神經網絡的大小爲2*2,可是偏置項只有一個數(由於深度爲1)
# 而2*2矩陣中的每個值都須要加上這個偏置項
bias = tf.nn.bias_add(conv, biases)
# 將計算結果經過ReLU激活函數完成去線性化
actived_conv = tf.nn.relu(bias)

  

卷積神經網絡經常使用結構——池化層

   池化層能夠很是有效的縮小矩陣的尺寸,從而減小最後全鏈接層的參數,使用池化層既能夠加快計算速度也有效防止過擬合問題的做用。

  池化層前向傳播的過程也是經過移動一個相似過濾器的結構完成的,不過池化層過濾器的計算不是節點的加權和,而是採用更加簡單的最大值或者平均值計算。使用最大值操做的池化層被稱爲最大池化層(max  pooling),這是被使用得最多的池化層結構。使用平均值操做的池化層被稱之爲平均池化層(average pooling)。其餘池化層在實踐中使用比較少。

  與卷積層的過濾器相似,池化層的過濾器也須要人工設定過濾器的尺寸,是否使用全0填充以及過濾器移動的步長等設置,並且這些設置的意義也是同樣的。卷積層和池化層中過濾器移動的方式是類似的,惟一的區別在於卷積層使用的過濾器是橫跨整個深度的,而池化層使用的過濾器隻影響一個深度上的節點。因此池化層的過濾器除了在長和寬兩個維度移動以外,它還須要在深度這個維度移動。下圖展現了一個最大池化層前向傳播計算過程。

  在上圖中,不一樣顏色或者不一樣線段(虛線或者實線)表明了不一樣的池化層過濾器。從圖中能夠看出,池化層的過濾器除了在長和寬的維度上移動,它還須要在深度的維度上移動。下面TensorFlow程序實現了最大池化層的前向傳播算法。

# tf.nn.max_pool 實現了最大池化層的前向傳播過程,他的參數和 tf.nn.conv2d函數相似
#ksize 提供了過濾器的尺寸,strides提供了步長信息,padding提供了是否使用全0填充
pool = tf.nn.max_pool(actived_conv, ksize=[1, 3, 3, 1],
                      strides=[1, 2, 2, 1], padding='SAME')

  對比池化層和卷積層前向傳播在TensorFlow中的實現,能夠發現函數的參數形式是類似的。在tf.nn.max_pool 函數中,首先須要傳入當前層的節點矩陣,這個矩陣是一個四維矩陣,格式和 tf.nn.conv2d 函數的第一個參數一致。第二個參數爲過濾器的尺寸,雖然給出的是一個長度爲4的一維數組,可是這個數組的第一個和最後一個數必須爲1。這意味着池化層的過濾器是不能夠跨不一樣輸入樣例或者節點矩陣深度的。在實際應用中使用的最多的池化層過濾器尺寸爲 [1, 2, 2, 1] 或者 [1, 3, 3, 1]。

  tf.nn.max_pool 函數的第三個參數爲步長,它和 tf.nn.conv2d 函數中步長的意義是同樣的,並且第一維和最後一維也只能爲1。這意味着在TensorFlow中,池化層不能減小節點矩陣的深度或者輸入樣例的個數。tf.nn.max_pool函數的最後一個參數指定了是否使用全0填充。這個參數也只能有兩種取值——VALID或者SAME。其中VALID表示不使用全0填充,SAME表示使用全0填充。TensorFlow還提供了 tf.nn.avg_pool來實現平均池化層。其調用格式和以前介紹的同樣。

經典卷積網絡模型——LeNet-5模型

  下面學習LeNet模型,並給出一個完整的TensorFlow程序來實現LeNet-5模型,經過這個模型,將給出卷積神經網絡結構設計的一個通用模式,而後再學習設計卷積神經網絡結構的另一種思路——Inception模型。

LeNet網絡背景

  LeNet誕生於1994年,由深度學習三巨頭之一的Yan LeCun提出,他也被稱爲卷積神經網絡之父。LeNet主要用來進行手寫字符的識別與分類,準確率達到了98%,並在美國的銀行中投入了使用,被用於讀取北美約10%的支票。LeNet奠基了現代卷積神經網絡的基礎。它是第一個成功應用於數字識別問題的卷積神經網絡。在MNIST數據集上,LeNet-5模型能夠達到大約99.2%的正確率。LeNet-5模型總共有7層,下圖展現了LeNet-5模型架構:

LeNet網絡結構

  第一層:卷積層

   這一層的輸入就是原始的圖像像素,LeNet-5 模型接受的輸入層大小爲32*32*1。第一層卷積層過濾器的尺寸爲5*5,深度爲6,不使用全0填充,步長爲1。由於沒有使用全0填充,因此這一層的輸出的尺寸爲32-5+1=28,深度爲6。這一個卷積層總共有5*5*1*6+6=156 個參數,其中6個未偏置項參數,由於下一層的節點矩陣有28*28*6=4704 個節點,每一個節點和 5*5=25 個當前層節點相連,因此本層卷積層共有 (5*5*1)*6*(28*28)=122304 個鏈接。

  第二層:池化層

  這一層的輸入爲第一層的輸出,是一個28*28*6 的節點矩陣。本層採用的過濾器大小爲2*2,長和寬的步長均爲2,因此本層的輸出矩陣大小爲14*14*6。

  第三層:卷積層

  本層的輸入矩陣大小爲14*14*6,使用的過濾器大小爲5*5,深度爲16。本層不使用全0填充,步長爲1。本層的輸出矩陣大小爲10*10*16 。按照標準的卷積層,本層應該有5*5*6*16+16=2416 個參數,10*10*16*(25+1)=41600 個鏈接。

  第四層:池化層

  本層的輸入矩陣大小爲10*10*16,採用的過濾器大小爲2*2,步長爲2,本層的輸出矩陣大小爲5*5*16。

  第五層:全鏈接層

  本層的輸入矩陣大小爲5*5*16,在LeNet-5 模型的論文中將這一層稱爲卷積層,可是由於過濾器的大小就是5*5 , 因此和全鏈接層沒有區別,在以後的TensorFlow程序實現中也會將這一層當作全鏈接層。若是將5*5*16 矩陣中的節點拉成一個向量,那麼這一層和以前學習的全鏈接層就同樣的了。本層的輸出節點個數爲120個,總共有5*5*16*120+120=48120 個參數。

  第六層:全鏈接層

  本層的輸入節點個數爲120個,輸出節點個數爲84個,總共參數爲120*84+84=10164 個。

  第七層:全鏈接層

  本層的輸入節點個數爲84個,輸出節點個數爲10個,總共參數爲84*10+10=850個。

   上面介紹了LeNet-5模型每一層結構和設置,下面給出TensorFlow的程序來實現一個相似LeNet-5 模型的卷積神經網絡來解決MNIST數字識別問題。經過TensorFlow訓練卷積神經網絡的過程和以前學習的是同樣的。損失函數和反向傳播過程的實現都可以複用其代碼。惟一的區別就是卷積神經網絡的輸入層是一個三維矩陣,因此須要調整一下輸入數據的格式。

下面看一下代碼。

  mnist_train.py

#_*_coding:utf-8_*_
import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 加載 bookmnist_inference.py 中定義的常量和前向傳播的函數
import bookmnist_inferencecnn as bookmnist_inference
import numpy as np

# 配置神經網絡的參數
BATCH_SIZE = 100
# 基礎的學習率,使用指數衰減設置學習率
LEARNING_RATE_BASE = 0.01  # 0.8
# 學習率的初始衰減率
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
# 訓練輪數
TRAINING_STEPS = 30000
# 滑動平均衰減值
MOVING_AVERAGE_DECAY = 0.99
# 模型保存的路徑和文件名
MODEL_SAVE_PATH = 'model_cnn1/'
if not os.path.exists(MODEL_SAVE_PATH):
    os.mkdir(MODEL_SAVE_PATH)
MODEL_NAME = 'model.ckpt'



def train(mnist):
    # 調整輸入數據placeholder的格式,輸入爲一個四維矩陣
    x = tf.placeholder(tf.float32, [
        BATCH_SIZE,  # 第一維表示一個batch中樣例的個數
        bookmnist_inference.IMAGE_SIZE,  # 第二維和第三維表示圖片的尺寸
        bookmnist_inference.IMAGE_SIZE,
        bookmnist_inference.NUM_CHANNELS  # 第四維表示圖片的深度,對於RGB格式的圖片,深度爲5
    ], name='x-input')

    y_ = tf.placeholder(
        tf.float32, [None, bookmnist_inference.OUTPUT_NODE], name='y-input'
    )

    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    # 直接使用bookmnost_inference.py中定義的前向傳播過程
    y = bookmnist_inference.inference(x, False,  regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 定義損失函數,學習率,滑動平均操做以及訓練過程
    variable_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY, global_step
    )
    variable_averages_op = variable_averages.apply(
        tf.trainable_variables()
    )
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits=y, labels=tf.argmax(y_, 1)
    )
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,
        global_step,
        mnist.train.num_examples / BATCH_SIZE,
        LEARNING_RATE_DECAY
    )
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(
        loss, global_step=global_step
    )
    with tf.control_dependencies([train_step, variable_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化類
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()

        # 在訓練過程當中再也不測試模型在驗證數據上的表現
        # 驗證和測試的過程都將會有一個獨立的程序完成
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            # 相似的將輸入的訓練數據格式調整爲一個四維矩陣,並將這個調整後的數據傳入 sess.run 過程
            reshaped_xs = np.reshape(xs, (BATCH_SIZE,
                                          bookmnist_inference.IMAGE_SIZE,
                                          bookmnist_inference.IMAGE_SIZE,
                                          bookmnist_inference.NUM_CHANNELS
                                          ))

            _, loss_value, step = sess.run([train_op, loss, global_step],
                                           feed_dict={x: reshaped_xs, y_: ys})
            # 每1000輪保存一次模型
            if i % 1000 == 0:
                # 輸出當前的訓練狀況,這裏只輸出了模型在當前訓練batch上的損失函數大小
                # 經過損失函數的大小能夠大概瞭解訓練的狀況,在驗證集上正確率信息會有一個單獨的程序來生成
                print("Afer %d training step(s), loss on training batch is %g"%(step, loss_value))

                # 保存當前模型,注意這裏給出的global_step參數,這樣可讓每一個被保存模型的文件名末尾加上訓練點額輪數
                saver.save(
                    sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME),
                    global_step=global_step
                )

def main(argv=None):
    mnist = input_data.read_data_sets('data', one_hot=True)
    train(mnist)


if __name__ == '__main__':
    main()

  獲得的輸出以下:

Extracting data\train-images-idx3-ubyte.gz
Extracting data\train-labels-idx1-ubyte.gz
Extracting data\t10k-images-idx3-ubyte.gz
Extracting data\t10k-labels-idx1-ubyte.gz
Afer 1 training step(s), loss on training batch is 4.79052
Afer 1001 training step(s), loss on training batch is 0.710321
Afer 2001 training step(s), loss on training batch is 0.697147
Afer 3001 training step(s), loss on training batch is 0.701041
Afer 4001 training step(s), loss on training batch is 0.633242
Afer 5001 training step(s), loss on training batch is 0.638359
Afer 6001 training step(s), loss on training batch is 0.63794
Afer 7001 training step(s), loss on training batch is 0.663004
... ...
Afer 29001 training step(s), loss on training batch is 0.631867

  

 mnist_inference.py的代碼:

#_*_coding:utf-8_*_
import tensorflow as tf

# 定義神經網絡結構相關參數
INPUT_NODE = 784         # 28*28=784
OUTPUT_NODE = 10

IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10

# 第一層卷積層的尺寸和深度
CONV1_DEEP = 32
CONV1_SIZE = 5

# 第二層卷積層的尺寸和深度
CONV2_DEEP = 64
CONV2_SIZE = 5

# 全鏈接層的節點個數
FC_SIZE = 512


# 定義神經網絡的前向傳播過程 這裏添加了一個新的參數 train 用於區分訓練過程和測試過程
# 在這個程序中將用到 droput方法,dropout能夠進一步提高模型可靠性並防止過擬合
# droput過程只在訓練時使用
def inference(input_tensor, train, regularizer):
    # 聲明第一層卷積層的變量並實現前向傳播過程
    # 經過使用不一樣的命名空間來隔離不一樣層的變量,這可讓每一層中的變量命名
    # 只須要考慮在當前層的做用,而不須要擔憂重名的問題
    # 和標準的LeNet-5模型不大同樣,這裏定義的卷積層輸入爲28*28*1的原始MNIST圖片像素
    # 由於卷積層中使用了全0填充,因此輸出爲28*28*32的矩陣
    with tf.variable_scope('layer1-conv1'):
        conv1_weights = tf.get_variable(
            'weight', [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
            initializer=tf.truncated_normal_initializer(stddev=0.1)
        )
        conv1_biases = tf.get_variable(
            'bias', [CONV1_DEEP],
            initializer=tf.constant_initializer(0.0)
        )
        # 使用邊長爲5,深度爲32的過濾器,過濾器移動的步長爲1,且使用全0填充
        conv1 = tf.nn.conv2d(
            input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME'
        )
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

    # 實現第二層池化層的前向傳播過程,這裏選用最大池化層,池化層過濾器的邊長爲2
    # 使用全0填充且移動的步長爲2,這一層的輸入時上一層的輸出,也就是28*28*32的矩陣
    # 輸出爲14*14*32的矩陣
    with tf.name_scope('layer2-pool1'):
        pool1 = tf.nn.max_pool(
            relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME'
        )

    # 聲明第三層卷積層的變量並實現前向傳播過程,這一層的輸入爲14*14*32 的矩陣
    # 輸出爲14*14*64
    with tf.variable_scope('layer3-conv2'):
        conv2_weights = tf.get_variable(
            'weight', [CONV2_SIZE, CONV2_SIZE, CONV1_DEEP, CONV2_DEEP],
            initializer=tf.truncated_normal_initializer(stddev=0.1)
        )
        conv2_biases = tf.get_variable(
            'bias', [CONV2_DEEP],
            initializer=tf.constant_initializer(0.0)
        )

        # 使用邊長爲5, 深度爲64的過濾器,過濾器移動的步長爲1,且使用全0填充
        conv2 = tf.nn.conv2d(
            pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME'
        )
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

    # 實現第四層池化層的前向傳播過程,這一層和第二層的結構是同樣的,
    # 這一層的輸入爲14*14*64 的矩陣,輸出爲7*7*64 的矩陣
    with tf.name_scope('layer4-pool2'):
        pool2 = tf.nn.max_pool(
            relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME'
        )

    # 將第四層池化層的輸出轉化爲第五層全鏈接層的輸入格式,第四層的輸出爲7*7*64 的矩陣
    # 然而第五層全鏈接層須要的輸入格式爲向量,因此在這裏須要將這個7*7*64 的矩陣拉直成一個向量
    # pool2.get_shape 函數能夠獲得第四層輸出矩陣的維度而不須要手工計算
    # 注意由於每一層神經網絡的輸出輸入都是一個 batch的矩陣,
    # 因此這裏獲得的維度也包含了一個batch中的數據的個數
    pool_shape = pool2.get_shape().as_list()
    # 計算將矩陣拉直成項鍊以後的長度,這個長度就是矩陣長寬及深度的乘積
    # 注意這裏 pool_shape[0] 爲一個batch中數據的個數
    nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]

    # 經過 tf.reshape 函數將第四層的輸出變成一個 batch 項鍊
    reshaped = tf.reshape(pool2, [pool_shape[0], nodes])

    # 聲明第五層全鏈接層的變量並實現前向傳播過程,這一層的輸入時拉直以後的一組向量
    # 向量長度爲3136,輸出是一組長度爲512 的向量
    # 這裏引入了dropout的概念,dropout在訓練時會隨機將部分節點的輸出改成0
    # dropout 能夠避免過擬合問題,從而使得模型在測試數據上的效果更好
    # dropout 通常只在全鏈接層而不是卷積層或者池化層使用
    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable(
            'weight', [nodes, FC_SIZE],
            initializer=tf.truncated_normal_initializer(stddev=0.1)
        )
        # 只有全鏈接層的權重須要加入正則化
        if regularizer != None:
            tf.add_to_collection('losses', regularizer(fc1_weights))
        fc1_biases = tf.get_variable(
            'bias', [FC_SIZE], initializer=tf.constant_initializer(0.1)
        )
        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        if train:
            fc1 = tf.nn.dropout(fc1, 0.5)

    # 聲明第六層全鏈接層的變量並實現前向傳播過程,這一層的輸入爲一組長度爲512的向量
    # 輸出爲一組長度爲10的向量,這一層的輸出經過softmax以後就獲得了最後的分類結果
    with tf.variable_scope('layer6-fc2'):
        fc2_weights = tf.get_variable(
            'weight', [FC_SIZE, NUM_LABELS],
            initializer=tf.truncated_normal_initializer(stddev=0.1)
        )
        if regularizer != None:
            tf.add_to_collection('losses', regularizer(fc2_weights))
        fc2_biases = tf.get_variable(
            'bias', [NUM_LABELS],
            initializer=tf.constant_initializer(0.1)
        )
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases

    # 返回第六層的輸出
    return logit

  mnist_eval.py

#_*_coding:utf-8_*_
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 加載 mnist_inference.py 和 mnist_train.py中定義的常量和函數
import bookmnist_inferencecnn as bookmnist_inference
import bookmnist_traincnn as bookmnist_train
import numpy as np

# 每10秒加載一次最新的模型,並在測試數據上測試最新模型的正確率
EVAL_INTERVAL_SECS = 10

def evalute(mnist):
    with tf.Graph().as_default() as g:
        # 定義輸入輸出格式,調整輸入數據的格式,輸入爲一個四維矩陣
        x = tf.placeholder(
            tf.float32, [
                bookmnist_train.BATCH_SIZE,                      # 第一維表示一個batch中樣例的個數
                bookmnist_inference.IMAGE_SIZE,  # 第二維和第三維表示圖片的尺寸
                bookmnist_inference.IMAGE_SIZE,
                bookmnist_inference.NUM_CHANNELS  # 第四維表示圖片的深度,對於RGB格式的圖片,深度爲5
            ], name='x-input'
        )
        y_ = tf.placeholder(
            tf.float32, [None, bookmnist_inference.OUTPUT_NODE], name='y-input'
        )

        xs, ys = mnist.test.next_batch(bookmnist_train.BATCH_SIZE)
        reshape_xs = np.reshape(xs, (bookmnist_train.BATCH_SIZE,
                                     bookmnist_inference.IMAGE_SIZE,
                                     bookmnist_inference.IMAGE_SIZE,
                                     bookmnist_inference.NUM_CHANNELS
                                     ))
        validate_feed = {x: reshape_xs, y_: ys}

        # 直接經過調用封裝好的函數來計算前向傳播的額結果
        #由於測試時不關注正則化損失的值,因此這裏用於計算正則化損失函數被設置爲None
        y = bookmnist_inference.inference(x, False, None)

        # 使用前向傳播的結果計算正確率,若是須要對未知的樣本進行分類,
        # 那麼使用 tf.argmax(y, 1)就能夠獲得輸入樣例的預測類別了
        # 判斷兩個張量的每一維是否相等,若是相等就返回True,不然返回False
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

        # 經過變量重命名的方式來加載模型,這樣在前向傳播的過程當中就不須要調用求滑動平均的函數獲取平均值
        # 這樣就能夠徹底共享以前mnist_inference.py中定義的前向傳播過程
        variable_averages = tf.train.ExponentialMovingAverage(
            bookmnist_train.MOVING_AVERAGE_DECAY
        )
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)

        # 每隔EVAL_INTERVAL_SECS 秒調用一次計算正確率的過程以檢測訓練過程當中正確率的變化
        while True:
            with tf.Session() as sess:
                # tf.train.get_checkpoint_state函數會經過checkpoint文件
                # 自動找到目錄中最新模型的文件名
                ckpt = tf.train.get_checkpoint_state(
                    bookmnist_train.MODEL_SAVE_PATH
                )
                if ckpt and ckpt.model_checkpoint_path:
                    # 加載模型
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    # 經過文件名獲得模型保存時迭代的輪數
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
                    accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                    print("After %s training step(s) , validation accuracy ='%g"%(global_step, accuracy_score))
                else:
                    print("No checkpoint file found")
                    return
                time.sleep(EVAL_INTERVAL_SECS)

def main(argv=None):
    mnist = input_data.read_data_sets('data', one_hot=True)
    evalute(mnist)


if __name__ == '__main__':
    main()

  運行測試代碼,獲得的結果以下:

Extracting data\train-images-idx3-ubyte.gz
Extracting data\train-labels-idx1-ubyte.gz
Extracting data\t10k-images-idx3-ubyte.gz
Extracting data\t10k-labels-idx1-ubyte.gz
After 29001 training step(s) , validation accuracy ='1
After 29001 training step(s) , validation accuracy ='1
After 29001 training step(s) , validation accuracy ='1

  

  在MNIST測試數據集上,上面的卷積神經網絡能夠達到大約   100% 的正確率,。相比較全鏈接層的98.4%的正確率,卷積神經網絡能夠巨幅提升神經網絡在MNIST數據集上的正確率。

LeNet-5 總結

  • LeNet-5是一種用於手寫體字符識別的很是高效的卷積神經網絡。
  • 卷積神經網絡可以很好的利用圖像的結構信息。
  • 卷積層的參數較少,這也是由卷積層的主要特性即局部鏈接和共享權重所決定。
  • 然而,LeNet模型就沒法處理ImageNet這樣比較大的圖像數據集

 如何設計卷積神經網絡的架構?

  下面的正則表達式總結了一些經典的用於圖片分類問題的卷積神經網絡架構:

輸入層   ——>  (卷積層 +   ——>  池化層 ?) +  ——>  全鏈接層  +

  在上面的公式中,「卷積層  + 」 表示一層或者多層卷積層,大部分卷積神經網絡中通常最多連續使用三層卷積層。 「池化層 ? 」 表示沒有或者一層池化層。池化層雖然能夠起到減小參數防止過擬合問題,可是在部分論文中能夠直接經過調整卷積層步長來完成。因此有些卷積神經網絡中沒有池化層。在多輪卷積層和池化層以後,卷積神經網絡在輸出以前通常會通過1-2個全鏈接層。好比LeNet

 Inception-v3模型

  上面學習了LeNet-5模型。這裏學習inception結構以及 Inception-v3卷積神經網絡模型。Inception結構是一種和LeNet-5結構徹底不一樣的額卷積神經網絡結構,在LeNet-5模型中,不一樣卷積層經過串聯的方式鏈接在一塊兒,而Inception-v3模型中的Inception結構是將不一樣的卷積層經過並聯的方式結合在一塊兒,下面學習inception結構,並經過Tensorflow-Slim工具來實現Inception-v3模型中的一個模塊。

  以前提到了一個卷積層可使用邊長爲1,3或者5 的過濾器,那麼如何在這些邊長中選呢?Inception模塊給出了一個方案,那就是同時使用全部不一樣尺寸的過濾器,而後再將獲得的矩陣拼接起來。下圖給出了inception模塊的一個單元結構示意圖:

   從圖中能夠看出,Inception模塊首先使用不一樣尺寸的過濾器處理輸入矩陣,在圖中,最上方舉證使用了邊長爲1的過濾器的卷積層前向傳播的結果。相似的,中間矩陣使用的過濾器邊長爲1,下方矩陣使用的過濾器邊長爲5,不一樣的矩陣表明了Inception模塊中的一條計算路徑。雖然過濾器的大小不一樣,但若是全部的過濾器都使用全0填充且步長爲1,那麼前向傳播獲得的結果矩陣的長和寬都與輸入矩陣一致。這樣通過不一樣過濾器處理的結果矩陣能夠拼接成一個更深的矩陣。如上圖,能夠將他們在深度這個維度上組合起來。

  上圖所示的Inception模塊獲得的結果矩陣的長和寬與輸入同樣,深度爲紅黃藍三個矩陣深度的和。上圖展現的是Inception模塊的核心思想,真正在 Inception-v3模型中使用的Inception模塊要更加複雜且多樣。

  下圖給出Inception-3模型的架構圖:

 

  Inception-3模型總共有46層,由11個inception模塊組成。上圖標誌出來的結構就是一個Inception模塊,在Inception-3模型中有86個卷積層,若是將以前的程序搬過來,那麼一個卷積就須要五行代碼,因而總共須要480行代碼來實現全部的卷積層,這樣使得代碼的可讀性很是低。爲了更好地實現相似Inception-3模塊這樣的複雜卷積神經網絡,在下面將先學習TensorFlow-Slim 工具來更加簡潔的實現一個卷積層,如下代碼對比了直接使用TensorFlow實現一個卷積層和使用TensorFlow-Slim實現一樣結構的神經網絡的代碼量。

# 直接使用TensorFlow原始API實現卷積層
with tf.variable_scope(scope_name):
    weights = tf.get_variable("weights", ...)
    biases = tf.get_variable('bias', ...)
    conv = tf.nn.conv2d(...)
relu = tf.nn.relu(tf.nn.bias_add(conv, biases))

# 使用TensorFlow-Slim實現卷積層,經過TensorFlow-Slim能夠在一行中實現一個卷積層的前向傳播算法
# slim.conv2d 函數的有三個參數是必填的。第一個參數爲輸入節點矩陣
# 第二個參數是當前卷積層過濾器的深度,第三個參數是過濾器的尺寸
# 可選的參數有過濾器移動的步長,是否使用全0 填充,激活函數的選擇以及變量的命名空間
net = slim.conv2d(input, 32, [3, 3])

  由於完整的Inception-v3 模型比較長,因此下面僅僅實現了一個Inception-v3模型中結構相對複雜的一個inception模塊的代碼實現:

#_*_coding:utf-8_*_
import tensorflow as tf

# slim.arg_scope 函數能夠用於設置默認的參數取值
# 此函數第一個參數是一個函數列表,在這個列表中的函數將使用默認的參數取值
# 好比下面定義,調用 slim.conv2d(net, 320, [1, 1]) 函數會自動加上stride=1 和padding='SAME'參數
# 若是在函數調用時指定了stride。那麼這裏設置的默認值就不會再使用。經過這種方式能夠減小冗餘代碼


with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
                    stride=1, padding='SAME'):
    ...
    # 此處省略了inception-v3模型中其餘的網絡結構而直接實現最後紅框的inception結構
    # 假設輸入圖片通過以前的神經網絡前向傳播的結果保存在變量net中
    net = 上一層的輸出節點矩陣
    # 爲一個inception模塊聲明一個統一的變量命名空間
    with tf.variable_scope('Mixed_7c'):
        # 給inception 模塊中每一條路徑聲明一個命名空間
        with tf.variable_scope('Branch_0'):
            # 實現一個過濾器邊長爲1,深度爲320的卷積層
            branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1')
        # Inception 模塊中第二條路徑,這條計算路徑上的結構自己也是一個Inception結構
        with tf.variable_scope('Branch_1'):
            branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1')
            # tf.concat 函數能夠將多個矩陣拼接起來。tf.concat函數的第一個參數指定了拼接的維度
            # 這裏的3表示矩陣是在深度這個維度上及很小拼接
            branch_1 = tf.concat(3, [
                # 此處2層卷積層的輸入都是 branch_1 而不是 net
                slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
                slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')
            ])

        # Inception 模塊中第三條路徑 ,此計算路徑也是一個inception結構
        with tf.variable_scope('Branch_2'):
            branch_2 = slim.conv2d(
                net, 448, [1, 1], scope='Conv2d_0a_1x1')
            branch_2 = slim.conv2d(
                branch_2, 384, [3, 3], scope='Conv2d_0b_3x3')
            branch_2 = tf.concat(3, [
                slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'),
                slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')
            ])
        
        # Inception模塊中的第四條路徑
        with tf.variable_scope("Branch_3"):
            branch_3 = slim.avg_pool2d(
                net, [3, 3], scope='AvgPool_0a_3x3'
            )
            branch_3 = slim.avg_pool2d(
                branch_3, 192, [1, 1], scope='Conv2d_0b_1x1'
            )
        
        # 當前Inception 模塊的最後輸出是由上面四個計算結果拼接獲得的
        net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])

  

卷積神經網絡遷移學習

  下面學習遷移學習的概念以及如何經過TensorFlow來實現遷移學習。首先學習遷移學習的機制,並學習如何將一個數據集上訓練好的卷積神經網絡模型快速轉義到另一個數據集上,而後在給出一個具體的TensorFlow程序將ImageNet上訓練好的inception-v3模型轉移到另一個圖像分類數據集上。

遷移學習介紹

   以前介紹了1998年提出的LeNet-5 模型和2015年提出的Inception-v3模型,對比兩個模型能夠發現,卷積神經模型的層數和複雜度都發生了巨大的變化,下表給出了從2012年到2015年ILSVRC(Large Scale Visual Recognition Challenge)第一名模型的層數以及前五個答案的錯誤率。

  從表中能夠看到,隨着模型層數及複雜度的增長,模型在ImageNet上的錯誤率也隨着下降。然而,訓練複雜的卷積神經網絡須要很是多的標註數據。好比ImageNet圖像分類數據集中有120萬標註圖片,因此才能將152層的ResNet的模型訓練到大約96.5%的正確率。在真實的應用中,很難收集到如此多的標註數據。即便能夠收集到,也須要花費大量人力物力。並且即便有海量的數據,要訓練出一個複雜的卷積神經網絡也須要幾天甚至幾周的時間。爲了解決標註數據和訓練時間的問題,可使用遷移學習。

  所謂遷移學習,就是將一個問題上訓練好的模型經過簡單的跳轉使其適用於一個新的問題,下面將學習如何利用ImageNet數據集上訓練好的Inception-v3模型來解決一個新的圖像分類問題。根據論文(A Deep Convolutional Activation Feature for Generic Visual Recognition)的結論,能夠保留訓練好的Inception——v3模型中全部卷積層的參數,只是替換最後一層全鏈接層。在最後這一層全鏈接層以前的網絡層稱之爲瓶頸處(bottleneck)。

  將新的圖像經過訓練好的卷積神經網絡直到瓶頸層的過程能夠當作是對圖像進行特徵提取的過程。在訓練好的Inception-v3模型中,由於將瓶頸層的輸出再經過一個單層的全鏈接層神經網絡能夠很好地區分1000種類別的圖像,因此有理由認爲瓶頸層輸出的節點向量能夠被做爲任何圖像的一個更加精簡且表達能力更強的特徵向量。因而在新的數據集上,能夠直接利用這個訓練好的神經網絡對圖像進行特徵提取,而後再將提取到的特徵向量做爲輸入來訓練一個新的單層全鏈接神經網絡處理新的分類問題。

  通常來講,在數據量足夠的狀況下,遷移學習的效果不如徹底從新訓練。可是遷移學習所須要的訓練時間和訓練樣本要遠遠小於訓練完整的模型。在沒有GPU的普通臺式電腦或者筆記本電腦上,下面給出的TensorFlow訓練過程只須要大約五分鐘,並且能夠達到大概90%的正確率。

TensorFlow實現遷移學習

  下面給出一個完整的Tensorflow程序來學習如何經過TensorFlow實現遷移學習。

   下載地址:http://download.tensorflow.org/example_images/flower_photos.tgz

  inception-v3下載地址:https://storage.googleapis.com/download.tensorflow.org/models/inception_dec_2015.zip

  解壓以後的文件夾包含了5個子文件夾,每個子文件夾的名稱爲一種花的名稱,表明了不一樣的類別。平均每一種花有734張圖片,每一張圖片都是RGB色彩模型的,大小也不相同。和以前的樣例不同,在這裏給出的程序將直接處理沒有整理過的圖像數據。同時,經過下面的命名能夠下載谷歌提供的訓練好的Inception-v3模型

   當新的數據集和已經訓練好的模型都準備好以後,能夠經過下面代碼完成遷移學習的過程。

# _*_coding:utf-8_*_
import glob
import os
import random
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile

# Inception-v3模型瓶頸層的節點個數
BOTTLENECK_TENSOR_SIZE = 2048

# Inception-v3 模型中表明瓶頸層結果的張量名稱
# 在谷歌提供的inception-v3模型中,這個張量名稱就是‘pool_3/_reshape:0’
# 在訓練的模型時,能夠經過tensor.name來獲取張量的名稱
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshape:0'

# 圖像輸入張量所對應的名稱
JEPG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'

# 下載的谷歌訓練好的inception-v3模型文件名
MODEL_DIR = 'inception_dec_2015'

#  下載的谷歌訓練好的Inception-v3 模型文件名
MODEL_FILE = 'tensorflow_inception_graph.pb'

# 由於一個訓練數據會被使用屢次,因此能夠將原始圖像經過inception-v3模型計算獲得
# 的特徵向量保存在文件中,免去重複的計算,下面的變量定義了這些文件的存放地址
CACHE_DIR = 'bottleneck1'
if not os.path.exists(CACHE_DIR): os.mkdir(CACHE_DIR)

# 圖片數據文件夾,在這個文件夾中每個子文件夾表明一個須要區分的類別
# 每一個子文件夾中存放了對應類別的圖片
INPUT_DATA = 'flower_photos'

# 驗證的數據百分比
VALIDATION_PERCENTAGE = 10
# 測試的數據百分比
TEST_PERCENTAGE = 10

# 定義神經網絡的設置
LEARNING_RETE = 0.01
STEPS = 4000
BATCH = 100


def create_image_lists(testing_percentage, validation_percentage):
    '''
    這些函數從數據文件夾中全部的圖片列表並按訓練,驗證,測試數據分開
    :param testing_percentage:   測試數據集的大小
    :param validation_percentage:  驗證數據集的大小
    :return:
    '''
    # 獲得的全部圖片都存在result這個字典(dictionary)裏
    # 這個字典的key爲類別的名稱,value是也是一個字典,字典存儲了全部的圖片名稱
    result = {}
    # 獲取當前目前下全部的子目錄  INPUT_DATA 是數據文件夾的名稱
    sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]
    # print(sub_dirs)  #['flower_photos', 'flower_photos\\daisy', 'flower_photos\\dandelion',
    # 'flower_photos\\roses', 'flower_photos\\sunflowers', 'flower_photos\\tulips']
    # 獲得的第一個目錄是當前目錄,不須要考慮
    is_root_dir = True
    for sub_dir in sub_dirs:
        # 下面這個函數的做用就是去掉沒有文件夾的目錄
        if is_root_dir:
            # print(is_root_dir)
            is_root_dir = False
            continue

        # 獲取當前目錄下全部的有效圖片文件
        extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']
        file_list = []
        # os.path.basename(path)  返回path最後的文件名。如何path以/或\結尾,那麼就會返回空值。
        # 即         os.path.split(path)的第二個元素
        dir_name = os.path.basename(sub_dir)
        # # print(dir_name)  # 各類花名的文件夾daisy dandelion  roses  sunflowers tulips
        for extension in extensions:
        #     # 得出的path爲 flower_photos/類別/*./照片類型  此時爲絕對路徑
            file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)
        #     # 返回全部匹配的文件路徑列表
            file_list.extend(glob.glob(file_glob))
        # print(len(file_list))   # [1266, 1796, 1282, 1398, 1598]
        if not file_list: continue

        # 經過目錄名獲取類別的名稱
        label_name = dir_name.lower()
        # 初始化當前類別的訓練數據集,測試數據集和驗證數據集
        training_images = []
        testing_images = []
        validation_images = []
        for file_name in file_list:
            # print(file_name)   # 'flower_photos\\daisy\\5794839_200acd910c_n.jpg',
            base_name = os.path.basename(file_name)
            # print(base_name)

            # 隨機將數據分到訓練數據集,測試數據集和驗證數據集
            chance = np.random.randint(100)
            if chance < validation_percentage:
                validation_images.append(base_name)
            elif chance < (testing_percentage + validation_percentage):
                testing_images.append(base_name)
            else:
                training_images.append(base_name)

        # 將當前類別的數據放入結果字典
        result[label_name] = {
            'dir': dir_name,
            'training': training_images,
            'testing': testing_images,
            'validation': validation_images,
        }

    # 返回整理好的全部數據
    # print(result.keys())  #(['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips'])
    print(len(result.items()))
    print(len(result.keys()))
    return result

def get_image_path(image_lists, image_dir, label_name, index, category):
    '''
    這個函數經過類別名稱,所屬數據集和圖片編碼獲取一張圖片的地址
    :param image_lists:  給出了全部圖片信息
    :param image_dir:給出了根目錄,存放圖片數據的根目錄和存放圖片特徵向量的根目錄地址不一樣
    :param label_name:給定了類別的名稱
    :param index:給定了須要獲取的圖片的編號
    :param category:指定了須要獲取的圖片是在訓練數據集,測試數據集仍是驗證數據
    :return:
    '''
    # 獲取給定類別的全部圖片的信息
    label_lists = image_lists[label_name]
    # 根據所屬數據集的名稱獲取集合中的所有圖片信息
    category_list = label_lists[category]
    mod_index = index % len(category_list)
    # 獲取圖片的文件名
    base_name = category_list[mod_index]
    sub_dir = label_lists['dir']
    # 最終地址爲數據根目錄的地址加上類別的文件夾加上圖片的名稱
    full_path = os.path.join(image_dir, sub_dir, base_name)
    return full_path


def get_bottleneck_path(image_lists, label_name, index, category):
    '''
    經過類別名稱,所屬數據集和圖片編號獲取通過Inception-v3模型處理以後的特徵文件地址
    :param image_lists:
    :param label_name:
    :param index:
    :param category:
    :return:
    '''
    # return get_image_path(image_lists, CACHE_DIR, label_name, index, category) + '.txt'
    return get_image_path(image_lists, CACHE_DIR, label_name, index, category)

# 這個函數使用加載的訓練好的Inception-v3模型處理一張圖片,獲得這個圖片的特徵向量
def run_bottleneck_on_image(sess, image_data, image_data_tensor, bottleneck_tensor):
    # 這個過程實際上就是將當前圖片做爲輸入計算瓶頸張量的值,
    # 這個瓶頸張量的值就是這張圖片新的特徵向量
    bottleneck_values = sess.run(bottleneck_tensor,
                                 {image_data_tensor: image_data})
    # 通過卷積神經網絡處理的結果是一個四維數組,須要將這個結果壓縮成一個特徵向量(一維數據)
    bottleneck_values = np.squeeze(bottleneck_values)
    return bottleneck_values


def get_or_create_bottleneck(sess, image_lists, label_name, index,
                             category, jpeg_data_tensor, bottleneck_tensor):
    '''
    這個函數獲取一張圖片通過Inception-v3模型處理以後的特徵向量,這個函數會先視圖找已經計算
    且保存下來的特徵向量,若是找不到則先計算這個特徵向量,而後保存到文件
    :param sess:  會話
    :param image_lists :  存全部圖片數據字典
    :param label_name:  類別名稱
    :param index:  編號
    :param category:  數據集的種類
    :param jpeg_data_tensor:  圖片數據張量
    :param bottleneck_tensor:  瓶頸層張量
    :return:   圖片的特徵向量一維的
    '''
    # 獲取相應類別的圖片路徑
    label_lists = image_lists[label_name]
    # 獲取圖片的子文件夾名稱
    sub_dir = label_lists['dir']
    # 緩存此類型圖片特徵向量對應的文件路徑
    sub_dir_path = os.path.join(CACHE_DIR, sub_dir)
    # 若是不存在這個文件路徑,則建立文件夾
    if not os.path.exists(sub_dir_path):
        os.makedirs(sub_dir_path)
    # 獲得inception-v3模型處理後的這個特定圖片的特徵向量的文件地址
    bottleneck_path = get_bottleneck_path(image_lists, label_name, index, category)

    # 若是這個特徵向量文件不存在,則經過inception-v3模型來計算特徵向量
    # 並將計算的結果存入文件
    if not os.path.exists(bottleneck_path):
        # 獲取原始的圖片路徑
        image_path = get_image_path(image_lists, INPUT_DATA, label_name, index, category)
        # 讀取圖片的原始數據
        image_data = gfile.FastGFile(image_path, 'rb').read()
        # 經過Inception-v3模型計算特徵向量 獲得圖片對應的特徵向量
        bottleneck_values = run_bottleneck_on_image(
            sess, image_data, jpeg_data_tensor, bottleneck_tensor
        )
        # 將計算獲得的特徵向量存入文件
        bottleneck_string = ','.join(str(x) for x in bottleneck_values)
        with open(bottleneck_path, 'w') as bottleneck_file:
            bottleneck_file.write(bottleneck_string)
    else:
        # 直接從文件中獲取圖片相應的特徵向量
        with open(bottleneck_path, 'r') as bottleneck_file:
            bottleneck_string = bottleneck_file.read()
            # 還原特徵向量
        bottleneck_values = [float(x) for x in bottleneck_string.split(',')]
    # 返回獲得的特徵向量
    return bottleneck_values

# 這個函數隨機獲取一個batch的圖片做爲訓練數據
def get_random_cached_bottlenecks(sess, n_classes, image_lists, how_many,
                                  category, jepg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    for _ in range(how_many):
        # 隨機一個類別和圖片的編號加入當前的訓練數據
        label_index = random.randrange(n_classes)
        label_name = list(image_lists.keys())[label_index]
        # 獲得一個隨機的圖片編號
        image_index = random.randrange(65536)
        # 獲得一個特定數據集編號隨機label的圖片的特徵向量
        bottleneck = get_or_create_bottleneck(
            sess, image_lists, label_name, image_index, category,
            jepg_data_tensor, bottleneck_tensor)
        # 這個其實至關於表示上面這個圖片特徵向量對應的label
        ground_truth = np.zeros(n_classes, dtype=np.float32)
        ground_truth[label_index] = 1.0

        # 特徵向量的集合list,其實就是一個隨機的訓練batch
        bottlenecks.append(bottleneck)
        ground_truths.append(ground_truth)

    return bottlenecks, ground_truths


# 這個函數獲取所有的測試數據,在最終測試的時候須要在全部的測試數據上計算正確率
def get_test_bottlenecks(sess, image_lists, n_classes,
                         jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    label_name_list = list(image_lists.keys())
    # 枚舉全部的類別和每一個類別彙總的測試圖片
    for label_index, label_name in enumerate(label_name_list):
        category = 'testing'
        for index, unused_base_name in enumerate(image_lists[label_name][category]):
            # 經過inception-v3模型計算圖片對應的特徵向量,並將其加入最終數據的列表
            bottleneck = get_or_create_bottleneck(
                sess, image_lists, label_name, index, category,
                jpeg_data_tensor, bottleneck_tensor)
            ground_truth = np.zeros(n_classes, dtype=np.float32)
            ground_truth[label_index] = 1.0
            bottlenecks.append(bottleneck)
            ground_truths.append(ground_truth)
    return bottlenecks, ground_truths


def main():
    # 讀取全部圖片
    image_lists = create_image_lists(TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
    # 獲得類別的分類數,此處是五分類
    n_classes = len(image_lists.keys())
    # 讀取已經訓練好的Inception-v3模型
    # 谷歌訓練好的模型保存在了GraphDef Protocol Buffer中
    # 裏面保存了每個節點取值的計算方法以及變量的取值
    with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
    # 加載讀取的Inception-v3模型,並返回數據輸入所對應的張量以及計算瓶頸層結果所對應的張量
    bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(
        graph_def,
        return_elements=[BOTTLENECK_TENSOR_NAME, JEPG_DATA_TENSOR_NAME]
    )

    # 定義新的神經網絡輸入,這個輸入就是新的圖片通過Inception-v3模型前向傳播到達瓶頸層
    # 是的節點取值,能夠將這個過程相似的理解爲一種特徵提取
    bottleneck_input = tf.placeholder(
        tf.float32, [None, BOTTLENECK_TENSOR_SIZE],
        name='BottleneckInputPlaceholder'
    )
    # 定義新的標準答案輸入
    ground_truth_input = tf.placeholder(
        tf.float32, [None, n_classes], name='GroundTruthInput'
    )
    # 定義一層全鏈接層來解決新的圖片分類問題,由於訓練好的inception-v3模型
    # 已經將原始的圖片抽象爲了更加容易分類的特徵向量了,因此不須要再訓練那麼複雜
    # 的神經網絡來完成這個新的分類任務
    with tf.name_scope('final_training_ops'):
        # 權重和偏置
        weights = tf.Variable(tf.truncated_normal(
            [BOTTLENECK_TENSOR_SIZE, n_classes], stddev=0.001
        ))
        biases = tf.Variable(tf.zeros([n_classes]))
        logits = tf.matmul(bottleneck_input, weights) + biases
        final_tensor = tf.nn.softmax(logits)

    # 定義交叉熵損失函數
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
        logits=logits, labels=ground_truth_input
    )
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    train_step = tf.train.GradientDescentOptimizer(LEARNING_RETE).minimize(cross_entropy_mean)

    # 計算正確率
    with tf.name_scope('evaluation'):
        correct_prediction = tf.equal(tf.argmax(final_tensor, 1),
                                      tf.argmax(ground_truth_input, 1))
        # cast將True 轉換爲1.0  False轉化爲0.0  以後算平均就爲正確率
        evaluation_step = tf.reduce_mean(
            tf.cast(correct_prediction, tf.float32)
        )

    with tf.Session() as sess:
        init = tf.global_variables_initializer()
        sess.run(init)

        # 訓練過程
        for i in range(STEPS):
            # 每次獲取一個batch的訓練數據
            train_bottlenecks, train_ground_truth = get_random_cached_bottlenecks(
                sess, n_classes, image_lists, BATCH, 'training',
                jpeg_data_tensor, bottleneck_tensor
            )
            sess.run(train_step,
                     feed_dict={bottleneck_input: train_bottlenecks,
                                ground_truth_input: train_ground_truth})

            # 在驗證數據上測試正確率
            if i % 100 == 0 or i+1 == STEPS:
                validation_bottenecks, validation_ground_truth = get_random_cached_bottlenecks(
                    sess, n_classes, image_lists, BATCH, 'validation',
                    jpeg_data_tensor, bottleneck_tensor
                )
                validation_accuracy = sess.run(evaluation_step,
                                               feed_dict={
                                                   bottleneck_input: validation_bottenecks,
                                                   ground_truth_input: validation_ground_truth
                                               })
                print('Step %d: Validation accuracy on random  sampled %d exmaples='
                      '%.1f%%'%(i, BATCH, validation_accuracy*100))

            # 在最後的測試數據上測試正確率
            test_bottlenecks, test_ground_truth = get_test_bottlenecks(
                sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor
            )
            test_accuracy = sess.run(evaluation_step, feed_dict={
                bottleneck_input: test_bottlenecks,
                ground_truth_input: test_ground_truth
            })
            print('Final test accuracy = %.1f%%' %(test_accuracy * 100))

if __name__ == '__main__':
    main()

  運行上面的程序將須要大約40分鐘(數據處理35分鐘,訓練5分鐘),能夠獲得相似下面的結果:

Step 0: Validation accuracy on random  sampled 100 exmaples=40.0%
Final test accuracy = 40.8%
Final test accuracy = 54.3%
Final test accuracy = 49.7%
... ...
Step 100: Validation accuracy on random  sampled 100 exmaples=83.0%
Final test accuracy = 85.7%
Final test accuracy = 85.8%
... ....
Final test accuracy = 93.2%
Step 3500: Validation accuracy on random  sampled 100 exmaples=94.0%
... ...
Final test accuracy = 93.6%
Final test accuracy = 93.8%
Final test accuracy = 93.6%
Step 3900: Validation accuracy on random  sampled 100 exmaples=93.0%
Final test accuracy = 93.6%
Final test accuracy = 93.6%
Final test accuracy = 93.4%
... ... 
Step 3999: Validation accuracy on random  sampled 100 exmaples=95.0%
Final test accuracy = 93.8%

  從上面的結果能夠看到,模型在新的數據集上很快可以收斂,並達到還不錯的分類效果。

 

 此文是本身的學習筆記總結,學習於《TensorFlow深度學習框架》,俗話說,好記性不如爛筆頭,寫寫老是好的,因此若侵權,請聯繫我,謝謝。

相關文章
相關標籤/搜索