機器之心GitHub專案:從零開始用TensorFlow搭建卷積神經網路

思源發表於2017-08-29
機器之心基於 Ahmet Taspinar 的博文使用 TensorFlow 手動搭建卷積神經網路,並提供所有程式碼和註釋的 Jupyter Notebook 文件。我們將不僅描述訓練情況,同時還將提供各種背景知識和分析。所有的程式碼和執行結果都已上傳至 Github,機器之心希望通過我們的試驗提供精確的程式碼和執行經驗,我們將持續試驗這一類高質量的教程和程式碼。

機器之心專案地址: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搭建卷積神經網路


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

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

graph = tf.Graph()
with graph.as_default():
a = tf.Variable(8, tf.float32)
b = tf.Variable(tf.zeros([2,2], tf.float32))

with tf.Session(graph=graph) as session:
tf.global_variables_initializer().run()
print(session.run(a))
print(session.run(b))

#輸出:
>>> 8
>>> [[ 0. 0.]
>>> [ 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)


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

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

a=tf.constant([1,2,3,4])
b=tf.constant([1,2,3,4])
result=a+b
sess=tf.Session()
print(sess.run(result))
sess.close

#輸出 [2 4 6 8]

with tf.Session() as sess:
   a=tf.constant([1,2,3,4])
   b=tf.constant([1,2,3,4])
   result=a+b
   print(sess.run(result))

#輸出 [2 4 6 8]

1.2 常量和變數

TensorFlow 中最基本的單位是常量(Constant)、變數(Variable)和佔位符(Placeholder)。常量定義後值和維度不可變,變數定義後值可變而維度不可變。在神經網路中,變數一般可作為儲存權重和其他資訊的矩陣,而常量可作為儲存超引數或其他結構資訊的變數。下面我們分別定義了常量與變數:

 a = tf.constant(2, tf.int16)
b = tf.constant(4, tf.float32)
c = tf.constant(8, tf.float32)

d = tf.Variable(2, tf.int16)
e = tf.Variable(4, tf.float32)
f = tf.Variable(8, tf.float32)



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


h = tf.zeros([11], tf.int16)
i = tf.ones([2,2], tf.float32)
j = tf.zeros([1000,4,3], tf.float64)

k = tf.Variable(tf.zeros([2,2], tf.float32))
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] 範圍內的隨機數。

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

weights = tf.Variable(tf.truncated_normal([256 * 256, 10]))
biases = tf.Variable(tf.zeros([10]))
print(weights.get_shape().as_list())
print(biases.get_shape().as_list())
#輸出
>>>[65536, 10]
>>>[10]


1.3 佔位符和 feed_dict

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

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

下面一段程式碼分別展示了使用常量和佔位符進行計算:

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

#因為需要重複輸入x,而每建一個x就會生成一個結點,計算圖的效率會低。所以使用佔位符
x=tf.placeholder(tf.float32,shape=(1,2))
x1=tf.constant([[0.7,0.9]])

a=x+w1
b=x1+w1

sess=tf.Session()
sess.run(tf.global_variables_initializer())

#執行y時將佔位符填上,feed_dict為字典,變數名不可變
y_1=sess.run(a,feed_dict={x:[[0.7,0.9]]})
y_2=sess.run(b)
print(y_1)
print(y_2)
sess.close

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

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

list_of_points1_ = [[1,2], [3,4], [5,6], [7,8]]
list_of_points2_ = [[15,16], [13,14], [11,12], [9,10]]

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


graph = tf.Graph()

with graph.as_default():  

   #我們使用 tf.placeholder() 建立佔位符 ,在 session.run() 過程中再投遞資料
   point1 = tf.placeholder(tf.float32, shape=(1, 2))
   point2 = tf.placeholder(tf.float32, shape=(1, 2))

   def calculate_eucledian_distance(point1, point2):
       difference = tf.subtract(point1, point2)
       power2 = tf.pow(difference, tf.constant(2.0, shape=(1,2)))
       add = tf.reduce_sum(power2)
       eucledian_distance = tf.sqrt(add)
       return eucledian_distance

   dist = calculate_eucledian_distance(point1, point2)


with tf.Session(graph=graph) as session:
   tf.global_variables_initializer().run()  
   for ii in range(len(list_of_points1)):
       point1_ = list_of_points1[ii]
       point2_ = list_of_points2[ii]

       #使用feed_dict將資料投入到[dist]中
       feed_dict = {point1 : point1_, point2 : point2_}
       distance = session.run([dist], feed_dict=feed_dict)
       print("the distance between {} and {} -> {}".format(point1_, point2_, distance))


#輸出:
>>> the distance between [[1 2]] and [[15 16]] -> [19.79899]
>>> the distance between [[3 4]] and [[13 14]] -> [14.142136]
>>> the distance between [[5 6]] and [[11 12]] -> [8.485281]
>>> the distance between [[7 8]] and [[ 9 10]] -> [2.8284271]

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

import tensorflow as tf
from numpy.random import RandomState


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


# None 可以根據batch 大小確定維度,在shape的一個維度上使用None
x=tf.placeholder(tf.float32,shape=(None,2))
y=tf.placeholder(tf.float32,shape=(None,1))


