转载自公众号机器之心
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