try ai
科普
编辑
分享
反馈
  • 展开计算图

展开计算图

SciencePedia玻尔百科
关键要点
  • 展开将循环计算图(常见于RNNs等模型)转换为深的、线性的有向无环图(DAGs),从而能够使用标准的反向传播算法。
  • 这一过程虽然强大,但也引入了挑战,例如在深度展开的图中出现的梯度消失/爆炸问题,以及在截断模型中对长程依赖的盲目性。
  • 展开的原则超越了时间序列,可用于分析GNN中的空间结构、算法的顺序性以及物理过程的动态性。
  • 展开的适用性受到具有非层级、交叉依赖关系的结构的限制,例如假结,这标志着计算复杂性的一个基本边界。

引言

现代机器学习建立在计算图这一抽象而强大的理念之上,该框架将复杂的计算描述为一个由节点和边组成的网络。这种表示方式实现了自动微分等非凡的功能,而自动微分正是驱动深度学习的引擎。然而,当计算不是线性的而是循环的,即会反馈到自身时,一个根本性的挑战便产生了,这在随时间演化的系统或依赖递归的算法中很常见。我们如何分析和优化一个包含循环的过程呢?

本文探讨了一种优雅而深刻的解决方案:展开的概念。我们将看到这一个理念如何提供一种统一的方式来处理循环和局部依赖,将看似棘手的问题转化为可管理的线性结构。通过展开这些紧凑的、递归的描述,我们可以应用强大的分析工具,但这个过程也揭示了其固有的局限性和权衡。

我们将从“原理与机制”一章开始,使用循环神经网络和时间反向传播作为主要示例,剖析展开的工作原理。我们将探讨该技术的实际后果,包括臭名昭著的梯度消失问题以及在图神经网络中发现的空间类似物。随后,“应用与跨学科联系”一章将拓宽我们的视野,揭示展开如何作为一个统一的原则,将机器学习与算法分析、化学模拟、计算生物学乃至数理逻辑联系起来,并最终界定出可解与难解复杂性之间的边界。

原理与机制

现在我们对计算图有了初步的了解,让我们深入探讨使其如此强大的内部机制。我们即将开始一段旅程,从简单的算术运算,一直到机器如何学习序列、结构乃至物理定律的核心。我们的指导原则将是一个单一而优雅的技巧:展开的艺术。

作为程序的图:一种新的计算语言

想象一下,你想描述一个计算,比如 z=((a+b)x+c×d)(e+f)z = ((a+b)x + c \times d)(e+f)z=((a+b)x+c×d)(e+f)。你可以将它写成一行代码。但还有另一种方式,一种更直观,并且正如我们将看到的,更强大的方式。我们可以把它画成一个图。每个数字或变量(x,a,b,…x, a, b, \dotsx,a,b,…)都是一个起点——一个叶节点。每个操作(+,×+,\times+,×)是一个内部节点,它从其他节点获取输入并产生输出。最终结果 zzz 是图的根节点。

这就是一个​​计算图​​。它是一种描述函数的语言。但与静态的一行代码不同,这个图是一个活的数据结构。一个现代的深度学习框架不仅仅是“运行”你的代码;它首先将代码翻译成这样一个图。为什么呢?因为这种表示揭示了计算的结构,而有了这种结构,我们就能做到惊人的事情。

其一,我们可以自动计算导数。这个过程被称为​​自动微分(AD)​​,是现代机器学习的引擎。通过简单地从最终输出反向追溯到图中任何一个变量的路径,并在每一步应用链式法则,我们就能知道改变那个变量会如何影响结果。这是一个非常机械化的过程,不需要人类的创造力,只需要忠实地遍历图即可。

