天天看點

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

機器之心項目位址: https://github.com/jiqizhixin/ML-Tutorial-Experiment

本文的重點是實作,并不會從理論和概念上詳細解釋深度神經網絡、卷積神經網絡、最優化方法等基本内容。但是機器之心發過許多詳細解釋的入門文章或教程,是以,我們希望讀者能先了解以下基本概念和理論。當然,本文注重實作,即使對深度學習的基本算法了解不那麼深同樣還是能實作本文所述的内容。

卷積神經網絡:

TensorFlow 入門:

優化方法:

首先是安裝 TensorFlow,我們可以直接按照 TensorFlow 官方教程安裝。機器之心在 Jupyter Notebook 上運作和測試本文所有代碼,但是 TensorFlow 在 Windows 上隻支援 Python 3.5x,而我們現在安裝的 Anaconda 支援的是 Python 3.6。是以如果需要在 Windows 上用 Jupyter Notebook 加載 TensorFlow,還需要另外一些操作。

TensorFlow 官方安裝教程:

https://www.tensorflow.org/install/

現在假定我們已經安裝了最新的 Anaconda 4.4.0,如果希望在 Jupyter notebook 中導入 TensorFlow 需要以下步驟。

在 Anaconda Prompt(CMD 指令行中也行)中鍵入以下指令以建立名為 tensorflow 的 conda 環境:

conda create -n tensorflow python=3.5 

然後再運作以下指令行激活 conda 環境:

activate tensorflow

運作後會變為「(tensorflow) C:\Users\使用者名>」,然後我們就可以繼續在該 conda 環境内安裝 TensorFlow(本文隻使用 CPU 進行訓練,是以可以隻安裝 CPU 版):

pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/windows/cpu/tensorflow-1.3.0-cp35-cp35m-win_amd64.whl

現在已經成功安裝了 TensorFlow,但是在 Jupyter Notebook 中并不能導入 TensorFlow,是以我們需要使用指令行在 TensorFlow 環境中安裝 Jupyter 和 Ipython:

conda install ipython 

conda install jupyter 

最後,運作以下指令就能完成安裝,并在 Jupyter Notebook 中導入 TensorFlow:

ipython kernelspec install-self --user 
機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

TensorFlow 基礎

下面我們首先需要了解 TensorFlow 的基本用法,這樣我們才能開始建構神經網絡。本小節将從張量與圖、常數與變量還有占位符等基本概念出發簡要介紹 TensorFlow,熟悉 TensorFlow 的讀者可以直接閱讀下一節。需要進一步了解 TensorFlow 的讀者最好可以閱讀谷歌 TensorFlow 的文檔,當然也可以閱讀其他中文教程或書籍,例如《TensorFlow:實戰 Google 深度學習架構》和《TensorFlow 實戰》等。

TensorFlow 文檔位址:

https://www.tensorflow.org/get_started/

1.1 張量和圖

TensorFlow 是一種采用資料流圖(data flow graphs),用于數值計算的開源軟體庫。其中 Tensor 代表傳遞的資料為張量(多元數組),Flow 代表使用計算圖進行運算。資料流圖用「結點」(nodes)和「邊」(edges)組成的有向圖來描述數學運算。「結點」一般用來表示施加的數學操作,但也可以表示資料輸入的起點和輸出的終點,或者是讀取/寫入持久變量(persistent variable)的終點。邊表示結點之間的輸入/輸出關系。這些資料邊可以傳送次元可動态調整的多元資料數組,即張量(tensor)。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

下面代碼是使用計算圖的案例:

  1. a = tf.constant(2, tf.int16)

  2. b = tf.constant(4, tf.float32)

  3. graph = tf.Graph()

  4. with graph.as_default():

  5.    a = tf.Variable(8, tf.float32)

  6.    b = tf.Variable(tf.zeros([2,2], tf.float32))

  7. with tf.Session(graph=graph) as session:

  8.    tf.global_variables_initializer().run()

  9.    print(f)

  10.    print(session.run(a))

  11.    print(session.run(b))

  12. #輸出:

  13. >>> <tf.Variable 'Variable_2:0' shape=() dtype=int32_ref>

  14. >>> 8

  15. >>> [[ 0.  0.]

  16. >>>  [ 0.  0.]]

在 Tensorflow 中,所有不同的變量和運算都是儲存在計算圖。是以在我們建構完模型所需要的圖之後,還需要打開一個會話(Session)來運作整個計算圖。在會話中,我們可以将所有計算配置設定到可用的 CPU 和 GPU 資源中。

如下所示代碼,我們聲明兩個常量 a 和 b,并且定義一個加法運算。但它并不會輸出計算結果,因為我們隻是定義了一張圖,而沒有運作它:

  1. a=tf.constant([1,2],name="a")

  2. b=tf.constant([2,4],name="b")

  3. result = a+b

  4. print(result)

  5. #輸出:Tensor("add:0", shape=(2,), dtype=int32)

下面的代碼才會輸出計算結果,因為我們需要建立一個會話才能管理 TensorFlow 運作時的所有資源。但計算完畢後需要關閉會話來幫助系統回收資源,不然就會出現資源洩漏的問題。下面提供了使用會話的兩種方式:

  1. a=tf.constant([1,2,3,4])

  2. b=tf.constant([1,2,3,4])

  3. result=a+b

  4. sess=tf.Session()

  5. print(sess.run(result))

  6. sess.close

  7. #輸出 [2 4 6 8]

  8. with tf.Session() as sess:

  9.    a=tf.constant([1,2,3,4])

  10.    b=tf.constant([1,2,3,4])

  11.    result=a+b

  12.    print(sess.run(result))

  13. #輸出 [2 4 6 8]

1.2 常量和變量

TensorFlow 中最基本的機關是常量(Constant)、變量(Variable)和占位符(Placeholder)。常量定義後值和次元不可變,變量定義後值可變而次元不可變。在神經網絡中,變量一般可作為儲存權重和其他資訊的矩陣,而常量可作為儲存超參數或其他結構資訊的變量。下面我們分别定義了常量與變量:

  1. a = tf.constant(2, tf.int16)

  2. b = tf.constant(4, tf.float32)

  3. c = tf.constant(8, tf.float32)

  4. d = tf.Variable(2, tf.int16)

  5. e = tf.Variable(4, tf.float32)

  6. f = tf.Variable(8, tf.float32)

  7. g = tf.constant(np.zeros(shape=(2,2), dtype=np.float32))

  8. h = tf.zeros([11], tf.int16)

  9. i = tf.ones([2,2], tf.float32)

  10. j = tf.zeros([1000,4,3], tf.float64)

  11. k = tf.Variable(tf.zeros([2,2], tf.float32))

  12. l = tf.Variable(tf.zeros([5,6,5], tf.float32))

在上面代碼中,我們分别聲明了不同的常量(tf.constant())和變量(tf.Variable()),其中 tf.float 和 tf.int 分别聲明了不同的浮點型和整數型資料。而 tf.ones() 和 tf.zeros() 分别産生全是 1、全是 0 的矩陣。我們注意到常量 g,它的聲明結合了 TensorFlow 和 Numpy,這也是可執行的。

w1=tf.Variable(tf.random_normal([2,3],stddev=1,seed=1))

以上語句聲明一個 2 行 3 列的變量矩陣,該變量的值服從标準差為 1 的正态分布,并随機生成。TensorFlow 還有 tf.truncated_normal() 函數,即截斷正态分布随機數,它隻保留 [mean-2*stddev,mean+2*stddev] 範圍内的随機數。

現在,我們可以應用變量來定義神經網絡中的權重矩陣和偏置項向量:

  1. weights = tf.Variable(tf.truncated_normal([256 * 256, 10]))

  2. biases = tf.Variable(tf.zeros([10]))

  3. print(weights.get_shape().as_list())

  4. print(biases.get_shape().as_list())

  5. #輸出

  6. >>>[65536, 10]

  7. >>>[10]

1.3 占位符和 feed_dict

我們已經建立了各種形式的常量和變量,但 TensorFlow 同樣還支援占位符。占位符并沒有初始值,它隻會配置設定必要的記憶體。在會話中,占位符可以使用 feed_dict 饋送資料。

feed_dict 是一個字典,在字典中需要給出每一個用到的占位符的取值。在訓練神經網絡時需要每次提供一個批量的訓練樣本,如果每次疊代選取的資料要通過常量表示,那麼 TensorFlow 的計算圖會非常大。因為每增加一個常量,TensorFlow 都會在計算圖中增加一個結點。是以說擁有幾百萬次疊代的神經網絡會擁有極其龐大的計算圖,而占位符卻可以解決這一點,它隻會擁有占位符這一個結點。

下面一段代碼分别展示了使用常量和占位符進行計算:

  1. w1=tf.Variable(tf.random_normal([1,2],stddev=1,seed=1))

  2. #因為需要重複輸入x,而每建一個x就會生成一個結點,計算圖的效率會低。是以使用占位符

  3. x=tf.placeholder(tf.float32,shape=(1,2))

  4. x1=tf.constant([[0.7,0.9]])

  5. a=x+w1

  6. b=x1+w1

  7. sess=tf.Session()

  8. sess.run(tf.global_variables_initializer())

  9. #運作y時将占位符填上,feed_dict為字典,變量名不可變

  10. y_1=sess.run(a,feed_dict={x:[[0.7,0.9]]})

  11. y_2=sess.run(b)

  12. print(y_1)

  13. print(y_2)

  14. sess.close

其中 y_1 的計算過程使用占位符,而 y_2 的計算過程使用常量。

下面是使用占位符的案例:

  1. list_of_points1_ = [[1,2], [3,4], [5,6], [7,8]]

  2. list_of_points2_ = [[15,16], [13,14], [11,12], [9,10]]

  3. list_of_points1 = np.array([np.array(elem).reshape(1,2) for elem in list_of_points1_])

  4. list_of_points2 = np.array([np.array(elem).reshape(1,2) for elem in list_of_points2_])

  5. graph = tf.Graph()

  6. with graph.as_default():  

  7.    #我們使用 tf.placeholder() 建立占位符 ,在 session.run() 過程中再投遞資料

  8.    point1 = tf.placeholder(tf.float32, shape=(1, 2))

  9.    point2 = tf.placeholder(tf.float32, shape=(1, 2))

  10.    def calculate_eucledian_distance(point1, point2):

  11.        difference = tf.subtract(point1, point2)

  12.        power2 = tf.pow(difference, tf.constant(2.0, shape=(1,2)))

  13.        add = tf.reduce_sum(power2)

  14.        eucledian_distance = tf.sqrt(add)

  15.        return eucledian_distance

  16.    dist = calculate_eucledian_distance(point1, point2)

  17. with tf.Session(graph=graph) as session:

  18.    tf.global_variables_initializer().run()  

  19.    for ii in range(len(list_of_points1)):

  20.        point1_ = list_of_points1[ii]

  21.        point2_ = list_of_points2[ii]

  22.        #使用feed_dict将資料投入到[dist]中

  23.        feed_dict = {point1 : point1_, point2 : point2_}

  24.        distance = session.run([dist], feed_dict=feed_dict)

  25.        print("the distance between {} and {} -> {}".format(point1_, point2_, distance))

  26. #輸出:

  27. >>> the distance between [[1 2]] and [[15 16]] -> [19.79899]

  28. >>> the distance between [[3 4]] and [[13 14]] -> [14.142136]

  29. >>> the distance between [[5 6]] and [[11 12]] -> [8.485281]

  30. >>> the distance between [[7 8]] and [[ 9 10]] -> [2.8284271]

