Skip to main content
  1. Posts/

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

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

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

真正的引子,是我复习时反复绕回来的一个问题:

在 RNN 里,位置 A 的信息想影响很远的位置 B,为什么一定要沿着 recurrent state 一步一步传过去?

这个问题乍一听像抬杠——recurrence 不就是沿时间传播的结构吗,不然叫什么 RNN。但我带着它把后面的演进重新走了一遍,发现从 attention 到 self-attention,再到《Attention Is All You Need》那一步跳跃,几乎就是对这个问题的一连串回答。

information moves through state
#

先把 RNN 的核心假设摆出来(展开版在rnn-state-transition 那篇):

$$h_t = f(x_t, h_{t-1})$$

一个 fixed-dimensional 的 state 承载历史,每一步把当前输入和上一步的 state 揉在一起,得到新的 state。信息不会跳跃,只能沿着这条链往前走:

1x_i
23h_i → h_{i+1} → h_{i+2} → ... → h_j

如果 \(x_i\) 的信息想影响很远的 \(x_j\),它必须途经 \(i\) 和 \(j\) 之间的每一个 timestep,一个都不能少。我当时把这个性质总结成一句:RNN: information moves through state——信息是沿着 state“搬”过去的。

起初我没觉得这有什么问题。序列有时间顺序,信息沿时间传播,顺理成章。“为什么两个位置之间的信息交互,一定要经过它们中间的所有 timestep”——这个疑问我记下了,但没有立刻追。

LSTM / GRU 修的是路,不是“要不要这条路”
#

第一站是 LSTM / GRU。上一篇理过 gate 怎么把 state 更新变成可学习的动态控制。放到这条主线上看,定位就很清楚了:它们改善的是信息沿 recurrent path 传播的方式——memory path 提供一条更耐久的路线,gate 决定什么写入、什么保留、什么遗忘。

但它们没有动 recurrent topology 本身:

1A → state → state → state → ... → B

这条 information path 原样还在,还是得一步一步走。哪怕记忆能力再强,A 到 B 之间隔着的 state transition 一次都不会少。

所以我后来是这么区分这两代工作的:LSTM / GRU 在解决“怎样让信息更安全地走完这条路”,而后来的 attention 这条线,起于另一个问题——“为什么一定要走这条路”。一个在优化路的质量,一个在质疑路的必要性,不在同一层。

从提前总结,到按需读取
#

fixed-vector Seq2Seq 到早期 attention 的过程,上一篇已经详细写过,这里只沿“信息路径”的视角快进一遍,因为它是后面一切的前提。

fixed-vector Seq2Seq 让 encoder 读完整个 source,只交出一个 \(c = h_T\)。RNN 本来就在每一步把历史压进 fixed-dimensional state;Seq2Seq 把这个压缩推到极限——整句话最终也要塞进一个 vector。原本隐含的 compression limitation 被放大成明晃晃的 bottleneck。

Bahdanau attention 的解法是不再提前总结:保留 \(h_1, \dots, h_T\) 作为 variable-size memory,让 decoder 在每一步按当前需要动态读出 fixed-size 的 \(c_t\):

$$\text{score} \rightarrow \text{softmax} \rightarrow \text{weighted sum} \rightarrow c_t$$

fixed-size constraint 从“整个 memory”挪到了“每次 read 的输出”。用上一篇憋出来的那句话总结:从提前总结,变成按需读取

顺着这个视角,attention 可以先理解成一种 content-dependent dynamic memory access:读什么由内容决定,而不是由位置或时间决定。

“人眼看图”的那个故事
#

这里岔出去澄清一件事,因为我曾被它带偏过。

很多教程介绍 attention 时喜欢用这个类比:人看一张图,不会平均地看所有区域,而是把视线集中在重要的部分。这个类比很直观,我自己讲给别人听时也常用。但把它当成 attention 的起源,就不准确了。

回去翻 Bahdanau et al. 2014 的上下文,他们面对的问题非常具体:神经机器翻译里 fixed-vector Seq2Seq 有 bottleneck,句子一长翻译质量明显下降,所以让 decoder 动态读取 encoder states,顺带学出 soft alignment。整个动机都在信息通路和 memory access 上,和“模拟人眼”没有关系。

视觉 attention 作为一个研究方向确实出现得更早,也确实受人类视觉系统启发;后来 attention 被大量用于 CV 之后,“人眼看图”的类比变得特别自然,当教学工具完全合格。但理解 architecture 演进时,我现在用的 mental model 是 dynamic read / content-dependent information routing,不是“模拟眼睛”。

为什么只有 decoder 能读 encoder
#

回到主线。早期 attention 的结构是:

1decoder representation(读取需求)
23     attention
45encoder representations(memory)

读取的一方是 decoder,被读的一方是 encoder。看习惯这个图之后,我冒出一个问题:如果“按需读取其他 representation”是个好 idea,为什么它只能发生在 decoder 和 encoder 之间?sequence 内部的位置,就不能互相读吗?

看一个句子:

1The animal didn't cross the street because it was tired.

构造 it 的 representation 时,单独的 it 几乎没有语义——它指向什么,完全由上下文决定。理想的 representation 应该是“这里的 it 指向 animal,并且和 tired 有强关系”这样的东西,也就是说,不该是 it,而该是 it-in-this-context

再看一对:

1The bank approved my loan.
2We sat on the bank of the river.

同一个 bank,在两句里的 representation 应该完全不同,而这个不同只能来自句内其他位置。

RNN encoder 里,it 的 representation 当然也能吸收到前面词的信息——但那些信息是沿着 state 链一路压缩、改写过来的,代价是什么,下一节再算。单说结构:既然 decoder 可以按内容动态读取 encoder 的 representations,那让 sequence 内部的某个位置按需读取同一个 sequence 里其他位置的 representations,几乎是顺势的一步。

这就是 self-attention。它并不是一套突然冒出来的新机制,真正变化的是谁在查询谁

1early attention:  A reads B   (decoder 读 encoder,两方来自不同 sequence)
2self-attention:   A reads A   (读取者与 memory 来自同一个 sequence)

顺带补一句历史:让 sequence 读自己的想法,也不是《Attention Is All You Need》才第一次出现——Cheng et al. 2016 的 LSTMN、Parikh et al. 2016 的 decomposable attention 都在不同任务里用过 intra-sequence attention。AIAYN 做的是另一件事,后面讲。

RNN 也能把信息传到后面,self-attention 到底多做了什么
#

这是整次复习里卡我最久的问题。

RNN 明明也能让后面的 token 拿到前面的信息——\(h_{100}\) 一路经过 \(h_1\) 到 \(h_{99}\),凭什么说它看不到前文?那 self-attention 难道只是换一种方法做同一件事?

想了很久,最后答案落在一个东西上:computational path length

设 \(x_1\) 和 \(x_{100}\) 需要发生交互。RNN 里:

1x1 → h1 → h2 → h3 → ... → h99 → h100

path length 随 sequence distance 线性增长,隔 100 个位置就要经过约 100 次 state transition。而 self-attention 同一层内,\(x_1\) 可以直接读到 \(x_{100}\):一步直达,path length 是 \(O(1)\),与距离无关。

RNN 的信息路径随距离增长,self-attention 允许直接建立通路

这个差别至少在三个层面上产生后果。

距离不再自动等于路长
#

先说清楚:RNN 不是不能表达远距离关系,这样说就冤枉它了。问题在于,距离越远,信息必须经过越多次 state transition,而每一次 transition 都可能修改、混合、压缩或丢失它。走到 \(h_{100}\) 时,\(x_1\) 的信息已经被反复改写了 99 次,剩下什么是路径决定的,不完全由任务决定。

self-attention 不要求“sequence distance ↑ → information path ↑”。long-range dependency 不再天然意味着 long computational path——这是我认为两者最本质的分界线,比“能不能看到上下文”准确得多。

梯度也要走同一条路
#

Jacobian 连乘那篇写过,BPTT 时梯度是一串局部 Jacobian 的连乘:

$$\frac{\partial h_T}{\partial h_k} = \frac{\partial h_T}{\partial h_{T-1}} \frac{\partial h_{T-1}}{\partial h_{T-2}} \cdots \frac{\partial h_{k+1}}{\partial h_k}$$

forward 的 information path 有多长,backward 的 credit assignment path 就有多长。连乘 \(T-k\) 个 Jacobian,梯度很难稳定,这是消失 / 爆炸的几何来源,LSTM 的 memory path 正是给这条路打的补丁。