但更重要的是,图本身可以被操纵。如果我们知道 a,b,c,d,e,fa, b, c, d, e, fa,b,c,d,e,f 是常量,为什么每次用新的 xxx 运行程序时都要重新计算 a+ba+ba+b 或 c×dc \times dc×d 呢?图清楚地告诉我们:代表 a+ba+ba+b 的节点只依赖于常量。一个智能系统可以预先计算,或“折叠”这个值,用一个单一的数字替换掉一个操作子树。对于我们的例子,这三个只涉及常量的操作可以被折叠,将运行时的6次浮点运算(FLOPs)减少到仅3次——瞬间实现2倍加速,并且如果算术处理得当,结果保证完全相同。

这仅仅是个开始。我们可以应用代数规则,比如交换律和结合律,将图重新排列成一个唯一的​​规范形式​​。我们甚至可以​​融合​​多个操作,比如一个卷积层和一个批归一化层,融合成一个单一的、高度优化的“超级节点”,运行速度会快得多,尤其是在GPU这样的硬件上。图不仅仅是对程序的描述;在非常真实的意义上,图就是程序——一个可以被分析、验证和优化的程序。

伟大的展开:将循环化为线性

这种基于图的视角对于直线式计算来说非常棒。但对于那些循环的、会反馈到自身的计算呢?考虑一个​​循环神经网络(RNN)​​的核心,它被设计用来处理序列:

ht=f(ht−1,xt)\mathbf{h}_t = f(\mathbf{h}_{t-1}, \mathbf{x}_t)ht​=f(ht−1​,xt​)

当前时间步的隐藏状态 ht\mathbf{h}_tht​ 是前一个时间步的隐藏状态 ht−1\mathbf{h}_{t-1}ht−1​ 和新输入 xt\mathbf{x}_txt​ 的函数。如果我们把它画成一个计算图,我们会得到一个循环:表示 h\mathbf{h}h 的节点有一条边指回自身。这是一条自食其尾的蛇。我们怎么可能对一个带环的图应用链式法则呢?

答案是本章的核心思想:我们执行​​展开​​。想象一下,这个循环图是一个紧凑的、盘绕的弹簧。为了理解它,我们把它拉伸开。一个长度为 TTT 的序列的计算在时间上被展开,将单个循环节点转化为一个由 TTT 个不同节点组成的长链,每个时间步一个节点。

⋯→(xt−1,ht−2)→compute ht−1→(xt,ht−1)→compute ht→(xt+1,ht)→compute ht+1→…\dots \rightarrow (\mathbf{x}_{t-1}, \mathbf{h}_{t-2}) \rightarrow \text{compute } \mathbf{h}_{t-1} \rightarrow (\mathbf{x}_t, \mathbf{h}_{t-1}) \rightarrow \text{compute } \mathbf{h}_t \rightarrow (\mathbf{x}_{t+1}, \mathbf{h}_t) \rightarrow \text{compute } \mathbf{h}_{t+1} \rightarrow \dots⋯→(xt−1​,ht−2​)→compute ht−1​→(xt​,ht−1​)→compute ht​→(xt+1​,ht​)→compute ht+1​→…

循环消失了!我们得到了一个非常深,但从根本上说是简单的有向无环图(DAG)。函数 fff 的参数(如权重矩阵)在这个展开链的每个阶段都是共享的。现在,我们可以将我们可靠的反向传播算法应用于这个展开的图。这个过程有一个特殊的名字:​​时间反向传播(BPTT)​​。但别被这个花哨的名字迷惑了。这里并没有什么新的魔法。BPTT仅仅是标准的、应用于展开计算图的反向模式自动微分。这证明了找到正确表示方法的强大力量。一旦我们展开图,“时间”这个问题就消失了,变成了这个非常深的网络中的另一个维度。

在开始这个昂贵的过程之前,我们必须确保我们的图构建正确。展开逻辑中的一个错误可能会意外地在图中留下一个循环。幸运的是,我们可以依赖计算机科学中的经典算法。我们可以将我们的计算图视为一个数学图,并使用​​深度优先搜索(DFS)​​来找到所有的​​强连通分量(SCCs)​​。任何循环都必须完全存在于一个SCC内部。如果我们发现一个SCC中有多于一个节点,或者一个节点有自环,我们就找到了一个错误——一个使得图对于反向传播无效的反馈循环。我们甚至可以利用这些信息来确定性地定位并提议切断一条边来打破循环,确保我们的图在开始训练前是一个有效的DAG。

