上一篇《从 RNN 到 Self-Attention》结尾记账时留了一条:去掉 recurrence 之后,顺序不再天然编码在计算过程里,摊平的 attention 层需要额外把顺序信息补回去,「具体怎么补,下一篇再说」。
当时我以为「补回去」就是加一行代码的事。真动手想清楚到底丢了什么之后,才发现这个问题比看起来深:我先要回答一个更基础的问题——
RNN 里的顺序,到底存在哪?
我去翻 RNN 的定义,找遍了也找不到一个叫 position 的变量。\(h_t = f(x_t, h_{t-1})\) 里没有位置编号,没有下标特征,什么都没有。可是把 RNN 换成纯 self-attention,顺序信息就确实丢了。一个从来没有显式存过的东西,怎么说丢就丢了?
RNN 里的顺序存在哪#
拿一个具体的 sequence 看:
1A B CRNN 处理它的时候,每个位置扮演的计算角色是不一样的:
- \(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:
现在把输入换成:
1C B Ainteraction 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 号,跟句子里还有谁、谁在看谁都没关系。
第二种回答的问题是:你相对我在哪里?
站在 C 的视角看整个 sequence:
1A:在我前面 2 格
2B:在我前面 1 格
3D:在我后面 1 格这叫 relative position(相对位置)。注意这里的位置关系不单独属于 C,也不单独属于 A——它属于 C ↔ A 这一对。换一个读取方,整套关系就换一套。
用一句话区分:absolute position 更像 node attribute,relative position 更像 relation / edge attribute。graph 的说法就借用这一句,不展开。但这个区分值得记住:同一个词「位置」,一个说的是「我自己是谁号」,一个说的是「我们俩隔多远」——这是两种不同性质的信息,后面所有 positional encoding 的设计分歧,几乎都能归到这两个路线上。
放进 representation,还是放进 relation#
两种位置信息,对应两种使用它们的思路。
思路一:把位置放进 token 自己的 representation。
1token representation = 我是谁 + 我在哪里同样是 dog,dog @ position 3 和 dog @ 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 information。content + 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 为什么有效是另一个话题,这里点到为止。
结论#
这条线走下来,值得留下来的几句话:
- sequence structure ≠ sequential computation。数据有顺序,不意味着 computation graph 必须沿着顺序串行执行——这是上一篇的结论,这篇看到了它的另一半:不串行执行之后,顺序本身也没人负责了。
- RNN 的 recurrent topology 不只是 information routing mechanism,它还隐式提供了 sequence order:不同位置扮演不同的计算角色,顺序写在图的结构里,不写在任何字段里。
- 去掉 recurrence,换来 parallelism 和 shorter computational path,同时也失去了 recurrence 免费提供的那部分 sequential inductive bias。
- 所以 positional information 不是 implementation trick,而是 architecture tradeoff 的自然结果——是主动移除 recurrence 之后必须重新显式表示的 structural information。
- content 回答「这是什么」,position 回答「它处于什么结构关系中」;两种信息,两类问题。
- absolute position 描述「我在哪里」,是 node attribute;relative position 描述「你相对我在哪里」,是 relation attribute。
- Neural representation is not a serialization format。表征的目标不是无损保存、可逆拆解,而是保留并组织 task-relevant information,让后续 computation 能够利用。
收住的地方也清楚了:positional structure 不再由 computation graph 免费提供,必须由模型自己表示——可以放进 representation,也可以放进 relation。但一个悬着的问题是:这些位置信息最终应该怎样真正融入一个完整的 sequence model,让「谁在谁前面」参与到每一次 token-to-token interaction 的计算里? 这就是下一站的事了。
参考资料#
- MIT 6.S191, Introduction to Deep Learning — https://introtodeeplearning.com/
- 从 RNN 到 Self-Attention:信息为什么一定要沿着 State 一步步传递?
- 从 RNN / LSTM / GRU 到早期 Attention:为什么“压缩历史”最终变成了“按需读取”
- 从 RNN 到 LSTM,再到 GRU:循环网络真正改变的是什么?