【從零開始學習Tensorflow】(三)第5章 MNIST數字識別問題

轉載請註明作者和出處: https://blog.csdn.net/weixin_37392582
代碼平臺https://gitee.com/wuweijun
開發平臺: Win10 + Python3.6 + Anaconda3
編  者: 無尾


文章性質:【從零開始學習Tensorflow】系列博客爲 《TensorFlow+實戰Google深度學習框架》一書的學習筆記。



5.1、MNIST數據處理

MNIST數據集,包含60000張圖片作爲訓練數據(28*28),10000張圖片作爲測試數據,標籤集用 ont_hot 編碼表示手寫數字(例:表示數字3→[0,0,0,1,0,0,0,0,0,0])。

TensorFlow提供了一個類來處理 MNIST 數據。這個類會自動下載並轉換MNIST數據的格式,將數據從原始的數據包中解析成訓練和測試神經網絡時使用的格式。下面給出使用這個函數的樣例程序。

from tensorflow.examples.tutorials.mnist import input_data

#載入 MNIST 數據集,若指定地址下沒有,則自動從網上下載
mnist = input_data.read_data_sets("F:\MNIST_data",one_hot = True)

print('Training data size',mnist.train.num_examples)
print('Validating data size', mnist.validation.num_examples)
print('Testing data size', mnist.test.num_examples)
print('example training data label:',mnist.train.labels[0])

train 集合內有 55000 張圖片,validation集合內有 5000 張圖片,test集合內有 10000 張圖片。每一張圖片是一個長度爲784的一維數組,對應了圖片像素矩陣中的每一個數字(28*28 = 784)。

爲了方便使用隨機梯度下降,input_data.read_data_sets 函數生成的類還提供了 mnist.train.next_batch 函數,它可以從所有的訓練數據中讀取一小部分作爲一個訓練 batch 。代碼如下:

batch_size = 100
#該函數每次執行都會從數據集中順序讀取
xs, ys = mnist.train.next_batch(batch_size)


# 從train集合中選取 batch_size 個訓練數據
print("X shape:",xs.shape)
print("Y shape:",ys.shape)

OUTX shape: (100, 784)
Y shape: (100, 10)

5.2、神經網絡模型訓練及不同模型結果對比


5.2.1、TensorFlow 訓練神經網絡


給出一個完整的 Tensorflow 程序來解決 MNIST 手寫體數字識別問題。

回顧第4章提到的主要概念。在神經網絡的結構上,深度學習一方面需要使用激活函數實現神經網絡模型的去線性化,另一方面需要使用一個或多個隱藏層使得神經網絡的結構更深,以解決複雜問題。在訓練神經網絡時,第4章介紹了使用帶指數衰減的學習率設置、使用正則化來避免過擬合,以及使用滑動平均模型來使得最終模型更加健壯。以下代碼給出了一個在 MNIST 數據集上實現這些功能的完整的 TensorFlow 程序。

Click here to get the code:
【神經網絡實現 MNIST 手寫體識別】


5.2.2、使用驗證數據集判斷模型效果


將上一節中的代碼做略微調整,使 validate_set 和 test_set 的正確率同時顯示,發現 validate dataset 分佈接近 test dataset 分佈,可驗證模型在驗證數據上的表現可以很好的體現模型在測試數據上的表現。因此,對於驗證數據的選擇很重要。

        #迭代地訓練神經網絡
        for i in range(0,TRAINING_STEPS):
            if i % 1000 == 0:
                validate_acc = sess.run(accuracy, feed_dict = validate_feed)
                test_acc = sess.run(accuracy, feed_dict = test_feed)
                print("在 %d 次迭代後,驗證數據集的正確率爲 : %g , 測試數據集的正確率爲 : %g" % (i, validate_acc,test_acc))
在 0 次迭代後,驗證數據集的正確率爲 : 0.093 , 測試數據集的正確率爲 : 0.0965
在 1000 次迭代後,驗證數據集的正確率爲 : 0.9772 , 測試數據集的正確率爲 : 0.9764
在 2000 次迭代後,驗證數據集的正確率爲 : 0.9828 , 測試數據集的正確率爲 : 0.9804

