令人困惑的 TensorFlow!(II)

選自Jacobbuckman

機器之心編譯

參與:高璇、王淑婷

六月底,機器之心發佈了「令人困惑的 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

輸出:

[, ] 

因為在創建 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

輸出:

[, ]

加載修正模型

上面例子中涵蓋的模型加載方案類似於物理中的「真空中無摩擦的完美球體」(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 和保存/恢復元圖,可能會在以後的文章中提到;但是根據我的經驗,這些並不常用,特別是對於初學者來說。

令人困惑的 TensorFlow!(II)

原文鏈接:https://jacobbuckman.com/post/tensorflow-the-confusing-parts-2/

✄------------------------------------------------

加入機器之心(全職記者 / 實習生):[email protected]

投稿或尋求報道:content@jiqizhixin.com

廣告 & 商務合作:[email protected]


分享到:


相關文章: