這次的實驗仍舊是圖像識别,使用的資料集是CIFAR-10,其中包含10個類别的RGB彩色圖檔:飛機、汽車、鳥類、貓、鹿、狗、蛙類、馬、船和卡車,其中訓練圖檔共50000張,測試圖檔共10000張。實驗來自于《21個項目玩轉深度學習:基于TensorFlow的實踐詳解》一書中的實驗,在這裡做一篇學習筆記,筆者不才,歡迎斧正!
項目檔案如下:

。其中,
一、下載下傳CIFAR-10資料
運作cifar10_download.py代碼,成功将資料集下載下傳到目錄中。
可以看到資料集中資料檔案名及用途:
二、Tensorflow的資料讀取機制
讀取資料集必須考慮的一個問題:GPU/CPU因為I/O而空閑的問題
簡單地将圖檔資料集從記憶體中讀進CPU或GPU中進行計算,讀取的時間便意味在降低記憶體的效率。為了解決這個問題,方法即将讀入資料和計算分别放在兩個線程中,先将資料讀入記憶體隊列中。讀取線程負責讀入資料到記憶體隊列,另一個線程則負責計算,計算需要資料時直接從記憶體隊列中取即可。
Tensorflow使用“檔案名隊列+記憶體隊列”雙隊列的形式讀入檔案,更好的管理epoch。
對于一個資料集來講,運作一個epoch就是将這個資料集中的圖檔全部計算一遍。運作一個epoch,就在檔案名隊列中把A、B、C各放入一次,并在之後标注隊列結束。
程式運作後,記憶體先讀取A,再依次讀取B、C。
由于系統檢測到“結束”,則自動抛出異常,外部捕捉到這個異常後就可以結束程式了。這就是Tensorflow中讀取資料的基本機制。如果要運作兩個epoch,那麼隻要在檔案名隊列中将A、B、C依次放入兩次再标記結束就可以了。
在Tensorflow中建立檔案名隊列和記憶體隊列
使用tf.train.string_input_producer函數。這個函數有兩個參數,一個是num_epoch是,即epoch數目;另一個是shuffle,指在一個epoch内檔案的順序是否被打亂。若設定shuffle=False,則每個epoch内,資料仍然按照A、B、C的順序進入檔案名隊列,順序不變。反之,則在一個epoch内資料的前後順序就會被打亂。在Tensorflow中記憶體隊列不需要自己建立,隻需要使用reader讀取資料就可以了。
在使用tf.train.string_input_producer建立檔案名隊列之後,整個系統還處于停滞狀态,就是說檔案名其實并沒有真正加入到隊列中。此時若開始計算,記憶體隊列為空,計算單元就會一直阻塞。使用tf.train.start_queue_runners之後,才會啟動填充隊列的線程,這時系統就不在停滞了。
在下面的代碼中,我們讀取三張照片的5個epoch,并把讀取的結果重新存到read檔案夾中。
# coding:utf-8
import os
if not os.path.exists('read'):
os.makedirs('read/')
# 導入TensorFlow
import tensorflow as tf
# 建立一個Session
with tf.Session() as sess:
# 我們要讀三幅圖檔A.jpg, B.jpg, C.jpg
filename = ['A.jpg', 'B.jpg', 'C.jpg']
# string_input_producer會産生一個檔案名隊列
filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)
# reader從檔案名隊列中讀資料。對應的方法是reader.read
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
# tf.train.string_input_producer定義了一個epoch變量,要對它進行初始化
tf.local_variables_initializer().run()
# 使用start_queue_runners之後,才會開始填充隊列
threads = tf.train.start_queue_runners(sess=sess)
i = 0
while True:
i += 1
# 擷取圖檔資料并儲存
image_data = sess.run(value)
with open('read/test_%d.jpg' % i, 'wb') as f:
f.write(image_data)
# 程式最後會抛出一個OutOfRangeError,這是epoch跑完,隊列關閉的标志
其中,我們使用“filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)” 來建立一個運作5個epoch的檔案名隊列,并使用reader讀取,reader每次讀取一張照片并儲存。
運作代碼後,程式最後會抛出OutOfRangeError異常,這就是epoch跑完隊列關閉的标志,得到read檔案夾中的圖檔正好是按順序的5個epoch。下圖是shuffle=False時的結果:
下面我們再來将CIFAR-10資料集儲存成圖檔形式。運作的代碼是cifar10_extract.py。Cifar資料集包括10000個樣本,每個樣本有3073個位元組,第一個位元組标簽,剩下的3072個位元組是圖像資料。樣本和樣本之間無多餘的位元組分割,是以這幾個二進制檔案的大小是30730000位元組。
- 第一步,用tf.train.string_input_producer建立隊列。
- 第二步,用reader.read讀取資料。注意:讀取以檔案格式存放的圖檔是tf.WholeFileReader(),但是Cifar資料是一個檔案中包含多個以位元組存儲的樣本,是以使用tf.FixedLengthRecordReaer()
- 第三步,調用tf.train.start_queue_runners
- 最後通過sess.run()取出圖檔結果
三、利用TensorFlow訓練CIFAR-10識别模型
(壹)資料增強
原理部分:
一般來說在深度學習中,資料的總量越多,訓練得到的模型效果會越好。在圖像任務中,對輸入的圖像進行簡單的平移、
縮放、顔色變換,并不會影響圖像類别。是以可以依據此增強訓練樣本的個數。
資料增強(Data Augmentation)方法是指利用平移、縮放、顔色變換,人工增大訓練集樣本個數,使得模型訓練的效果更好。
常見的圖像資料增強方法:
- 平移
- 旋轉
- 翻轉:水準翻轉或上下翻轉圖像
- 裁剪:在原有圖像上裁剪出一塊
- 縮放
- 顔色變換:對圖像的RGB顔色空間進行一些變換
- 噪聲擾動:給圖像加入一些人工生成的噪聲
這些資料增強方法不會改變圖像原有标簽。
TensorFlow中資料增強的實作
代碼為:
# Randomly crop a [height, width] section of the image.
distorted_image = tf.random_crop(reshaped_image, [height, width, 3])
# Randomly flip the image horizontally.
distorted_image = tf.image.random_flip_left_right(distorted_image)
# Because these operations are not commutative, consider randomizing
# the order their operation.
distorted_image = tf.image.random_brightness(distorted_image,max_delta=63)
distorted_image = tf.image.random_contrast(distorted_image,lower=0.2, upper=1.8)
進行的操作分别是:
- 随機裁剪
- 對裁剪後的小塊進行水準反轉
- 對得到的圖檔進行亮度和對比度的随機訓練
(貳)Cifar-10識别模型
該模型通過三個子產品來構造訓練圖,最大限度得提高代碼複用率:
- 模型輸入:讀取資料集中的圖像并進行預處理
- 模型預測:進行統計計算
- 模型訓練:計算損失、計算梯度、進行變量更新、儲存最終結果
(一)模型輸入
在模型輸入部分将圖像的加載和變換過程放入16個線程中,以減慢訓練過程。
其中,在distorted_inputs()函數中采取對圖像随機左右翻轉、變換圖像亮度、變換圖像對比度等操作。該函數的核心代碼為:
# Randomly crop a [height, width] section of the image.
distorted_image = tf.random_crop(reshaped_image, [height, width, 3])
# Randomly flip the image horizontally.
distorted_image = tf.image.random_flip_left_right(distorted_image)
# Because these operations are not commutative, consider randomizing
# the order their operation.
distorted_image = tf.image.random_brightness(distorted_image,
max_delta=63)
distorted_image = tf.image.random_contrast(distorted_image,
lower=0.2, upper=1.8)
# Subtract off the mean and divide by the variance of the pixels.
float_image = tf.image.per_image_standardization(distorted_image)
- tf.random_crop:為圖檔随機裁剪
- tf.image.random_flip_left_right:随機左右翻轉
- tf.image.random_brightness:随機亮度變化
- tf.image.random_contrast:随機對比度變化
- tf.image.per_image_standardization:減去均值像素,并除以像素方差(圖檔标準化)
(二)模型預測
預測流程由inference() 構造。這個函數會添加必要的操作步驟用于計算預測值的logits。
1、建構第一個卷積層
卷積層:
- 輸入:images
- 卷積核:寬度為5×5 、通道為3、共64個
- 步長:1×1
- padding:SAME
- 偏置項:通過函數生成的初始化為全0的64維向量
- W*x+b:通過函數tf.nn.bias_add(conv,biases)實作
- relu:使用ReLu激活函數完成修正線性激活
池化層:
- 輸入:conv1
- 池化視窗大小:3×3
- 滑動步長:2×2
- padding:SAME
局部響應歸一化
- 原因:
- 公式
i:代表下标,你要計算像素值的下标,從0計算起
j:平方累加索引,代表從j~i的像素值平方求和
x,y:像素的位置,公式中用不到
a:代表feature map裡面的 i 對應像素的具體值
N:每個feature map裡面最内層向量的列數
k:超參數,由原型中的bias指定
α:超參數,由原型中的alpha指定
n/2:超參數,由原型中的deepth_radius指定
β:超參數,由原型中的belta指定
2、建構第二個卷積層
卷積層
- 輸入:norm1
- 卷積核:寬度為5×5、通道數為64、共64個
- 步長:1×1
- padding:SAME
- 偏置項:初始化全為0.1的64維向量
- W*A+b:通過函數tf.nn.bias_add(conv,biases)實作
- 修正線性激活函數:ReLu
池化層:
- 輸入:norm2
- 池化視窗大小:3×3
- 滑動步長:2×2
- padding:SAME
3、基于修正線性激活的全連接配接層
- 原因:前面的卷積和池化相當于做了特征工程,而全連接配接層則負責特征權重,簡單來說全連接配接的目的即對特征高度提純,友善交給最後的分類器或者回歸。
- 本質: 由一個特征空間變換到另一個特征空間
- 為了提高CNN網絡性能,在這裡全連接配接層每個神經元的激勵函數都采用ReLu函數
4、基于修正線性激活的全連接配接層
5、softmax邏輯回歸輸出分類結果
現在避免全連接配接的方法是全局平均值法,即将最後一層卷積的輸出結果(featuremap)求平均值。
(三)模型訓練
訓練可進行N維分類的網絡常用多項式邏輯回歸,即softmax回歸。Softmax 回歸在網絡的輸出層上附加了一個softmax nonlinearity,并且計算歸一化的預測值和label的1-hot encoding的交叉熵。在正則化過程中,我們會對所有學習變量應用權重衰減損失。模型的目标函數是求交叉熵損失和所有權重衰減項的和,
loss()
函數的傳回值就是這個值。
在TensorBoard中可以檢視這個值的變化:
使用标準的梯度下降算法訓練的模型,其學習率随時間以指數形式衰減(本實驗隻讓伺服器跑了十萬多次,衰減程度不是很明顯):
訓練速度的變化:
(叁)測試模型效果
本實驗跑了3000個訓練樣本,用記事本打開checkpoint發現:
說明model_checkpoint_path表示最新的模型是model.ckpt-112632。112632即為第112632步的模型。後面5個all_model_checkpoint_paths表示所有存儲下來的5個模型和它們的步數。