1. 程式人生 > >機器學習筆記(十一): TensorFlow實戰三(MNIST數字識別問題)

機器學習筆記(十一): TensorFlow實戰三(MNIST數字識別問題)

1 - MNIST數字識別問題

前面介紹了這樣用TensorFlow訓練一個神經網路模型和主要考慮的問題及解決這些問題的常用方法。下面我們用一個實際的問題來驗證之前的解決方法。

我們使用的是MNIST手寫數字識別資料集。在很多深度學習教程中,這個資料集都會被當做一個案例。

1.1 - MNIST資料處理

MNIST是一個非常有名的手寫體數字識別資料集,在很多資料中,這個資料集都會被用作深度學習的入門樣例。TensorFlow的封裝讓使用MNIST資料集變得更加方便。MNIST資料集是NIST資料集的一個子集,它包含了60000張圖片作為訓練資料,10000張圖片作為測試資料。在MNIST資料集中的每一張圖片都代表了0~9中的一個數字。圖片的大小都為28X28,且數字都會出現在圖片的正中間。下圖展示了一張數字圖片及和它對應的畫素矩陣。
在這裡插入圖片描述

MNIST 資料集可在 http://yann.lecun.com/exdb/mnist/ 獲取, 它包含了四個部分:

  • Training set images: train-images-idx3-ubyte.gz (9.9 MB, 解壓後 47 MB, 包含 60,000 個樣本)
  • Training set labels: train-labels-idx1-ubyte.gz (29 KB, 解壓後 60 KB, 包含 60,000 個標籤)
  • Test set images: t10k-images-idx3-ubyte.gz (1.6 MB, 解壓後 7.8 MB, 包含 10,000 個樣本)
  • Test set labels: t10k-labels-idx1-ubyte.gz (5KB, 解壓後 10 KB, 包含 10,000 個標籤)
    MNIST 資料集來自美國國家標準與技術研究所, National Institute of Standards and Technology (NIST). 訓練集 (training set) 由來自 250 個不同人手寫的數字構成, 其中 50% 是高中學生, 50% 來自人口普查局 (the Census Bureau) 的工作人員. 測試集(test set) 也是同樣比例的手寫數字資料.

雖然這個資料集只提供了訓練和測試資料,但是為了驗證模型訓練的效果,一般會從訓練資料中劃分出一部分資料作為驗證資料。為了方便使用,TensorFlow提供了一個類來處理MNIST資料。這個類會自動下載並轉化MNIST資料的格式,將資料從原始的資料包中解析成訓練和測試神經網路時使用的格式。下面給出了使用這個函式的樣例程式。

import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from tensorflow.examples.tutorials.mnist import input_data

#載入MNIST資料集,如果指定地址/path/to/MNIST_data下沒有已經下載好的資料,
#那麼TensorFlow會自動從上述網址中下載資料
mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)

#列印Training data size:55000
print("Traning data size:",mnist.train.num_examples)

#列印Validating data size :5000
print("Validating data size:",mnist.validation.num_examples)

#列印Testing data size: 10000
print("Testing data size:",mnist.test.num_examples)

從上面的程式碼可以看出,通過input_data.read_data_sets函式生成的類會自動將MNIST資料集劃分成為train、validation和test三個資料集。處理後的每一張圖片是一個長度為784的一維陣列,這個陣列中的元素對應了圖片畫素矩陣中的每一個數字(28X28=784)。因為神經網路的輸入是一個特徵向量,所以在此把一張二維影象的畫素矩陣放到一個一維陣列中,可以方便TensorFlow將圖片的畫素矩陣提供給神經網路的輸入層。畫素矩陣中元素的取值範圍為【0,1】,它代表了顏色的深淺。

為了方便使用隨機梯度下降,它可以從所有的訓練資料中讀取一小部分作為一個訓練的batch,以下程式碼顯示瞭如何使用這個功能。

import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from tensorflow.examples.tutorials.mnist import input_data

#載入MNIST資料集,如果指定地址/path/to/MNIST_data下沒有已經下載好的資料,
#那麼TensorFlow會自動從上述網址中下載資料
mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)

batch_size = 100
xs,ys = mnist.train.next_batch(batch_size)
#從train的集合中選取batch_size個訓練資料。
print("X shape:", xs.shape)
#輸出X shape:(100,784)
print("Y shape:", ys.shape)
#輸出Y shape:(100,100)

1.2 - MNIST資料視覺化

我們可以用以下程式碼將MNIST中的圖片顯示出來:

import tensorflow as tf
import  numpy as np
import matplotlib.pyplot as plt
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from tensorflow.examples.tutorials.mnist import input_data
index=3
#載入MNIST資料集,如果指定地址/path/to/MNIST_data下沒有已經下載好的資料,
#那麼TensorFlow會自動從上述網址中下載資料
mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)

image=np.reshape(mnist.train.images[index],[28,-1])
print(mnist.train.labels[index])  #顯示label
plt.imshow(image, cmap=plt.get_cmap('gray_r'))  #畫圖
plt.show()

輸出:[0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
應該顯示的是“6”這個數字的圖片
在這裡插入圖片描述

2 - 神經網路模型訓練及不同模型結果對比

下面我們要用不同的優化方法來觀察神經網路模型在MNIST測試資料集上的表現,看看這些優化方法到底對神經網路分類的正確率有多少提升。(神經網路的各種優化方法在機器學習筆記(十)有詳細介紹)

2.1 - TensorFlow訓練神經網路

先在MNIST資料集上實現一個完整的TensorFlow程式:

# -*- 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("/path/to/MNIST_data", one_hot=True)
    train(mnist)

# TensorFlow提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式
if __name__ == "__main__":
    tf.app.run()

After 0 training step(s), validation accuracy using average model is 0.129
After 1000 training step(s), validation accuracy using average model is 0.9752
After 2000 training step(s), validation accuracy using average model is 0.9816
·
·
·After 25000 training step(s), validation accuracy using average model is 0.9862
After 26000 training step(s), validation accuracy using average model is 0.9858
After 27000 training step(s), validation accuracy using average model is 0.986
After 28000 training step(s), validation accuracy using average model is 0.987
After 29000 training step(s), validation accuracy using average model is 0.9862
After 30000 training step(s), test accuracy using average model is 0.9848

3 - 變數管理

在前面的例項中,我們使用了

def inference(input_tensor, avg_class, weights1, biases1,weights1, biases2):

這個函式來得到前向傳播的結果。但是從這個函式定義我們不難看出如果當神經網路的結構更加複雜之後,需要傳遞進去的引數weights和biases也會變多,從而就需要一個更好的方式來傳遞和管理神經網路中的引數了。

TensorFlow提供了通過變數名稱來建立或者獲取一個變數的機制。

  • tf.get_variable
  • tf.variable_scope

通過這個機制,在不同的函式中可以直接通過變數的名字來使用變數。
tf.get_variable函式與tf.Variable函式最大的區別在於指定變數名稱的引數。
tf.get_variable函式的變數名稱是一個必填的引數。會根據這個名字去建立或獲取變數。

通過上面2個函式,對計算前向傳播結果的函式做了一些改進。

import  tensorflow as tf

def inference(input_tensor,reuse = False):
    #定義第一層神經網路的變數和前向傳播過程。
    with tf.variable_scope('layer1',reuse=reuse):
        #根據傳進來的reuse來判斷是建立新變數還是使用已經建立好的,在第一次構造網路時
        #需要建立新的變數,以後每次呼叫這個函式都直接使用reuse = True就不需要
        #每次將變數傳進來了
        weights = tf.get_variable("weights",[INPUT_NODE,LAYER1_NODE],initializer=tf.truncated_normal_initializer(stddev=0.1))

        biases = tf.get_variable("biases",[LAYER1_NODE],initializer=tf.constant_initializer(0.0))

        layer1 = tf.nn.relu(tf.matmul(input_tensor,weights)+biases)

    with tf.variable_scope('layer2',reuse=reuse):

        weights = tf.get_variable("weights", [LAYER1_NODE, OUTPUT_NODE],
                                  initializer=tf.truncated_normal_initializer(stddev=0.1))

        biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))

        layer2 = tf.nn.relu(tf.matmul(layer1, weights) + biases)
    #最後返回前向傳播結果
    return layer2

new_x = tf.placeholder(tf.float32,[None,INPUT_NODE],name='x-input')
new_y = inference(new_x,True)