#啟用函式使用ReLU
a=tf.nn.relu(tf.matmul(x,w1))
yhat=tf.nn.relu(tf.matmul(a,w2))


#定義交叉熵為損失函式,訓練過程使用Adam演算法最小化交叉熵
cross_entropy=-tf.reduce_mean(y*tf.log(tf.clip_by_value(yhat,1e-10,1.0)))
train_step=tf.train.AdamOptimizer(0.001).minimize(cross_entropy)

rdm=RandomState(1)
data_size=516


#生成兩個特徵,共data_size個樣本
X=rdm.rand(data_size,2)
#定義規則給出樣本標籤,所有x1+x2<1的樣本認為是正樣本,其他為負樣本。Y,1為正樣本
Y = [[int(x1+x2 < 1)] for (x1, x2) in X]

with tf.Session() as sess:
   sess.run(tf.global_variables_initializer())
   print(sess.run(w1))
   print(sess.run(w2))
   steps=11000
   for i in range(steps):


       #選定每一個批量讀取的首尾位置,確保在1個epoch內取樣訓練
       start = i * batch_size % data_size
       end = min(start + batch_size,data_size)
       sess.run(train_step,feed_dict={x:X[start:end],y:Y[start:end]})
       if i % 1000 == 0:
           training_loss= sess.run(cross_entropy,feed_dict={x:X,y:Y})
           print("在迭代 %d 次後,訓練損失為 %g"%(i,training_loss))

上面的程式碼定義了一個簡單的三層全連線網路(輸入層、隱藏層和輸出層分別為 2、3 和 1 個神經元),隱藏層和輸出層的啟用函式使用的是 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搭建卷積神經網路


首先,讓我們定義一些函式,它們能幫助我們載入和預處理影象資料。

def randomize(dataset, labels):
    permutation = np.random.permutation(labels.shape[0])
    shuffled_dataset = dataset[permutation, :, :]
    shuffled_labels = labels[permutation]
    return shuffled_dataset, shuffled_labels

def one_hot_encode(np_array):
    return (np.arange(10) == np_array[:,None]).astype(np.float32)

def reformat_data(dataset, labels, image_width, image_height, image_depth):
    np_dataset_ = np.array([np.array(image_data).reshape(image_width, image_height, image_depth) for image_data in dataset])
    np_labels_ = one_hot_encode(np.array(labels, dtype=np.float32))
    np_dataset, np_labels = randomize(np_dataset_, np_labels_)
    return np_dataset, np_labels

def flatten_tf_array(array):
    shape = array.get_shape().as_list()
    return tf.reshape(array, [shape[0], shape[1] * shape[2] * shape[3]])

def accuracy(predictions, labels):
    return (100.0 * np.sum(np.argmax(predictions, 1) == np.argmax(labels, 1)) / predictions.shape[0])def randomize(dataset, labels):
    permutation = np.random.permutation(labels.shape[0])
    shuffled_dataset = dataset[permutation, :, :]
    shuffled_labels = labels[permutation]
    return shuffled_dataset, shuffled_labels

def one_hot_encode(np_array):
    return (np.arange(10) == np_array[:,None]).astype(np.float32)

def reformat_data(dataset, labels, image_width, image_height, image_depth):
    np_dataset_ = np.array([np.array(image_data).reshape(image_width, image_height, image_depth) for image_data in dataset])
    np_labels_ = one_hot_encode(np.array(labels, dtype=np.float32))
    np_dataset, np_labels = randomize(np_dataset_, np_labels_)
    return np_dataset, np_labels

def flatten_tf_array(array):
    shape = array.get_shape().as_list()
    return tf.reshape(array, [shape[0], shape[1] * shape[2] * shape[3]])

def accuracy(predictions, labels):
    return (100.0 * np.sum(np.argmax(predictions, 1) == np.argmax(labels, 1)) / predictions.shape[0])

影象的標籤使用 one-hot 編碼,並且將資料載入到隨機陣列中。在定義這些函式後,我們可以載入資料:

機器之心GitHub專案:從零開始用TensorFlow搭建卷積神經網路


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

  • MNIST 資料集:http://yann.lecun.com/exdb/mnist/
  • python-mnist 工具:https://github.com/sorki/python-mnist
  • CIFAR-10 資料集:https://www.cs.toronto.edu/~kriz/cifar.html


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

機器之心GitHub專案:從零開始用TensorFlow搭建卷積神經網路

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

機器之心GitHub專案:從零開始用TensorFlow搭建卷積神經網路


我們需要再次匯入 CIFAR-10 資料集,這一段程式碼也會出錯,原因是有變數沒有定義。下面程式碼將匯入資料集:

cifar10_folder = './data/cifar10/'
train_datasets = ['data_batch_1', 'data_batch_2', 'data_batch_3', 'data_batch_4', 'data_batch_5', ]
test_dataset = ['test_batch']

c10_image_height = 32
c10_image_width = 32
c10_image_depth = 3
c10_num_labels = 10
c10_image_size = 32 #Ahmet Taspinar的程式碼缺少了這一語句


with open(cifar10_folder + test_dataset[0], 'rb') as f0:
   c10_test_dict = pickle.load(f0, encoding='bytes')

c10_test_dataset, c10_test_labels = c10_test_dict[b'data'], c10_test_dict[b'labels']
test_dataset_cifar10, test_labels_cifar10 = reformat_data(c10_test_dataset, c10_test_labels, c10_image_size, c10_image_size, c10_image_depth)

c10_train_dataset, c10_train_labels = [], []

for train_dataset in train_datasets:
   with open(cifar10_folder + train_dataset, 'rb') as f0:
       c10_train_dict = pickle.load(f0, encoding='bytes')
       c10_train_dataset_, c10_train_labels_ = c10_train_dict[b'data'], c10_train_dict[b'labels']

       c10_train_dataset.append(c10_train_dataset_)
       c10_train_labels += c10_train_labels_

c10_train_dataset = np.concatenate(c10_train_dataset, axis=0)
train_dataset_cifar10, train_labels_cifar10 = reformat_data(c10_train_dataset, c10_train_labels, c10_image_size, c10_image_size, c10_image_depth)
del c10_train_dataset
del c10_train_labels



print("訓練集包含以下標籤: {}".format(np.unique(c10_train_dict[b'labels'])))
print('訓練集維度', train_dataset_cifar10.shape, train_labels_cifar10.shape)
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% 的測試集準確度,以下是該模型程式碼:

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data



#載入MNIST資料集
mnist = input_data.read_data_sets("./data/MNIST/", one_hot=True)


INPUT_NODE = 784    
OUTPUT_NODE = 10    
LAYER1_NODE = 500        

BATCH_SIZE = 100

       

# 模型相關的引數
LEARNING_RATE_BASE = 0.8      
LEARNING_RATE_DECAY = 0.99    
REGULARAZTION_RATE = 0.0001  
TRAINING_STEPS = 10000        
MOVING_AVERAGE_DECAY = 0.99



def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
   # 使用滑動平均類
   if avg_class == None:
       layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
       return tf.matmul(layer1, weights2) + biases2

   else:

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


def train(mnist):
   x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
   y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')

   # 生成隱藏層的引數。
   weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
   biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))

   # 生成輸出層的引數。
   weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
   biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))


   # 計算不含滑動平均類的前向傳播結果
   y = inference(x, None, weights1, biases1, weights2, biases2)


   # 定義訓練輪數及相關的滑動平均類
   global_step = tf.Variable(0, trainable=False)
   variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
   variables_averages_op = variable_averages.apply(tf.trainable_variables())
   average_y = inference(x, variable_averages, weights1, biases1, weights2, biases2)

   # 計算交叉熵及其平均值
   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)

   # 定義交叉熵損失函式加上正則項為模型損失函式
   regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
   regularaztion = regularizer(weights1) + regularizer(weights2)
   loss = cross_entropy_mean + regularaztion

   # 設定指數衰減的學習率。
   learning_rate = tf.train.exponential_decay(
       LEARNING_RATE_BASE,
       global_step,
       mnist.train.num_examples / BATCH_SIZE,
       LEARNING_RATE_DECAY,
       staircase=True)

   # 隨機梯度下降優化器優化損失函式
   train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)

   # 反向傳播更新引數和更新每一個引數的滑動平均值
   with tf.control_dependencies([train_step, variables_averages_op]):
       train_op = tf.no_op(name='train')

   # 計算準確度
   correct_prediction = tf.equal(tf.argmax(average_y, 1), tf.argmax(y_, 1))
   accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

   # 初始化會話並開始訓練過程。
   with tf.Session() as sess:
       tf.global_variables_initializer().run()
       validate_feed = {x: mnist.validation.images, y_: mnist.validation.labels}
       test_feed = {x: mnist.test.images, y_: mnist.test.labels}

       # 迴圈地訓練神經網路。
       for i in range(TRAINING_STEPS):
           if i % 1000 == 0:
               validate_acc = sess.run(accuracy, feed_dict=validate_feed)
               print("After %d training step(s), validation accuracy using average model is %g " % (i, validate_acc))

           xs,ys=mnist.train.next_batch(BATCH_SIZE)
           sess.run(train_op,feed_dict={x:xs,y_:ys})

       test_acc=sess.run(accuracy,feed_dict=test_feed)
       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 正則化層同樣也能輕鬆構建。

如下所示為定義卷積層網路的程式碼:

import tensorflow as tf



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


layer1_conv = tf.nn.conv2d(data, w1, [1, 1, 1, 1], padding='SAME')
layer1_relu = tf.nn.relu(layer1_conv + b1)
layer1_pool = tf.nn.max_pool(layer1_pool, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

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

from tflearn.layers.conv import conv_2d, max_pool_2d



layer1_conv = conv_2d(data, filter_depth, filter_size, activation='relu')
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 層)。因為程式碼比較多,所以我們最好在計算圖之外就定義好獨立的函式:

LENET5_BATCH_SIZE = 32
LENET5_PATCH_SIZE = 5
LENET5_PATCH_DEPTH_1 = 6
LENET5_PATCH_DEPTH_2 = 16
LENET5_NUM_HIDDEN_1 = 120
LENET5_NUM_HIDDEN_2 = 84

def variables_lenet5(patch_size = LENET5_PATCH_SIZE, patch_depth1 = LENET5_PATCH_DEPTH_1, 
                     patch_depth2 = LENET5_PATCH_DEPTH_2, 
                     num_hidden1 = LENET5_NUM_HIDDEN_1, num_hidden2 = LENET5_NUM_HIDDEN_2,
                     image_depth = 1, num_labels = 10):
    
    w1 = tf.Variable(tf.truncated_normal([patch_size, patch_size, image_depth, patch_depth1], stddev=0.1))
    b1 = tf.Variable(tf.zeros([patch_depth1]))

    w2 = tf.Variable(tf.truncated_normal([patch_size, patch_size, patch_depth1, patch_depth2], stddev=0.1))
    b2 = tf.Variable(tf.constant(1.0, shape=[patch_depth2]))

    w3 = tf.Variable(tf.truncated_normal([5*5*patch_depth2, num_hidden1], stddev=0.1))
    b3 = tf.Variable(tf.constant(1.0, shape = [num_hidden1]))

    w4 = tf.Variable(tf.truncated_normal([num_hidden1, num_hidden2], stddev=0.1))
    b4 = tf.Variable(tf.constant(1.0, shape = [num_hidden2]))
    
    w5 = tf.Variable(tf.truncated_normal([num_hidden2, num_labels], stddev=0.1))
    b5 = tf.Variable(tf.constant(1.0, shape = [num_labels]))
    variables = {
        'w1': w1, 'w2': w2, 'w3': w3, 'w4': w4, 'w5': w5,
        'b1': b1, 'b2': b2, 'b3': b3, 'b4': b4, 'b5': b5
    }
    return variables

def model_lenet5(data, variables):
    layer1_conv = tf.nn.conv2d(data, variables['w1'], [1, 1, 1, 1], padding='SAME')
    layer1_actv = tf.sigmoid(layer1_conv + variables['b1'])
    layer1_pool = tf.nn.avg_pool(layer1_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

    layer2_conv = tf.nn.conv2d(layer1_pool, variables['w2'], [1, 1, 1, 1], padding='VALID')
    layer2_actv = tf.sigmoid(layer2_conv + variables['b2'])
    layer2_pool = tf.nn.avg_pool(layer2_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

    flat_layer = flatten_tf_array(layer2_pool)
    layer3_fccd = tf.matmul(flat_layer, variables['w3']) + variables['b3']
    layer3_actv = tf.nn.sigmoid(layer3_fccd)
    
    layer4_fccd = tf.matmul(layer3_actv, variables['w4']) + variables['b4']
    layer4_actv = tf.nn.sigmoid(layer4_fccd)
    logits = tf.matmul(layer4_actv, variables['w5']) + variables['b5']
    return logits

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

#parameters determining the model size
image_size = mnist_image_size
num_labels = mnist_num_labels

#the datasets
train_dataset = mnist_train_dataset
train_labels = mnist_train_labels
test_dataset = mnist_test_dataset
test_labels = mnist_test_labels

#number of iterations and learning rate
num_steps = 10001
display_step = 1000
learning_rate = 0.001

graph = tf.Graph()
with graph.as_default():
    #1) First we put the input data in a tensorflow friendly form. 
    tf_train_dataset = tf.placeholder(tf.float32, shape=(batch_size, image_width, image_height, image_depth))
    tf_train_labels = tf.placeholder(tf.float32, shape = (batch_size, num_labels))
    tf_test_dataset = tf.constant(test_dataset, tf.float32)

    #2) Then, the weight matrices and bias vectors are initialized
    <strong>variables = variables_lenet5(image_depth = image_depth, num_labels = num_labels)</strong>

    #3. The model used to calculate the logits (predicted labels)
    <strong>model = model_lenet5</strong>
    <strong>logits = model(tf_train_dataset, variables)</strong>

    #4. then we compute the softmax cross entropy between the logits and the (actual) labels
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=tf_train_labels))
    
    #5. The optimizer is used to calculate the gradients of the loss function 
    optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)

    # Predictions for the training, validation, and test data.
    train_prediction = tf.nn.softmax(logits)
    test_prediction = tf.nn.softmax(model(tf_test_dataset, variables))
with tf.Session(graph=graph) as session:
    tf.global_variables_initializer().run()
    print('Initialized with learning_rate', learning_rate)
    for step in range(num_steps):

        #Since we are using stochastic gradient descent, we are selecting  small batches from the training dataset,
        #and training the convolutional neural network each time with a batch. 
        offset = (step * batch_size) % (train_labels.shape[0] - batch_size)
        batch_data = train_dataset[offset:(offset + batch_size), :, :, :]
        batch_labels = train_labels[offset:(offset + batch_size), :]
        feed_dict = {tf_train_dataset : batch_data, tf_train_labels : batch_labels}
        _, l, predictions = session.run([optimizer, loss, train_prediction], feed_dict=feed_dict)
        
        if step % display_step == 0:
            train_accuracy = accuracy(predictions, batch_labels)
            test_accuracy = accuracy(test_prediction.eval(), test_labels)
            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)
            print(message)

機器之心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 中,感興趣的讀者可以進一步修正該模型以期望達到更好的效果。