Ahmet Taspinar 在第二部分就直接開始建構深度神經網絡了,雖然我們在前一章增加了許多代碼段以幫助讀者了解 TensorFlow 的基本法則,但上面是遠遠不夠的。是以如果我們能先解析一部分神經網絡代碼,那麼将有助于入門讀者鞏固以上的 TensorFlow 基本知識。下面,我們将先解析一段建構了三層全連接配接神經網絡的代碼。

  1. import tensorflow as tf

  2. from numpy.random import RandomState

  3. batch_size=10

  4. w1=tf.Variable(tf.random_normal([2,3],stddev=1,seed=1))

  5. w2=tf.Variable(tf.random_normal([3,1],stddev=1,seed=1))

  6. # None 可以根據batch 大小确定次元,在shape的一個次元上使用None

  7. x=tf.placeholder(tf.float32,shape=(None,2))

  8. y=tf.placeholder(tf.float32,shape=(None,1))

  9. #激活函數使用ReLU

  10. a=tf.nn.relu(tf.matmul(x,w1))

  11. yhat=tf.nn.relu(tf.matmul(a,w2))

  12. #定義交叉熵為損失函數,訓練過程使用Adam算法最小化交叉熵

  13. cross_entropy=-tf.reduce_mean(y*tf.log(tf.clip_by_value(yhat,1e-10,1.0)))

  14. train_step=tf.train.AdamOptimizer(0.001).minimize(cross_entropy)

  15. rdm=RandomState(1)

  16. data_size=516

  17. #生成兩個特征,共data_size個樣本

  18. X=rdm.rand(data_size,2)

  19. #定義規則給出樣本标簽,所有x1+x2<1的樣本認為是正樣本,其他為負樣本。Y,1為正樣本

  20. Y = [[int(x1+x2 < 1)] for (x1, x2) in X]

  21. with tf.Session() as sess:

  22.    sess.run(tf.global_variables_initializer())

  23.    print(sess.run(w1))

  24.    print(sess.run(w2))

  25.    steps=11000

  26.    for i in range(steps):

  27.        #標明每一個批量讀取的首尾位置,確定在1個epoch内采樣訓練

  28.        start = i * batch_size % data_size

  29.        end = min(start + batch_size,data_size)

  30.        sess.run(train_step,feed_dict={x:X[start:end],y:Y[start:end]})

  31.        if i % 1000 == 0:

  32.            training_loss= sess.run(cross_entropy,feed_dict={x:X,y:Y})

  33.            print("在疊代 %d 次後,訓練損失為 %g"%(i,training_loss))

上面的代碼定義了一個簡單的三層全連接配接網絡(輸入層、隐藏層和輸出層分别為 2、3 和 2 個神經元),隐藏層和輸出層的激活函數使用的是 ReLU 函數。該模型訓練的樣本總數為 512,每次疊代讀取的批量為 10。這個簡單的全連接配接網絡以交叉熵為損失函數,并使用 Adam 優化算法進行權重更新。

其中需要注意的幾個函數如 tf.nn.relu() 代表調用 ReLU 激活函數,tf.matmul() 為矩陣乘法等。tf.clip_by_value(yhat,1e-10,1.0) 這一語句代表的是截斷 yhat 的值,因為這一語句是嵌套在 tf.log() 函數内的,是以我們需要確定 yhat 的取值不會導緻對數無窮大。

tf.train.AdamOptimizer(learning_rate).minimize(cost_function) 是進行訓練的函數,其中我們采用的是 Adam 優化算法更新權重,并且需要提供學習速率和損失函數這兩個參數。後面就是生成訓練資料,X=rdm.rand(512,2) 表示随機生成 512 個樣本,每個樣本有兩個特征值。最後就是疊代運作了,這裡我們計算出每一次疊代抽取資料的起始位置(start)和結束位置(end),并且每一次抽取的資料量為前面我們定義的批量,如果一個 epoch 最後剩餘的資料少于批量大小,那就隻是用剩餘的資料進行訓練。最後兩句代碼是為了計算訓練損失并疊代一些次數後輸出訓練損失。這一部分代碼運作的結果如下:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

TensorFlow 中的神經網絡

2.1 簡介

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

上圖所描述的圖像識别流程需要包含以下幾步:

  • 輸入資料集,資料集分為訓練資料集和标注、測試資料集和标注(包括驗證資料集和标注)。測試和驗證集能指派到 tf.constant() 中,而訓練集可以導入 tf.placeholder() 中,訓練集隻有導入占位符我們才能在随機梯度下降中成批量地進行訓練。
  • 确定神經網絡模型,該模型可以是簡單的一層全連接配接網絡或 9 層、16 層的複雜卷積網絡組成。
  • 網絡定義的權重矩陣和偏置向量後需要執行初始化,每一層需要一個權重矩陣和一個偏置向量。
  • 建構損失函數,并計算訓練損失。模型會輸出一個預測向量,我們可以比較預測标簽和真實标簽并使用交叉熵函數和 softmax 回歸來确定損失值。訓練損失衡量預測值和真實值之間差距,并用于更新權重矩陣。
  • 優化器,優化器将使用計算的損失值和反向傳播算法更新權重和偏置項參數。

2.2 加載資料

首先我們需要加載資料,加載的資料用來訓練和測試神經網絡。在 Ahmet Taspinar 的部落格中,他用的是 MNIST 和 CIFAR-10 資料集。其中 MNIST 資料集包含 6 萬張手寫數字圖檔,每一張圖檔的大小都是 28 x 28 x 1(灰階圖)。而 CIFAR-10 資料集包含 6 萬張彩色(3 通道)圖檔,每張圖檔的大小為 32 x 32 x 3,該資料集有 10 種不同的物體(飛機、機車、鳥、貓、狗、青蛙、馬、羊和卡車)。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

