Tensorflow_03_Checkpoint 與 Tensorboard

Brief 概述

在理解了建構神經網絡的大致函數用途,且熟悉了神經網絡原理後,我們已經大致具備可以編寫神經網絡的能力了,在涉及比較複雜的神經網絡結構前,還有兩件重要的事情需要了解,那就是中途存檔和事後讀取的函數,它攸關到龐大的算力和時間投入後產出的結果是否能夠被再次使用,是一個絕對必須弄清楚的環節。另外是 Tensorflow 提供的的一個工具名爲 Tensorboard,它可以很有效率的爲我們呈現數據流圖可視化的過程,包含了計算的結果和數據分佈的狀態,讓我們在尋找錯誤的時候有一個更爲清晰的邏輯脈絡,因此本節主要圍繞這兩個主題展開:

  1. Checkpoint
    • tf.train.Saver().save()
    • tf.train.Saver().restore()
  2. Tensorboard 

前者如同會議記錄一般,可以針對性的把訓練過程記錄下來,除了避免前功盡棄之外,還可以讓我們有機會一窺訓練過程的究竟,從演變過程中尋找改善算法的方案;而後者提供一個在瀏覽器梳理計算過程的核心工具,提升了整體的開發效率與優化參數的過程。

p.s. 關於設備如果手邊沒有,非常建議直接使用雲端的計算服務,如 AWS, FloydHub 等平臺

其他在深度學習中常用的函數定義方法可以參考上一篇文章: Tensorflow_02_Useful Functions 常用函數大全

Checkpoint 檢查點

在初期一般訓練模型簡單且訓練速度極快,對於參數中間變化的過程我們也不會特別在意,但是到了複雜的神經網絡訓練過程時,爲參數訓練過程中途存檔這件事情就會變得非常重要,這就像我們玩電玩遊戲闖關的時候,希望最好能夠中途存檔,如果死在半路上可以直接從存檔的地方恢復遊戲。

 

Save checkpoints 儲存檢查點

1-1. Save checkpoints 儲存檢查點

同理深度學習訓練過程,一般訓練耗費時間約爲幾天乃至一週,如果中途發生機器停機或是任何意外導致訓練終止,我們可以從檢查點記錄的地方重新開始。抑或者如果我們要分析訓練過程中參數的變化走勢,檢查點也非常實用。使用的類爲:

  • tf.train.Saver(max_to_keep=None) 檔名: 「.ckpt」
    • .Saver({’save_w‘: weight}) 括弧中可以用字典的方式指定只要儲存哪一個參數
    • max_to_keep=None: 最多有幾個檢查點被保存下來,如果是 None 或是 0 則表示全保存
    • keep_checkpoint_every_n_hours=1: 設置幾個小時保存一次檢查點

變量以二進制的方式被存在名爲 .ckpt 的檔案中,內容包含了變量的名字和對應張量的數值,創建一個該類的示例,就可以呼叫裏面儲存與載入儲存文件內容的函數方法:

  • tf.train.Saver().save(sess, './file_directory', global_step=int(num))
    • sess: 表示要儲存哪個繪話裏面的參數
    • './file_directory/file_name': 儲存的路徑沿着執行訓練的 .py 文檔路徑位置繼續指定路徑,如果文件夾不存在指定目錄的話,它會自行創建。官網教程中建議檔名後面連同後綴一起加上,如下代碼...
    • global_step:指定一個數字,將一起被納入檢查點文件命名中

!!! 儲存這些參數的時候特別需要注意申明清楚參數的數據類型非常重要,它攸關到之後要呼叫回這些參數的時候是否順利,如果沒有事先申明清楚,大概率上會有錯誤發生。

下面代碼展示如何保存檢查點:

import numpy as np
import tensorflow as tf

x_data = np.random.rand(100).astype(np.float32)
y_data = x_data * 0.1 + 0.3

weight = tf.Variable(tf.random_uniform(shape=[1], minval=-1.0, maxval=1.0), 
                     dtype=np.float32)#, name='weight')
bias = tf.Variable(tf.zeros(shape=[1]), dtype=np.float32, name='bias')
y = weight * x_data + bias

loss = tf.reduce_mean(tf.square(y - y_data))
optimizer = tf.train.GradientDescentOptimizer(0.5)
training = optimizer.minimize(loss)

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)

# The instance is created to call the method saving checkpoint
saver = tf.train.Saver()
save_w = tf.train.Saver({'a_name': weight})