LENET5_LIKE_BATCH_SIZE = 32
LENET5_LIKE_FILTER_SIZE = 5
LENET5_LIKE_FILTER_DEPTH = 16
LENET5_LIKE_NUM_HIDDEN = 120


def variables_lenet5_like(filter_size = LENET5_LIKE_FILTER_SIZE,
                         filter_depth = LENET5_LIKE_FILTER_DEPTH,
                         num_hidden = LENET5_LIKE_NUM_HIDDEN,
                         image_width = 32, image_height = 32, image_depth = 3, num_labels = 10):

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

   w2 = tf.Variable(tf.truncated_normal([filter_size, filter_size, filter_depth, filter_depth], stddev=0.1))
   b2 = tf.Variable(tf.constant(1.0, shape=[filter_depth]))

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

   w4 = tf.Variable(tf.truncated_normal([num_hidden, num_hidden], stddev=0.1))
   b4 = tf.Variable(tf.constant(1.0, shape = [num_hidden]))

   w5 = tf.Variable(tf.truncated_normal([num_hidden, num_labels], stddev=0.1))
   b5 = tf.Variable(tf.constant(1.0, shape = [num_labels]))
   variables = {
       'w1': w1, 'w2': w2, 'w3': w3, 'w4': w4, 'w5': w5,
       'b1': b1, 'b2': b2, 'b3': b3, 'b4': b4, 'b5': b5
   }
   return variables



def model_lenet5_like(data, variables):
   layer1_conv = tf.nn.conv2d(data, variables['w1'], [1, 1, 1, 1], padding='SAME')
   layer1_actv = tf.nn.relu(layer1_conv + variables['b1'])
   layer1_pool = tf.nn.avg_pool(layer1_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

   layer2_conv = tf.nn.conv2d(layer1_pool, variables['w2'], [1, 1, 1, 1], padding='SAME')
   layer2_actv = tf.nn.relu(layer2_conv + variables['b2'])
   layer2_pool = tf.nn.avg_pool(layer2_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

   flat_layer = flatten_tf_array(layer2_pool)
   layer3_fccd = tf.matmul(flat_layer, variables['w3']) + variables['b3']
   layer3_actv = tf.nn.relu(layer3_fccd)
   layer3_drop = tf.nn.dropout(layer3_actv, 0.5)

   layer4_fccd = tf.matmul(layer3_actv, variables['w4']) + variables['b4']
   layer4_actv = tf.nn.relu(layer4_fccd)
   layer4_drop = tf.nn.dropout(layer4_actv, 0.5)

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



num_steps = 10001
display_step = 1000
learning_rate = 0.001
batch_size = 16


#定義資料的基本資訊,傳入變數
image_width = 32
image_height = 32
image_depth = 3
num_labels = 10


test_dataset = test_dataset_cifar10
test_labels = test_labels_cifar10
train_dataset = train_dataset_cifar10
train_labels = train_labels_cifar10




graph = tf.Graph()

with graph.as_default():

   #1 首先使用佔位符定義資料變數的維度
   tf_train_dataset = tf.placeholder(tf.float32, shape=(batch_size, image_width, image_height, image_depth))
   tf_train_labels = tf.placeholder(tf.float32, shape = (batch_size, num_labels))
   tf_test_dataset = tf.constant(test_dataset, tf.float32)


   #2 然後初始化權重矩陣和偏置向量
   variables = variables_lenet5_like(image_width = image_width, image_height=image_height, image_depth = image_depth, num_labels = num_labels)



   #3 使用模型計算分類
   logits = model_lenet5_like(tf_train_dataset, variables)


   #4 使用帶softmax的交叉熵函式計算預測標籤和真實標籤之間的損失函式
   loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=tf_train_labels))


   #5  採用Adam優化演算法優化上一步定義的損失函式,給定學習率
   optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)


   
   
# 執行預測推斷
   train_prediction = tf.nn.softmax(logits)
   test_prediction = tf.nn.softmax(model_lenet5_like(tf_test_dataset, variables))


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

   #初始化全部變數
   tf.global_variables_initializer().run()
   print('Initialized with learning_rate', learning_rate)
   for step in range(num_steps):
       offset = (step * batch_size) % (train_labels.shape[0] - batch_size)
       batch_data = train_dataset[offset:(offset + batch_size), :, :, :]
       batch_labels = train_labels[offset:(offset + batch_size), :]

       #在每一次批量中,獲取當前的訓練資料,並傳入feed_dict以饋送到佔位符中
       feed_dict = {tf_train_dataset : batch_data, tf_train_labels : batch_labels}
       _, l, predictions = session.run([optimizer, loss, train_prediction], feed_dict=feed_dict)
       train_accuracy = accuracy(predictions, batch_labels)

       if step % display_step == 0:
           test_accuracy = accuracy(test_prediction.eval(), test_labels)
           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)
           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 種花的圖片,並且畫素正好是我們所需要的:

ox17_image_width = 224
ox17_image_height = 224
ox17_image_depth = 3
ox17_num_labels = 17

