在数据流图中,节点之间的某些类型的连接是不被允许的,最常见的一种是将造成循环依赖(circular dependency)的连接。为理解循环依赖这个概念,需

要先理解何为依赖关系。再次观察下面的数据流图。

循环依赖这个概念其实非常简单:对于任意节点A,如果其输出对于某个后继节点B的计算是必需的,则称节点A为节点B的依赖节点。如果某个节点A和节 点B彼此不需要来自对方的任何信息,则称两者是独立的。为对此进行可视化,首先观察当乘法节点c出于某种原因无法完成计算时会出现何种情况。

可以预见,由于节点e需要来自节点c的输出,因此其运算无法执行,只能无限等待节点c的数据的到来。容易看出,节点c和节点d均为节点e的依赖节点,因 为它们均将信息直接传递到最后的加法函数。然而,稍加思索便可看出节点a和节点b也是节点e的依赖节点。如果输入节点中有一个未能将其输入传递给数据流 图中的下一个函数,情形会怎样?

可以看出,若将输入中的某一个移除,会导致数据流图中的大部分运算中断,从而表明依赖关系具有传递性。即,若A依赖于B,而B依赖于C,则A依赖于 C。在本例中,最终节点e依赖于节点c和节点d,而节点c和节点d均依赖于输入节点b。因此,最终节点e也依赖于输入节点b。同理可知节点e也依赖于输入节点 a。此外,还可对节点e的不同依赖节点进行区分:

1)称节点e直接依赖 于节点c和节点d。即为使节点e的运算得到执行,必须有直接来自节点c和节点d的数据。

2)称节点e间接依赖 于节点a和节点b。这表示节点a和节点b的输出并未直接传递到节点e,而是传递到某个(或某些)中间节点,而这些中间节点可能是节 点e的直接依赖节点,也可能是间接依赖节点。这意味着一个节点可以是被许多层的中间节点相隔的另一个节点的间接依赖节点(且这些中间节点中的每一个也 是后者的依赖节点)。

最后来观察将数据流图的输出传递给其自身的某个位于前端的节点时会出现何种情况。

不幸的是,上面的数据流图看起来无法工作。我们试图将节点e的输出送回节点b,并希望该数据流图的计算能够循环进行。这里的问题在于节点e现在变为 节点b的直接依赖节点;而与此同时,节点e仍然依赖于节点b(前文已说明过)。其结果是节点b和节点e都无法得到执行,因为它们都在等待对方计算的完成。

也许你非常聪明,决定将传递给节点b或节点e的值设置为某个初始状态值。毕竟,这个数据流图是受我们控制的。不妨假设节点e的输出的初始状态值为 1,使其先工作起来。

上图给出了经过几轮循环各数据流图中各节点的状态。新引入的依赖关系制造了一个无穷反馈环,且该数据流图中的大部分边都趋向于无穷大。然而,出 于多种原因,对于像TensorFlow这样的软件,这种类型的无限循环是非常不利的。

1)由于数据流图中存在无限循环,因此程序无法以优雅的方式终止。 2)依赖节点的数量变为无穷大,因为每轮迭代都依赖于之前的所有轮次的迭代。不幸的是,在统计依赖关系时,每个节点都不会只被统计一次,每当其输

出发生变化时,它便会被再次记为依赖节点。这就使得追踪依赖信息变得不可能,而出于多种原因(详见本节的最后一部分),这种需求是至关重要的。

3)你经常会遇到这样的情况:被传递的值要么在正方向变得非常大(从而导致上溢),要么在负方向变得非常大(导致下溢),或者非常接近于0(使得 每轮迭代在加法上失去意义)。

基于上述考虑,在TensorFlow中,真正的循环依赖关系是无法表示的,这并非坏事。在实际使用中,完全可通过对数据流图进行有限次的复制,然后将它们 并排放置,并将代表相邻迭代轮次的副本的输出与输入串接。该过程通常被称为数据流图的展开(unrolling)。第6章还将对此进行更为详细的介绍。为了以图 形化的方式展示数据流图的展开效果,下面给出一个将循环依赖展开5次后的数据流图。

对这个数据流图进行分析,便会发现这个由各节点和边构成的序列等价于将之前的数据流图遍历5次。请注意原始输入值(以数据流图顶部和底部的跳跃箭 头表示)是传递给数据流图的每个副本的,因为代表每轮迭代的数据流图的每个副本都需要它们。按照这种方式将数据流图展开,可在保持确定性计算的同时 模拟有用的循环依赖。

既然我们已理解了节点的依赖关系,接下来便可分析为什么追踪这种依赖关系十分有用。不妨假设在之前的例子中,我们只希望得到节点c(乘法节点)的 输出。我们已经定义了完整的数据流图,其中包含独立于节点c和节点e(出现在节点c的后方)的节点d,那么是否必须执行整个数据流图的所有运算,即便并不 需要节点d和节点e的输出?答案当然是否定的。观察该数据流图,不难发现,如果只需要节点c的输出,那么执行所有节点的运算便是浪费时间。但这里的问题 在于:如何确保计算机只对必要的节点执行运算,而无需手工指定?答案是:利用节点之间的依赖关系!

这背后的概念相当简单,我们唯一需要确保的是为每个节点的直接(而非间接)依赖节点维护一个列表。可从一个空栈开始,它最终将保存所有我们希望 运行的节点。从你希望获得其输出的节点开始。显然它必须得到执行,因此令其入栈。接下来查看该输出节点的依赖节点列表,这意味着为计算输出,那些节 点必须运行,因此将它们全部入栈。然后,对所有那些节点进行检查,看它们的直接依赖节点有哪些,然后将它们全部入栈。继续这种追溯模式,直到数据流 图中的所有依赖节点均已入栈。按照这种方式,便可保证我们获得运行该数据流图所需的全部节点,且只包含所有必需的节点。此外,利用上述栈结构,可对 其中的节点进行排序,从而保证当遍历该栈时,其中的所有节点都会按照一定的次序得到运行。唯一需要注意的是需要追踪哪些节点已经完成了计算,并将它 们的输出保存在内存中,以避免对同一节点反复计算。按照这种方式,便可确保计算量尽可能地精简,从而在规模较大的数据流图上节省以小时计的宝贵处理 时间。