首先,讓我們定義一些函數,它們能幫助我們加載和預處理圖像資料。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

圖像的标簽使用 one-hot 編碼,并且将資料加載到随機數組中。在定義這些函數後,我們可以加載資料:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

我們能從 Yann LeCun 的網站下載下傳 MNIST 資料集,下載下傳并解壓後就能使用 python-mnist 工具加載該資料集。

在 Ahmet Taspinar 提供的上述代碼中,我們運作會出錯,因為「MNIST」并沒有定義,而我們機器之心在安裝完 python-mnist,并加上「from mnist import MNIST」語句後,仍然不能導入。是以我們可以修改以上代碼,使用 TensorFlow 官方教程中自帶的 MNIST 加載工具加載 MNIST。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

如下所示,我們可以使用這種方法成功地導入 MNIST 資料集:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

我們需要再次導入 CIFAR-10 資料集,這一段代碼也會出錯,原因是有變量沒有定義。下面代碼将導入資料集:

  1. cifar10_folder = './data/cifar10/'

  2. train_datasets = ['data_batch_1', 'data_batch_2', 'data_batch_3', 'data_batch_4', 'data_batch_5', ]

  3. test_dataset = ['test_batch']

  4. c10_image_height = 32

  5. c10_image_width = 32

  6. c10_image_depth = 3

  7. c10_num_labels = 10

  8. c10_image_size = 32 #Ahmet Taspinar的代碼缺少了這一語句

  9. with open(cifar10_folder + test_dataset[0], 'rb') as f0:

  10.    c10_test_dict = pickle.load(f0, encoding='bytes')

  11. c10_test_dataset, c10_test_labels = c10_test_dict[b'data'], c10_test_dict[b'labels']

  12. test_dataset_cifar10, test_labels_cifar10 = reformat_data(c10_test_dataset, c10_test_labels, c10_image_size, c10_image_size, c10_image_depth)

  13. c10_train_dataset, c10_train_labels = [], []

  14. for train_dataset in train_datasets:

  15.    with open(cifar10_folder + train_dataset, 'rb') as f0:

  16.        c10_train_dict = pickle.load(f0, encoding='bytes')

  17.        c10_train_dataset_, c10_train_labels_ = c10_train_dict[b'data'], c10_train_dict[b'labels']

  18.        c10_train_dataset.append(c10_train_dataset_)

  19.        c10_train_labels += c10_train_labels_

  20. c10_train_dataset = np.concatenate(c10_train_dataset, axis=0)

  21. train_dataset_cifar10, train_labels_cifar10 = reformat_data(c10_train_dataset, c10_train_labels, c10_image_size, c10_image_size, c10_image_depth)

  22. del c10_train_dataset

  23. del c10_train_labels

  24. print("訓練集包含以下标簽: {}".format(np.unique(c10_train_dict[b'labels'])))

  25. print('訓練集次元', train_dataset_cifar10.shape, train_labels_cifar10.shape)

  26. print('測試集次元', test_dataset_cifar10.shape, test_labels_cifar10.shape)

在試驗中,我們需要注意放置資料集的位址。MNIST 可以自動檢測指定的目錄下是否有資料集,如果沒有就自動下載下傳資料集至該目錄下。在上面的兩段代碼中,「./data/MNIST/」就代表着我們放置資料集的位址,它表示在 Python 根目錄下「data」檔案夾下的「MNIST」檔案夾内。CIFAR-10 同樣也是這樣,隻不過它不會自動下載下傳資料集。

2.3 建立簡單的多層全連接配接神經網絡

Ahmet Taspinar 後面建立了一個單隐藏層全連接配接網絡,不過我們還是報錯了。他在部落格中給出了以下訓練準确度,我們看到該模型在 MNIST 資料集效果并不是很好。是以我們另外使用一個全連接配接神經網絡來實作這一過程。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

