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框架編寫模型,分為兩個部分,前期需要構建完整的計算圖,后期運行模型,並且可以利用會話在計算圖上的任意節點上運行。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM