在TensorFlow中每开发一个模型,都可以使用可视化调试工具TensorBoard得到这个session的Graph,这张图的结构和内容都不同于机器学习教材上介绍的典型神经网络结构图。本文试图通过代码实验理解Graph的含义,用以指导日常调试。
代码:https://github.com/wangyaobupt/TF_Graph singleNerualNode.py
运行环境:
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
>>> tf.__version__
‘1.0.0-rc2’
在TensorFlow开发中,TensorBoard是一项很有用的可视化调试工具。在TensorBoard中,除了开发者自定义输出的数据结构之外,还包括表征神经网络模型的GRAPH。
在常见的机器学习教材中,神经网络的结构一般通过类似于如下的图形表示,下图引用自http://www.extremetech.com/extreme/215170-artificial-neural-networks-are-changing-the-world-what-are-they
但是,TensorBoard生成的Graph与上述形态完全不同,我自己开发的某个神经网络生成的Graph如下图所示
如何理解这张图?本文试图通过代码实验做一些尝试。
为了简化分析场景,我们设计一个由单个神经元构成的神经网络,这个神经元存在numberOfInputDims
输入,神经元的每条输入边都有权重因子wi,此外神经元还有bias偏置项,激活函数为sigmoid. 这个结构可以用如下的结构图描述
在TensorFlow中,如下代码即可定义出满足上述结构的神经网络,其中a就是上图中的Y
inputTensor = tf.placeholder(tf.float32, [None, numberOfInputDims], name='inputTensor')
labelTensor=tf.placeholder(tf.float32, [None, 1], name='LabelTensor')
W = tf.Variable(tf.random_uniform([numberOfInputDims, 1], -1.0, 1.0), name='weights')
b = tf.Variable(tf.zeros([1]), name='biases')
a = tf.nn.sigmoid(tf.matmul(inputTensor, W) + b, name='activation')
根据TF官方文档对于Graph的说明,上图中实线表示数据依赖(tensor在运算符之间的流动关系),而虚线表示控制依赖关系,原文引用如下
TensorFlow graphs have two kinds of connections: data dependencies and control dependencies. Data dependencies show the flow of tensors between two ops and are shown as solid arrows, while control dependencies use dotted lines.
但是,我们得到的Graph中所有的实线都没有标出箭头方向,他们之间是谁依赖谁呢?
回答这个问题需要回到代码中,从代码可以知道:weight是由tf.random_uniform([numberOfInputDims, 1]初始化得到的;weight和inputTensor做矩阵乘法得到中间变量;中间变量再加上bias得到激活函数的输入;以此类推。
因此,TensorBoard Graph的上下方位代表了数据依赖的方向:数据总是从下方的节点流向上方的节点,上方节点依赖于下方节点
接下来讨论控制依赖。
上图中weight和bias节点都存在依赖于init运算的虚线,这说明weight和bias节点都需要初始化。虚线指向的运算符(op)是被依赖的运算符(op)
从中可以看出,不考虑summary node的情况下,节点要么是常数Constant,要么是运算符Operation Node,要么是前两者的组合。
回到我们这个具体问题,random_uniform/weight/bias就是组合节点,而其他节点就是运算符。注意:从上图可以看到,inputTensor也是被视作一个独立的运算符。
放大其中一个节点weights,我们可以看到其内部结构如下,包含:赋值(assign)运算符、(weight)运算符、读取(read)运算符
结合上述实验和分析,可以初步判断运算符Operation Node包含以下情况
关于Operation Node代表常量的情况,在本文的案例2中会有体现
案例1中的神经网络只包含了前向计算逻辑,作为神经网络,其最主要的功能在于被训练满足某一目标,因此损失函数和误差反向传播梯度下降调整参数是必不可少的。
在此前的代码基础上新增如下两行就可以实现上述功能。这里损失函数使用预测值与目标值的L2距离
loss = tf.nn.l2_loss(a - labels, name='L2Loss')
train_step = tf.train.AdamOptimizer(1e-4).minimize(loss)
对比上图和案例1中的Graph,在案例2 Graph的Main View(图像左侧)新增了如下信息
接下来重点讨论Gradient(梯度)和Adam(寻优算法)节点的内部结构
Gradient(梯度)节点
Gradient(梯度)节点内部是一个链式结构,分为对L2Loss求导、对减法求导、对激活函数求导……
总结起来,这就是数学上求导数的链式法则的图形化表示,我们的最终目的是求出损失函数对某个参数的导数,那么根据链式法则,只要从损失函数L2Loss出发,将每一层求导结果相乘就可以得到,笔者前一个系列利用Python实现神经网络中也使用了类似的求导方法
Adam(寻优算法)节点
Adam算法本身的原理和实现可以参阅笔者此前的文章:Python实现神经网络Part 2: 训练单个神经元找到最优解
上述图形就是根据Adam算法原理,使用梯度、beta1\beta2,结合每一轮训练中weights和bias的原始值,计算weight和bias的更新值,通过控制依赖关系进一步调节weights和bias
TensorBoard中的Graph不同于一般的神经网络结构图,它是一种计算图。图中每个节点要么是Tensor本身,要么是运算符,每一条边要么代表Tensor的流动,要么代表控制关系。这张图完备的表达了通过代码定义的神经网络中所有计算步骤,可以据此说明前向计算、误差反相传播、梯度下降调整参数等过程。
在实际工作中,理解了上述含义,就可以将Graph利用起来,在Debug过程中可视化的发现网络计算流程中的问题。复杂程序的调试总是困难的,引入可视化工具对于调试效率会非常有帮助。