import tflearn.datasets.oxflower17 as oxflower17
train_dataset_, train_labels_ = oxflower17.load_data(one_hot=True)
train_dataset_ox17, train_labels_ox17 = train_dataset_[:1000,:,:,:], train_labels_[:1000,:]
test_dataset_ox17, test_labels_ox17 = train_dataset_[1000:,:,:,:], train_labels_[1000:,:]

print('Training set', train_dataset_ox17.shape, train_labels_ox17.shape)
print('Test set', test_dataset_ox17.shape, test_labels_ox17.shape)

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

ALEX_PATCH_DEPTH_1, ALEX_PATCH_DEPTH_2, ALEX_PATCH_DEPTH_3, ALEX_PATCH_DEPTH_4 = 96, 256, 384, 256
ALEX_PATCH_SIZE_1, ALEX_PATCH_SIZE_2, ALEX_PATCH_SIZE_3, ALEX_PATCH_SIZE_4 = 11, 5, 3, 3
ALEX_NUM_HIDDEN_1, ALEX_NUM_HIDDEN_2 = 4096, 4096


def variables_alexnet(patch_size1 = ALEX_PATCH_SIZE_1, patch_size2 = ALEX_PATCH_SIZE_2, 
                      patch_size3 = ALEX_PATCH_SIZE_3, patch_size4 = ALEX_PATCH_SIZE_4, 
                      patch_depth1 = ALEX_PATCH_DEPTH_1, patch_depth2 = ALEX_PATCH_DEPTH_2, 
                      patch_depth3 = ALEX_PATCH_DEPTH_3, patch_depth4 = ALEX_PATCH_DEPTH_4, 
                      num_hidden1 = ALEX_NUM_HIDDEN_1, num_hidden2 = ALEX_NUM_HIDDEN_2,
                      image_width = 224, image_height = 224, image_depth = 3, num_labels = 17):
 
    w1 = tf.Variable(tf.truncated_normal([patch_size1, patch_size1, image_depth, patch_depth1], stddev=0.1))
    b1 = tf.Variable(tf.zeros([patch_depth1]))

    w2 = tf.Variable(tf.truncated_normal([patch_size2, patch_size2, patch_depth1, patch_depth2], stddev=0.1))
    b2 = tf.Variable(tf.constant(1.0, shape=[patch_depth2]))

    w3 = tf.Variable(tf.truncated_normal([patch_size3, patch_size3, patch_depth2, patch_depth3], stddev=0.1))
    b3 = tf.Variable(tf.zeros([patch_depth3]))

    w4 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth3, patch_depth3], stddev=0.1))
    b4 = tf.Variable(tf.constant(1.0, shape=[patch_depth3]))
 
    w5 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth3, patch_depth3], stddev=0.1))
    b5 = tf.Variable(tf.zeros([patch_depth3]))
 
    pool_reductions = 3
    conv_reductions = 2
    no_reductions = pool_reductions + conv_reductions
    w6 = tf.Variable(tf.truncated_normal([(image_width // 2**no_reductions)*(image_height // 2**no_reductions)*patch_depth3, num_hidden1], stddev=0.1))
    b6 = tf.Variable(tf.constant(1.0, shape = [num_hidden1]))

    w7 = tf.Variable(tf.truncated_normal([num_hidden1, num_hidden2], stddev=0.1))
    b7 = tf.Variable(tf.constant(1.0, shape = [num_hidden2]))
 
    w8 = tf.Variable(tf.truncated_normal([num_hidden2, num_labels], stddev=0.1))
    b8 = tf.Variable(tf.constant(1.0, shape = [num_labels]))
 
    variables = {
                 'w1': w1, 'w2': w2, 'w3': w3, 'w4': w4, 'w5': w5, 'w6': w6, 'w7': w7, 'w8': w8, 
                 'b1': b1, 'b2': b2, 'b3': b3, 'b4': b4, 'b5': b5, 'b6': b6, 'b7': b7, 'b8': b8
                }
    return variables


def model_alexnet(data, variables):
    layer1_conv = tf.nn.conv2d(data, variables['w1'], [1, 4, 4, 1], padding='SAME')
    layer1_relu = tf.nn.relu(layer1_conv + variables['b1'])
    layer1_pool = tf.nn.max_pool(layer1_relu, [1, 3, 3, 1], [1, 2, 2, 1], padding='SAME')
    layer1_norm = tf.nn.local_response_normalization(layer1_pool)
 
    layer2_conv = tf.nn.conv2d(layer1_norm, variables['w2'], [1, 1, 1, 1], padding='SAME')
    layer2_relu = tf.nn.relu(layer2_conv + variables['b2'])
    layer2_pool = tf.nn.max_pool(layer2_relu, [1, 3, 3, 1], [1, 2, 2, 1], padding='SAME')
    layer2_norm = tf.nn.local_response_normalization(layer2_pool)
 
    layer3_conv = tf.nn.conv2d(layer2_norm, variables['w3'], [1, 1, 1, 1], padding='SAME')
    layer3_relu = tf.nn.relu(layer3_conv + variables['b3'])
 
    layer4_conv = tf.nn.conv2d(layer3_relu, variables['w4'], [1, 1, 1, 1], padding='SAME')
    layer4_relu = tf.nn.relu(layer4_conv + variables['b4'])
 
    layer5_conv = tf.nn.conv2d(layer4_relu, variables['w5'], [1, 1, 1, 1], padding='SAME')
    layer5_relu = tf.nn.relu(layer5_conv + variables['b5'])
    layer5_pool = tf.nn.max_pool(layer4_relu, [1, 3, 3, 1], [1, 2, 2, 1], padding='SAME')
    layer5_norm = tf.nn.local_response_normalization(layer5_pool)
 
    flat_layer = flatten_tf_array(layer5_norm)
    layer6_fccd = tf.matmul(flat_layer, variables['w6']) + variables['b6']
    layer6_tanh = tf.tanh(layer6_fccd)
    layer6_drop = tf.nn.dropout(layer6_tanh, 0.5)
 
    layer7_fccd = tf.matmul(layer6_drop, variables['w7']) + variables['b7']
    layer7_tanh = tf.tanh(layer7_fccd)
    layer7_drop = tf.nn.dropout(layer7_tanh, 0.5)
 
    logits = tf.matmul(layer7_drop, variables['w8']) + variables['b8']
    return logits

3.2 VGGNet-16

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

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

機器之心GitHub專案:從零開始用TensorFlow搭建卷積神經網路

#The VGGNET Neural Network 
VGG16_PATCH_SIZE_1, VGG16_PATCH_SIZE_2, VGG16_PATCH_SIZE_3, VGG16_PATCH_SIZE_4 = 3, 3, 3, 3
VGG16_PATCH_DEPTH_1, VGG16_PATCH_DEPTH_2, VGG16_PATCH_DEPTH_3, VGG16_PATCH_DEPTH_4 = 64, 128, 256, 512
VGG16_NUM_HIDDEN_1, VGG16_NUM_HIDDEN_2 = 4096, 1000

def variables_vggnet16(patch_size1 = VGG16_PATCH_SIZE_1, patch_size2 = VGG16_PATCH_SIZE_2, 
                       patch_size3 = VGG16_PATCH_SIZE_3, patch_size4 = VGG16_PATCH_SIZE_4, 
                       patch_depth1 = VGG16_PATCH_DEPTH_1, patch_depth2 = VGG16_PATCH_DEPTH_2, 
                       patch_depth3 = VGG16_PATCH_DEPTH_3, patch_depth4 = VGG16_PATCH_DEPTH_4,
                       num_hidden1 = VGG16_NUM_HIDDEN_1, num_hidden2 = VGG16_NUM_HIDDEN_2,
                       image_width = 224, image_height = 224, image_depth = 3, num_labels = 17):
    
    w1 = tf.Variable(tf.truncated_normal([patch_size1, patch_size1, image_depth, patch_depth1], stddev=0.1))
    b1 = tf.Variable(tf.zeros([patch_depth1]))
    w2 = tf.Variable(tf.truncated_normal([patch_size1, patch_size1, patch_depth1, patch_depth1], stddev=0.1))
    b2 = tf.Variable(tf.constant(1.0, shape=[patch_depth1]))

    w3 = tf.Variable(tf.truncated_normal([patch_size2, patch_size2, patch_depth1, patch_depth2], stddev=0.1))
    b3 = tf.Variable(tf.constant(1.0, shape = [patch_depth2]))
    w4 = tf.Variable(tf.truncated_normal([patch_size2, patch_size2, patch_depth2, patch_depth2], stddev=0.1))
    b4 = tf.Variable(tf.constant(1.0, shape = [patch_depth2]))
    
    w5 = tf.Variable(tf.truncated_normal([patch_size3, patch_size3, patch_depth2, patch_depth3], stddev=0.1))
    b5 = tf.Variable(tf.constant(1.0, shape = [patch_depth3]))
    w6 = tf.Variable(tf.truncated_normal([patch_size3, patch_size3, patch_depth3, patch_depth3], stddev=0.1))
    b6 = tf.Variable(tf.constant(1.0, shape = [patch_depth3]))
    w7 = tf.Variable(tf.truncated_normal([patch_size3, patch_size3, patch_depth3, patch_depth3], stddev=0.1))
    b7 = tf.Variable(tf.constant(1.0, shape=[patch_depth3]))

    w8 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth3, patch_depth4], stddev=0.1))
    b8 = tf.Variable(tf.constant(1.0, shape = [patch_depth4]))
    w9 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth4, patch_depth4], stddev=0.1))
    b9 = tf.Variable(tf.constant(1.0, shape = [patch_depth4]))
    w10 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth4, patch_depth4], stddev=0.1))
    b10 = tf.Variable(tf.constant(1.0, shape = [patch_depth4]))
    
    w11 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth4, patch_depth4], stddev=0.1))
    b11 = tf.Variable(tf.constant(1.0, shape = [patch_depth4]))
    w12 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth4, patch_depth4], stddev=0.1))
    b12 = tf.Variable(tf.constant(1.0, shape=[patch_depth4]))
    w13 = tf.Variable(tf.truncated_normal([patch_size4, patch_size4, patch_depth4, patch_depth4], stddev=0.1))
    b13 = tf.Variable(tf.constant(1.0, shape = [patch_depth4]))
    
    no_pooling_layers = 5

    w14 = tf.Variable(tf.truncated_normal([(image_width // (2**no_pooling_layers))*(image_height // (2**no_pooling_layers))*patch_depth4 , num_hidden1], stddev=0.1))
    b14 = tf.Variable(tf.constant(1.0, shape = [num_hidden1]))
    
    w15 = tf.Variable(tf.truncated_normal([num_hidden1, num_hidden2], stddev=0.1))
    b15 = tf.Variable(tf.constant(1.0, shape = [num_hidden2]))
   
    w16 = tf.Variable(tf.truncated_normal([num_hidden2, num_labels], stddev=0.1))
    b16 = tf.Variable(tf.constant(1.0, shape = [num_labels]))
    variables = {
        'w1': w1, 'w2': w2, 'w3': w3, 'w4': w4, 'w5': w5, 'w6': w6, 'w7': w7, 'w8': w8, 'w9': w9, 'w10': w10, 
        'w11': w11, 'w12': w12, 'w13': w13, 'w14': w14, 'w15': w15, 'w16': w16, 
        'b1': b1, 'b2': b2, 'b3': b3, 'b4': b4, 'b5': b5, 'b6': b6, 'b7': b7, 'b8': b8, 'b9': b9, 'b10': b10, 
        'b11': b11, 'b12': b12, 'b13': b13, 'b14': b14, 'b15': b15, 'b16': b16
    }
    return variables

def model_vggnet16(data, variables):
    layer1_conv = tf.nn.conv2d(data, variables['w1'], [1, 1, 1, 1], padding='SAME')
    layer1_actv = tf.nn.relu(layer1_conv + variables['b1'])
    layer2_conv = tf.nn.conv2d(layer1_actv, variables['w2'], [1, 1, 1, 1], padding='SAME')
    layer2_actv = tf.nn.relu(layer2_conv + variables['b2'])
    layer2_pool = tf.nn.max_pool(layer2_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

    layer3_conv = tf.nn.conv2d(layer2_pool, variables['w3'], [1, 1, 1, 1], padding='SAME')
    layer3_actv = tf.nn.relu(layer3_conv + variables['b3'])   
    layer4_conv = tf.nn.conv2d(layer3_actv, variables['w4'], [1, 1, 1, 1], padding='SAME')
    layer4_actv = tf.nn.relu(layer4_conv + variables['b4'])
    layer4_pool = tf.nn.max_pool(layer4_pool, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

    layer5_conv = tf.nn.conv2d(layer4_pool, variables['w5'], [1, 1, 1, 1], padding='SAME')
    layer5_actv = tf.nn.relu(layer5_conv + variables['b5'])
    layer6_conv = tf.nn.conv2d(layer5_actv, variables['w6'], [1, 1, 1, 1], padding='SAME')
    layer6_actv = tf.nn.relu(layer6_conv + variables['b6'])
    layer7_conv = tf.nn.conv2d(layer6_actv, variables['w7'], [1, 1, 1, 1], padding='SAME')
    layer7_actv = tf.nn.relu(layer7_conv + variables['b7'])
    layer7_pool = tf.nn.max_pool(layer7_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

    layer8_conv = tf.nn.conv2d(layer7_pool, variables['w8'], [1, 1, 1, 1], padding='SAME')
    layer8_actv = tf.nn.relu(layer8_conv + variables['b8'])
    layer9_conv = tf.nn.conv2d(layer8_actv, variables['w9'], [1, 1, 1, 1], padding='SAME')
    layer9_actv = tf.nn.relu(layer9_conv + variables['b9'])
    layer10_conv = tf.nn.conv2d(layer9_actv, variables['w10'], [1, 1, 1, 1], padding='SAME')
    layer10_actv = tf.nn.relu(layer10_conv + variables['b10'])
    layer10_pool = tf.nn.max_pool(layer10_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

    layer11_conv = tf.nn.conv2d(layer10_pool, variables['w11'], [1, 1, 1, 1], padding='SAME')
    layer11_actv = tf.nn.relu(layer11_conv + variables['b11'])
    layer12_conv = tf.nn.conv2d(layer11_actv, variables['w12'], [1, 1, 1, 1], padding='SAME')
    layer12_actv = tf.nn.relu(layer12_conv + variables['b12'])
    layer13_conv = tf.nn.conv2d(layer12_actv, variables['w13'], [1, 1, 1, 1], padding='SAME')
    layer13_actv = tf.nn.relu(layer13_conv + variables['b13'])
    layer13_pool = tf.nn.max_pool(layer13_actv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')
    
    flat_layer  = flatten_tf_array(layer13_pool)
    layer14_fccd = tf.matmul(flat_layer, variables['w14']) + variables['b14']
    layer14_actv = tf.nn.relu(layer14_fccd)
    layer14_drop = tf.nn.dropout(layer14_actv, 0.5)
    
    layer15_fccd = tf.matmul(layer14_drop, variables['w15']) + variables['b15']
    layer15_actv = tf.nn.relu(layer15_fccd)
    layer15_drop = tf.nn.dropout(layer15_actv, 0.5)
    
    logits = tf.matmul(layer15_drop, variables['w16']) + variables['b16']
    return logits

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

參考部落格:http://ataspinar.com/2017/08/15/building-convolutional-neural-networks-with-tensorflow/

相關文章