TensorFlow入門之MNIST樣例代碼分析

這幾天想系統的學習一下TensorFlow,爲以後的工做打下一些基礎。看了下《TensorFlow:實戰Google深度學習框架》這本書,目前我的以爲這本書仍是對初學者挺友好的,做者站在初學者的角度講解TensorFlow,因此比較容易理解。這篇博文主要是爲了分析其中的一個經典代碼,MNIST手寫數字識別。做者用了一個三層的全鏈接網絡來實現手寫數字識別。具體的一些信息能夠在書中5.2節查看。在下面的代碼中有些註釋是做者的,固然我也在一些地方添加了本身的理解,在博文最後我會作一個總結。git

# -*- 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框架編寫模型,分爲兩個部分,前期須要構建完整的計算圖,後期運行模型,而且能夠利用會話在計算圖上的任意節點上運行。數組

相關文章
相關標籤/搜索