CSDN 無尾君


5.2.3、不同模型效果比較


在第4章中,提到了設計神經網絡時的 5 種優化方法。在神經網絡結構的設計上,需要使用激活函數和多層隱藏層。在神經網絡優化時,可以使用指數衰減的學習律、加入正則化的損失函數以及滑動平均模型。在圖 5-3 中,給出了在相同神經網絡參數下,使用不同優化方法沒經過 30000 輪訓練迭代後,得到的最終模型的正確率。

這裏寫圖片描述

從圖中的反映可以說明,神經網絡的結構對最終模型的效果有本質性的影響。

我們看到,滑動平均模型和指數衰減的學習率對正確率的結果似乎沒有什麼影響,這是因爲滑動平均模型和指數衰減的學習率在一定程度上都是限制神經網絡中參數更新的速度,在 MNIST 數據集中,迭代在4000輪的時候就已經接近收斂,收斂速度很快,所以這兩種優化對最終模型的影響不大。但是當問題更加複雜時,迭代不會這麼快接近收斂,這時滑動平均模型和指數衰減的學習率可以發揮出更大的作用。比如 Cifar-10 圖像分類數據集,使用滑動平均模型可以將錯誤率降低 11%,而使用指數衰減的學習率可以將錯誤率降低 7%。

正則化損失對於模型的影響較大。

這裏寫圖片描述


5.3、變量管理

前面,將前向傳播過程抽象成了一個函數,在訓練和測試的過程中可以統一調用同一個函數來得到模型的前向傳播結果。這個函數的定義爲:

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

從定義中可以看到,這個函數的參數中包括了神經網絡中的所有參數。然而,當網絡結構更加複雜、參數更多時,就需要一個更好的方式來傳遞和管理神經網絡中的參數了。TensorFlow 提供了通過變量名稱來創建或獲取一個變量的機制。通過這個機制,在不同的函數中可以直接通過變量的名字來使用變量,而不需要將變量通過參數的形式到處傳遞。 TensorFlow 中通過變量名稱獲取變量的機制主要是通過 tf.get_variabletf.variable_scope 函數實現的。

tf.get_variabletf.Variable 的功能時基本等價的。

v = tf.get_variable("v", shape = [1], initializer = tf.constant_initializer(1.0))
v = tf.Variable(tf.constant(1.0, shape = [1])m name = "v")

兩個函數創建變量的過程基本上是一樣的。tf.get_variable 函數調用時提供的維度(shape)信息以及初始化方法(initializer)的參數和 tf.Variable 函數調用時提供的初始化過程中的參數也類似。 TensorFlow 中提供的 initializer 函數 同 3.4.3 小節中介紹地隨機數以及常量生成函數大部分是一一對應的。 比如常數初始化函數 tf.constantinitializer 和常數生成函數 tf.constant 功能上就是一致地。 TensorFlow 提供了 7 種不同地初始化函數,如下:
這裏寫圖片描述

tf.get_variable 函數與 tf.Variable 函數最大的區別在於指定變量名稱的參數。對於 tf.Variable 函數,變量名稱是一個可選的參數,通過 name=”v” 的形式給出。但是對於 tf.get_variable 函數,變量名稱是一個必填的參數, tf.get_variable 首先會試圖取創建一個名字爲 v 的參數,如果創建失敗(比如已經有同名的參數),那麼這個程序就會報錯。這是爲了避免無意識的變量複用造成的錯誤。比如在定義神經網絡參數時,第一層網絡權重已經叫 weights 了,那麼在創建第二層神經網絡時,如果參數名仍叫 weights,就會觸發變量重用的錯誤。否則兩層神經網絡共用一個權重會出現一些比較難以發現的錯誤。

如果通過 tf.get_variable 獲取一個已經創建的變量,需要通過 tf.variable_scope 來生成一個上下文管理器,並明確指定在這個上下文管理器中, tf.get_variable 將直接獲取已經聲稱的變量。下面給出了一段代碼說明如何通過 tf.variablescope 函數來控制 tf.get_variable 函數獲取已經創建過的變量。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 無尾君
"""
import tensorflow as tf

# 在名字爲 foo 的命名空間的創建名字爲 v 的變量
with tf.variable_scope('foo'):
    v = tf.get_variable("a", [1], initializer= tf.constant_initializer(1.0))

# 下面代碼會報錯,因爲命名空間 foo 中已經存在名字爲 v 的變量
# ValueError: Variable foo/v already exists, disallowed. Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope? Originally defined at:
with tf.variable_scope('foo'):
    v = tf.get_variable("v", [1])

# 在生成上下文管理器時, 將參數 reuse 設置爲 True。這樣 tf.get_variable 函數將直接獲取已經聲明的變量
with tf.variable_scope("foo", reuse= True):
    v1 = tf.get_variable('a', [1])
    print(v == v1) #輸出爲 True, 代表v, v1 代表的是相同的 TensorFlow 中變量

# 當參數 reuse 設置爲 True 時, tf.variable_scope 將只能獲取已經創建過的變量。
# 因爲在命名空間 bar 中還咩有創建變量 v,所以下面的代碼會報錯
# ValueError: Variable bar/v does not exist, or was not created with tf.get_variable(). Did you mean to set reuse=tf.AUTO_REUSE in VarScope?
with tf.variable_scope('bar', reuse = True):
    v = tf.get_variable("v", [1])

樣例簡單說明了通過 tf.variablescope 函數可以空值 tf.get_variable 函數的語義。 當參數 reuse 爲 False 或 None 時創建上下文管理器,將創建新的變量。如果同名變量已經存在則報錯。 tf.variable_scope 是可以嵌套的。下面程序說明了當 tf.variable_scope 函數嵌套時, resuse 參數的取值是如何確定的。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 無尾君
"""
import tensorflow as tf

with tf.variable_scope("root"):
    #可以通過 tf.get_variable_scope().reuse 函數來獲得當前上下文管理器中 reuse 參數的值
    print(tf.get_variable_scope().reuse) # 輸出 False,即最外層 reuse 是 False

    with tf.variable_scope('foo', reuse = True):
        print(tf.get_variable_scope().reuse) #輸出True

        with tf.variable_scope('bar'): # 新建一個嵌套的上下文管理器但不指定 reuse,#這時reuse 的取值會和外面一層保持一致
            print (tf.get_variable_scope().reuse) # 輸出 True
    print(tf.get_variable_scope().reuse) # 退出 reuse設置爲 True的上下文之後, reuse的值又回到了False

tf.variable_scope 函數聲稱的上下文管理器也會創建一個 TensorFlow 中的命名空間,在命名空間內創建的變量名稱都會帶上這個命名空間名作爲前綴。所以 tf.variable_scope 函數除了可以控制 tf.get_variable 執行的功能之外, 也提供了一個管理變量命名空間的方式。代碼如下,

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 無尾君
"""
import tensorflow as tf

v1 = tf.get_variable("v",[1])
print(v1.name) # 輸出爲 v:0  變量名:生成變量這個運算的第一個結果

with tf.variable_scope('foo'):
    v2 = tf.get_variable('v', [1])
    print(v2.name) # 輸出 foo/v:0

with tf.variable_scope('foo'):
    with tf.variable_scope('bar'):
        v3 = tf.get_variable('v', [1])
        print(v3.name) #輸出 foo/bar/v:0

    v4 = tf.get_variable('v1',[1])
    print(v4.name)

with tf.variable_scope('', reuse = True):
    v5 = tf.get_variable('foo/bar/v', [1])
    print(v5 == v3)  # 輸出 True, 調用了 該name的變量,v5輸出該name下的value

    v6 = tf.get_variable('foo/v1', [1])
    print(v6 == v4) # 輸出 True

通過 tf.variable_scope 和 tf.get_variable 函數,以下代碼對 5.2.1 小節中定義的計算前向傳播結果的函數做了一些改進。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 無尾君
"""
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_trnsor, weights) + biases)

    #類似的定義第二層神經網絡的變量和前向傳播過程。
    with tf.variable_scope('layer2'. reuse = reuse):
        weights = tf.get_variable('weights',[LAYER1_NODE, OUT_NODE],
                                  initializer= tf.truncated_normal_initializer(stddev= 0.1))
        biases = tf.get_variable('biases',[OUT_NODE],
                                 initializer = tf.constant_initializer(0.0))
        layer2 = tf.nn.relu(tf.matmul(layer1, weights) + biases)        

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

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

# 在程序中需要使用訓練好的神經網絡進行推倒時,可以直接調用 inference(new_x,True)、
# 如果需要使用滑動平均模型可以參考 5.2.1 小節中使用的代碼,把滑動平均的類傳到 inference 函數中即可。獲取或創建變量的部分不需要改變。

new_x = ...
new_y = inference(new_x, True)

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


5.4 TensorFlow 模型持久化


5.2.1小節中給出的樣例代碼在訓練完成之後就直接退出了,並沒有將訓練得到的模型保存下來方便下次直接使用。爲了讓訓練結果可以複用,需要將訓練得到的神經網絡模型持久化。


5.4.1 持久化代碼實現


TensorFLow 提供了一個非常簡單的 API 來保存和還原一個神經網絡模型。這個 API 就是 tf.train.Saver 類。以下代碼給出了保存 TensorFlow 計算圖的方法。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 無尾君
"""
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')

#聲明 tf.train.Saver() 用於保存模型
saver = tf.train.Saver()

with tf.Session() as sess:
    tf.global_variables_initializer().run()
    saver.save(sess,'./path/to/model/model.ckpt')
# 注,此處保存文件,應在建立了 path/to 兩個文件夾的前提下保#存,這樣才能夠創建 model文件夾,否則會報錯

這裏寫圖片描述

在指定的保存路徑下,生成了四個文件(書上說3個)。這是因爲 TensorFlow 會將計算圖的結構和圖上參數取值分開保存。

第一個文件爲 5.4.1_model.ckpt.meta,它保存了 TensorFlow 計算圖的結構,即神經網絡的網絡結構。
第二個文件爲 5.4.1_model.ckpt.index,I DON’T KNOW
第三個文件爲 5.4.1_model.ckpt.data-00000-of-00001,保存了每一個變量的取值
最後一個文件爲 checkpoint 文件,這個文件中保存了一個目錄下所有的模型文件列表。

以下代碼給出了加載這個已經保存的 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)) 

加載模型的代碼中沒有運行變量的初始化過程,而是將變量的值通過已經保存的模型加載進來。如果不希望重複定義圖上的運算,也可以直接加在已經持久化的圖。代碼如下:

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 無尾君
"""

# 直接加載持久化的圖
import tensorflow as tf
saver = tf.train.import_meta_graph('./path/to/model/model.ckpt.meta')
with tf.Session() as sess:
    saver.restore(sess,r'path/to/model/model.ckpt')
    print(sess.run(tf.get_default_graph().get_tensor_by_name('add:0')))

OUT:[3.]

上面的程序中,默認保存和加載了 TensorFlow 計算圖上定義的全部變量。但是有時可能只需要保存或者加載部分變量。比如,可能有一個之前訓練好的五層神經網絡模型,但現在想嘗試一個六層的神經網絡,那麼可以將前面五層神經網絡中的參數直接加在到新的模型,而僅僅將最後一層神經網絡重新訓練。

爲了保存或者加載部分變量,再聲明 tf.train.Saver 類時可以提供一個列表來指定需要保存或者加載的變量。比如在加載模型的代碼中使用 saver = tf.train.Saver([v1]) 命令來構建 tf.train.Saver 類,那麼只有變量 v1 會被加載進來。如果運行修改後只加載了 v1 的代碼會得到變量未初始化的錯誤,因爲 v2 沒有被加載,所以 v2 在運行初始化之前是沒有值的。除了可以選取需要被加載的變量, tf.train.Saver 類也支持在保存或者加載時給變量重命名。

