轉載自公眾號機器之心
Debugging 是程序員必備技能,TensorFlow 程序員也不例外。然而 TensorFlow 的運行模式是先構造一張 graph,再執行 session.run(),這就為調試帶來一些困難。普通調試工具如 pdb 只能看到 graph 外部的變量和控制流程,無法深入 graph 內部一探究竟。
TensorFlow debugger(簡稱 tfdbg)是 TensorFlow 的專用調試器,它可以讓你看到運行 TensorFlow graph 時的內部結構體和狀態。有了這個神器就如同調試普通 Python 程序一樣調試 TensorFlow 程序了!
這里看個簡單例子(lms.py),使用 LMS 算法估計線性濾波器權值。LMS 是數字信號處理中最基本的自適應濾波算法,結構簡單,計算量低,便於硬件實現,適用於實時信號處理場景,但學習速率選取不當時容易造成發散,出現 Inf 和 NaN 值。以下程序就存在這個問題:
import numpy as np import tensorflow as tf from tensorflow.python import debug as tfdbg H = 128 # 濾波器長度 L = 10240 # 輸入信號長度 miu = 1 # 學習速率 # 載入濾波器權值 filter = np.random.randn(H) # 載入信號值 sig_in = np.random.randn(L) weights = tf.Variable(tf.zeros([H], dtype=tf.float32)) # 訓練權值 input_vec = tf.placeholder(tf.float32, shape=(H)) # 輸入信號窗口 prod = tf.reduce_sum(tf.multiply(input_vec, weights)) # 輸出信號 desired = tf.reduce_sum(tf.multiply(input_vec, filter)) # 期望信號 err = tf.nn.l2_loss(desired - prod) # 誤差信號 opt = tf.train.GradientDescentOptimizer(miu).minimize(err) # 梯度下降優化器 mse = tf.nn.l2_loss(weights - filter) # 訓練權值與預期權值的 MSE with tf.Session() as sess: sess = tfdbg.LocalCLIDebugWrapperSession(sess) # 被調試器封裝的會話 sess.add_tensor_filter("has_inf_or_nan", tfdbg.has_inf_or_nan) # 調試器添加過濾規則 tf.global_variables_initializer().run() # 變量初始化 print('Initialized!') for step in xrange(L - H): sess.run(opt, feed_dict={input_vec:sig_in[step:(step+H)]}) # 輸入信號窗口每次滑動一個單位 print sess.run(mse) # 打印輸出,觀察訓練權值是否收斂到與預期權值一致
使用 TensorFlow 1.3.0 運行上述程序。
進入交互式調試界面,這里可以輸入調試命令,常用的有:
tfdbg> run
運行一次 session.run(),並將所有內部 tensor 導出,界面如下:
這時可以用鼠標點擊“Tensor name”,查看相應的 tensor 詳情(等價為輸入命令:pt tensor_name)。下圖為點擊“Mul_1/y:0”之后的界面:
可以按鍵盤上“Page Up/Page Down”按鍵來翻頁顯示,tfdbg> 后輸入“@數字”可以直接跳到對應的元素位置,如 “@125” 后,界面跳轉如下:
這時可以查看 Mu_1/y:0[125] 元素的值。
我們如果對 graph 中某個 node 感興趣,可以使用 ni 命令查看:
上圖顯示 Mul:0 節點對應 Op 為 Mul,運行時使用 gpu:0 設備,有兩個輸入,一個為 placeholder,一個為 Variable;下一個節點為 Sum。通過這些信息可以辨識調試器中某個特定節點對應程序中代碼位置。為了提高辨識度,在程序中使用 tf api 構建 graph 時,最好加入 name 參數。
通過 ls 命令可以查看創建節點的框架源碼:
從上圖看到源碼和創建的節點統計值,鼠標點擊源碼則自動跳轉到創建代碼處,適合跟蹤框架代碼。
前面手動查找 Tensor 值的方式比較低效,還可以使用過濾器幫助查找異常值。在代碼中我們已經創建了一個名為“has_inf_or_nan” 的過濾器,在運行時指定參數:run -f has_inf_or_nan 則使用該過濾器,檢查運行時的 tensor 出現 NaN 或 Inf 的情況。
調試器運行到第 30 輪時發現 L2Loss_1:0 tensor 中出現了 NaN/Inf,程序中斷,我們可以輸入“pt L2Loss_1:0” 打印這個 tensor:
它的值為 Inf,為什么會變為 Inf 呢?我們繼續跟蹤它前一級 tensor(通過命令 “ni L2Loss_1:0” 得知前一個 tensor 為 sub_1,再使用 “pt sub_1” 命令打印 sub_1 的值)
發現這些值都非常大,再計算 L2Loss 會超過 float32 能表示的最大值,所以生成了 Inf。
繼續向前跟蹤 tensor,可以找到這些異常大的值來源為 Variable,即訓練的權值。這些值為什么會變得這么大?一定是迭代更新的步子太大造成發散。找到了原因,解決起來就很簡單了,我們將代碼第 7 行學習速率(miu = 1)調小一些,比如 0.01,保存后再次運行(先 python lms.py 進入 tfdbg> ,再運行 run -f has_inf_or_nan),這時會發現程序一直運行到結束,不會中斷,說明沒有 NaN/Inf 產生,bug 已經消除。
通過上述例子,相信童鞋們可以掌握 tfdbg 基本用法,並可以舉一反三調試自己程序中的 bug。
最近 TensorFlow 又有一大利好,開始支持 Eager Execution 模式,可以直接執行 tf api,無需使用 session.run(),這樣方便了程序調試。不過該功能尚在開發階段,不可避免會踩到一些坑。願意嘗鮮的童鞋可以體驗下:
Docker 安裝:docker pull tensorflow/tensorflow:nightly
PIP 安裝:pip install tf-nightly
測試程序:
import tensorflow as tf import tensorflow.contrib.eager as tfe tfe.enable_eager_execution() x = tf.add(1, 1) y = tf.constant([1, 2, 3]) z = x + y print(z)
參考
【1】Debugging TensorFlow Programs, https://www.tensorflow.org/programmers_guide/debugger
【2】TensorFlow Eager Execution, https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/eager/python/g3doc/guide.md