for step in range(101):
    sess.run(training)
    if step % 10 == 0:
        print('Round {}, weight: {}, bias: {}'
              .format(step, sess.run(weight[0]), sess.run(bias[0])))
        saver.save(sess, './checkpoint/linear.ckpt', global_step=step)
        save_w.save(sess, './weight/linear.ckpt', global_step=step)
        
saver.save(sess, './checkpoint/linear.ckpt')
sess.close()

Round 0, weight: 0.6087742447853088, bias: 0.031045857816934586
Round 10, weight: 0.3177388906478882, bias: 0.18408644199371338
Round 20, weight: 0.19332920014858246, bias: 0.2503160834312439
Round 30, weight: 0.14000359177589417, bias: 0.27870404720306396
Round 40, weight: 0.11714668571949005, bias: 0.2908719480037689
Round 50, weight: 0.10734956711530685, bias: 0.29608744382858276
Round 60, weight: 0.10315024852752686, bias: 0.29832297563552856
Round 70, weight: 0.10135028511285782, bias: 0.29928117990493774
Round 80, weight: 0.10057878494262695, bias: 0.29969191551208496
Round 90, weight: 0.10024808347225189, bias: 0.2998679280281067
Round 100, weight: 0.10010634362697601, bias: 0.2999434173107147

檢查點的路徑設置需要使用 「./.../.../...」 的格式去寫路徑,尤其是開頭的 ./ 必須加上,否則在某些平臺上會出現錯誤,等代碼運行完畢後在下面 .py 文檔執行路徑下出現我們設置的儲存文件夾和文件名稱,如下圖:

在默認情況下 tf.train.Saver(max_to_keep=5) 是我們無特別設定的結果,因此只會保存離最近更新的五個參數,其他的參數將即自動刪除。

 

1-2. Read checkpoints 讀取檢查點

文件存好之後接下來就是讀取上圖中儲存的文件,儲存在文件裏面的數據是一個原封不動的 tf.Variable() 物件,有着與儲存前一模一樣的名字和屬性,甚至在呼叫回該儲存的變量時也不用初始化,是一個非常全面的保存結果, 只是需要記得: 「同樣變量名的物件需要事先存在在代碼中, 並且數據類型和長相必須一模一樣。

讀取的方式也很直觀,同樣的創建一個 tf.train.Saver() 示例,並用該示例裏面的方法 .restore() 完成讀取,讀取完畢後儲存的參數就回像起死回生一般重新回到我們的代碼中。

  • tf.train.Saver().restore(sess, 'file_directory')
    • sess: 表示我們希望把該儲存的內容重新叫回哪一個繪話中
    • './file_directory/file_name': 表示我們要呼叫的該存檔文件

p.s. 如果在儲存過程中有加上 global_step 參數,呼叫文檔名的時候就必須一起把數字也加上去,如下代碼。

呼叫儲存文件的時候有以下三種情況:

  1. 最直接: 使用 tf.train.Saver() 創建示例後,呼叫 .restore() 方法配合對應名字,成功回到訓練中途的記錄
  2. 第一個方法受阻: 繞道使用 .meta 儲存文件,並使用 tf.import_meta_graph() 示例的 .restore() 方法,同樣可以成功回到訓練中途的記錄
  3. 呼叫只儲存部分參數的記錄檔: 創建一個示例前先在 tf.train.Saver() 括弧中使用字典形式聲明好當時部分儲存的時候對應一模一樣名字的字典鍵和參數名,再用 .restore() 方法成功回到訓練中途的記錄

詳細代碼如下演示:

import tensorflow as tf

# tf.reset_default_graph()
weight = tf.Variable([33], dtype=tf.float32)#, name='weight')
bias = tf.Variable([3], dtype=tf.float32, name='bias')

saver = tf.train.Saver()
# saver = tf.train.import_meta_graph('./checkpoint/linear.ckpt.meta')
saver_2 = tf.train.Saver({'a_name': weight})
init = tf.global_variables_initializer()

sess = tf.Session()
sess.run(init)
path1 = saver.restore(sess, './checkpoint/linear.ckpt-90')
path2 = saver_2.restore(sess, './weight/linear.ckpt-60')
print(sess.run(weight))
print(sess.run(bias))
sess.close()

