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(',')]
    # 返回得到的特征向量
    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深度學習框架》,俗話說,好記性不如爛筆頭,寫寫總是好的,所以若侵權,請聯系我,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM