原標題:機器之心GitHub項目:從零開始用TensorFlow搭建卷積神經網絡
機器之心原創
參與:蔣思源
機器之心基於 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
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)。
下面代碼是使用計算圖的案例:
a = tf.constant(2, tf.int16)
b = tf.constant(4, tf.float32)
graph = tf.Graph()
withgraph.as_default():
a = tf.Variable(8, tf.float32)
b = tf.Variable(tf.zeros([2,2], tf.float32))
withtf.Session(graph=graph) assession:
tf.global_variables_initializer().run()
print(f)
print(session.run(a))
print(session.run(b))
#輸出:
>>> <tf.Variable'Variable_2:0'shape=() dtype=int32_ref>
>>> 8
>>> [[ 0.0.]
>>> [ 0.0.]]
在 Tensorflow 中,所有不同的變量和運算都是儲存在計算圖。所以在我們構建完模型所需要的圖之後,還需要打開一個會話(Session)來運行整個計算圖。在會話中,我們可以將所有計算分配到可用的 CPU 和 GPU 資源中。
如下所示代碼,我們聲明兩個常量 a 和 b,並且定義一個加法運算。但它並不會輸出計算結果,因為我們只是定義了一張圖,而沒有運行它:
a=tf.constant([1,2],name="a")
b=tf.constant([2,4],name="b")
result = a+b
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]
withtf.Session() assess:
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) forelem inlist_of_points1_])
list_of_points2 = np.array([np.array(elem).reshape(1,2) forelem inlist_of_points2_])
graph = tf.Graph()
withgraph.as_default():
#我們使用 tf.placeholder() 創建佔位符 ,在 session.run() 過程中再投遞數據
point1 = tf.placeholder(tf.float32, shape=(1, 2))
point2 = tf.placeholder(tf.float32, shape=(1, 2))
defcalculate_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)
returneucledian_distance
dist = calculate_eucledian_distance(point1, point2)
withtf.Session(graph=graph) assession:
tf.global_variables_initializer().run()
forii inrange(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 [[12]] and[[1516]] -> [19.79899]
>>> the distance between [[34]] and[[1314]] -> [14.142136]
>>> the distance between [[56]] and[[1112]] -> [8.485281]
>>> the distance between [[78]] and[[ 910]] -> [2.8284271]
Ahmet Taspinar 在第二部分就直接開始構建深度神經網絡了,雖然我們在前一章增加了許多代碼段以幫助讀者了解 TensorFlow 的基本法則,但上面是遠遠不夠的。所以如果我們能先解析一部分神經網絡代碼,那麼將有助於入門讀者鞏固以上的 TensorFlow 基本知識。下面,我們將先解析一段構建了三層全連接神經網絡的代碼。
importtensorflow astf
fromnumpy.random importRandomState
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) inX]
withtf.Session() assess:
sess.run(tf.global_variables_initializer())
print(sess.run(w1))
print(sess.run(w2))
steps=11000
fori inrange(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]})
ifi % 1000== 0:
training_loss= sess.run(cross_entropy,feed_dict={x:X,y:Y})
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 最後剩餘的數據少於批量大小,那就只是用剩餘的數據進行訓練。最後兩句代碼是為了計算訓練損失并迭代一些次數後輸出訓練損失。這一部分代碼運行的結果如下:
TensorFlow 中的神經網絡
2.1 簡介
上圖所描述的圖像識別流程需要包含以下幾步:
輸入數據集,數據集分為訓練數據集和標註、測試數據集和標註(包括驗證數據集和標註)。測試和驗證集能賦值到 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 種不同的物體(飛機、摩託車、鳥、貓、狗、青蛙、馬、羊和卡車)。
首先,讓我們定義一些函數,它們能幫助我們加載和預處理圖像數據。
圖像的標籤使用 one-hot 編碼,並且將數據加載到隨機數組中。在定義這些函數後,我們可以加載數據:
我們能從 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。
如下所示,我們可以使用這種方法成功地導入 MNIST 數據集:
我們需要再次導入 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的代碼缺少了這一語句
withopen(cifar10_folder + test_dataset[0], 'rb') asf0:
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 = [], []
fortrain_dataset intrain_datasets:
withopen(cifar10_folder + train_dataset, 'rb') asf0:
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)
delc10_train_dataset
delc10_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 數據集效果並不是很好。所以我們另外使用一個全連接神經網絡來實現這一過程。
下面我們實現的神經網絡共有三層,輸入層有 784 個神經元,隱藏層與輸出層分別有 500 和 10 個神經元。這所以這樣設計是因為 MNIST 的像素為 28×28=784,所以每一個輸入神經元對應於一個灰度像素點。機器之心執行該模型得到的效果非常好,該模型在批量大小為 100,並使用學習率衰減的情況下迭代 10000 步能得到 98.34% 的測試集準確度,以下是該模型代碼:
importtensorflow astf
fromtensorflow.examples.tutorials.mnist importinput_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
definference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
# 使用滑動平均類
ifavg_class == None:
layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
returntf.matmul(layer1, weights2) + biases2
else:
layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + avg_class.average(biases1))
returntf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)
deftrain(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)
# 反向傳播更新參數和更新每一個參數的滑動平均值
withtf.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))
# 初始化會話並開始訓練過程。
withtf.Session() assess:
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}
# 循環地訓練神經網絡。
fori inrange(TRAINING_STEPS):
ifi % 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)))
該模型運行的結果如下:
在上面定義的整個計算圖中,我們先加載數據並定義權重矩陣和模型,然後在計算損失值並傳遞給優化器來優化權重。模型在迭代次數設定之內會一直循環地計算損失函數的梯度以更新權重。
在上面的全連接神經網絡中,我們使用梯度下降優化器來優化權重。然而,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 正則化層同樣也能輕鬆構建。
如下所示為定義卷積層網絡的代碼:
importtensorflow astf
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')
它們可以使用簡單的函數來替代上面的定義:
fromtflearn.layers.conv importconv_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 的架構如下:
LeNet5 包含 5 層網絡:
第一層:卷積層,該卷積層使用 Sigmoid 激活函數,並且在後面帶有平均池化層。
第二層:卷積層,該卷積層使用 Sigmoid 激活函數,並且在後面帶有平均池化層。
第三層:全連接層(使用 Sigmoid 激活函數)。
第四層:全連接層(使用 Sigmoid 激活函數)。
第五層:輸出層。
上面的 LeNet5 架構意味著我們需要構建 5 個權重和偏置項矩陣,我們模型的主體大概需要 12 行代碼完成(5 個神經網絡層級、2 個池化層、4 個激活函數還有 1 個 flatten 層)。因為代碼比較多,所以我們最好在計算圖之外就定義好獨立的函數:
通過上面獨立定義的變量和模型,我們可以一點點調整數據流圖而不像前面的全連接網絡那樣。
我們看到 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',最後四行/列是不包括在輸出圖像內的。
沒有 padding 的圖片,最後四個像素點是無法包含在內的,因為卷積濾波器已經移動到了圖片的邊緣。這就意味著輸入 28 x 28 尺寸的圖片,輸出尺寸只有 24 x 24。如果 padding = 'SAME',那麼輸出尺寸就是 28 x 28。
如果我們輸入圖片尺寸是 28 x 28、濾波器尺寸為 5 x 5,步幅分別設置為 1 到 4,那麼就能得到下表
對於任意給定的步幅 S、濾波器尺寸 K、圖像尺寸 W、padding 尺寸 P,輸出的圖像尺寸可以總結上表的規則如下:
2.6 調整 LeNet5 架構
LeNet5 架構在原論文中使用的是 Sigmoid 激活函數和平均池化。然而如今神經網絡使用 ReLU 激活函數更為常見。所以我們可以修改一下 LeNet5 架構,並看看是否能獲得性能上的提升,我們可以稱這種修改的架構為類 LeNet5 架構。
最大的不同是我們使用 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
defvariables_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
}
returnvariables
defmodel_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']
returnlogits
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()
withgraph.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))
withtf.Session(graph=graph) assession:
#初始化全部變量
tf.global_variables_initializer().run()
print('Initialized with learning_rate', learning_rate)
forstep inrange(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)
ifstep % 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 數據集上的性能。
上圖展示了模型在兩個測試集上的準確度和迭代次數,其代表的模型從左至右分別為全連接神經網絡、LeNet5 和 改進後的 LeNet5。不過由於 MNIST 太簡單,全連接網絡也能做得挺好。不過在 CIFAR-10 數據集中,全連接網絡的性能明顯下降了不少。
上圖展示了三種神經網絡在 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 的基本架構:
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 種花的圖片,並且像素正好是我們所需要的:
下面,我們可以定義 AlexNet 中的權重矩陣和不同的層級。正如我們前面所看到的,我們需要定義很多權重矩陣和偏置向量,並且它們還需要和每一層的濾波器尺寸保持一致。
3.2 VGGNet-16
VGGNet 比 AlexNet 擁有的層級更多(16-19 層),但是每一層的設計都簡單了許多,所有層的濾波器大小都是 3 x 3、步幅都是 1,而所有的最大池化層的步幅都是 2。所以它雖然是一種深度 CNN,但結構比較簡單。
VGGNet 有 16 層或 19 層兩種配置,如下所示,這兩種配置的不同之處在於它在第二個、第三個和第四個最大池化層後面到底是採用三個卷積層還是四個卷積層。
上面已經為大家介紹了卷積神經網絡,我們從 TensorFlow 的安裝與基礎概念、簡單的全連接神經網絡、數據的下載與導入、在 MNIST 上訓練全連接神經網絡、在 CIFAR-10 上訓練經過修正的 LeNet 還有深度卷積神經網絡等方面向大家介紹了神經網絡,機器之心本文所有實驗的代碼、結果以及代碼注釋都將在 Github 上開放,這也是機器之心第一次試驗性地向大家介紹教程以及實現。我們希望在為讀者提供教程的同時也提供實際操作的經驗,希望能為大家學習該教程起到積極的作用。
參考博客:http://ataspinar.com/2017/08/15/building-convolutional-neural-networks-with-tensorflow/
本文為機器之心原創,轉載請聯繫本公眾號獲得授權。返回搜狐,查看更多
責任編輯: