本系列為Tensorflow實戰Google深度學習框架知識筆記,僅為博主看書過程中覺得較為重要的知識點,簡單摘要下來,內容較為零散,請見諒。
2017-11-06
[第五章] MNIST數字識別問題
1. MNIST數據處理
為了方便使用,Tensorflow提供了一個類來處理MNIST數據,這個類會自動下載並轉化MNIST數據的格式,將數據從原始的數據包中解析成訓練和測試神經網絡時使用的格式。
2. 神經網絡模型訓練及不同模型結果對比
為了評測神經網絡模型在不同參數下的效果,一般會從訓練數據中抽取一部分作為驗證數據。使用驗證數據就可以評判不同參數取值下模型的表現。除了使用驗證數據集,還可以采用交叉驗證(cross validation)的方式來驗證模型效果,但因為神經網絡訓練實踐本身就比較長,采用cross validation會花費大量時間。所以在海量數據的情況下,一般會更多地采用驗證數據集的形式來評測模型的效果。
為了說明驗證數據在一定程度上可以作為模型效果的評判標准,我們將對比在不同迭代輪數的情況下,模型在驗證數據和測試數據上的正確率。為了同時得到同一個模型在驗證數據和測試數據上的正確率,可以在每1000輪的輸出中加入在測試數據集上的正確率。
在神經網絡結構的設計上,需要使用激活函數和多層隱藏層。在神經網絡優化時,可以使用指數衰減的學習率,加入正則化的損失函數以及滑動平均模型。
3. 變量管理
Tensorflow提供了通過變量名稱來創建或者獲取一個變量的機制,通過這個機制,在不同的函數中可以直接通過變量的名字來使用變量,而不需要將變量通過參數的形式到處傳遞。Tensorflow中通過變量名稱獲取變量的機制主要時通過tf.get_variable和tf.variable_scope函數實現的。
除了tf.Variable函數,Tensorflow還提供了tf.get_variable函數來創建或者獲取變量。當tf.get_variable用於創建變量時,它和tf.Variable的功能是基本等價的。
如:#下面這兩個定義是等價的。
v = tf.get_variable("v",shape=[1],initializer=tf.constant_initilizer(1.0))
v = tf.Variable(tf.constant(1.0,shape=[1]),name="v")
Tensorflow提供了7種不同的初始化函數,如下:
初始化函數 功能 主要參數
tf.constant_initializer--->將變量初始化為給定常量----> 常量的取值
tf.random_normal_initializer--->將變量初始化為滿足正太分布的隨機值--->正太分布的均值和標准差
tf.truncated_normal_initializer--->將變量初始化為滿足正太分布的隨機值,但若隨機出來的值偏離平均值超過兩個標准差,那么這個數將會被重新隨機---->正太分布的均值和標准差
tf.random_uniform_initializer--->將變量初始化為滿足平均分布的隨機值---->最大,最小值
tf.uniform_unit_scaling_initializer--->將變量初始化為滿足平均分布但不影響輸出數量級的隨機值---->factor(產生隨機值時乘以的系數)
tf.zeros_initializer--->將變量設置為全為0--->變量維度
tf.ones_initializer--->將變量設置為全為1--->變量維度
在上面的樣例定義程序中,tf.get_variable首先會試圖去創建一個名字為v的參數,如果創建失敗(比如已經有同名的參數),那么這個程序就會報錯。這是為了避免無意識的變量復用造成的錯誤。比如在定義神經網絡參數時,第一層網絡的權重已經叫weights了,那么在創建第二層神經網絡時,如果參數名仍然叫weights,就會觸發變量重用的錯誤。否則兩層神經網絡共用一個權重會出現一些比較難以發現的錯誤。如果需要通過tf.get_variable獲取一個已經創建的變量,需要通過tf.variable_scope函數來生成一個上下文管理器,並明確指定在這個上下文管理器中,tf.get_variable將直接獲取已經生成的變量。下面給出一段代碼說明如何通過tf.variable_scope函數來控制tf.get_variable函數獲取已經創建過的變量。
#在名字為foo的命名空間內創建名字為v的變量
with tf.variable_scope("foo"):
v = tf.get_variable("v",[1],initializer=tf.constant_initializer(1.0))
#因為在命名空間foo已經存在名字為v的變量,所有下面的代碼將會報錯:
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("v",[1])
print v == v1 #輸出為True,代表v,v1是相同的Tensorflow中的變量
#將參數reuse設置為True時,tf.variable_scope將只能獲取已經創建的變量,因為在命名空間bar中還沒有創建變量v,所以下面的代碼將會報錯:
with tf.variable_scope("bar",reuse=True):
v = tf.get_variable("v",[1])
同樣,如果tf.variable_scope函數使用參數reuse=None或者reuse=False創建上下文管理器,tf.get_variable操作將創建新的變量,如果同名的變量已經存在,則tf.get_variable函數將報錯。另外,Tensorflow中tf.variable_scope函數是可以嵌套的。
使用變量管理后,就不再需要將所有變量都作為參數傳遞到不同的函數中了,當神經網絡結構更加復雜,參數更多時,使用這種變量管理的方式將大大提高程序的可讀性。
4. Tensorflow模型持久化
為了讓訓練結果可以復用,需要將訓練得到的神經網絡模型持久化(保存下來方便下次使用)。
Tensorflow提供了一個非常簡單的API來保存和還原一個神經網絡模型,這個API就是tf.train.Saver類。
saver = tf.train.Saver()
saver.save(sess,"/path/to/model/model.ckpt")
Tensorflow模型一般會存在后綴為.ckpt文件中,雖然上面的程序只指定了一個文件路徑,但是在這個文件目錄下會出現三個文件,這是因為Tensorflow會將計算圖的結構和塗上的參數取值分來保存。第一個文件為model.ckpt.meta,它保存了Tensorflow計算圖的結構,第二個文件為model.ckpt,這個文件保存了Tensorflow程序中每一個變量的取值,最后一個文件為checkpoint文件,這個文件保存了一個目錄下所有的模型文件列表。
加載模型的代碼:
saver = tf.train.Saver()
saver.restore(sess,"/path/to/model/model.ckpt")
上面給出的程序中,默認保存和加載了Tensorflow計算圖上定義的全部變量。但有時候可能只需要保存或者加載部分變量,比如,可能有一個之前訓練好的五層神經網絡模型,但現在想嘗試一個六層的神經網絡,那么可以將前面五層神經網絡中的參數直接加載到新的模型,而僅僅將最后一層神經網絡重新訓練。為了保存或者加載部分變量,在聲明tf.train.Saver類時可以提供一個列表來指定需要保存或者加載的變量。比如在加載模型的代碼中使用saver=tf.train.Saver([v1])命令來構建tf.train.Saver類,那么只有變量v1會被加載進來,如果運行修改后之家在v1的代碼會得到變量未初始化的錯誤:
tensorflow.python.framework.errors.FailedPreconditionError:Attempting to use uninitialized value v2
因為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")
#如果直接使用tf.train.Saver類來加載模型會報變量找不到的錯誤。
#使用一個字典(dictionary)來重命名變量就可以加載原來的模型了。這個字典指定了原來名稱為v1的變量現在加載到變量v1中(名稱為other-v1),
#名稱為v2的變量加載到變量v2中(名稱為other-v2)
saver = tf.train.Saver({"v1":v1, "v2":v2})
在這個程序中,對變量v1和v2的名稱進行了修改,如果直接通過tf.train.Saver默認的構造函數來加載保存的模型,那么程序會報變量找不到的錯誤,因為保存時候變量的名稱和加載時變量的名稱不一致。為了解決這個問題,Tensorflow可以通過字典(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_average_op = ema.apply(tf.all_variables())
#在申明滑動平均模型之后,Tensorflow會自動生成一個影子變量v/ExponentialMoving Average
#於是下面的語句會輸出"v:0"和“v/ExponentialMoving Average:0”
for variables in tf.all_variables():
print(variables.name)
saver = tf.train.Saver()
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
sess.run(tf.assign(v,10))
sess.run(maintain_average_op)
#保存時Tensorflow會將v:0和v/ExponentialMoving Average:0兩個變量都保存下來
saver.save(sess,"path/to/model/model.ckpt")
print(sess.run([v,ema.average(v)])) #輸出[10.0,0.09999]
使用tf.train.Saver會保存運行Tensorflow程序所需要的全部信息,然后有時並不需要某些信息。比如在測試或者離線預測時,只需要知道如何從神經網絡的輸入層經過前向傳播計算得到輸出層即可,而不需要類似於變量初始化,模型保存等輔助節點的信息。而且,將變量取值和計算圖結構分成不同的文件存儲有時候也不方便,於是Tensorflow提供了convert_variables_to_constants函數,通過這個函數可以將計算圖中的變量及其取值通過常量的方式保存,這樣整個Tensorflow計算圖可以統一存放在一個文件中。如下:
import tensorflow as tf
from tensorflow.python.framework import graph_util
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()
with tf.Session() as sess:
sess.run(init_op)
#導出當前計算圖的GraphDef部分,只需要這部分就可以完成從輸入層到輸出層的計算過程
graph_def = tf.get_default_graph().as_graph_def()
#將圖中的變量及其取值轉化為常量,同時將圖中不必要的節點去掉
output_graph_def = graph_util.convert_variables_to_constants(sess,graph_def,['add'])
with tf.gfile.GFile("/path/to/model/combined_model.pb","wb") as f:f.write(output_graph_def.SerializeToString())
通過下面的程序可以直接計算定義的加法運算的結果:
import tensorflow as tf
from tensorflow.python.platform import gfile
with tf.Session() as sess:
model_filename = "/path/to/model/combined_model.pb"
#讀取保存的模型文件,並將文件解析成對應的GraphDef Protocol Buffer
with gfile.FastGFile(model_filename,'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
#將graph_def中保存的圖加載到當前的圖中.return_elements=["add:0"]給出了返回的張量的名稱。
#在保存能的時候給出的時計算節點的名稱,所以為"add",在加載的時候給出的是張量的名稱,所以時add:0
result = tf.import_graph_def(graph_def,return_elements=["add:0"])
print(sess.run(result))
Tensorflow是一個通過圖的形式來表達計算的編程系統,Tensorflow程序中的所有計算都會表達為計算圖上的節點。Tensorflow通過元圖(MetGraph)來記錄計算圖中節點的信息以及運行計算圖中節點所需要的元數據。Tensorflow中元圖是由MetaGraphDef Protocol BUffer定義的,MetaGraphDef中的內容就構成了Tensorflow持久化時的第一個文件。關於MetaGraphDef類型的定義以及模型持久化的原理和數據的格式請另查閱相關資料。[page 116 129/297]
5. Tensorflow最佳實踐樣例程序
本節中將介紹一個Tensorflow訓練神經網絡模型的最佳實踐,將訓練和測試分成兩個獨立的程序,這可以使得每一個組件更加靈活。比如訓練神經網絡的程序可以持續輸出訓練好的模型,而測試程序可以每隔一段實踐檢驗最新模型的正確率,如果模型效果更好,則將這個模型提供給產品使用。除了將不同的功能模塊分開,本節還將前向傳播的過程抽象成一個單獨的庫函數。因為神經網絡的前向傳播過程在訓練和測試的過程中都會用到,所以通過庫函數的方式使用起來既方便又可以保證訓練和測試過程中使用的前向傳播方法是一致的。
下面將提供重構之后的程序來解決MNIST問題:第一個是mnist_inference.py,它定義了前向傳播的過程以及神經網絡中的參數,第二個時minst_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的集合
#這是自定義的集合,不再Tensorflow自動管理的集合列表中
if regularizer != None:
tf.add_to_collection('losses',regularizer(weights))
return weights
#定義神經網絡的前向傳播過程
def inference(input_tensor,regularizer):
#聲明第一層神經網絡的變量並完成前向傳播的過程
with tf.variable_scope('layer1'):
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.nn.relu(tf.matmul(layer1, weights) + biases)
#返回最后前向傳播的結果
return layer2
mnist_train.py:
#-*- coding:utf-8 -*-
import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
#加載mnist_inference.py中定義的常量和前向傳播的函數
import mnist_inference
#配置神經網絡的參數
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 30000
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(REGULARAZTION_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(y,tf.arg_max(y_,1))
cross_entropy_mean = tf.reduce_mean(cross_entropy)
loss = cross_entropy_mean+tf.add_n(tf.get_collection_ref('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,global_step],feed_dict={x:xs,y_:ys})
#每1000輪保存一次模型
if i % 1000 == 0:
#輸出當前的訓練情況
print("After %d training steps, loss on training batch is %g."%(step,loss_value))
#保存當前的模型
saver.save(sess,os.path.join(MODEL_SAVE_PATH,MODEL_NAME),global_step=global_step)
def main(argv=None):
mnist = input_data.read_data_sets("MNIST_DATA",one_hot=True)
train(mnist)
if __name__ == '__main__':
tf.app.run()
mnist_eval.py:
#-*- 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}
#直接通過調用封裝好的函數來計算前向傳播的結果,因為測試時不關注正則化損失的值,所以這里用於計算正則化損失的函數被設置為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)
variable_to_restore = variable_averages.variables_to_restore()
saver = tf.train.Saver(variable_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 steps, validation accuracy = %g." %(global_step,accuracy_score))
else:
print('No checkpoint file found.')
return
time.sleep(EVAL_INTERVAL_SECS)
def main(argv=None):
mnist = input_data.read_data_sets("MNIST_DATA",one_hot=True)
evaluate(mnist)
if __name__ == '__main__':
tf.app.run()