无穷的代价:路径爆炸与短视

展开一个循环图是一个优雅的解决方案,但它也伴随着代价。展开后的图很深,而深层网络是出了名的棘手。这就是臭名昭著的​​梯度消失和爆炸问题​​出现的地方。通过观察展开图的结构,我们可以对此获得深刻的直觉。

考虑一个非常简单的标量RNN:ht=αht−1+βtanh⁡(ht−1)h_t = \alpha h_{t-1} + \beta \tanh(h_{t-1})ht​=αht−1​+βtanh(ht−1​)。当我们计算时间 TTT 的损失相对于遥远过去(比如 h0h_0h0​)的导数时,链式法则告诉我们,我们必须将中间每一步的局部雅可比矩阵相乘:∂hT∂h0=∏t=1T∂ht∂ht−1\frac{\partial h_T}{\partial h_0} = \prod_{t=1}^T \frac{\partial h_t}{\partial h_{t-1}}∂h0​∂hT​​=∏t=1T​∂ht−1​∂ht​​。每个局部雅可比矩阵 ∂ht∂ht−1\frac{\partial h_t}{\partial h_{t-1}}∂ht−1​∂ht​​ 是两项之和(一项来自线性路径,一项来自非线性路径)。当我们展开这个和的乘积时,我们发现从未来到过去的“梯度路径”数量呈指数级爆炸。对于一个长度为 TTT 的序列,最终的梯度表达式中有 2T2^T2T 个加法项!

这种路径的组合爆炸是问题的核心。如果局部雅可比矩阵平均略大于1,梯度将爆炸至无穷大。如果它们略小于1,梯度将消失为零。这就像一个信号通过数千个分支路径被放大或衰减;它几乎不可能保持稳定。这正是像LSTMs和GRUs这样的架构被发明出来的原因。它们的门控机制充当交通控制器,学习选择性地关闭或开放这些梯度路径,以在长时间内保存信息。在我们简单的例子中,引入一个以概率 qqq 开启非线性路径的随机门,将期望的路径数从 2T2^T2T 变为更易于管理的 (1+q)T(1+q)^T(1+q)T。

我们付出的另一个代价是计算成本。如果序列长达数百万步呢?我们无法承担展开整个图的代价。实际的解决方案是​​截断时间反向传播(TBPTT)​​,我们只展开固定的步数,比如 kkk 步。但这是一个有严重副作用的妥协。通过在步骤 t−kt-kt−k 处截断图,我们明确地告诉我们的学习算法,时间 ttt 的损失与时间 t−kt-kt−k 之前发生的任何事情都没有关系。相对于输入 xt−τx_{t-\tau}xt−τ​ (其中 τ>k\tau > kτ>k)的梯度不只是小,而是精确地为零。模型变得对长程依赖盲目,使其无法学习它们。这就像试图通过只看1分钟的片段来理解一部电影的情节;你可以理解当下的动作,但会错过整个故事线。像LSTMs或更奇特的技巧如合成梯度等补救措施,旨在通过创建关于遥远过去的更好“摘要”,并将其跨越这些截断边界来克服这种短视。

超越时间的展开:空间与结构中的局部性

展开的概念并不仅限于时间序列。它是一个处理任何具有局部、重复性结构的计算的通用原则。一个极好的例子是​​图神经网络(GNN)​​。

想象一下对一个分子进行建模。原子是节点,共价键是边。GNN通过在相邻原子之间传递“消息”来工作。在消息传递GNN的单层中,每个原子根据其直接邻居的状态更新自己的状态。这是一次“跳跃”。如果我们堆叠 TTT 层,来自一个原子的信息可以传播到 TTT 跳远的地方。这种层的堆叠是空间中的一种展开形式,与在时间中展开RNN直接类似。