使用上面這段程式碼所示的方式,就不需要需要將所有變數都作為引數傳遞到不同的函式中了。當神經網路結構更加複雜、引數更多時,使用這種變數管理的方式將大大提高程式的可讀性

4 - TensorFlow模型持久化

以上的程式碼都是在訓練完成之後就直接退出了,而且沒有將訓練得到的模型儲存下來方便下次直接使用。為了讓訓練結果可以複用,TensorFlow可以持久化一個訓練好的模型。

4.1 - 持久化程式碼實現

TensorFlow提供了一個非常簡單的API來儲存和還原一個神經網路模型。這個API就是tf.train.Saver類。
import tensorflow as tf

#宣告兩個變數並計算它們的和
v1 = tf.Variable(tf.constant(1.0,shape=[1],name='v1'))
v2 = tf.Variable(tf.constant(2.0,shape=[1],name='v2'))
result = v1 + v2

init_op = tf.global_variables_initializer()
#宣告tf.train.Saver類用於儲存模型。
saver = tf.train.Saver()

with tf.Session() as sess:
    sess.run(init_op)
    #將模型儲存到/path/to/model/model.ckpt檔案
    saver.save(sess,"/path/to/model/model.ckpt")

上面的程式碼將模型儲存到了/path/to/model/model.ckpt檔案中,檔案目錄下會出現三個檔案。因為TensorFlow會將計算圖的結構和圖上引數取值分開儲存。

以下程式碼給出了載入這個已經儲存的TensorFlow模型的方法

import tensorflow as tf

#使用和儲存模型程式碼中一樣的方法來宣告變數
v1 = tf.Variable(tf.constant(1.0,shape=[1],name='v1'))
v2 = tf.Variable(tf.constant(2.0,shape=[1],name='v2'))
result = v1 + v2

saver = tf.train.Saver()

with tf.Session() as sess:
    #載入已經儲存的模型,並通過已經儲存的模型變數值來計算加法。
    saver.restore(sess,"/path/to/model/model.ckpt")
    print(sess.run(result))
    #將模型儲存到/path/to/model/model.ckpt檔案

或者是

import tensorflow as tf

#使用和儲存模型程式碼中一樣的方法來宣告變數
saver = tf.train.import_meta_graph("/path/to/model/model.ckpt.meta")

with tf.Session() as sess:
    #載入已經儲存的模型,並通過已經儲存的模型變數值來計算加法。
    saver.restore(sess,"/path/to/model/model.ckpt")
    print(sess.run(tf.get_default_graph().get_tensor_by_name("add:0")))
    #將模型儲存到/path/to/model/model.ckpt檔案


上面給出的程式中,預設儲存和載入了TensorFlow計算圖上定義的全部變數,但有可能只需要儲存或者載入部分變數。比如,有可能一個之前訓練好的五層神經網路模型,但現在想嘗試一個六層的神經網路,那麼可以將前面五層神經網路的引數直接載入到新的模型,而僅僅將最後一層神經網路重新訓練。

為了儲存或者載入部分變數,在宣告tf.train.Saver類時可以提供一個列表來指定需要儲存或載入的變數。
比如saver = tf.train.Saver([v1]),那麼只有變數v1會被載入進來

下面給出一個簡單的樣例程式說明變數重新命名是如何被使用的。

import tensorflow as tf

#這裡宣告的變數名稱已經和儲存的模型中的變數名稱不同
v1 = tf.Variable(tf.constant(1.0,shape=[1],name='other-v1'))
v2 = tf.Variable(tf.constant(2.0,shape=[1],name='other-v2'))

#如果直接使用tf.train.Saver()來載入模型會報變數找不到的錯誤。
#那麼要使用一個字典(dictionary)來重新命名變數

saver = tf.train.Saver({"v1":v1,"v2":v2})

5 - TensorFlow最佳實踐樣例程式

上面實現了一個完整的TensorFlow程式來解決MNIST問題。但是這個程式的可擴充套件性不好,而且可讀性非常差,有很多的冗餘程式碼,並且沒有應用持久化機制,沒有儲存訓練的中間結果會浪費大量的時間和資源。所以,在訓練的過程中最好需要每隔一段時間儲存一次模型訓練的中間結果。

因此,重構之後的程式將拆分為3個子程式,

  • mnist_inference.py
    它定義了前向傳播的過程以及神經網路中的引數。
  • mnist_train.py
    它定義了神經網路的訓練過程
  • mnist_eval.py
    它定義了測試過程。

mnist_inference.py 程式碼如下:

# -*- coding: utf-8 -*-
import tensorflow as tf

# 定義神經網路結構相關的引數
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500

# 通過tf.get_variable函式來獲取變數。在訓練神經網路時會建立這些變數;在測試時會通
# 過儲存的模型載入這些變數的取值。而且更加方便的是,因為可以在變數載入時將滑動平均變
# 量重新命名,所以可以直接通過相同的名字在訓練時使用變數自身,而在測試時使用變數的滑動
# 平均值。在這個函式中也會將變數的正則化損失加入到損失集合。
def get_weight_variable(shape, regularizer):
    weights = tf.get_variable(
        "weights", shape,
        initializer=tf.truncated_normal_initializer(stddev=0.1)
    )
    # 當給出了正則化生成函式時,將當前變數的正則化損失加入名字為losses的集合。在這裡
    # 使用了add_to_collection函式將一個張量加入一個集合,而這個集合的名稱為losses。
    # 這是自定義的集合,不在TensorFlow自動管理的集合列表中。
    if regularizer != None:
        tf.add_to_collection('losses', regularizer(weights))
    return weights

# 定義神經網路的前向傳播過程
def inference(input_tensor, regularizer):
    # 宣告第一層神經網路的變數並完成前向傳播過程。
    with tf.variable_scope('layer1'):
        # 這裡通過tf.get_variable或者tf.Variable沒有本質區別,因為在訓練或者測試
        # 中沒有在同一個程式中多次呼叫這個函式。如果在同一個程式中多次呼叫,在第一次
        # 呼叫之後需要將reuse引數設定為True。
        weights = get_weight_variable(
            [INPUT_NODE, LAYER1_NODE], regularizer
        )
        biases = tf.get_variable(
            "biases", [LAYER1_NODE],
            initializer=tf.constant_initializer(0.0)
        )
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights)+biases)

    # 類似的宣告第二層神經網路的變數並完成前向傳播過程。
    with tf.variable_scope('layer2'):
        weights = get_weight_variable(
            [LAYER1_NODE, OUTPUT_NODE], regularizer
        )
        biases = tf.get_variable(
            "biases", [OUTPUT_NODE],
            initializer=tf.constant_initializer(0.0)
        )
        layer2 = tf.matmul(layer1, weights) + biases

    # 返回最後前向傳播的結果
    return layer2

** mnist_train.py程式碼如下**

import os

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

#載入mnist_inference.py中定義的常量和前向傳播的函式。
import mnist_inference

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 # 滑動平均模型的衰減率,最後我會講解滑動平均模型
#模型儲存的路徑和中文名
MODEL_SAVE_PATH = "/path/to/model/"
MODEL_NAME = "model.ckpt"

def train(mnist):
    # 定義輸入輸出placeholder。
    x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')

    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    # 直接使用mnist_inference.py中定義的前向傳播過程
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 定義損失函式、學習率、滑動平均操作以及訓練過程
    variable_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY, global_step
    )
    variable_averages_op = variable_averages.apply(
        tf.trainable_variables()
    )
    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)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,
        global_step,
        mnist.train.num_examples / BATCH_SIZE,
        LEARNING_RATE_DECAY
    )
    train_step = tf.train.GradientDescentOptimizer(learning_rate)\
                   .minimize(loss, global_step=global_step)
    with tf.control_dependencies([train_step, variable_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化類
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()

        # 在訓練過程中不再測試模型在驗證資料上的表現,驗證和測試的過程將會有一個獨
        # 立的程式來完成。
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            _, loss_value, step = sess.run([train_op, loss, global_step],
                                           feed_dict={x: xs, y_: ys})
            # 每1000輪儲存一次模型
            if i % 1000 == 0:
                # 輸出當前的訓練情況。這裡只輸出了模型在當前訓練batch上的損失
                # 函式大小。通過損失函式的大小可以大概瞭解訓練的情況。在驗證數
                # 據集上正確率的資訊會有一個單獨的程式來生成
                print("After %d training step(s), loss on training "
                      "batch is %g." % (step, loss_value))
                # 儲存當前的模型。注意這裡給出了global_step引數,這樣可以讓每個
                # 被儲存的模型的檔名末尾加上訓練的輪數,比如“model.ckpt-1000”,
                # 表示訓練1000輪之後得到的模型。
                saver.save(
                    sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME),
                    global_step=global_step
                )
# 主程式入口
def main(argv=None):
    # 宣告處理MNIST資料集的類,這個類在初始化時會自動下載資料。
    mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)
    train(mnist)

