TensorFlow入門之MNIST樣例程式碼分析
這幾天想系統的學習一下TensorFlow,為之後的工作打下一些基礎。看了下《TensorFlow:實戰Google深度學習框架》這本書,目前個人覺得這本書還是對初學者挺友好的,作者站在初學者的角度講解TensorFlow,所以比較容易理解。這篇博文主要是為了分析其中的一個經典程式碼,MNIST手寫數字識別。作者用了一個三層的全連線網路來實現手寫數字識別。具體的一些資訊可以在書中5.2節檢視。在下面的程式碼中有些註釋是作者的,當然我也在一些地方添加了自己的理解,在博文最後我會做一個總結。
# -*- coding: utf-8 -*- # 由於書上使用的TensorFlow版本比較舊,所以有些程式碼有所改動, # 本人使用的TensorFlow版本為1.2.0 import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data # 定義MNIST資料集相關的常數 INPUT_NODE = 784 # 每一張圖片都是28*28的 OUTPUT_NODE = 10 # 輸出是一個10分類 LAYER1_NODE = 500 # 隱藏層節點數 BATCH_SIZE = 100 # 每個Batch的大小 LEARNING_RATE_BASE = 0.8 # 最開始的學習率 LEARNING_RATE_DECAY = 0.99 # 在指數衰減學習率的過程中用到 REGULARIZATION_RATE = 0.0001 # 描述模型複雜度的正則化項在損失函式中的係數 TRAINING_STEPS = 30000 # 訓練輪數,注意,訓練一個Batch就是一個step MOVING_AVERAGE_DECAY = 0.99 # 滑動平均模型的衰減率,最後我會講解滑動平均模型 # 一個輔助函式,給定神經網路的輸入和所有引數,計算神經網路的前向傳播結果。在這裡 # 定義了一個使用ReLU啟用函式的三層全連線神經網路。通過加入隱藏層實現了多層網路結構 # 通過ReLU啟用函式實現了去線性化。在這個函式中也支援傳入用於計算引數平均值的類, # 這樣方便在測試時使用滑動平均模型。 def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2): # 當沒有提供滑動平均類時,直接使用引數當前的取值 if avg_class == None: # 計算隱藏層的前向傳播結果,這裡使用了ReLU啟用函式。 layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1) # 計算輸出層的前向傳播結果。因為在計算損失函式時會一併計算softmax函式, # 所以這裡不需要加入啟用函式。而且不加入softmax不會影響預測結果。因為預測時 # 使用的是不同類別對應節點輸出值的相對大小,有沒有softmax層對最後分類結果的 # 計算沒有影響。於是在計算整個神經網路的前向傳播時可以不加入最後的softmax層。 return tf.matmul(layer1, weights2) + biases2 else: # 首先使用avg_class.average函式來計算得出變數的滑動平均值, # 然後再計算相應的神經網路前向傳播結果。 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) # 訓練模型的過程 # 寫TensorFlow程式的時候一定要注意邏輯結構,一般都是下面這個結構: # 1. 搭建模型:資料輸入、資料label、權值初始化、前向傳播、反向傳播、更新引數 # 2. 執行模型:上面雖然把模型已經搭建好了,但是模型沒有真正執行起來 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]) ) # 注意這裡:計算在當前引數下神經網路前向傳播的結果。這裡給出的用於計算滑動平均的類為None, # 所以函式不會使用引數的滑動平均值。 y = inference(x, None, weights1, biases1, weights2, biases2) # 定義儲存訓練輪數的變數。這個變數不需要計算滑動平均值,所以這裡指定這個變數為 # 不可訓練的變數(trainable=False)。在使用TensorFlow訓練神經網路時, # 一般會將代表訓練輪數的變數指定為不可訓練的引數。 # 為什麼要把它設為0,參見學習率指數衰減的公式,最開始的指數我們讓它為0 # 而且在訓練過程中,每一次train_step,global_step都會增加1,所以後面這個值會越來越大 global_step = tf.Variable(0, trainable=False) # 給定滑動平均衰減率和訓練輪數的變數,初始化滑動平均類。在第4章中介紹過給 # 定訓練輪數的變數可以加快訓練早期變數的更新速度。 variable_averages = tf.train.ExponentialMovingAverage( MOVING_AVERAGE_DECAY, global_step ) # 在所有代表神經網路引數的變數上使用滑動平均。其他輔助變數(比如global_step)就 # 不需要了。tf.trainable_variable返回的就是圖上集合 # GraphKeys.TRAINABLE_VARIABLES中的元素。這個集合的元素就是所有沒有指定 # trainable=False的引數。 variable_averages_op = variable_averages.apply( tf.trainable_variables() ) # 注意這個與上面的y有什麼區別。計算使用了滑動平均之後的前向傳播結果。第4章中介紹過滑動平均不會改變 # 變數本身的取值,而是會維護一個影子變數來記錄其滑動平均值。所以當需要使用這個滑動平均值時, # 需要明確呼叫average函式。 average_y = inference( x, variable_averages, weights1, biases1, weights2, biases2 ) # 計算交叉熵作為刻畫預測值和真實值之間差距的損失函式。這裡使用了TensorFlow中提 # 供的sparse_softmax_cross_entropy_with_logits函式來計算交叉熵。當分類 # 問題只有一個正確答案時,可以使用這個函式來加速交叉熵的計算。MNIST問題的圖片中 # 只包含了0~9中的一個數字,所以可以使用這個函式來計算交叉熵損失。這個函式的第一個 # 引數是神經網路不包括softmax層的前向傳播結果,第二個是訓練資料的正確答案。因為 # 標準答案是一個長度位10的一維陣列,而該函式需要提供的是一個正確答案的數字,所以需 # 要使用tf.argmax函式來得到正確答案對應的類別編號。 # 注意這裡用的是y來計算交叉熵而不是average_y cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits( logits=y, labels=tf.argmax(y_, 1) ) # 計算在當前batch中所有樣例的交叉熵平均值 cross_entropy_mean = tf.reduce_mean(cross_entropy) # 計算L2正則化損失函式 regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE) # 計算模型的正則化損失。一般只計算神經網路邊上權重的正則化損失,而不使用偏置項。 regularization = regularizer(weights1) + regularizer(weights2) # 總損失等於交叉熵損失和正則化損失的和 loss = cross_entropy_mean + regularization # 設定指數衰減的學習率 learning_rate = tf.train.exponential_decay( LEARNING_RATE_BASE, # 基礎的學習率,隨著迭代的進行,更新變數時使用的 # 學習率在這個基礎上遞減 global_step, # 當前迭代的輪數 mnist.train.num_examples / BATCH_SIZE, # 過完所有的訓練資料需要的迭代次數 LEARNING_RATE_DECAY # 學習率的衰減速度 ) # 使用tf.train.GradientDescentOptimizer優化演算法來優化損失函式。注意這裡損失函式 # 包含了交叉熵損失和L2正則化損失。 # 在這個函式中,每次執行global_step都會加一。注意這個函式優化的損失函式跟y有關, # 跟average_y無關。 train_step = tf.train.GradientDescentOptimizer(learning_rate)\ .minimize(loss, global_step=global_step) # 在訓練神經網路模型時,每過一遍資料既需要通過反向傳播來更新神經網路中的引數, # 又要更新每個引數的滑動平均值。為了一次完成多個操作,TensorFlow提供了 # tf.control_dependencies和tf.group兩種機制。下面兩行程式和 # train_op = tf.group(train_step, variables_average_op)是等價的。 with tf.control_dependencies([train_step, variable_averages_op]): train_op = tf.no_op(name='train') # tf.no_op是一個沒有實際意義的函式 # 檢驗使用了滑動平均模型的神經網路前向傳播結果是否正確。tf.argmax(average_y, 1) # 計算每一個樣例的預測結果。其中average_y是一個batch_size * 10的二維陣列,每一行 # 表示一個樣例的前向傳播結果。tf.argmax的第二個引數“1”表示選取最大值的操作僅在第一 # 個維度中進行,也就是說,只在每一行選取最大值對應的下標。於是得到的結果是一個長度為 # batch的一維陣列,這個一維陣列中的值就表示了每一個樣例對應的數字識別結果。tf.equal # 判斷兩個張量的每一維是否相等,如果相等返回True,否則返回False。 correct_prediction = tf.equal(tf.argmax(average_y, 1), tf.argmax(y_, 1)) # 注意這個accuracy是隻跟average_y有關的,跟y是無關的 # 這個運算首先講一個布林型的數值轉化為實數型,然後計算平均值。這個平均值就是模型在這 # 一組資料上的正確率 accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) # 前面的所有步驟都是在構建模型,將一個完整的計算圖補充完了,現在開始執行模型 # 初始化會話並且開始訓練過程 with tf.Session() as sess: # 初始化變數 init_op = tf.global_variables_initializer() sess.run(init_op) # 準備驗證資料。一般在神經網路的訓練過程中會通過驗證資料要大致判斷停止的 # 條件和評判訓練的效果。 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): # 每1000輪輸出一次在驗證資料集上的測試結果 if i % 1000 == 0: # 計算滑動平均模型在驗證資料上的結果。因為MNIST資料集比較小,所以一次 # 可以處理所有的驗證資料。為了計算方便,本樣例程式沒有將驗證資料劃分為更 # 小的batch。當神經網路模型比較複雜或者驗證資料比較大時,太大的batch # 會導致計算時間過長甚至發生記憶體溢位的錯誤。 # 注意我們用的是滑動平均之後的模型來跑我們驗證集的accuracy 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)) # 產生這一輪使用的一個batch的訓練資料,並執行訓練過程。 xs, ys = mnist.train.next_batch(BATCH_SIZE) sess.run(train_op, feed_dict={x: xs, y_: ys}) # 在訓練結束之後,在測試資料上檢測神經網路模型的最終正確率。 # 同樣,我們最終的模型用的是滑動平均之後的模型,從這個accuracy函式 # 的呼叫就可以看出來了,因為accuracy只與average_y有關 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)) # 主程式入口 def main(argv=None): # 宣告處理MNIST資料集的類,這個類在初始化時會自動下載資料。 mnist = input_data.read_data_sets("./data", one_hot=True) train(mnist) # TensorFlow提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式 if __name__ == "__main__": tf.app.run()
總結
在書中的第四章講了幾個網路優化與避免過擬合的解決方法。在上面這個程式中我們主要用到的還是指數衰減學習率的優化方法,與滑動平均模型的避免過擬合方法。具體這兩個方法的原理與公式可以在書上了解。
在上述程式碼中,我們首先用訓練資料訓練模型,但是在訓練的過程中我們得到了兩套引數,一套是正常的沒有滑動平均的引數,另外一套就是那些引數的影子變數,這些影子變數都是前一套引數的滑動平均之後的值。最後我們不管是在驗證集還是在測試集上我們用的都是滑動平均之後的引數。
具體可以結合滑動平均模型的公式來看。在迭代初期,滑動平均模型中的衰減率比較小,影子變數與它相應的變數更新基本一致,但是隨著迭代次數越來越多,衰減率逐漸變大,這個時候模型基本將樣本的規律學習完畢了,如果再學習下去那麼模型很有可能過擬合,所以這個時候衰減率變大而且影子變數基本不隨它對應的變數更新了,這樣就保證了影子變數不會學習到訓練樣本的特殊規律。最終我們使用影子變數這套模型來最對驗證集與測試集進行評估,魯棒性也變強了。
總的來說使用TensorFlow框架編寫模型,分為兩個部分,前期需要構建完整的計算圖,後期執行模型,並且可以利用會話在計算圖上的任意節點上執行。