下面我們實作的神經網絡共有三層,輸入層有 784 個神經元,隐藏層與輸出層分别有 500 和 10 個神經元。這是以這樣設計是因為 MNIST 的像素為 28×28=784,是以每一個輸入神經元對應于一個灰階像素點。機器之心執行該模型得到的效果非常好,該模型在批量大小為 100,并使用學習率衰減的情況下疊代 10000 步能得到 98.34% 的測試集準确度,以下是該模型代碼:

  1. import tensorflow as tf

  2. from tensorflow.examples.tutorials.mnist import input_data

  3. #加載MNIST資料集

  4. mnist = input_data.read_data_sets("./data/MNIST/", one_hot=True)

  5. INPUT_NODE = 784    

  6. OUTPUT_NODE = 10    

  7. LAYER1_NODE = 500        

  8. BATCH_SIZE = 100

  9. # 模型相關的參數

  10. LEARNING_RATE_BASE = 0.8      

  11. LEARNING_RATE_DECAY = 0.99    

  12. REGULARAZTION_RATE = 0.0001  

  13. TRAINING_STEPS = 10000        

  14. MOVING_AVERAGE_DECAY = 0.99

  15. def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):

  16.    # 使用滑動平均類

  17.    if avg_class == None:

  18.        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)

  19.        return tf.matmul(layer1, weights2) + biases2

  20.    else:

  21.        layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + avg_class.average(biases1))

  22.        return tf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)  

  23. def train(mnist):

  24.    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')

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

  26.    # 生成隐藏層的參數。

  27.    weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))

  28.    biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))

  29.    # 生成輸出層的參數。

  30.    weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))

  31.    biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))

  32.    # 計算不含滑動平均類的前向傳播結果

  33.    y = inference(x, None, weights1, biases1, weights2, biases2)

  34.    # 定義訓練輪數及相關的滑動平均類

  35.    global_step = tf.Variable(0, trainable=False)

  36.    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)

  37.    variables_averages_op = variable_averages.apply(tf.trainable_variables())

  38.    average_y = inference(x, variable_averages, weights1, biases1, weights2, biases2)

  39.    # 計算交叉熵及其平均值

  40.    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))

  41.    cross_entropy_mean = tf.reduce_mean(cross_entropy)

  42.    # 定義交叉熵損失函數加上正則項為模型損失函數

  43.    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)

  44.    regularaztion = regularizer(weights1) + regularizer(weights2)

  45.    loss = cross_entropy_mean + regularaztion

  46.    # 設定指數衰減的學習率。

  47.    learning_rate = tf.train.exponential_decay(

  48.        LEARNING_RATE_BASE,

  49.        global_step,

  50.        mnist.train.num_examples / BATCH_SIZE,

  51.        LEARNING_RATE_DECAY,

  52.        staircase=True)

  53.    # 随機梯度下降優化器優化損失函數

  54.    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)

  55.    # 反向傳播更新參數和更新每一個參數的滑動平均值

  56.    with tf.control_dependencies([train_step, variables_averages_op]):

  57.        train_op = tf.no_op(name='train')

  58.    # 計算準确度

  59.    correct_prediction = tf.equal(tf.argmax(average_y, 1), tf.argmax(y_, 1))

  60.    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

  61.    # 初始化會話并開始訓練過程。

  62.    with tf.Session() as sess:

  63.        tf.global_variables_initializer().run()

  64.        validate_feed = {x: mnist.validation.images, y_: mnist.validation.labels}

  65.        test_feed = {x: mnist.test.images, y_: mnist.test.labels}

  66.        # 循環地訓練神經網絡。

  67.        for i in range(TRAINING_STEPS):

  68.            if i % 1000 == 0:

  69.                validate_acc = sess.run(accuracy, feed_dict=validate_feed)

  70.                print("After %d training step(s), validation accuracy using average model is %g " % (i, validate_acc))

  71.            xs,ys=mnist.train.next_batch(BATCH_SIZE)

  72.            sess.run(train_op,feed_dict={x:xs,y_:ys})

  73.        test_acc=sess.run(accuracy,feed_dict=test_feed)

  74.        print(("After %d training step(s), test accuracy using average model is %g" %(TRAINING_STEPS, test_acc)))

該模型運作的結果如下:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

在上面定義的整個計算圖中,我們先加載資料并定義權重矩陣和模型,然後在計算損失值并傳遞給優化器來優化權重。模型在疊代次數設定之内會一直循環地計算損失函數的梯度以更新權重。

在上面的全連接配接神經網絡中,我們使用梯度下降優化器來優化權重。然而,TensorFlow 中還有很多優化器,最常用的是 GradientDescentOptimizer、AdamOptimizer 和 AdaGradOptimizer。

下面我們就需要建構卷積神經網絡了,不過在使用 TensorFlow 建構卷積網絡之前,我們需要了解一下 TensorFlow 中的函數

TensorFlow 包含很多操作和函數,很多我們需要花費大量精力完成的過程可以直接調用已封裝的函數,比如說「logits = tf.matmul(tf_train_dataset, weights) + biases」可以由函數「logits = tf.nn.xw_plus_b(train_dataset, weights, biases)」代替。

還有很多函數可以讓建構不同層級的神經網絡變得十分簡單。例如 conv_2d() 和 fully_connected() 函數分别建構了卷積層和全連接配接層。通過這些函數,層級的數量、濾波器的大小/深度、激活函數的類型等都可以明确地作為一個參數。權重矩陣和偏置向量能自動建立,附加激活函數和 dropout 正則化層同樣也能輕松建構。