# TensorFlow提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式
if __name__ == "__main__":
    tf.app.run()

After 1 training step(s), loss on training batch is 2.823.
After 1001 training step(s), loss on training batch is 0.236903.
After 2001 training step(s), loss on training batch is 0.248572.
·
·
·
After 26001 training step(s), loss on training batch is 0.0374676.
After 27001 training step(s), loss on training batch is 0.0378088.
After 28001 training step(s), loss on training batch is 0.0375806.
After 29001 training step(s), loss on training batch is 0.0363075.
可以看到誤差從2.8經過訓練下降到0.036

在訓練程式碼中,不再將訓練和測試跑在一起,訓練過程中,每1000輪輸出一次在當前訓練batch上損失函式的大小來大致估計訓練的效果。在上面的程式中,每1000輪儲存一次訓練好的模型,這樣可以通過一個單獨的測試程式,更加方便的滑動平均模型上做測試。

# -*- coding: utf-8 -*-
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 載入mnist_inference.py 和mnist_train.py中定義的常量和函式。
import mnist_inference
import mnist_train

# 每10秒載入一次最新的模型,並且在測試資料上測試最新模型的正確率
EVAL_INTERVAL_SECS = 10

def evaluate(mnist):
    with tf.Graph().as_default() as g:
        # 定義輸入輸出的格式。
        x = tf.placeholder(
            tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input'
        )
        y_ = tf.placeholder(
            tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input'
        )
        validate_feed = {x: mnist.validation.images,
                         y_: mnist.validation.labels}

        # 直接通過呼叫封裝好的函式來計算前向傳播的結果。因為測試時不關注ze正則化損失的值
        # 所以這裡用於計算正則化損失的函式被設定為None。
        y = mnist_inference.inference(x, None)

        # 使用前向傳播的結果計算正確率。如果需要對未知的樣例進行分類,那麼使用
        # tf.argmax(y,1)就可以得到輸入樣例的預測類別了。
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

        # 通過變數重新命名的方式來載入模型,這樣在前向傳播的過程中就不需要呼叫求滑動平均
        # 的函式來獲取平均值了。這樣就可以完全共用mnist_inference.py中定義的
        # 前向傳播過程。
        variable_averages = tf.train.ExponentialMovingAverage(
            mnist_train.MOVING_AVERAGE_DECAY
        )
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)

        # 每隔EVAL_INTERVAL_SECS秒呼叫一次計算正確率的過程以檢驗訓練過程中正確率的
        # 變化。
        while True:
            with tf.Session() as sess:
                # tf.train.get_checkpoint_state函式會通過checkpoint檔案自動
                # 找到目錄中最新模型的檔名。
                ckpt = tf.train.get_checkpoint_state(
                    mnist_train.MODEL_SAVE_PATH
                )
                if ckpt and ckpt.model_checkpoint_path:
                    # 載入模型。
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    # 通過檔名得到模型儲存時迭代的輪數。
                    global_step = ckpt.model_checkpoint_path\
                                      .split('/')[-1].split('-')[-1]
                    accuracy_score = sess.run(accuracy,
                                              feed_dict=validate_feed)
                    print("After %s training step(s), validation "
                          "accuracy = %g" % (global_step, accuracy_score))
                else:
                    print("No checkpoint file found")
                    return
            time.sleep(EVAL_INTERVAL_SECS)

 #主程式入口
def main(argv=None):
    # 宣告處理MNIST資料集的類,這個類在初始化時會自動下載資料。
    mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)
    evaluate(mnist)

# TensorFlow提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式
if __name__ == "__main__":
    tf.app.run()