令人困惑的 TensorFlow!(II)

机器之心發表於2018-10-05

六月底,機器之心釋出了「令人困惑的 TensorFlow」,講述了初上手 TensorFlow 時會遇到的麻煩。日前,作者更新部落格,對該文寫了續篇,主要講述了儲存和載入 TensorFlow 模型以及上下文管理器的一些問題。

命名和域

命名變數和張量

正如我們在第一部分討論的,每次呼叫 tf.get_variable() 時,都需要為變數賦予一個新的唯一名稱。實際上,圖中的每個張量也需要一個唯一的名稱。可以透過張量、操作和變數的 .name 屬性訪問該名稱。絕大多數情況下,名稱會自動建立;例如,一個常量節點會以 Const 命名,當建立更多常量節點時,其名稱將是 Const_1,Const_2 等。還可以透過 name=的屬性設定節點名稱,列舉字尾仍會自動新增:

程式碼:

import tensorflow as tf
a = tf.constant(0.)
b = tf.constant(1.)
c = tf.constant(2., name="cool_const")
d = tf.constant(3., name="cool_const")
print a.name, b.name, c.name, d.name

輸出:

Const:0 Const_1:0 cool_const:0 cool_const_1:0

雖然節點命名並非必要,但在除錯時非常有用。當 Tensorflow 程式碼崩潰時,error trace 將指向一個特定的操作。如果有很多同型別的操作,那麼很難確定是哪一個出了問題。而透過明確命名每個節點,可以獲得資訊詳細的 error trace,並更快地識別問題。

使用範圍

隨著圖形越來越複雜,手動命名所有內容變得愈加困難。Tensorflow 提供 tf.variable_scope 物件,它透過將圖形細分為更小的組塊,使圖形更易梳理。透過將一段圖形建立程式碼封裝在 with tf.variable_scope(scope_name):語句中,建立的所有節點名稱都將自動以 scope_name 字串作為字首。此外,這些作用域堆疊,在另一個範圍內建立的作用域會簡單地將字首連結在一起,用斜槓分隔。

程式碼:

import tensorflow as tf
a = tf.constant(0.)
b = tf.constant(1.)
with tf.variable_scope("first_scope"):
  c = a + b
  d = tf.constant(2., name="cool_const")
  coef1 = tf.get_variable("coef", [], initializer=tf.constant_initializer(2.))
  with tf.variable_scope("second_scope"):
    e = coef1 * d
    coef2 = tf.get_variable("coef", [], initializer=tf.constant_initializer(3.))
    f = tf.constant(1.)
    g = coef2 * f

print a.name, b.name
print c.name, d.name
print e.name, f.name, g.name
print coef1.name
print coef2.name

輸出:

Const:0 Const_1:0
first_scope/add:0 first_scope/cool_const:0
first_scope/second_scope/mul:0 first_scope/second_scope/Const:0 first_scope/second_scope/mul_1:0
first_scope/coef:0
first_scope/second_scope/coef:0

我們能夠使用程式碼 coef 建立兩個名稱相同的變數。這是因為作用域可以將名稱轉換為 first_scope/coef:0 和 first_scope/second_scope/coef:0,它們是不同的。

儲存和載入

訓練好的神經網路包括兩個基本組成部分:

  • 已經學習過某些任務最佳化的網路權重

  • 說明如何利用權重獲得結果的網路圖

Tensorflow 將這兩個元件分開,但很明顯它們需要緊密匹配。如果沒有圖結構進行說明,那權重也無用,而帶有隨機權重的圖也效果也不好。事實上,即使僅交換兩個權重矩陣也可能完全破壞模型。這通常會讓 Tensorflow 初學者感覺很挫敗。使用預先訓練好的模型作為神經網路的一個組成部分不失為加速訓練的好方法,但是也有可能搞砸一切。

儲存模型

當只有單個模型時,Tensorflow 用於儲存和載入的內建工具使用很方便:只需建立一個 tf.train.Saver()。類似於 tf.train.Optimizer,tf.train.Saver 本身並不是一個節點,而是在已有圖形上執行有用功能的更高階類別。你可能已經預料到 tf.train 的「有用功能」了,即儲存和載入模型。

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
b = tf.get_variable('b', [])
init = tf.global_variables_initializer()

saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.save(sess, './tftcp.model')

輸出

四個新檔案:

checkpoint
tftcp.model.data-00000-of-00001
tftcp.model.index
tftcp.model.meta

具體內容分析如下:

首先:當我們只儲存一個模型時,為什麼會輸出四個檔案?重建模型所需的資訊被分散到它們當中。如果想複製或者備份模型,需要有四個檔案(字首為檔名)。下面簡述答案:

  • tftcp.model.data-00000-of-00001 包含模型權重(上述第一個要點)。它可能這裡最大的檔案。

  • tftcp.model.meta 是模型的網路結構(上述第二個要點)。它包含重建圖形所需的所有資訊。

  • tftcp.model.index 是連線前兩點的索引結構。用於在資料檔案中找到對應節點的引數

  • checkpoint 實際上不需要重建模型,但如果在整個訓練過程中儲存了多個版本的模型,那它會跟蹤所有內容。

其次,我為什麼一定要為該示例建立 tf.Session 和 tf.global_variables_initializer 呢?

因為,如果要儲存一個模型,我們需要儲存相關的內容。計算存於圖中,但數值存於會話中。tf.train.Saver 可以透過指向圖表的全域性指標訪問網路結構。但當我們儲存變數的值(即網路權重)時,我們需要訪問 tf.Session 來確定這些值;這就是為什麼 sess 作為 save 函式的第一個引數傳入。此外,嘗試儲存未初始化的變數會引發錯誤,因為嘗試訪問未初始化變數的值總是會引發錯誤。因此,我們需要一個會話和一個初始化程式(或等價的 tf.assign)。

載入模型

既然我們已經儲存了模型,現在重新載入它。第一步是重新建立變數:我們希望變數的名稱、形狀和型別都與儲存時一致。第二步是建立與之前一樣的 tf.train.Saver,並呼叫 restore 函式。

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
b = tf.get_variable('b', [])

saver = tf.train.Saver()
sess = tf.Session()
saver.restore(sess, './tftcp.model')
sess.run([a,b])

輸出:

[1.3106428, 0.6413864]

在執行之前,我們不需要初始化 a 或 b!這是因為 restore 運算將值從檔案移動到會話的變數中。由於會話不再包含任何空值變數,因此不再需要初始化。(如果不小心,會適得其反:還原後執行 init 會使隨機初始化的值覆蓋載入的值。)

選擇變數

當一個 tf.train.Saver 程式初始化後,它會檢視當前圖形並獲取變數列表;這是 saver「關心」的永久儲存的變數列表。我們可以用._var_list 屬性來檢查:

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
b = tf.get_variable('b', [])
saver = tf.train.Saver()
c = tf.get_variable('c', [])
print saver._var_list

輸出:

[<tf.Variable 'a:0' shape=() dtype=float32_ref>, <tf.Variable 'b:0' shape=() dtype=float32_ref>]

因為在建立 saver 時 c 還沒有出現,所以它並沒有成為函式的一部分。一般來說,你要在建立 saver 之前確保已經建立了所有的變數。

當然,在某些特定的情況下,可能只需儲存變數的一個子集。當建立 var_list 以期望它跟蹤可用變數子集時,tf.train.Saver 允許傳遞 var_list。

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
b = tf.get_variable('b', [])
c = tf.get_variable('c', [])
saver = tf.train.Saver(var_list=[a,b])
print saver._var_list

輸出:

[<tf.Variable 'a:0' shape=() dtype=float32_ref>, <tf.Variable 'b:0' shape=() dtype=float32_ref>]

載入修正模型

上面例子中涵蓋的模型載入方案類似於物理中的「真空中無摩擦的完美球體」(perfect sphere in frictionless vacuum)場景。只要你使用自己的程式碼儲存和載入模型,且不擅自更改二者,實現儲存和載入輕而易舉。但很多情況下,並不會有如此完美的場景。在這些情況下,我們需要多加思量。

讓我們透過幾個場景來說明這些問題。首先,如果我們想儲存一個完整的模型,但只想載入其中的一部分怎麼辦?(在下面程式碼示例中,我依次執行兩個指令碼。)

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
b = tf.get_variable('b', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.save(sess, './tftcp.model')
import tensorflow as tf
a = tf.get_variable('a', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.restore(sess, './tftcp.model')
sess.run(a)

輸出:

1.1700551

OK。當我們在相反的場景裡,就會出現失敗的狀況:我們希望將一個模型作為大型模型的元件載入。

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.save(sess, './tftcp.model')

import tensorflow as tf
a = tf.get_variable('a', [])
d = tf.get_variable('d', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.restore(sess, './tftcp.model')

輸出:

Key d not found in checkpoint
         [[{{node save/RestoreV2}} = RestoreV2[dtypes=[DT_FLOAT, DT_FLOAT, DT_FLOAT], _device="/job:localhost/replica:0/task:0/device:CPU:0"](_arg_save/Const_0_0, save/RestoreV2/tensor_names, save/RestoreV2/shape_and_slices)]]

我們只想載入 a,卻忽略了新變數 b。我們犯了一個錯誤,卻抱怨 d 沒有出現在 checkpoint 中。

第三種情況是,我們想將一個模型的引數載入到另一個模型的計算圖中。這也會引發一個錯誤,原因很明顯:Tensorflow 不知道把載入的所有引數放置在何處。幸好有個方法可以給它點提示。

還記得 var_list 嗎?或者更準確來說是「var_list_or_dictionary_mapping_names_to_vars」,但這個名字有點拗口,所以他們使用第一個。

儲存模型是 Tensorflow 要求使用全域性唯一變數名的關鍵原因之一。在儲存-模型-檔案中,每個儲存變數的名稱都與其形狀和值有關。將其載入到新的計算圖中與將想要載入的變數的原始名稱對映到當前模型的變數中一樣簡單。示例如下:

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.save(sess, './tftcp.model')

import tensorflow as tf
d = tf.get_variable('d', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver(var_list={'a': d})
sess = tf.Session()
sess.run(init)
saver.restore(sess, './tftcp.model')
sess.run(d)

輸出:

-0.9303965

這是一種關鍵機制,透過這個機制,可以將沒有相同計算圖的模型組合在一起。例如,你可能從網上獲得了一個預訓練好的語言模型,希望重用詞嵌入。或者你可能在兩次訓練之間改變了模型的引數化,想讓這個新版本在舊版本的基礎上繼續前進;但你又不想重新訓練整個過程。在這兩種情況下,你只需手動建立一個字典,將舊變數名稱對映到新變數即可。

需要注意的是:你要明確地知道正在載入的引數是如何使用的。如果可以,你應該使用原作者用來構建模型的確切程式碼,以確保計算圖的元件與訓練時看起來一樣。如果需要復現模型,務必記住,無論多微小的更改,都可能嚴重損害預訓練網路的效能。所以始終要將復現結果和原來的結果進行對比。

模型檢查

如果想載入的模型來源於網路或由自己建立(兩個月前),那你很可能不知道原始變數是如何命名的。要檢查儲存的模型,需要使用官方 Tensorflow 庫的一些工具。

連結:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/framework/python/framework/checkpoint_utils.py

程式碼:

import tensorflow as tf
a = tf.get_variable('a', [])
b = tf.get_variable('b', [10,20])
c = tf.get_variable('c', [])
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.Session()
sess.run(init)
saver.save(sess, './tftcp.model')
print tf.contrib.framework.list_variables('./tftcp.model')

輸出:

[('a', []), ('b', [10, 20]), ('c', [])]

利用這些工具(結合原始程式碼庫一起使用)通常可以找到你想要的變數名稱。

結論

希望本文能幫你瞭解關於儲存和載入 Tensorflow 模型的基礎知識。還有其他一些高階技巧,比如自動 checkpoint 和儲存/恢復元圖,可能會在以後的文章中提到;但是根據我的經驗,這些並不常用,特別是對於初學者來說。


原文連結:https://jacobbuckman.com/post/tensorflow-the-confusing-parts-2/

相關文章