而如果 forward 方向上两个位置之间就是一条直接的边,backward 也只需要走这一条边。所以 computational path length 同时牵动三件事:information flow、gradient propagation、long-range dependency 能不能学出来。只说“attention 缓解梯度消失”太便宜了——要点是路径在 architecture 层面变短了,连乘项数从随距离增长变成 \(O(1)\),问题的性质变了。

relationship 不必再间接形成
#

RNN 构造当前位置的 representation 时,远处的信息通常已经历过很多次 sequential transformation,是被所有中间 timestep“加工”过的形态。self-attention 里,当前位置可以按需要直接读到远处的 representation,两个位置之间的关系直接建立,不再必须通过一串 intermediate states 间接蒸馏出来。

一个不太严谨但好记的类比:RNN 像传话,员工 1 的话要经过 99 次转述才到员工 100;self-attention 像员工 100 直接去找当前最相关的人问。差别不在谁更聪明,而在 architecture 改变了谁必须经过谁才能交流。类比到此为止。

sequence 有顺序,为什么计算要串行
#

想通 path length 之后,我发现自己还有个一直没质疑过的默认假设:

1数据是 sequence → 有时间顺序 → 所以计算也应该按顺序执行

RNN 把这三件事捆在一起卖。但它们其实不是一回事:数据具有 sequence structure,并不意味着 computation graph 必须是 sequential 的。顺序是数据的属性;计算要不要串行,是 architecture 的选择。RNN 选择把时间顺序直接编码进计算的执行顺序里,这是一个设计决定,不是义务。

这句话是后面敢把 recurrence 整个拆掉的思想基础之一。

parallelism 首先是 architecture property
#

做 ML Infra 这些年,我第一次听到“self-attention 快是因为能用矩阵乘法”时是不以为意的——matmul 快谁不知道。后来才想明白这个因果讲反了:不是“能写成 dense matmul 所以快”,而是 dependency graph 先变了,dense matmul 才成为可能。

RNN 的依赖图:

RNN:h_t 之间存在算法层面的串行依赖

\(h_3\) 必须等 \(h_2\) 算完,这是 algorithmic dependency,不是实现问题。就算 CUDA kernel 无限快,也消不掉这条依赖;GPU 再宽,sequence 方向上仍然只能一步一步走。

self-attention 同一层内部:

self-attention 同层:所有位置只依赖上一层,彼此不串行

同一层里不同位置都只依赖上一层的 representations,位置之间没有先后约束——不需要 position 1 算完 position 2 才能算。sequence-level serialization 被大幅削掉。

dependency graph 摊平之后,才轮到后面那串好处出场:所有位置合进一个 batch、计算呈 dense matrix 形态、hardware utilization 上得去。顺序不能倒。

parallelism 首先是 architecture property,其次才是 implementation property。 很多性能问题不是写一个更好的 kernel 能救回来的,因为串行性写在算法的依赖结构里。

《Attention Is All You Need》真正押的注
#

到这里可以谈这篇论文了。先分清一件容易糊的事:self-attention 和《Attention Is All You Need》不在同一个抽象层级。

self-attention 是一个 mechanism:sequence 内部不同 representations 直接交互。它完全可以和 RNN 共存——RNN backbone 加 self-attention,在当时没有任何矛盾,也确实有模型这么做。

AIAYN 进一步问的是:如果 self-attention 已经能承担 sequence 内部的 information routing,recurrence 还是不是不可替代的 backbone?

这才是论文最关键的跳跃。所以准确的写法不是“AIAYN 发明了 self-attention”,而是:self-attention 的存在,让“去掉 recurrence”成为一个合理的 architecture hypothesis;AIAYN 把这个 hypothesis 做成了完整模型,并证明它在 sequence transduction / machine translation 上可行而且有效——一个既不依赖 recurrence、也不依赖 convolution 的架构。

attention 的地位变化可以这么概括:

1attention = 辅助 recurrent backbone 的附加组件(Bahdanau 2014)
23attention = 核心 information-routing mechanism,backbone 可以被拆掉(AIAYN 2017)

论文最激进的地方不是“attention 更强”,而是开始质疑 recurrence 是不是 sequence modeling 的必要条件。

被解绑的两件事
#

回头看 RNN 的 \(h_t = f(x_t, h_{t-1})\),我注意到它其实同时在干两件事:维护 state,和传播信息。在 RNN 里这两件事几乎是同一件——information routing 本身就是通过 state update 完成的,想让信息流动,就必须维护一条 state 链。

