Tensorflow and CNNs 使用tensorflow構建卷積神經網路
TensorFlow的layers
模組提供了比較高階的API,使得我們能可以很方便的構建各種神經網路。它提供的方法可以方便的新增全連線層、卷積層、啟用函式以及dropout。在這個教程中,你將瞭解如何使用layers
模組來構建用於識別MNIST資料集的卷積神經網路
MNIST資料集包括60000張用於訓練的圖片和10000張用於測試的圖片。每張圖片一張畫素為28*28的手寫數字。
開始
首先構建這個TensorFlow專案的框架,建立一個名為cnn_mnist.py
的檔案,然後新增以下程式碼。
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
# Imports
import numpy as np
import tensorflow as tf
tf.logging.set_verbosity(tf.logging.INFO)
# Our application logic will be added here
if __name__ == "__main__":
tf.app.run()
在這個教程中,我們會逐漸的新增程式碼已完成整個卷積神經網路的訓練和測試部分。
最終完整的程式碼可以在github看到。
卷積神經網路簡介
卷積神經網路(Convolutional neural networks, CNNS)是如今在圖片分類領域最先進的方法。它通過一系列的卷積核(Convolutional Filter)從原始圖片中抽取特徵,然後進行分類。CNNs包含三個部分:
- 卷積層(Convolutional layers),使用一定數量的卷積核處理原始圖片。一般使用ReLU啟用函式。
- 池化層(pooling layer),對於卷積層得到的資料進行下采樣以減少feature map的維度,這樣可以縮短處理時間。常用的池化方法是最大池化(max pooling)。
- 全連線層(fully connected layers),根據經過卷積層和池化層的特徵進行分類。
通常來講,CNNs是由執行特徵提取的一堆卷積模組組成的。每個模組由一個卷積層和一個池層組成。最後一個卷積模組之後是一個或多個執行分類的全連線層。CNN中的最後一個Dense層包含模型中每個目標類的單個節點(模型中所有可能的類),使用softmax啟用函式為每個節點生成0-1之間的值(這些值的和為1)。我們可以將每個節點對應的值看作將圖片分為該類的可能性的度量。
構建分類器
使用以下CNN結構對MNIST資料集中的影象進行分類:
- Convolutional Layer #1:包含32個5x5的卷積核(抽取5x5畫素子區域),使用ReLU作為啟用函式。
- Pooling Layer #1: 使用2x2過濾器執行最大池化,stride為2(指定合併的區域不重疊)
- Convolutional Layer #2:包含64個5x5的卷積核,使用ReLU作為啟用函式。
- Pooling Layer #1: 使用2x2過濾器執行最大池化,stride為2
- Dense Layer #1:1024個神經元, dropout regularization rate 為0.4(在訓練的過程中,每個神經元都有0.4的概率被丟棄)
- Dense Layer #2 (Logits Layer): 10 個神經元, 對應著十個分類。
tf.layers
模組包含建立上述三種網路層的方法:
conv2d()
。 構造一個二維卷積層。 引數:卷積核數量,卷積核大小,padding, 啟用函式。max_pooling2d()
。 使用最大池化演算法構造一個池化層。引數: 過濾器大小、步幅。dense()
。 構建一個全連線層。 引數:神經元數量、啟用函式。
以上每一個方法都接受tensor
作為輸入,並將變換後的tensor
作為輸出返回。 這樣可以很容易地將一個層連線到另一個層:只需將前一層的輸出作為輸入提供給後一層即可。
開啟cnn_mnist.py
新增以下程式碼
def cnn_model_fn(features, labels, mode):
"""Model function for CNN."""
# Input Layer
input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])
# Convolutional Layer #1
conv1 = tf.layers.conv2d(
inputs=input_layer,
filters=32,
kernel_size=[5, 5],
padding="same",
activation=tf.nn.relu)
# Pooling Layer #1
pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)
# Convolutional Layer #2 and Pooling Layer #2
conv2 = tf.layers.conv2d(
inputs=pool1,
filters=64,
kernel_size=[5, 5],
padding="same",
activation=tf.nn.relu)
pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)
# Dense Layer
pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)
dropout = tf.layers.dropout(
inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)
# Logits Layer
logits = tf.layers.dense(inputs=dropout, units=10)
predictions = {
# Generate predictions (for PREDICT and EVAL mode)
"classes": tf.argmax(input=logits, axis=1),
# Add `softmax_tensor` to the graph. It is used for PREDICT and by the
# `logging_hook`.
"probabilities": tf.nn.softmax(logits, name="softmax_tensor")
}
if mode == tf.estimator.ModeKeys.PREDICT:
return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)
# Calculate Loss (for both TRAIN and EVAL modes)
loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)
# Configure the Training Op (for TRAIN mode)
if mode == tf.estimator.ModeKeys.TRAIN:
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
train_op = optimizer.minimize(
loss=loss,
global_step=tf.train.get_global_step())
return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)
# Add evaluation metrics (for EVAL mode)
eval_metric_ops = {
"accuracy": tf.metrics.accuracy(
labels=labels, predictions=predictions["classes"])}
return tf.estimator.EstimatorSpec(
mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)
以下部分深入介紹用於建立每個模組的tf.layers程式碼,以及如何計算損失函式,配置訓練操作並生成預測。
Input Layer
layers
模組中的該方法用於建立適用於卷積層和池化層的輸入。
引數:
- batch_size
:在訓練的過程中每輸入batch_size
個樣例進行一次梯度下降。
- image_width
:樣例圖片的寬度
- image_height
:樣例圖片的高度
- channels
:圖片的通道數
這裡,我們的MNIST資料集由單色的28x28畫素影象組成,所以我們輸入圖層的期望形狀是[batch_size,28,28,1]。
為了將我們的輸入feature map(特徵)轉換成這個形狀,我們可以執行下面的整形操作:
input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])
要注意的是, 我們已指定batch_size
大小為-1,它指定此維度應該根據features[“x”]中的輸入值的大小動態計算,並保持所有其他維度的大小不變。舉個例子,如果每個batch中包含五個樣例,那麼feature[“x”]中將包含3920個數值(每個數值表示圖片中的一個畫素點),input_layer
的shape
將會是[5, 28, 28, 1]
。類似的,如果每個batch中包含一百個樣例,那麼feature[“x”]中將包含78400個數值,input_layer
的shape
將會是[100, 28, 28, 1]
。
Convolutional Layer #1
在第一個卷積層包含32個5x5的卷積核,並使用ReLU作為啟用函式。 我們可以使用layers
模組中的conv2d()
方法來建立該層,如下所示:
conv1 = tf.layers.conv2d(
inputs=input_layer,
filters=32,
kernel_size=[5, 5],
padding="same",
activation=tf.nn.relu)
inputs
:引數指定輸入張量,這裡設定為為input_layer
,它的shape
必須為[batch_size, image_width, image_height, channels]
,這裡我們將input_layer
連線到第一個卷積層,它的shape
為[5, 28, 28, 1]
。filters>
:指定卷積核的數量kernel_size
:制定卷積核的大小(tip:如果卷積核的寬和高相等,那麼kernel_size
的值可以為一個整型變數。例如這裡可以寫為kernel_size=5
)padding
: 引數為兩個列舉值之一(不區分大小寫):same
和valid
。用於決定輸出張量是否和輸入張量具有相同過的維度。這裡,設定padding = same
,意味著TensorFlow將0值新增到輸入張量的邊緣以保持輸入張量的寬度和高度為28.(否則,28x28張量經過5x5卷積將產生24x24張量)activation
: 用於指定啟用函式
Pooling Layer #1
接下來,把剛建立的卷積層連線到第一個池化層。 我們可以使用layers
模組中的max_pooling2d()
方法來構建一個使用最大池化演算法的池化層,指定步幅為2, 大小為2x2:
pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)
inputs
指定輸入張量,shape為[batch_size,image_width,image_height,channels]
。 在這裡,我們的輸入張量是conv1,來自第一個卷積層的輸出,其shape為[batch_size,28,28,32]
。pool_size
引數指定池化層的filter的大小為[width, height]
,如果寬和高相同,則可以用一個數字代替。strides
引數指定步幅的大小。 在這裡,我們設定為2,這意味著由濾波器提取的子區域應該在寬度和高度兩個方向上分開2個畫素(對於2x2濾波器,這意味著沒有提取的區域將重疊)。 如果要為寬度和高度設定不同的跨度值,則可以指定元組或列表(例如,stride = [3,6])。
由max_pooling2d()
(pool1)生成的輸出張量的形狀為[batch_size,14,14,32]
:2x2濾波器將寬度和高度分別減少50%。
Convolutional Layer #2 and Pooling Layer #2
和上面一樣,我們可以使用conv2d()
和max_pooling2d()
向CNN中新增第二個卷積層和池化層
conv2 = tf.layers.conv2d(
inputs=pool1,
filters=64,
kernel_size=[5, 5],
padding="same",
activation=tf.nn.relu)
pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)
值得注意的是, 第二個卷積層的輸入為第一個池化層的輸出,輸出為conv2
,它的shape為[batch_size,14,14,64]
,寬和高與池化層的輸出相同(由於padding="same"
,64是因為第二個卷積層有64個卷積核。)
第二個池化層的輸入為conv2
, 輸出為pool2
,shape為[batch_size,7, 7,64]
Dense Layer
接下來,我們想要在我們的CNN中新增一個全連線層(神經元個數為1024,啟用函式為ReLU)來對卷積/池化層提取的特徵進行分類。 然而,在我們連線圖層之前,我們會將我們的feature map(pool2
)扁平化以形成[batch_size,features],這樣我們的張量只有兩個維度:
pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
在上面的reshape()
操作中,-1表示batch_size
維度將根據輸入資料中的示例數量動態計算。 每個示例都有7個(pool2
的寬度)* 7(pool2
的高度)* 64(pool2
的通道數)個feature,所以我們希望feature能維度的值為7 * 7 * 64(共3136個)。 輸出張量pool2_flat的形狀為[batch_size,3136]
。
接下來,我們可以使用layers
模組中的dense()
方法來新增全連線層,如下所示:
dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu
inputs
指定輸入張量:這裡將feature map扁平化後的pool2_flat
作為引數。units
引數指定神經元個數(1,024)。activation
採用啟用函式。
為了幫助改進我們的模型的結果,我們也將dropout正則化應用於我們的全連線層:
dropout = tf.layers.dropout(
inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)
rate
: 這裡設定為0.4,表示每個神經元都有0.4的概率會在訓練的過程中被捨棄.training
:採用布林值來指定模型當前是否在訓練模式下執行,只有在訓練模式下,才會進行dropout
dropout
的輸入張量,shape為[batch_size, 1024]
Logits Layer
最後一層是logits層,它將返回我們預測的原始值。 我們建立了一個包含10個神經元(每個目標類為0-9)的全連線層,使用線性啟用函式:
logits = tf.layers.dense(inputs=dropout, units=10)
輸出張量的shape為[batch_size, 10]
預測
logits
層將會輸出為shape為[batch_size, 10]
的原始值,我們可以將這些原始值轉化成一下兩種格式:
- 對於每個樣例的預測類別(0-9)的數字
- 樣例屬於每個類別的概率。
對於給定的一個樣例,我們選擇在logits
輸出張量中最大的值對應的分類作為最終的預測分類,可以通過tf.argmax
來找到這個對應的分類:
tf.argmax(input=logits, axis=1)
我們可以通過使用tf.nn.softmax
應用softmax
從logits層中得出概率:
tf.nn.softmax(logits, name="softmax_tensor")
用字典表示預測,返回一個EstimatorSpec 物件
predictions = {
"classes": tf.argmax(input=logits, axis=1),
"probabilities": tf.nn.softmax(logits, name="softmax_tensor")
}
if mode == tf.estimator.ModeKeys.PREDICT:
return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)
損失函式
對於訓練和評估,我們需要定義一個損失函式來衡量模型的預測與目標類別的匹配程度。 對於像MNIST這樣的多類分類問題,通常使用交叉熵作為損失度量。 以下程式碼計算模型在TRAIN或EVAL模式下執行時的交叉熵:
onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
loss = tf.losses.softmax_cross_entropy(
onehot_labels=onehot_labels, logits=logits)
我們來解釋一下上面的程式碼
label
張量包含了訓練樣例的預測值,如[1, 9, ...]
,為了計算交叉熵,我們需要將其轉化成one-hot encoding
[[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
...]
我們使用tf.one_hot
進行這種轉換,tf.one_hot
需要兩個引數:
- indices
:one-hot張量中1的位置
- depth
:one-hot 張量的長度,在這裡是10
下面這行程式碼就可以將label
轉化成one-hot張量,onehot_label
:
onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
接下來,我們計算onehot_labels
與自logits
層的預測值之間的交叉熵。tf.losses.softmax_cross_entropy()
將onehot_labels
和logits
作為引數,對logits
使用softmax
處理,計算交叉熵,並將我們的損失作為標量Tensor返回:
loss = tf.losses.softmax_cross_entropy(onehot_labels=onehot_labels, logits=logits)
訓練
我們已經將CNN的損失定義為logits
層和標籤softmax的交叉熵。 接下來配置模型,在訓練期間優化這個損失值。 我們將使用0.001的學習率和隨機梯度下降作為優化演算法:
if mode == tf.estimator.ModeKeys.TRAIN:
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
train_op = optimizer.minimize(
loss=loss,
global_step=tf.train.get_global_step())
return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)
評估
要在模型中新增 準確性度量,我們在EVAL模式中定義eval_metric_ops
字典,如下所示:
eval_metric_ops = {
"accuracy": tf.metrics.accuracy(
labels=labels, predictions=predictions["classes"])}
return tf.estimator.EstimatorSpec(
mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)
訓練及評估CNN MNIST分類器
我們已經構建好了模型,接下來進行訓練和評估
載入資料
首先,載入訓練和測試資料,在cnn_mnist.py
中新增main()
def main(unused_argv):
# Load training and eval data
mnist = tf.contrib.learn.datasets.load_dataset("mnist")
train_data = mnist.train.images # Returns np.array
train_labels = np.asarray(mnist.train.labels, dtype=np.int32)
eval_data = mnist.test.images # Returns np.array
eval_labels = np.asarray(mnist.test.labels, dtype=np.int32)
我們將訓練特徵資料(55,000個手繪數字影象的原始畫素值)和訓練標籤(每個影象0-9的相應值)分別儲存在train_data
和train_labels
中。 同樣,我們將評估特徵資料(10,000個影象)和評估標籤分別儲存在eval_data
和eval_labels
中。
評價模型
接下來建立一個Estimator
(TensorFlow的一個類,用於高階模型的訓練和推導)
# Create the Estimator
mnist_classifier = tf.estimator.Estimator(
model_fn=cnn_model_fn, model_dir="/tmp/mnist_convnet_model")
model_fn
:指定用於訓練、評估和預測的模型函式;我們將之前建立的cnn_model_fn
作為引數。model_dir
:指定用於儲存模型資料(檢查點)的目錄(這裡,我們指定臨時目錄/ tmp / mnist_convnet_model
,但可以隨意更改為您選擇的另一個目錄)。
記錄日誌
# Set up logging for predictions
tensors_to_log = {"probabilities": "softmax_tensor"}
logging_hook = tf.train.LoggingTensorHook(
tensors=tensors_to_log, every_n_iter=50)
tensors_to_log
是我們想要儲存的張量組成的字典,每個鍵都是我們選擇的標籤,將被列印在日誌輸出中。相應的標籤是TensorFlow圖中張量的名稱。在這裡,probabilities
可以在softmax_tensor
中找到,這是我們在cnn_model_fn
中生成概率時先前給出的softmax操作的名字。
然後建立LoggingTensorHook,將tensors_to_log
傳遞給tensors
。 我們設定every_n_iter
= 50,它指定在每50次訓練之後記錄probabilities
。
訓練模型
現在我們準備開始訓練模型,可以通過在mnist_classifier
上建立train_input_fn
並呼叫 train()
來完成,在main()
中新增如下程式碼:
# Train the model
train_input_fn = tf.estimator.inputs.numpy_input_fn(
x={"x": train_data},
y=train_labels,
batch_size=100,
num_epochs=None,
shuffle=True)
mnist_classifier.train(
input_fn=train_input_fn,
steps=20000,
hooks=[logging_hook])
在呼叫numpy_input_fn
中,我們將訓練特徵資料和標籤分別傳遞給x(作為字典)和y。
- batch_size設定為100(這意味著模型每次傳入100張圖片)。
num_epochs = None
-意味著模型將一直訓練,直到達到指定的步數
shuffle = True
-, 意味著每次訓練前將會打亂資料。
train()
在呼叫中,我們設定了
steps= 20000(這意味著模型將訓練總計20000次)。 我們將
logging_hook傳遞給
hooks`引數,以便在訓練過程中觸發它。
評估模型
一旦訓練完成,我們要評估我們的模型,以確定其在MNIST測試集上的準確性。 我們呼叫評估方法,評估我們在model_fn
中的定義的eval_metric_ops
引數中指定的度量。
# Evaluate the model and print results
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
x={"x": eval_data},
y=eval_labels,
num_epochs=1,
shuffle=False)
eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
print(eval_results)
執行模型
我們已經完成了模型的構建、評估等模組,現在可以執行一下模型
在訓練的過程中,你應該會看到如下的日誌:
INFO:tensorflow:loss = 2.36026, step = 1
INFO:tensorflow:probabilities = [[ 0.07722801 0.08618255 0.09256398, ...]]
...
INFO:tensorflow:loss = 2.13119, step = 101
INFO:tensorflow:global_step/sec: 5.44132
...
INFO:tensorflow:Loss for final step: 0.553216.
INFO:tensorflow:Restored model from /tmp/mnist_convnet_model
INFO:tensorflow:Eval steps [0,inf) for training step 20000.
INFO:tensorflow:Input iterator is exhausted.
INFO:tensorflow:Saving evaluation summary for step 20000: accuracy = 0.9733, loss = 0.0902271
{'loss': 0.090227105, 'global_step': 20000, 'accuracy': 0.97329998}
可以看到,準確率為97%
其他資源
想要了解TensorFlow中的Estimators
和使用TensorFlow實現CNNs,可以參考一下資源:
Creating Estimators in tf.estimator
提供了關於TensorFlow中Estimator
API的介紹。包括配置Estimator
,編寫模型函式,計算損失,定義訓練操作。Convolutional Neural Networks
介紹瞭如何使用較低階的TensorFlow操作而不是Estimator
構建一個MNIST CNN分類模型