''' 
print(sess.run(biases))

### ----- Result as follow ----- ###
FailedPreconditionError: 
Attempting to use uninitialized value Variable
[[Node: _retval_Variable_0_0 = _Retval[T=DT_FLOAT, index=0, 
  _device="/job:localhost/replica:0/task:0/device:CPU:0"](Variable)]]
'''


### ----- Result is shown below ----- ###
INFO:tensorflow:Restoring parameters from ./checkpoint/linear.ckpt-90
INFO:tensorflow:Restoring parameters from ./weight/linear.ckpt-60
[0.10315025]
[0.29986793]

可以觀察到,如果沒有成功導入內容, sess.run() 執行一個參數的時候就會被通知該參數沒有初始化,需要特別注意。另外如果重複導入同樣的值到該代碼中,那麼該值以最後一次導入爲主,如上面代碼中的 weight,最近導入的 60 個回合訓練的 weight 值比訓練 90 個回合的 bias 值還要不準得多。


ValueError: At least two variables have the same name: Variable

花了大把的時間才找出在回傳參數的時候發生錯誤的癥結點,最後原因還是在於 tf.Variable() 的格式沒有完全一樣,前面只專注在數據格式上面,但是其節點名稱必須也完全一致纔可以! 如果表明名稱 name='a_name', 那麼就都不要寫,如果表明了名稱,那就必須完全一致才行!

下面提煉三個有關儲存和導出的要點:

  1. 儲存的時候官網建議我們呢加上 .ckpt 後綴的目的僅僅只是爲了區隔該儲存文件與別的文件之間的不同,其實並非真正去修改其後檔名,因此沒有加上此後綴的情況下,儲存的參數照樣可以被導入
  2. 儲存後的文件狀態有些電腦中是三個,有些是四個,他們的全名甚至沒有與當時儲存文件時的名字完全相同,但是實際上不用擔心,在回傳參數的時候只要想着當時儲存的檔名是什麼即可,完全可以忽視不同檔名的影響
  3. 回傳參數的時候必須與法官一樣嚴格的審視要被導入的框架容器是否跟當時儲存參數的時候完全一樣,包含命名節點的名字,和每個節點設置的數據類型

p.s. 如果是使用 Jupyter Notebook 啓動代碼的話,切記在使用 .restore 回傳參數之前確定沒有先啓動了訓練的過程,需要該變量的值是空的情況下才能順利傳參


Save / Restore Related Useful Functions 好用函數

1. tf.train.latest_checkpoint('./.../...')

回傳的是該目錄下最近一次被儲存的 checkpoint 文件完整位置,數據類型是字符串,此類方法放入的路徑切記是文件夾目錄,而非文件本身的目錄,因此通常只要找到存放儲存點的文件夾目錄,用此方法回傳一個字符串結果後,放入 .restore() 內就可以順利呼叫最新的存檔參數內容。

 

2. Tensorboard 可視化工具

在使用 Tensorflow 之初,我們首先了解的兩個觀念肯定是節點 node 和邊 edge,藉由在這兩者裏面添加張量和運算單元等方法,我們最終可以驅動計算機完成我們期望的運算結果。然而這些運算過程都是在我們腦子裏面抽象概念,放到計算機中也只是一行一行的代碼,並沒有辦法提供給我們太直觀的感受,而 Tensorboard 就是中間的潤滑劑,它良好的構建一個硬邦邦的代碼與直觀感受之間的橋樑,讓如同一個黑箱般的神經網絡運算過程透出一線光亮,只要在現有的代碼中添加一些 作用域 Scope 的設置後,導出文件並使用終端的 Tensorboard 指令執行,Tensorflow 就會自動爲我們建構出一個完整的數據布告欄在瀏覽器中使用。

而這些額外添加的代碼這裏,目的就是用來給不同的節點和邊之間設置各自歸屬的作用域,分爲下面兩種:

  • node names
  • name scopes

繼續沿用上面的代碼,加上作用域的方法如下:

import numpy as np
import tensorflow as tf

# Create a graph and set this graph as the default one.
# By doing this, the original default graph would not be called
graph = tf.Graph()
with graph.as_default():
    x_data = np.random.rand(100).astype(np.float32)
    y_data = x_data * 0.1 + 0.3

    with tf.name_scope('linear'):
        weight = tf.Variable(tf.random_uniform(shape=[1], minval=-1.0, maxval=1.0), 
                             dtype=np.float32, name='weight')
        bias = tf.Variable(tf.zeros(shape=[1]), dtype=np.float32, name='bias')
        y = weight * x_data + bias

    with tf.name_scope('gradient_descent'):
        loss = tf.reduce_mean(tf.square(y - y_data), name='loss')
        optimizer = tf.train.GradientDescentOptimizer(0.5)
        training = optimizer.minimize(loss)
        init = tf.global_variables_initializer()

    