现在,假设我们想用这样的GNN来预测一个依赖于长程物理力(如分子的静电能)的性质。库仑定律规定,能量是所有原子对之间相互作用的总和,并且力随距离缓慢衰减。但我们的GNN本质上是局部的!如果图是由共价键定义的,并且两个原子相隔超过 TTT 个键,那么模型从根本上就无法看到它们之间的直接相互作用。即使我们通过连接某个截止半径内的所有原子来构建一个图,我们仍然忽略了相互作用的长程尾部。

此外,GNN还存在一个叫做​​过度挤压​​的问题。当我们增加层数以便看得更远时,来自感受野中呈指数增长的节点数量的信息,不得不在每一步被“挤压”进一个固定大小的状态向量中。这造成了一个信息瓶颈,使得几乎不可能保留任何单个遥远原子的具体细节。这正是RNN中梯度消失问题的空间模拟。其根本原理是相同的:一个局部的、迭代的更新机制难以捕捉全局的、长程的依赖关系。

这种美妙的统一性延伸到更复杂的结构。一个​​双向RNN​​可以被看作是两个独立的展开——一个在时间上向前,一个向后——其结果仅在每个时间步结合起来进行预测。因为这两个展开的链在结构上是独立的,反向传播自然地分解为两个独立的扫描,每个方向一次。计算图框架使得这种复杂的编排清晰可管理。即使展开的长度不固定,比如在一个批次中的可变长度序列,图范式也提供了简洁的解决方案,要么为每个样本创建真正的动态图,要么使用一个巧妙的掩码技巧来在一个填充批次中运行所有序列,同时确保来自填充步骤的梯度贡献精确为零。展开是一个多功能且强大的概念工具。

应用与跨学科联系

现在我们已经掌握了展开计算图的机制,我们就像一个刚得到一个功能强大的新放大镜的孩子。突然之间,我们开始在所到之处都看到它的模式。起初看似只是循环神经网络的一个抽象技巧,现在揭示出它是一个基本原则,一个我们可以借以理解算法结构、自然动态、数据形态,乃至我们计算能力极限的透镜。“展开”——将一个紧凑的、递归的或随时间演化的描述展开成一个明确的操作序列——是剖析复杂性的通用工具。让我们踏上一段旅程,探索其中一些意想不到的美妙应用。

算法的隐藏链条

我们可以从像计算多项式这样基础的事情开始。对于计算机来说,计算 p(x)=anxn+an−1xn−1+⋯+a0p(x) = a_n x^n + a_{n-1} x^{n-1} + \dots + a_0p(x)=an​xn+an−1​xn−1+⋯+a0​ 这样一个看似简单的任务,可以用几种方式完成。一个自古以来就已知的巧妙而高效的方法是霍纳方案。它不是计算 xxx 的所有幂然后相乘相加,而是使用一种嵌套形式:p(x)=a0+x(a1+x(a2+… ))p(x) = a_0 + x(a_1 + x(a_2 + \dots))p(x)=a0​+x(a1​+x(a2​+…))。这可以表示为一个简单的递推关系。

如果我们将这个递推关系视为一个紧凑的计算图,我们可以展开它来理解其深层属性。每一步的计算都直接依赖于前一步的结果。当我们展开这个过程时,我们看到的不是一个宽阔、分叉的结构,而是一条长长的、线性的依赖链。这就像把一根盘绕的绳子拉直;它的长度被揭示出来。这个展开的视图告诉我们一些深刻的东西:该算法在本质上是顺序的。你不能在链的开端完成之前计算它的末端。这意味着无论你为单次计算投入多少并行处理器,所需的时间总是与多项式的次数成正比。展开的图使这一限制变得异常明显。

这一原则延伸到远为复杂的递归算法。考虑快速傅里叶变换(FFT),这是科学和工程中最重要的算法之一。其结构是著名的递归结构,通常被可视化为一个美丽的“蝶形”图。为了理解其资源需求,我们可以把计算看作是在其展开图的节点上放置鹅卵石的游戏。一个鹅卵石代表一块存放在内存中的数据。要计算一个节点(在其上放置一个鹅卵石),其所有父节点必须已经有鹅卵石。通过分析展开的FFT图中的依赖关系,我们可以确定完成整个计算所需的最小鹅卵石数量——也就是最小内存量。展开算法不仅揭示了其并行执行的潜力,也揭示了其对内存的基本需求。

