Skip to main content
  1. Posts/

去掉 RNN 之后,顺序去哪了?

·5345 words·11 mins
Table of Contents
Note: This article is available in Chinese only. 本文暂无英文版本。 View original

上一篇《从 RNN 到 Self-Attention》结尾记账时留了一条:去掉 recurrence 之后,顺序不再天然编码在计算过程里,摊平的 attention 层需要额外把顺序信息补回去,「具体怎么补,下一篇再说」。

当时我以为「补回去」就是加一行代码的事。真动手想清楚到底丢了什么之后,才发现这个问题比看起来深:我先要回答一个更基础的问题——

RNN 里的顺序,到底存在哪?

我去翻 RNN 的定义,找遍了也找不到一个叫 position 的变量。\(h_t = f(x_t, h_{t-1})\) 里没有位置编号,没有下标特征,什么都没有。可是把 RNN 换成纯 self-attention,顺序信息就确实丢了。一个从来没有显式存过的东西,怎么说丢就丢了?

RNN 里的顺序存在哪
#

拿一个具体的 sequence 看:

1A B C

RNN 处理它的时候,每个位置扮演的计算角色是不一样的:

RNN 的 sequential topology:不同位置扮演不同的计算角色
  • \(h_1\) 是第 1 个被计算的,它只看得到 A;
  • \(h_2\) 是第 2 个被计算的,它拿到的是 A 已经揉进 state 之后的形态;
  • \(h_3\) 是第 3 个被计算的,它前面的信息已经过两次 state transition。

A 天然发生在 B 之前,B 天然发生在 C 之前——不是谁告诉模型的,而是 computation graph 里那条有方向的 recurrent path 决定的。位置 1 和位置 3 在图里就不是对称的角色:一个在链的开头,一个在链的末尾,谁先被计算、谁能看到谁,都被 topology 写死了。

我差点把这件事总结成「RNN 自带 position ID」,但这个说法不准确。RNN 里没有任何 ID;准确的说法是:

RNN 的 sequential topology 天然让不同 sequence position 扮演不同的计算角色。

顺序不是存在某个字段里的数据,而是计算结构本身的性质。这就是为什么我找不到它——我一直在找一个变量,而它是图的结构。这是 RNN 一个容易被忽略的免费能力:它做 information routing 的同时,sequence order 是顺带白送的。

拆掉 recurrence 的时候,把它也拆掉了
#

回顾上一篇的主线:RNN 里 A 想影响很远的 B,必须沿着 state 链一步步传;self-attention 允许 A 直接读到 B,computational path 从随距离线性增长变成 \(O(1)\),sequence 维度上的串行依赖也被拆掉——parallelism 首先是 architecture property,dependency graph 摊平了,并行才成为可能。

这次我盯着代价那一面看。还是 A B C,纯 self-attention、不加任何额外的位置信息,每个 token 都能参考其他所有 token:

纯 self-attention 的 interaction topology:全连接、对称

现在把输入换成:

1C B A

interaction topology 变了吗?没有。还是同样的 all-to-all 结构,还是每对 token 之间都能直接交互。变的只是每个位置上摆的内容。

也就是说,模型能从这张图里读出「哪些 content 和我相关」,但没有任何依据回答「对方在我前面还是后面」「谁是第一个」「这两个 token 隔了多远」。

这里我纠正过自己一个想歪的方向:self-attention 的问题不是「看不到远处」——恰恰相反,它看得太清楚了,任何距离的 token 都一步直达。问题出在这种 interaction 太自由、太对称:RNN 那条链虽然让远距离交互很费劲,但链本身就是方向;把链拆掉换成全连接之后,方向也一起没了。

给这种对称性起个名字
#

查资料的时候发现,这种「对称」有个正式的名字:permutation equivariance(排列等变性)。

permutation 就是排列、重排。equivariance 说的是:把输入重排,输出跟着做同样的重排。具体到刚才的例子——输入 A B C,模型输出三个位置各自的新 representation,记作 A’ B’ C’;如果把输入重排成 C B A,输出就相应变成 C’ B’ A’。

$$f(Px) = P f(x)$$

其中 \(P\) 是任意一个重排操作。输入怎么排,输出跟着怎么排,仅此而已——计算本身对「谁在前谁在后」没有意见。

顺手区分一个容易混的词:permutation invariance(排列不变性),那是另一种性质:

$$f(Px) = f(x)$$

输入怎么排,最终输出都一样,比如把一袋向量做 pooling 得到一个汇总结果。我一开始想说纯 self-attention 是 invariant 的,想了想不对——每个位置的输出明明会随输入内容变。准确的说法是:纯 self-attention 是 permutation equivariant,而不是 permutation invariant。整层计算对重排「不抵抗」,输入序列被打乱,它就老老实实按打乱后的对应关系给出结果。

对 pooling 来说,invariance 是卖点;对 sequence modeling 来说,equivariance 是事故——语言的顺序是有意义的,模型却对顺序无感。

dog bites man 和 man bites dog
#

为什么顺序无感是事故,一对经典例子就能说明:

1dog bites man
2man bites dog

两句话的 token 集合完全一样,每个 token 的 content 也一样。如果模型只回答「有哪些 token、哪些 token 相关」,这两句话在它眼里几乎无法区分。但任何读过报纸的人都知道,这是两条完全不同的新闻。

差别只在顺序:谁咬了谁,取决于谁在动词前面。

于是 sequence 里的信息其实分成两种,回答的是两个不同的问题:

  • content:这是什么?——dog、bites、man 各自的身份和语义;
  • position:它处在什么结构关系中?——谁在谁前面、谁在谁后面、隔了多远。

RNN 时代不需要把这个区分说破,因为 position 那一半藏在 topology 里。拆掉 topology 之后,position 成了必须单独面对的东西:给 sequence 重新建立一种 coordinate system。这里说的 coordinate 不是二维空间那种几何坐标,而是 sequence 自己的结构坐标——before / after、距离、序数位置、边界(哪里是开头、哪里是结尾)。

这是账单,不是 bug
#

把两代 architecture 的账摆在一起算,逻辑就完整了。

RNN 提供的东西里,有一项之前从没被我列进清单:

  • built-in sequential order——顺序编码在计算过程里;
  • 明确的 temporal progression——第 t 步就是发生在第 t-1 步之后;
  • sequential inductive bias(归纳偏置,即 architecture 对问题预先内置的假设)——「相邻位置关系更紧密」「历史单向流动」这些假设不用学,结构自带。

代价在上一篇算过:long computational path、timestep 之间的串行依赖、sequence 方向的 serialization。

self-attention 把代价全部消掉:direct interaction、short path、parallelism。但对照上面那份清单,它同时也把 RNN 白送的那三项一并退掉了——computation topology 不再提供任何 positional asymmetry。

所以这篇文章里我最想立住的一个判断是:

positional information 不是一个后来「顺手补上」的 implementation trick,而是当我们主动移除 recurrence 时,必须重新显式表示的一类 structural information。

它是 architecture tradeoff 的直接结果。选了全连接的对称 interaction,就得自己把顺序还回去;这不是 self-attention 的设计缺陷,而是那笔交易的账单。顺带说,这也解释了为什么顺序问题在 RNN 时代几乎没人讨论——不是那个时代的人没意识到顺序重要,而是顺序是免费的,免费的东西不会出现在论文里。

我在哪里,和你相对我在哪里
#

顺序要还回去,先要想清楚还的是什么。查下去发现,「位置」这个词其实对应两种不同层面的信息。

第一种回答的问题是:我在哪里?

1A → position 1
2B → position 2
3C → position 3

这叫 absolute position(绝对位置):给每个 token 一个全局坐标。位置被看成 token 自己的属性——A 就是 1 号,C 就是 3 号,跟句子里还有谁、谁在看谁都没关系。

absolute position:位置是每个 token 自己的属性

第二种回答的问题是:你相对我在哪里?

站在 C 的视角看整个 sequence:

1A:在我前面 2 格
2B:在我前面 1 格
3D:在我后面 1 格

这叫 relative position(相对位置)。注意这里的位置关系不单独属于 C,也不单独属于 A——它属于 C ↔ A 这一对。换一个读取方,整套关系就换一套。

relative position:位置关系属于一对 token 之间的 relation

用一句话区分:absolute position 更像 node attribute,relative position 更像 relation / edge attribute。graph 的说法就借用这一句,不展开。但这个区分值得记住:同一个词「位置」,一个说的是「我自己是谁号」,一个说的是「我们俩隔多远」——这是两种不同性质的信息,后面所有 positional encoding 的设计分歧,几乎都能归到这两个路线上。

放进 representation,还是放进 relation
#

两种位置信息,对应两种使用它们的思路。

思路一:把位置放进 token 自己的 representation。

1token representation = 我是谁 + 我在哪里

同样是 dog,dog @ position 3dog @ position 20 的 content 相同,但最终 representation 不同。位置成为每个 token 表示的一部分,跟着 token 走。这大致对应 absolute position 的路线。

思路二:把位置放进 token 之间的交互里。

先解释一下 token-to-token interaction:一个 token 在计算自己时参考另一个 token 的信息,也就是 token 之间的信息交互——self-attention 做的就是这件事。思路二说的是:当 C 去参考 A 时,直接告诉 C「A 在你前面两格」。位置不(只)属于任何一个 token,而是在交互发生的那一刻,作为这对关系的属性被使用。这大致对应 relative position 的路线。

两条路线各自怎么具体实现、哪种在什么任务上更好,这篇不展开——这篇的任务是走到「为什么必须有 positional information、它可以长什么形状」为止。

加起来不会把信息弄坏吗
#

思路一里那个「我是谁 + 我在哪里」,我第一遍读到时是存疑的:两种信息直接加起来?content 向量和 position 向量逐维相加,不会互相覆盖、把原来的信息破坏掉吗?

这个疑问值得认真对付,因为它暴露的其实是我自己的一个思维定式。我做工程出身,看到「两种信息合成一个向量」,本能地用 serialization(序列化)的思路去理解:

1原始信息 → 编码 → 必须能无损恢复

就像 JSON、protobuf,字段写进去,之后必须能一个不少地拆出来。拿这个标准去检查 content + position,当然不合格——两个向量相加之后,通常没法唯一地拆回原来的两份。

但 neural representation 的目标通常不是这个。它真正被要求的只有一件事:后续计算还能不能利用其中的 task-relevant informationcontent + position 即使无法唯一还原成 content 和 position 两份,只要下游的计算仍然能利用 token identity、positional structure、语义和句法信息,它就可以是一个好的 representation。能不能拆回去,根本不是评判标准。

所以这里留下这篇文章里我另一个想记住的判断:

Neural representation is not a serialization format. 神经网络的表征不是一种序列化格式——它不是为了让人之后把每个字段原封不动拆出来,而是为了形成一种对后续计算有用的状态。

这背后还有一层:learned representation 通常是 distributed representation(分布式表征)。不要想象向量里 dimension 1 存词性、dimension 2 存 token identity、dimension 3 存 position——真实的 representation 里,多种信息共同分布在整个高维向量空间中,没有哪个 dimension 单独「负责」某个字段。「不同信息混在一个 vector 里」在 representation learning 里不是异常,而是常态;embedding 从一开始就是这么工作的,content 加 position 只是往同一个向量里再混进一种信息。

但边界也要补上:不要求可逆,不等于怎么混都无所谓。加法会把两种信息压进同一个空间,它们会不会在训练中学会「让开」彼此、各自保持对下游可读,这是真实的设计问题——一个好的 representation 仍然应该让 task-relevant information 容易被后续 computation 利用。只是这个「容易」由训练和任务来检验,不由「能否无损解压」来检验。

为什么经常看到的是加法
#

还有一个自然疑问:就算决定把 position 混进 representation,为什么很多 architecture 都选择 A + B,而不是 concat 成更长的向量?

对 positional information 来说,content + position 可以理解成一种 information fusion:

  • representation 的 dimension 不变,后面的计算组件不用为「多了位置信息」改接口;
  • 「我是谁」和「我在哪里」融合成同一个 state,对下游来说就是一个带了位置感的 token representation。

但要说清楚:加法不是数学上唯一正确的答案,concat 当然也可以设计,各有各的代价(维度膨胀 vs 信息叠加)。positional addition 是一种 architecture choice,不是什么定理。

顺带提醒一个我自己混过的地方:不要把它和 residual connection 的 \(x + F(x)\) 混为一谈。两者形式都是加法,semantics 不同——content + position 更接近两种信息的融合,\(x + F(x)\) 更接近 old state 加上一个 update / correction。residual 为什么有效是另一个话题,这里点到为止。

结论
#

这条线走下来,值得留下来的几句话:

  1. sequence structure ≠ sequential computation。数据有顺序,不意味着 computation graph 必须沿着顺序串行执行——这是上一篇的结论,这篇看到了它的另一半:不串行执行之后,顺序本身也没人负责了。
  2. RNN 的 recurrent topology 不只是 information routing mechanism,它还隐式提供了 sequence order:不同位置扮演不同的计算角色,顺序写在图的结构里,不写在任何字段里。
  3. 去掉 recurrence,换来 parallelism 和 shorter computational path,同时也失去了 recurrence 免费提供的那部分 sequential inductive bias。
  4. 所以 positional information 不是 implementation trick,而是 architecture tradeoff 的自然结果——是主动移除 recurrence 之后必须重新显式表示的 structural information。
  5. content 回答「这是什么」,position 回答「它处于什么结构关系中」;两种信息,两类问题。
  6. absolute position 描述「我在哪里」,是 node attribute;relative position 描述「你相对我在哪里」,是 relation attribute。
  7. Neural representation is not a serialization format。表征的目标不是无损保存、可逆拆解,而是保留并组织 task-relevant information,让后续 computation 能够利用。

收住的地方也清楚了:positional structure 不再由 computation graph 免费提供,必须由模型自己表示——可以放进 representation,也可以放进 relation。但一个悬着的问题是:这些位置信息最终应该怎样真正融入一个完整的 sequence model,让「谁在谁前面」参与到每一次 token-to-token interaction 的计算里? 这就是下一站的事了。

参考资料
#

Related

从 RNN 到 Self-Attention:信息为什么一定要沿着 State 一步步传递?

上一篇《从 RNN / LSTM / GRU 到早期 Attention》结尾我留了一句话:早期的 attention 并不是为了取代 RNN 而出现的,它只是夹在 encoder–decoder 中间解决 fixed-context bottleneck;但“根据当前需求去读取一组 representation”这种机制,并不一定要依附在 RNN 上。这篇接着讲那个“另一个故事”。