sess = tf.Session(graph=graph)

writer = tf.summary.FileWriter('/Users/kcl/Documents/Python_Projects/01_AI_Tutorials/tb')
writer.add_graph(graph)

sess.run(init)

for step in range(101):
    sess.run(training)
    
writer.close()
sess.close()

代碼的運行結果會在指定的目錄下創建一個文件,該文件只能夠在終端 (Terminal or CMD) 使用下面指令開啓: tensorboard --logdir="/Users/01_AI_Tutorials/tb" 或者 tensorboard --logdir /Users/01_AI_Tutorials/tb 的方式開啓,之後終端裏面會創建一個本地伺服器和一個對應網址,用來讓使用者在瀏覽器中開啓 Tensorboard 的頁面,下圖即爲開啓上面代碼背後的 Tensorboard 模樣:

在每個 Tensor 後面都可以加上 name 予以名稱,而 tf.name_scope() 則可以把一搓的 Tensors 打包起來成爲一個更大的涵蓋範圍。

p.s. 但是值得我們注意的一點,只有屬於 tf 的節點與運算子,或者是跟節點產生關聯的算式才能夠被記錄在 Tensorboard 上面,如果跟 tf 一點關係都沒有的算式如 x_data,y_data 就不會顯示在數據流圖中。

 

Dive into the Details of Graph 深究細節

在建立數據流圖的上面過程中,實際上完整的步驟如下:

  1. 創建一個 tf.Graph() 對象
  2. 使用對象的方法 .as_default() 設定該圖爲指定的圖
  3. 在該圖中設定需要的節點內容並給予名稱
  4. 使用 tf.name_scope() 等方法設定更大作用域的名稱,名稱注意不能夠有空格
  5. 使用 tf.summary.FileWriter() 方法在指定路徑下創建一個文件
  6. 把步驟五的方法指向一個對象,然後使用 .add_graph() 添加指定 session 創建好的 graph

完成後,纔可以如上述步驟去終端指定路徑下開啓 Tensorboard 觀察數據流圖,一般代碼中之所以沒那麼複雜,原因是在創建數據流圖的時候,Tensorflow 框架本身已經爲我們預設了一個數據流圖,因此省去許多麻煩。

 

Unveil the default Graph 一探預設流圖

1. Graph is the foundation of All

我們在使用 Tensorflow 創建自己的數據流圖的時候,都是基於 一張圖 的構建的,此圖就如同一張無邊際的畫布讓我們在上面自由的創建張量和運算子,即便在我們不特別去設定一個數據流圖的時候,tf 框架本身也已經自動幫使用者生成好了,因此可以確保每個節點和運算子都落在這張圖裏面。

但是如果如上圖的情況一樣,是我們自己從頭到尾設定的數據流圖作用域,那就必須考慮到是否把全部的節點和運算子全部都建立在圖上,一旦某個東西落在了圖外面,到了執行的時候 tf 框架就會不明白該對象是誰家的孩子。

2. Calculation and the Graph

把哪個節點和運算子歸類到了哪個作用域這件事情,說到底其實也就只是在 Tensorboard 上面呈現的畫面不同而已,並不會對運算的功能產生任何影響。

另外,Tensorboard 只會打印數據流圖本身的結構,並不會參與 sess.run() 執行運算的結果輸出,因此不論儲存文檔的代碼放在 sess.run() 的前還是後,結果都會是一樣的結構呈現,但並沒有運算數值。

Import Data 數據導入

直到目前爲止我們在 "圖" 上創建的數據流圖也僅僅只是數據流圖,並沒有數據實際上在 Tensorboard 中被參與進來,把參數寫入到 Tensorboard 的方法不外乎 tf.summary 系列的函數。然而,用這些函數寫入數據到 Tensorboard 的過成類似於 sess.run() 的過程,對同一個對象而言一次只會寫一個數字,完成圖之所以會有一個連貫的結果,那是因爲使用 Python 建構一個迴圈運行出來的結果導致,系列函數包含了如下幾個:

  • tf.summary.scalar()
  • tf.summary.image()
  • tf.summary.audio()
  • tf.summary.histogram()
  • tf.summary.tensor()

上面陳列方法的參數項內容都是一樣的,皆爲 ...('a_name/node', object),第一個參數是一個字符串,其內容是什麼,在圖上顯示出來座標的名稱就是什麼,而第二項則是一個經過運算後的對象,誰被放到這裏的話,誰就能夠在圖上的座標軸中顯示方程式經過訓練的變化過程。

面對簡單的數據流圖,我們尚且可以數得出來一共有多少個數值需要被放入 Tensorboard 中,但是如果是像 Inception 這類的巨大神經網絡,那我們就需要一個工具統合所有需要被寫入的對象,一次性的完成寫入的動作:tf.summary.merge_all(),我們可以進一步想像上面陳列的五種方法就像是剪刀,負責剪下我們期望截取的數值,然而一次一次貼上 board 實在太麻煩,因此先全部把這些剪下來的值融爲一體後,再使用 .FileWriter() 方法一次貼上,並以 .add_summary() 方法逐步更新。函數方法使用方式如下代碼:

import numpy as np
import tensorflow as tf

graph = tf.Graph()
with graph.as_default():
    x_data = np.random.rand(100).astype(np.float32)
    y_data = x_data * 0.1 + 0.3

    with tf.name_scope('linear'):
        weight = tf.Variable(tf.random_uniform(shape=[1], minval=-1.0, maxval=1.0), 
                             dtype=np.float32, name='weight')
        # In order to watch the changes of weight, use histogram method
        tf.summary.histogram('linear_weight', weight)
        
        bias = tf.Variable(tf.zeros(shape=[1]), dtype=np.float32, name='bias')
        # Mind that we don't need to assign the method to an object
        tf.summary.histogram('linear_bias', bias)
        y = weight * x_data + bias

    with tf.name_scope('gradient_descent'):
        loss = tf.reduce_mean(tf.square(y - y_data), name='loss')
        # The changes of loss would be updated in each iteration of optimization
        tf.summary.scalar('linear_loss', loss)
        optimizer = tf.train.GradientDescentOptimizer(0.5)
        training = optimizer.minimize(loss)

    ### --! Mind the graph scope if we are using the graph set by ourselves !-- ###
    init = tf.global_variables_initializer()
    # As we can see above, we have total 3 values needed to be merged
    merge = tf.summary.merge_all()

# If we are using the default graph, "graph=graph" would not need to be emphasized
sess = tf.Session(graph=graph)

# A graph should only be created once outside of the for loop
writer = tf.summary.FileWriter('/Users/kcl/Documents/Python_Projects/01_AI_Tutorials/tb')
# If the default graph is used, "graph" should be replaced by "sess.graph" 
writer.add_graph(graph)

sess.run(init)

for step in range(101):
    if step%2 == 0:
        # In order to update the value throughout training iteration,
        # we should use add_summary method to record the values into the graph.
        # But before the recording, we have to merge all summary nodes again first.
        m = sess.run(merge)
        writer.add_summary(m, step)
    
    sess.run(training)
    if step % 10 == 0:
        print('Round {}, weight: {}, bias: {}'
              .format(step, sess.run(weight[0]), sess.run(bias[0])))

writer.close()
sess.close()

p.s. 需要非常注意如果是自己定義的 Graph,就必須非常小心作用域錯誤位造成的程序崩潰。

代碼當中,我們可以設定要在經歷幾個 step 之後,才 merge 一次上面的所有值並把這些值寫入到文件當中,成爲 Tensorboard 裏面顯示座標圖的精度控制,下面是經過不同的 tf.summary 方法回傳值到文件中並用瀏覽器顯示出來的結果:

利用 Tensorboard 提供的諸多工具可以讓我們在訓練模型的過程中更容易找到問題所在,並根據過程的變化來判斷優化模型的方向。此工具是 Google 話費大量人力所共同完成的一項厲害的作品,同時也是其他深度學習框架中沒有的工具之一,如果使用其他的模型同時需要觀察例如損失函數的數值變化,那麼使用者只能夠自己從頭建構座標軸,使用如 matplotlib 等工具一點一點的把數值記錄到圖上,但這麼一來又將降低代碼的執行效率,是一個魚與熊掌的關係。反觀 Tensorboard 是一個嵌入在 tf 的工具,有非常好的效率和兼容性,值得讓我們花時間一探究竟。

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