从数字逻辑到自然过程

当我们把放大镜从人类设计的算法世界转向自然过程的世界时,这种视角的威力才真正绽放。物理、化学和生物学中的许多系统都是动态的;它们随时间演化。支配它们的定律通常以微分方程的形式表达,这些方程本质上是紧凑的规则,陈述着:“给定系统现在的状态,这就是它在下一瞬间将如何变化。”

为了在计算机上模拟这样的系统,我们必须将时间离散化,以小增量向前推进。每一步都根据前一步的状态更新当前状态。这无非就是系统动态随时间的展开!考虑一个化学反应网络,其中物种 AAA 转化为 BBB,而 BBB 可以变成有用的产物 CCC 或无用的废物。我们可以将浓度随时间的变化建模为一系列离散的步骤。

由此产生的展开图代表了反应的整个历史。奇迹就在这里发生。就像我们对神经网络所做的那样,我们现在可以使用链式法则,通过这段历史向后传播信息。我们可以问:“如果我想最大化产物 CCC 的最终量,我应该如何微调初始反应速率 k1,k2,k3k_1, k_2, k_3k1​,k2​,k3​?”通过对化学反应的展开图应用反向模式自动微分——这与反向传播背后的引擎完全相同——我们可以计算出最终产率相对于每个参数的梯度。这使我们能够使用强大的优化算法来发现化学合成的完美“配方”,这在药物发现和材料科学中是一项极其重要的任务。实际上,我们不是在对图像或文本进行机器学习,而是在对化学定律本身进行机器学习。

展开数据的隐藏几何

到目前为止,我们的图是由算法或物理模型的明确规则定义的。但如果图是隐藏的,编织在数据本身的结构中呢?这个问题将我们引向流形学习和计算生物学领域。

想象一下试图理解蛋白质折叠成其功能形状的过程。分子动力学模拟为我们提供了一部电影:一系列“帧”,其中每一帧都是数千个原子位置的快照。这是一段穿越天文数字般浩瀚的高维空间的旅程。仅仅看每一帧的时间戳通常并不能很好地衡量折叠的实际进展。蛋白质可能会在很长一段时间内四处晃动,然后突然发生一次关键的构象变化。

我们如何才能找到一个更真实的进展度量,一个能够追踪折叠路径的“伪时间”?关键思想是假设蛋白质的有意义的状态位于一个嵌入在高维空间中的、维度低得多的弯曲表面上——一个流形。我们可能不知道这个流形的形状,但我们可以通过构建一个图来近似它。我们将每个快照(帧)视为一个节点,并连接附近的节点——也就是几何上非常相似的构象。两个快照之间的距离不是用环境高维空间中的直线来衡量,而是用沿图表面的最短路径长度来衡量。这种“测地距离”是衡量蛋白质沿其折叠景观真正行进了多远的一个好得多的度量。

这个过程是Isomap算法的核心组成部分,是一种概念上的展开。通过寻找最短路径距离,我们正在“展平”蛋白质构象的弯曲流形。从起始状态开始的一系列测地距离为我们提供了一个自然的、一维的坐标,代表了折叠的真实进展。

这个抽象的想法有一个非常具体和直观的几何类比。假设你是一只在晶体表面上的蚂蚁,一个美丽的凸多面体,你想找到从一个顶点到另一个顶点的最短路径。这条路当然不是穿过晶体内部的直线!最短路径位于表面上。你如何找到它?你可以通过找到路径穿过的面序列,将它们剪下来,然后真正地展开它们,让它们平铺在桌子上。在这个展开的平面上,最短路径现在是一条简单的直线!。这种物理上的展开,正是在更抽象的意义上流形学习算法所做的事情:它们通过将过程“展开”到一个更简单、更平坦的空间来发现其内在几何。

