1. 程式人生 > >tensorflow學習筆記——影象識別與卷積神經網路

tensorflow學習筆記——影象識別與卷積神經網路

  無論是之前學習的MNIST資料集還是Cifar資料集,相比真實環境下的影象識別問題,有兩個最大的問題,一是現實生活中的圖片解析度要遠高於32*32,而且影象的解析度也不會是固定的。二是現實生活中的物體類別很多,無論是10種還是100種都遠遠不夠,而且一張圖片中不會只出現一個種類的物體。為了更加貼近真實環境下的影象識別問題,由李飛飛教授帶頭整理的ImageNet很大程度上解決了這個問題。

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

  下面主要使用的是ILSVRC2012影象分類資料集,ILSVRC2012影象分類資料集的任務和Cifar資料集是基本一致的,也是識別影象中的主要物體。ILSVRC2012影象分類資料集包含了來自1000個類別的120萬張圖片,其中每張圖片資料且只屬於一個類別。因為ILSVRC2012影象分類資料集的圖片是直接從網際網路上爬取得到的,所以圖片的大小從幾千位元組到幾百萬位元組不等。

卷積神經網路簡介

  為了將只包含全連線層的神經網路和卷積神經網路,迴圈神經網路區分開,我們將只包含全連線層的神經網路稱之為全連線神經網路。下面先學習卷積神經網路與全連線神經網路的差異,並介紹組成一個卷積神經網路的基本網路結構,下圖顯示了全連線神經網路與卷積神經網路的結構對比圖。

  上圖顯示的全連線神經網路結構和卷積神經網路的結構直觀上差異比較大,但是實際上他們的整體架構是非常相似的。從上圖中可以看出,卷積神經網路也是通過一層一層的節點組織起來的。和全連線神經網路一樣,卷積神經網路中的每一個節點都是一個神經元。在全連線神經網路中,每相鄰兩層之間的節點都是有邊相連,於是一般會將每層全連線層中的節點組織成一列,這樣方便顯示連線結構。而對於卷積神經網路,相鄰兩次之間只有部分節點相連,為了展示每一層神經元的維度,一般會將每一層卷積層的節點組織成一個三維矩陣。

  除了結構相似,卷積神經網路的輸入輸出以及訓練流程與全連線神經網路也基本一致。以圖形分類為例,卷積神經網路的輸入層就是影象的原始畫素,而輸出層中的每一個節點代表了不同類別的可信度。這和全連線神經網路的輸入輸出是一致的。卷積神經網路和全連線神經網路的唯一區別就在於神經網路中相鄰兩次的連線方式。

  下面我們瞭解一下為什麼全連線神經網路無法很好地處理影象資料。

  使用全連線神經網路處理影象的最大問題在於全連線層的引數太多。對於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(',')]
    # 返回得到的特徵向量