attention 提供了一种新 primitive:representation 可以根据内容直接从其他 representations 获取信息,不需要一条中间 state 的搬运链。representation interaction 和 state propagation 不必绑定

这两种 architecture,我各用一句话收束:RNN 是 information moves through state,信息沿着 state 移动;attention 是 information is retrieved from relevant representations,信息从相关的 representation 那里被取回。

另一个变化在 inductive bias 上。RNN 的默认 topology 是“前一个位置 → 下一个位置”,CNN 的默认 topology 是“当前位置 ↔ local neighborhood”——两者都把“谁能和谁交互”写死在结构里。attention 允许当前 representation 根据内容直接寻找和自己相关的位置:距离近不一定相关,距离远不一定不相关。relationship 从结构的默认假设,变成了一等公民。

和 CV 的那点缘分
#

补一段个人经历。2017 年我在商汤实习时,最早听到《Attention Is All You Need》其实是在 CV 的环境里。多年后重新从 RNN 走到 self-attention,才反应过来:这篇论文并不是 CV 工作,它来自 machine translation;但它承载的想法为什么这么容易跨到 CV,其实不难理解。

convolution 也是 local operation。feature map 上两个远距离区域 A 和 B 需要交互时,传统 convolution 只能靠堆层数扩大 receptive field:

1A → local conv → local conv → ... → B

“long-range interaction 必须经过 repeated local transformations”——这和 RNN 里 \(A \to \text{state} \to \cdots \to B\) 的困境在结构上是相似的。所以 CV 的人看到“任意两个位置可以直接按内容建立关系”,会感兴趣是自然的。attention 真正强的地方,是它提供了一种跨 domain 的 information-routing primitive,不绑定在序列这种数据形态上。

再往后的 CV attention 发展就不展开了,超出这篇的范围。

账的另一面
#

self-attention 不是免费的,tradeoff 要老实记。

RNN 提供的东西其实不少:天然的 sequential inductive bias、明确的 temporal progression、每一步只和固定大小的 state 交互——计算和内存都随长度线性增长。

self-attention 换来的是更短的 computational path、更直接的 long-range interaction、sequence 级的 parallelism。代价是:

  • 一个位置要和大量其他位置建立关系,interaction 数量随 sequence length 平方级增长,computation 和 memory cost 都跟着涨;
  • 去掉 recurrence 之后,顺序不再天然编码在计算过程里——RNN 串行执行这件事本身就体现了“先后”,摊平的 attention 层需要额外把顺序信息补回去(具体怎么补,下一篇再说)。

这两条后来各自养出了一大片工作,但已经属于优化和变体的范畴,这篇停在 architecture 假设的层面。

结论
#

这条演进链走完,我最想留下来的几句话:

  1. LSTM / GRU 改善的是 information 如何沿 recurrent path 传播;self-attention 开始质疑为什么一定需要这条 path。
  2. attention 的核心不是“模拟人的注意力”,而是 content-dependent dynamic read / information routing。
  3. early attention 的关键变化:从提前总结,变成按需读取。
  4. self-attention 把这种按需读取从 encoder–decoder 之间推广到 sequence 内部——变的不是机制,是谁在查询谁。
  5. long-range dependency 的问题不只是“能不能记住”,还有两个 representations 之间的 computational path 有多长——path length 同时决定 information flow、gradient propagation 和学习难度。
  6. sequence structure 不意味着 sequential computation。
  7. parallelism 首先是 architecture property,然后才是 implementation property。
  8. 《Attention Is All You Need》真正大胆之处,不是发明 attention,而是认真验证了:recurrence 可能并不是 sequence modeling 必需的 backbone。

一句话收束:从 RNN 到 self-attention,真正发生的变化,是我们不再默认 information 必须沿着 state 一步一步移动,而开始允许 representations 根据当前内容直接建立关系。

问题也随之落到下一站:既然允许一个位置直接读取其他位置,模型到底怎么判断——谁和我相关?相关程度是多少?我该从对方那里读什么? early attention 还能借翻译任务的对齐先验打个底,把 encoder–decoder 拆掉之后,这一切都要在一个 sequence 内部自己回答。这就是 Transformer 下一层设计要解决的事了。

参考资料
#

Related