循环与无穷的奇异性

我们迄今为止的旅程处理的是可以展开成有限的有向无环图(DAG)的过程。但当底层结构是循环的时会发生什么?展开一个循环意味着什么?在这里我们发现了一些最美妙和令人费解的联系。

在数理逻辑中,方程 x≐f(x)x \doteq f(x)x≐f(x) 在常规规则下是矛盾的。一个变量不能出现在它自己的定义中。然而,如果我们从图的角度思考,这很容易表示:它是一个单一节点,代表 xxx,有一条标记为 fff 的有向边指回自身。这是一个包含循环的有限图。如果我们试图“展开”这个循环定义会发生什么?我们从节点 xxx 开始。定义说 xxx 是 f(… )f(\dots)f(…),而参数是……又是 xxx。所以我们代入,得到 f(x)f(x)f(x)。我们可以再做一次:f(f(x))f(f(x))f(f(x))。再来一次:f(f(f(… )))f(f(f(\dots)))f(f(f(…)))。这个简单的有限循环的展开是一棵无限的正则树。这是一个深刻的洞见:有限的循环描述可以是无限结构化对象的生成器。简单的循环,当被展开时,延伸至无穷。

在生物学中,我们以一种非常实际的方式遇到循环。许多细菌和质粒都有环状染色体。它们的基因组是一个环。然而,我们许多最强大的基因组分析算法都要求输入图是无环的。因此,我们被迫做一些激烈的事情:我们必须打破这个环。我们选择一个任意点,切开基因组,并将其展开成一个线性序列。这使得数据与我们的工具兼容,但这并非没有代价。序列末端与开端之间的邻接关系丢失了。为了保留最后一个基因与第一个基因相邻的信息,我们常常必须复制第一个基因并将其粘贴到我们线性表示的末尾。这种将真实的生物循环“展开”成DAG的做法是一种务实的必要,但它引入了人为痕迹并破坏了对象的自然对称性,这是生物信息学家每天都要面对的权衡。

展开的失效之处:结的挑战

最后,这个强大思想的极限在哪里?什么样的结构难以被整齐地展开?答案颇具诗意地在于结。

考虑一个RNA分子的折叠。单链RNA会折回自身形成碱基对,创造出复杂的三维结构。许多这些结构是“无假结的”,意思是如果你把序列画成一条线,把碱基对画成上方的弧线,没有两条弧线会交叉。这样的结构就像一组嵌套的括号:( ( [ ] ) )。它有一个清晰的、层次化的结构。因此,我们可以使用动态规划——一种本身就是展开形式的计算方法——来分析它。我们可以通过先计算其最内部、独立的子部分的属性,然后逐步向外推,来计算整个结构的属性。

但一些RNA分子会形成“假结”,其中配对依赖关系相互交叉:( [ ) ]。这不再是一个简单的层次结构。[...] 括号内的子问题不再独立于(...)括号外的世界,因为闭合的括号)被困在了里面!这个单一的交叉依赖破坏了可分解性原则。这个问题不能再被展开成一系列独立的子问题。

其后果不仅仅是一个小小的算法难题;这是计算复杂性的一个根本性飞跃。虽然无假结RNA的折叠问题可以被高效解决(在多项式时间内,如 O(n3)O(n^3)O(n3)),但任意假结结构的问题被证明属于一个名为#P-hard(读作“sharp-P hard”)的类别。这些问题被认为是完全棘手的,甚至比著名的NP-complete问题还要难得多。

于是,我们的旅程以对结构的深刻欣赏而告终。展开计算这一简单而优雅的行为,在依赖关系是局部的、层次化的、非交叉的时候行之有效。它让我们能够分析、模拟和优化种类惊人的各种系统。但当大自然向我们展示一个根本上打了结、缠绕在一起的结构时,我们简单的放大镜就失效了,揭示了一个需要全新思想的复杂性前沿。展开在何处有效、何处失效的边界本身,就教给了我们一些关于复杂性本质的深刻道理。