# 變量重命名
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name = 'other-v1')
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name = 'other-v2')
saver = tf.train.Saver({'v1':v1, 'v2':v2})

這個程序中,如果直接通過 tf.train.Saver 默認的構造函數來加載保存的模型,那麼程序會報找不到的錯誤。因爲保存時變量的名稱和加載時變量的名稱不一致。爲了解決這個問題,可以通過字典(dictionary)將模型保存時的變量名和需要加在的變量聯繫起來。

這樣做的主要目的之一是方便使用變量的滑動平均值。在 TensorFLow 中,每一個變量的滑動平均值是通過影子變量維護的,所以要獲取變量的滑動平均值實際上就是獲取這個影子變量的取值。如果在加載模型時直接將影子變量映射到變量自身,那麼在使用訓練好的模型時就不需要再調用函數來獲取變量的滑動平均值了。

import tensorflow as tf
v = tf.Variable(0, dtype = tf.float32, name = 'v')

# 在沒有申明滑動平均模型時只有一個變量v,所以下面的語句只會輸出“v:0”
for variables in tf.all_variables():
    print(variables.name)

這裏寫圖片描述

ema = tf.train.ExponentialMovingAverage(0.99)
maintain_averages_op = ema.apply(tf.all_variables())
# 在申明滑動平均模型之後, TensorFlow 會自動生成一個影子變量 v/ExponentiallMoving Average:0
for variables in tf.all_variables():
    print(variables.name)

這裏寫圖片描述

saver = tf.train.Saver()
with tf.Session() as sess:
    tf.global_variables_initializer().run()
    sess.run(tf.assign(v, 10))
    sess.run(maintain_averages_op)

    # 保存時,TensorFLow 會將 v:0 和v/ExponentialMovingAverage:0 連個變量都保存下來
    saver.save(sess,'./path/to/model/model1.ckpt')
    print(sess.run([v,ema.average(v)]))

這裏寫圖片描述

以下代碼給出瞭如何通過變量重命名直接讀取變量的滑動平均值。


5.4.2、持久化原理及數據格式


當調用了 saver.save 函數時, TensorFlow 程序會自動生成幾個文件。TensorFLow 是一個通過圖的形式來表述計算的變成系統,程序中的所有計算都會被表達爲計算圖上的節點。 TensorFlow 通過元圖(MetaGraph) 來記錄計算圖中節點的信息及運行計算圖中節點所需要的元數據。元圖是由 MetaGraphDef Protocol Buffrt 定義的。MetaFraphDef 中的內容就構成了 TensorFLow 持久化時的第一個文件。以下代碼給出了 MetaGraphdef
類型的定義。

message MetaGraphDef{
    MetaInfoDef meta_info_def = 1;
    GraphDef graph_def = 2;
    SaverDef saver_def = 3;
    map<string, CollectionDef> collection_def = 4;
    map<string, SignatureDef> signature_def = 5;
    }

從上面的代碼中可以看到,元圖中記錄了 5 類信息。保存 MetaGraphDef 信息的文件默認以 .meta 爲後綴名。通過 export_meta_graph 函數,支持以 json 格式導出 MetaGraphDef Protocol Buffer .

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()
saver.export_meta_graph('/path/to/model.ckpt,meda.json', as_text=True)

這裏寫圖片描述

通過上面給出的代碼,將5.4.1中的計算圖元圖以 json 的格式導出並存儲在 model.ckpt.meta.json 文件中。下文結合 model.ckpt.metamjson 文件具體介紹 TensorFlow 元圖中存儲的信息。

meta_info_def 屬性
meta_info_def 屬性是通過 MetaInfoDef 定義的,它記錄了 TensorFlow 計算圖中的元數據以及 TensorFlow 程序中所有是用到的運算方法的信息。

…………………………
…………省略…………
…………………………

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章