如下所示為定義卷積層網絡的代碼:

  1. import tensorflow as tf

  2. w1 = tf.Variable(tf.truncated_normal([filter_size, filter_size, image_depth, filter_depth], stddev=0.1))

  3. b1 = tf.Variable(tf.zeros([filter_depth]))

  4. layer1_conv = tf.nn.conv2d(data, w1, [1, 1, 1, 1], padding='SAME')

  5. layer1_relu = tf.nn.relu(layer1_conv + b1)

  6. layer1_pool = tf.nn.max_pool(layer1_pool, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

它們可以使用簡單的函數來替代上面的定義:

  1. from tflearn.layers.conv import conv_2d, max_pool_2d

  2. layer1_conv = conv_2d(data, filter_depth, filter_size, activation='relu')

  3. layer1_pool = max_pool_2d(layer1_conv_relu, 2, strides=2)

正如我們前面所說的,我們并不需要定義權重、偏置和激活函數,特别是在定義多層神經網絡的時候,這一點讓我們的代碼可以看起來十分整潔。

2.4 建立 LeNet5 卷積網絡

LeNet5 卷積網絡架構最早是 Yann LeCun 提出來的,它是早期的一種卷積神經網絡,并且可以用來識别手寫數字。雖然它在 MNIST 資料集上執行地非常好,但在其它高分辨率和大資料集上性能有所降低。對于這些大資料集,像 AlexNet、VGGNet 或 ResNet 那樣的深度卷積網絡才執行地十分優秀。

因為 LeNet5 隻由 5 層網絡,是以它是學習如何建構卷積網絡的最佳起點。LeNet5 的架構如下:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

LeNet5 包含 5 層網絡:

  • 第一層:卷積層,該卷積層使用 Sigmoid 激活函數,并且在後面帶有平均池化層。
  • 第二層:卷積層,該卷積層使用 Sigmoid 激活函數,并且在後面帶有平均池化層。
  • 第三層:全連接配接層(使用 Sigmoid 激活函數)。
  • 第四層:全連接配接層(使用 Sigmoid 激活函數)。
  • 第五層:輸出層。

上面的 LeNet5 架構意味着我們需要建構 5 個權重和偏置項矩陣,我們模型的主體大概需要 12 行代碼完成(5 個神經網絡層級、2 個池化層、4 個激活函數還有 1 個 flatten 層)。因為代碼比較多,是以我們最好在計算圖之外就定義好獨立的函數:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

通過上面獨立定義的變量和模型,我們可以一點點調整資料流圖而不像前面的全連接配接網絡那樣。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡
機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡
機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

我們看到 Ahmet Taspinar 建構的 LeNet5 網絡要比他所訓練的全連接配接網絡在 MNIST 資料集上有更好的性能。但是在我們所訓練的全連接配接神經網絡中,因為使用了 ReLU、學習率指數衰減、滑動平均類和正則化等機制,我們的準确度達到了 98% 以上。

2.5 超參數如何影響一層網絡的輸出尺寸

一般來說,确實是層級越多神經網絡的性能就越好。我們可以添加更多的層級、更改激活函數和池化層、改變學習率并檢視每一步對性能的影響。因為層級 i 的輸出是層級 i+1 的輸入,是以我們需要知道第 i 層神經網絡的超參數如何影響其輸出尺寸。

為了了解這一點我們需要讨論一下 conv2d() 函數。

該函數有四個參數:

  • 輸入圖像,即一個四維張量 [batch size, image_width, image_height, image_depth]
  • 權重矩陣,即一個四維張量 [filter_size, filter_size, image_depth, filter_depth]
  • 每一個次元的步幅數
  • Padding (= 'SAME' / 'VALID')

這四個參數決定了輸出圖像的尺寸。

前面兩個參數都是四維張量,其包括了批量輸入圖像的資訊和卷積濾波器的權值。

第三個參數為卷積的步幅(stride),即卷積濾波器在 4 個次元中的每一次移動的距離。四個中間的第一個次元代表着圖像的批量數,這個次元肯定每次隻能移動一張圖檔。最後一個次元為圖檔深度(即色彩通道數,1 代表灰階圖檔,而 3 代表 RGB 圖檔),因為我們通常并不想跳過任何一個通道,是以這一個值也通常為 1。第二個和第三個次元代表 X 和 Y 方向(圖檔寬度和高度)的步幅。如果我們希望能應用步幅參數,我們需要設定每個次元的移動步幅。例如設定步幅為 1,那麼步幅參數就需要設定為 [1, 1, 1, 1],如果我們希望在圖像上移動的步幅設定為 2,步幅參數為 [1, 2, 2, 1]。

最後一個參數表明 TensorFlow 是否需要使用 0 來填補圖像周邊,這樣以確定圖像輸出尺寸在步幅參數設定為 1 的情況下保持不變。通過設定 padding = 'SAME',圖像會隻使用 0 來填補周邊(輸出尺寸不變),而 padding = 'VALID'則不會使用 0。

在下圖中,我們将看到兩個使用卷積濾波器在圖像上掃描的案例,其中濾波器的大小為 5 x 5、圖像的大小為 28 x 28。左邊的 Padding 參數設定為'SAME',并且最後四行/列的資訊也會包含在輸出圖像中。而右邊 padding 設定為 'VALID',最後四行/列是不包括在輸出圖像内的。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

沒有 padding 的圖檔,最後四個像素點是無法包含在内的,因為卷積濾波器已經移動到了圖檔的邊緣。這就意味着輸入 28 x 28 尺寸的圖檔,輸出尺寸隻有 24 x 24。如果 padding = 'SAME',那麼輸出尺寸就是 28 x 28。

如果我們輸入圖檔尺寸是 28 x 28、濾波器尺寸為 5 x 5,步幅分别設定為 1 到 4,那麼就能得到下表

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

對于任意給定的步幅 S、濾波器尺寸 K、圖像尺寸 W、padding 尺寸 P,輸出的圖像尺寸可以總結上表的規則如下:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

2.6 調整 LeNet5 架構

LeNet5 架構在原論文中使用的是 Sigmoid 激活函數和平均池化。然而如今神經網絡使用 ReLU 激活函數更為常見。是以我們可以修改一下 LeNet5 架構,并看看是否能獲得性能上的提升,我們可以稱這種修改的架構為類 LeNet5 架構。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

最大的不同是我們使用 ReLU 激活函數代替 Sigmoid 激活函數。除了激活函數意外,我們還修改了優化器,因為我們可以看到不同優化器對識别準确度的影響。在這裡,機器之心在 CIFAR-10 上使用該修正的 LeNet 進行了訓練,詳細代碼如下。機器之心訓練的準确度并不高,可能是學習率、批量數或者其他設定有些問題,也可能是 LeNet 對于三通道的圖太簡單了。該運作結果展現在機器之心該項目的 Github 中,感興趣的讀者可以進一步修正該模型以期望達到更好的效果。

  1. LENET5_LIKE_BATCH_SIZE = 32

  2. LENET5_LIKE_FILTER_SIZE = 5

  3. LENET5_LIKE_FILTER_DEPTH = 16

  4. LENET5_LIKE_NUM_HIDDEN = 120

  5. def variables_lenet5_like(filter_size = LENET5_LIKE_FILTER_SIZE,

  6.                          filter_depth = LENET5_LIKE_FILTER_DEPTH,

  7.                          num_hidden = LENET5_LIKE_NUM_HIDDEN,

  8.                          image_width = 32, image_height = 32, image_depth = 3, num_labels = 10):

  9.    w1 = tf.Variable(tf.truncated_normal([filter_size, filter_size, image_depth, filter_depth], stddev=0.1))

  10.    b1 = tf.Variable(tf.zeros([filter_depth]))

  11.    w2 = tf.Variable(tf.truncated_normal([filter_size, filter_size, filter_depth, filter_depth], stddev=0.1))

  12.    b2 = tf.Variable(tf.constant(1.0, shape=[filter_depth]))

  13.    w3 = tf.Variable(tf.truncated_normal([(image_width // 4)*(image_height // 4)*filter_depth , num_hidden], stddev=0.1))

  14.    b3 = tf.Variable(tf.constant(1.0, shape = [num_hidden]))

  15.    w4 = tf.Variable(tf.truncated_normal([num_hidden, num_hidden], stddev=0.1))

  16.    b4 = tf.Variable(tf.constant(1.0, shape = [num_hidden]))

  17.    w5 = tf.Variable(tf.truncated_normal([num_hidden, num_labels], stddev=0.1))

  18.    b5 = tf.Variable(tf.constant(1.0, shape = [num_labels]))

  19.    variables = {

  20.        'w1': w1, 'w2': w2, 'w3': w3, 'w4': w4, 'w5': w5,

  21.        'b1': b1, 'b2': b2, 'b3': b3, 'b4': b4, 'b5': b5

  22.    }

  23.    return variables

  24. def model_lenet5_like(data, variables):

  25.    layer1_conv = tf.nn.conv2d(data, variables['w1'], [1, 1, 1, 1], padding='SAME')

  26.    layer1_actv = tf.nn.relu(layer1_conv + variables['b1'])

  27.    layer1_pool = tf.nn.avg_pool(layer1_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

  28.    layer2_conv = tf.nn.conv2d(layer1_pool, variables['w2'], [1, 1, 1, 1], padding='SAME')

  29.    layer2_actv = tf.nn.relu(layer2_conv + variables['b2'])

  30.    layer2_pool = tf.nn.avg_pool(layer2_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

  31.    flat_layer = flatten_tf_array(layer2_pool)

  32.    layer3_fccd = tf.matmul(flat_layer, variables['w3']) + variables['b3']

  33.    layer3_actv = tf.nn.relu(layer3_fccd)

  34.    layer3_drop = tf.nn.dropout(layer3_actv, 0.5)

  35.    layer4_fccd = tf.matmul(layer3_actv, variables['w4']) + variables['b4']

  36.    layer4_actv = tf.nn.relu(layer4_fccd)

  37.    layer4_drop = tf.nn.dropout(layer4_actv, 0.5)

  38.    logits = tf.matmul(layer4_actv, variables['w5']) + variables['b5']

  39.    return logits

  40. num_steps = 10001

  41. display_step = 1000

  42. learning_rate = 0.001

  43. batch_size = 16

  44. #定義資料的基本資訊,傳入變量

  45. image_width = 32

  46. image_height = 32

  47. image_depth = 3

  48. num_labels = 10

  49. test_dataset = test_dataset_cifar10

  50. test_labels = test_labels_cifar10

  51. train_dataset = train_dataset_cifar10

  52. train_labels = train_labels_cifar10

  53. graph = tf.Graph()

  54. with graph.as_default():

  55.    #1 首先使用占位符定義資料變量的次元

  56.    tf_train_dataset = tf.placeholder(tf.float32, shape=(batch_size, image_width, image_height, image_depth))

  57.    tf_train_labels = tf.placeholder(tf.float32, shape = (batch_size, num_labels))

  58.    tf_test_dataset = tf.constant(test_dataset, tf.float32)

  59.    #2 然後初始化權重矩陣和偏置向量

  60.    variables = variables_lenet5_like(image_width = image_width, image_height=image_height, image_depth = image_depth, num_labels = num_labels)

  61.    #3 使用模型計算分類

  62.    logits = model_lenet5_like(tf_train_dataset, variables)

  63.    #4 使用帶softmax的交叉熵函數計算預測标簽和真實标簽之間的損失函數

  64.    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=tf_train_labels))

  65.    #5  采用Adam優化算法優化上一步定義的損失函數,給定學習率

  66.    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)

  67.         # 執行預測推斷

  68.    train_prediction = tf.nn.softmax(logits)

  69.    test_prediction = tf.nn.softmax(model_lenet5_like(tf_test_dataset, variables))

  70. with tf.Session(graph=graph) as session:

  71.    #初始化全部變量

  72.    tf.global_variables_initializer().run()

  73.    print('Initialized with learning_rate', learning_rate)

  74.    for step in range(num_steps):

  75.        offset = (step * batch_size) % (train_labels.shape[0] - batch_size)

  76.        batch_data = train_dataset[offset:(offset + batch_size), :, :, :]

  77.        batch_labels = train_labels[offset:(offset + batch_size), :]

  78.        #在每一次批量中,擷取目前的訓練資料,并傳入feed_dict以饋送到占位符中

  79.        feed_dict = {tf_train_dataset : batch_data, tf_train_labels : batch_labels}

  80.        _, l, predictions = session.run([optimizer, loss, train_prediction], feed_dict=feed_dict)

  81.        train_accuracy = accuracy(predictions, batch_labels)

  82.        if step % display_step == 0:

  83.            test_accuracy = accuracy(test_prediction.eval(), test_labels)

  84.            message = "step {:04d} : loss is {:06.2f}, accuracy on training set {:02.2f} %, accuracy on test set {:02.2f} %".format(step, l, train_accuracy, test_accuracy)

  85.            print(message)

2.7 學習率和優化器的影響

我們可以在下圖看到這些 CNN 在 MNIST 和 CIFAR-10 資料集上的性能。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

上圖展示了模型在兩個測試集上的準确度和疊代次數,其代表的模型從左至右分别為全連接配接神經網絡、LeNet5 和 改進後的 LeNet5。不過由于 MNIST 太簡單,全連接配接網絡也能做得挺好。不過在 CIFAR-10 資料集中,全連接配接網絡的性能明顯下降了不少。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

上圖展示了三種神經網絡在 CIFAR-10 資料集上使用不同的優化器而得出的性能。可能 L2 正則化和指數衰減學習率能進一步提高模型的性能,不過要獲得更大的提升,我們需要使用深度神經網絡。

TensorFlow 中的深度神經網絡

LeNet5 由兩個卷積層加上三個全連接配接層組成,是以它是一種淺層神經網絡。下面我們将了解其它卷積神經網絡,它們的層級更多,是以可以稱為深度神經網絡。下面介紹的深度卷積神經網絡我們并沒有根據 Ahmet Taspinar 提供的代碼進行實踐,因為我們暫時安裝的是 TensorFlow 的 CPU 版,而使用 CPU 訓練前面的 LeNet 就已經十分吃力了,是以我們暫時沒有實作這幾個深度 CNN。我們将會在後面實作它們,并将修改的代碼上傳到機器之心的 Github 中。

卷積神經網絡最出名的就是 2012 年所提出的 AlexNet、2013 年的 7 層 ZF-Net 和 2014 年提出的 16 層 VGGNet。到了 2015 年,谷歌通過 Inception 子產品開發出 22 層的卷積神經網絡(GoogLeNet),而微軟亞洲研究院創造出了 152 層的卷積神經網絡:ResNet。

下面,我們将學習如何使用 TensofFlow 建構 AlexNet 和 VGGNet16。

3.1 AlexNet

AlexNet 是由 Alex Krizhevsky 和 Geoffrey Hinton 等人提出來的,雖然相對于現在的卷積神經網絡來說它的架構十分簡單,但當時它是十分成功的一個模型。它赢得了當年的 ImageNet 挑戰賽,并開啟了深度學習和 AI 的變革。下面是 AlexNet 的基本架構:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

AlexNet 包含 5 個卷積層(帶有 ReLU 激活函數)、3 個最大池化層、3 個全連接配接層和兩個 dropout 層。該神經網絡的架構概覽如下:

  • 層級 0:規格為 224 x 224 x 3 的輸入圖檔。
  • 層級 1:帶有 96 個濾波器(filter_depth_1 = 96)的卷積層,濾波器的尺寸為 11 x 11(filter_size_1 = 11)、步幅為 4。該層的神經網絡使用 ReLU 激活函數,并且後面帶有最大池化層和局部響應歸一化層。
  • 層級 2:帶有 256 個濾波器(filter_depth_2 = 256)的卷積層,濾波器的尺寸為 5 x 5(filter_size_2 = 5)、步幅為 1。該層的神經網絡使用 ReLU 激活函數,并且後面帶有最大池化層和局部響應歸一化層。
  • 層級 3:帶有 384 個濾波器(filter_depth_3 = 384)的卷積層,濾波器的尺寸為 3 x 3(filter_size_3 = 3)、步幅為 1。該層的神經網絡使用 ReLU 激活函數。
  • 層級 4 和層級 3 的結構是一樣的。
  • 層級 5:帶有 256 個濾波器(filter_depth_4 = 256)的卷積層,濾波器的尺寸為 3 x 3(filter_size_4 = 3)、步幅為 1。該層的神經網絡使用 ReLU 激活函數。
  • 層級 6-8:這幾個卷積層每一個後面跟着一個全連接配接層,每一層有 4096 個神經元。在原論文中,他們是為了 1000 個類别的分類,當我們這邊并不需要這麼多。

注意 AlexNet 或其他深度 CNN 并不能使用 MNIST 或者 CIFAR-10 資料集,因為這些圖檔的分辨率太小。正如我們所看到的,池化層(或者步幅為 2 的卷積層)減少了兩倍的圖像大小。AlexNet 有 3 個最大池化層和一個步幅為 4 的卷積層,這就意味着原圖檔會被縮小很多倍,而 MNIST 資料集的圖像尺寸太小而不能進行着一系列操作。

是以,我們需要加載有更高像素圖像的資料集,最好是和原論文一樣采用 224 x 224 x 3。aka oxflower17 資料集可能是比較理想的資料集,它含有 17 種花的圖檔,并且像素正好是我們所需要的:

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

下面,我們可以定義 AlexNet 中的權重矩陣和不同的層級。正如我們前面所看到的,我們需要定義很多權重矩陣和偏置向量,并且它們還需要和每一層的濾波器尺寸保持一緻。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

3.2 VGGNet-16

VGGNet 比 AlexNet 擁有的層級更多(16-19 層),但是每一層的設計都簡單了許多,所有層的濾波器大小都是 3 x 3、步幅都是 1,而所有的最大池化層的步幅都是 2。是以它雖然是一種深度 CNN,但結構比較簡單。

VGGNet 有 16 層或 19 層兩種配置,如下所示,這兩種配置的不同之處在于它在第二個、第三個和第四個最大池化層後面到底是采用三個卷積層還是四個卷積層。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡
機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡
機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

上面已經為大家介紹了卷積神經網絡,我們從 TensorFlow 的安裝與基礎概念、簡單的全連接配接神經網絡、資料的下載下傳與導入、在 MNIST 上訓練全連接配接神經網絡、在 CIFAR-10 上訓練經過修正的 LeNet 還有深度卷積神經網絡等方面向大家介紹了神經網絡,機器之心本文所有實驗的代碼、結果以及代碼注釋都将在 Github 上開放,這也是機器之心第一次試驗性地向大家介紹教程以及實作。我們希望在為讀者提供教程的同時也提供實際操作的經驗,希望能為大家學習該教程起到積極的作用。

機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡

參考部落格:

http://ataspinar.com/2017/08/15/building-convolutional-neural-networks-with-tensorflow/