跳过正文
  1. Posts/

从 RNN / LSTM / GRU 到早期 Attention:为什么“压缩历史”最终变成了“按需读取”

·5735 字·12 分钟
目录

上一篇《从 RNN 到 LSTM,再到 GRU》把 gate 机制理了一遍:LSTM / GRU 把 recurrent state 的信息流变成了可学习的动态控制,决定什么写入、什么保留、什么暴露。写到那里,我以为 RNN 这条线差不多收尾了。

结果接着往下复习时,卡在一个听起来不太技术的问题上:

LSTM / GRU 不是已经解决长期记忆了吗,那后来的 attention 是来干嘛的?

我第一反应是,两者是同一问题的升级关系:都在处理“长距离依赖”,attention 更强一些。后来发现这个理解是错的,它们根本不在同一个问题上。而从“LSTM 能记很久”走到“attention 出现”,中间还隔着一个 fixed-vector Seq2Seq——正是它把 RNN 的一个隐含假设逼成了绕不开的瓶颈。

这篇想把这条线理清楚:RNN 世界是怎么一步步走到 attention 的。

是一个问题,还是两个问题
#

“LSTM 解决了长期记忆”这个说法,其实混着两个不同的问题。我花了点时间才把它们拆开。

问题 A:长期信息能不能顺利活下来?

vanilla RNN 的状态更新是 \(h_t = f(x_t, h_{t-1})\),历史必须沿着一条链逐步传播:

$$h_1 \rightarrow h_2 \rightarrow \cdots \rightarrow h_T$$

BPTT 时梯度要沿同一条链往回走,每一步乘一次局部 Jacobian,连乘几十次之后就很难保持稳定——这是梯度消失 / 爆炸的来源(rnn-bptt 那篇Jacobian 连乘那篇分别写过,这里不重复)。

LSTM / GRU 做的事情,是给信息构造一条更耐久的传播路径:cell state 提供近似 \(\frac{\partial c_t}{\partial c_{t-1}} \approx f_t\) 的 memory path,gate 决定忘掉什么、写入什么。这一部分回答的是 memory retention / state transition——信息能不能从 \(h_1\) 一路活到 \(h_T\)。

问题 B:为什么所有历史一定要压进一个固定维度的 state?

就算问题 A 被完美解决——假设 LSTM 该留的都留住了——仍然有一个事实没有变:

$$x_1, \dots, x_T \rightarrow h_T \in \mathbb{R}^d$$

序列不断变长,state 的维度却固定是 \(d\)。“信息都活到了 \(h_T\)”和“\(h_T\) 装得下这些信息”是两回事。

这样看,LSTM / GRU 的定位就清楚了:它们改善的是“怎么维护这个 state”,并没有质疑“历史应该被压进 state”这个 architecture assumption 本身。gate 决定什么写入、什么丢弃,但前提始终是:筛选完还得塞进同一个固定容量的容器。

所以这两个问题得分开:

长期依赖问题是“信息能不能活到 \(h_T\)”;fixed-vector bottleneck 是“即使都活到了 \(h_T\),让一个固定维度 vector 同时承担所有未来需求是否合理”。

前者是信息在时间轴上能不能存活,后者是容器容量够不够。gate 解决的是前者,attention 后来面对的是后者。

fixed-size state 一直都在,为什么以前没炸
#

问题 B 一旦被指出来,还会跟出一个疑问:RNN 的 fixed-size state 从第一天起就存在,为什么这个毛病直到 Seq2Seq 时代才变得刺眼?

我想了想,我的理解是:不是它以前不存在,而是以前的任务形态下它不疼。

拿语言模型来说:

$$x_1, \dots, x_t \rightarrow h_t \rightarrow x_{t+1}$$

任务本身就是根据过去预测下一步。每一步的 prediction 只面对一个 next token,\(h_t\) 作为历史的 summary 与任务形式相当自然。压缩损失当然存在,但预测目标是局部的,问题不尖锐。

再拿 sequence labeling 来说:

1John   lives   in   London
2 ↓      ↓      ↓      ↓
3PER     O      O      LOC

每个位置直接用自己的 \(h_i\) 做预测,\(h_1, h_2, h_3, h_4\) 各司其职。没有人强迫先把整个序列压成 \(h_4\),再让所有输出只依赖 \(h_4\)。

也就是说,问题不在“压缩”这件事突然变坏了,而在任务形态变了:

RNN 的 information compression 问题一直存在,但 fixed-vector Seq2Seq 把它从一个隐含限制,放大成了 architecture 的核心瓶颈。

Seq2Seq 把这个假设推到了极限
#

机器翻译是最经典的 Seq2Seq 任务:

$$\text{source sequence} \rightarrow \text{target sequence}$$

输入输出都是变长序列,长度可以不同,词序甚至可能变化——英文句子里靠后的词,翻成中文可能靠前。所以没法像 sequence labeling 那样做 timestep-to-timestep 的对位预测,只能先读完整个 source,再逐词生成 target。

经典做法是把模型分成两段:

1source
23Encoder RNN
45context c
67Decoder RNN
89target

这里有几件容易糊在一起的事:encoder 和 decoder 通常是两个参数独立的 RNN / LSTM / GRU,但它们不是两个分别训练的系统——而是一个整体模型,靠 target 侧的 prediction loss 端到端联合训练,梯度会穿过 decoder 一路传回 encoder。

在经典的 fixed-vector Seq2Seq(Sutskever et al., 2014)里,encoder 读完整个 source:

$$x_1, \dots, x_T \xrightarrow{\text{Encoder}} h_T$$

然后直接把最后一个 state 当作 context:

$$c = h_T$$

decoder 之后的每一步,都只依赖这个 \(c\)。

fixed-vector Seq2Seq:整个 source 被压成一个 c

到这里,问题 B 才真正变尖锐:

  • source 是 5 个 token 还是 50 个 token,最后都必须压成同样维度的 \(c\)。序列越长,压缩越狠;
  • decoder 第 1 步和第 20 步需要的 source 信息可能完全不同——翻译长句时,开头看的和结尾看的根本不是 source 的同一处——但它们拿到的是同一个提前生成好的 summary。

gate 在这里帮不上忙。LSTM 把 state 维护得再好,encoder 读完最后一个 token 的那一刻,能交出去的也只有一个 \(h_T\)。问题 A 的解法优化的是信息活到 \(h_T\) 的过程,问题 B 卡在交接这一步本身。

encoder 明明是训练出来的,怎么会“不知道 decoder 要什么”
#

解释这个瓶颈时,很多资料会说:encoder 不知道 decoder 需要什么。我第一次看到这个说法就觉得不对劲——encoder 明明是端到端训练出来的,loss 能从 decoder 一路传回来,它怎么会“不知道”?

这个说法确实不准确,值得掰一下。

联合训练时,encoder 当然能学到什么样的信息对 translation task 通常有用;LSTM / GRU 也不是无差别压缩机,gate 本身就在做有选择的保留。所以 encoder 不是瞎压缩,它在“对后面的任务大概率有用”这个方向上,已经压得尽力了。

真正的限制在时机:encoder 处理 source 的那个时刻,拿不到 decoder 某个具体 timestep 的即时需求。

它能学到的是:

以后哪些东西大概率有用?

它做不到的是:

decoder 现在正在生成“John”,所以这一刻请重点读取 source 里对应 John 的信息。

因为 \(c\) 在 decode 开始之前就已经定死了,所有 decoder step 共享同一份提前生成的 summary,没有任何机制能让它随当前需求重新生成。

所以两种方案的区别,更准确的说法是:

  • 经典 fixed context:task-level、提前完成的 summary;
  • attention:decoder-step-conditioned、每一步按需生成的 summary。

前者赌的是“整个解码过程大概会需要什么”,一次押注;后者把“现在需要什么”的决定,推迟到了解码的每一步。

为什么不一开始就把 hidden states 都留下来
#

站在今天回头看,最让人费解的是:\(h_1, \dots, h_T\) 反正都算出来了,为什么不全部留下?为什么非得把前面的信息压掉,只交出一个 \(h_T\)?

我最先想到的答案是“当年算力差”。算力和显存确实是因素,但我不认为这是根本解释。这个问题我卡了很久,最后是从三个角度把自己说服的。

“state 总结过去”本来就是 RNN 的思路
#

RNN 设计上的 inductive bias 就是:

$$h_t = \text{summary of } x_1, \dots, x_t$$

于是读到 source 结尾时,\(h_T\) 自然就是整个 source 的 summary。在当时的建模视角里,这不是无奈的妥协,而是一个优雅的目标——如果一整句话能被编码成一个 semantic vector,这件事本身就很有吸引力。

从这个角度看,fixed-vector Seq2Seq 根本不是一个愚蠢设计,它是“state summarizes history”这个思想在 encoder–decoder 结构下非常自然的延伸。带着 Transformer 时代的 hindsight 去嘲笑它,无助于理解这段历史。

光存下来,解决不了真正的问题
#

这是我卡最久的一点。假设真的把 \(h_1, \dots, h_T\) 都留下了——然后呢?decoder 怎么消费它们?

  • concat?source 长度不固定,decoder 第一层的输入维度都没法确定;
  • average?又是一次粗暴压缩,只是从顺序压变成了并排压;
  • max pooling?信息损失同样严重;
  • 随便挑一个?没有意义;
  • 每一步挑不同的?——那怎么挑?

最后一问才是真正的题目。存不难,tensor 什么时候都能存;难的是:怎么给 variable-length memory 设计一个可学习的 read interface?

attention 的价值就在这里。它把 \(\{h_1, \dots, h_T\}\) 保留成 variable-size memory,而每次读取仍然产生一个 fixed-size 的结果:

$$c_t \in \mathbb{R}^d$$

attention 把 fixed-size constraint 从“整个 memory”挪到了“每次 read 的输出”。

换一种说法:经典 Seq2Seq 在 write side 解决 variable length——写的时候全部压缩;attention 在 read side 解决——memory 保留 variable size,每次按需读出 fixed-size result。

compute / memory 是 tradeoff,不是根本原因
#

账也要老实记。经典 fixed-vector 方案里,encoder 做完之后只需留一个 \(c\);而 attention:

  • 需要保存全部的 \(h_1, \dots, h_T\);
  • decoder 每一步都要和所有 source state 做匹配;
  • source 长度 \(T\)、target 长度 \(N\) 时,attention matching 引入大约 \(O(TN)\) 的额外工作量。

这些开销是真实的,在 2014 年的算力条件下也不算便宜。但不该把历史解释成“大家早就想到了 attention,只是 GPU 太差”。更准确的说法是:fixed representation 本来就是当时合理的 modeling abstraction;后来才发现,variable-size memory 加 dynamic read 更适合 Seq2Seq。

拿什么去查:decoder 自己的 state
#

到这里 attention 才正式出场。

encoder 保留 \(h_1, \dots, h_T\),decoder 第 \(t\) 步拿什么来表达“我现在需要什么”?答案是它自己的 state:

$$s_{t-1}$$

这里有个容易看岔的细节:\(s_{t-1}\) 是一个 decoder hidden-state vector,不是 \(\{s_1, \dots, s_{t-1}\}\) 这个集合。但因为 decoder 也是 recurrent network,\(s_{t-1}\) 已经压缩了 target 侧到目前为止的生成历史。所以它大致能回答一个问题:

我已经生成到哪里了,现在大概需要什么?

拿 \(s_{t-1}\) 当作当前的读取需求,去查询全部 \(h_1, \dots, h_T\)。

我刚看公式时在这里最容易混的是两条时间轴,必须分开:

  • \(i = 1, \dots, T\):source / encoder position;
  • \(t = 1, \dots, N\):target / decoder timestep。

decoder 第 \(t\) 步访问的是完整的 source \(h_1, \dots, h_T\),而不是 \(h_1, \dots, h_t\)——encoder 在 decode 开始之前已经编码完了整个 source,memory 是现成的、完整的。

attention:拿 s_{t-1} 去 variable-size memory 里读出 fixed-size 的 c_t

score 一开始什么都不知道
#

读取的第一步,是给每个 source position 打分:

$$e_{t,i} = \operatorname{score}(s_{t-1}, h_i)$$

很多资料写到这里会说“计算 decoder state 与每个 encoder state 的相关性”,然后继续往下。我恰好卡在这一步:凭什么一个公式就知道什么叫“相关”?

答案是:它一开始什么都不知道。score 只是一个参数化的函数。可以是 dot product:

$$s_{t-1}^{T} h_i$$

也可以是 Bahdanau attention 里那样的一个小神经网络:

$$e_{t,i} = v^T \tanh(W_s s_{t-1} + W_h h_i)$$

公式的具体形式不是重点,重点是整条下游链路都是 differentiable 的:

$$e_{t,i} \rightarrow \alpha_{t,i} \rightarrow c_t \rightarrow \text{Decoder} \rightarrow \text{prediction} \rightarrow L$$

于是 \(\frac{\partial L}{\partial e_{t,i}}\) 算得出来。假设当前一步该生成“红色”,模型却把权重大量放在 source 里 a 的位置上,prediction 就会变差、loss 变大;梯度会压低错误位置的 score,推高“red”对应位置的 score。这样的更新在训练集上重复亿万次,score 函数的参数就被逐渐塑造成“怎么打分才能让翻译正确”。

attention score 不是被人工定义成“相关性”的,它是在 end-to-end training 中被逐渐塑造成一个 useful compatibility function。

所以“相关性”这个词在这里其实不准确,更贴切的词是 compatibility:decoder 当前的需求,和某个 encoder representation 匹配不匹配。它不等于统计学意义的 correlation,也不一定等于我们直觉上说的语义相似。

为什么最后一步是 weighted sum
#

score 出来之后,经过 softmax 归一成权重,再对所有 source state 做加权和:

$$\alpha_{t,i} = \operatorname{softmax}_i(e_{t,i})$$$$c_t = \sum_i \alpha_{t,i} h_i$$

为什么不干脆选 score 最高的那个位置(hard select),而要所有位置都掺一份?

一个原因是,一步解码经常同时需要多个 source position。比如 “deep learning” 对应“深度学习”:

1source:   deep     learning
2             \       /
3target:       深度学习

两个 source state 共同贡献一个 target 词,hard-select 只能押其中一个。

更重要的原因在 architecture 层面:weighted sum 让 memory access 本身变成了一个 differentiable operation。hard select 的话,梯度在选择那一步就断了——argmax 不可导,\(s_{t-1}\) 永远学不会“该看哪里”;soft 加权则让梯度直接流回每个 \(\alpha_{t,i}\),再流回 score 的参数。“读哪里”这件事本身,变成了可以被训练的东西。

从 gate 到 attention:从 write control 到 read control
#

到这里可以接回 LSTM / GRU 那条线了。

LSTM 的 forget gate \(f_t\),也没有人工监督告诉模型“这里该 forget 0.2、那里该 forget 0.9”。它之所以成为有效的控制信号,是因为最终 loss 能反向训练它。attention weight 完全同理:同样是没有直接 supervision 的控制信号,靠任务 loss 间接获得意义。

把两者放在一起,它们在 architecture 层面做的事非常对称:

  • gate 控制 state 怎么写、怎么保留——learned write control;
  • attention 控制当前从哪里读——learned read control。

这样看,attention 就不是凭空出现的魔法。它是把 RNN 世界里“控制信号也可以被学习”这件事,从写的一侧推广到了读的一侧。

结论
#

回头看,这条演进链上每一步都是被上一步的问题逼出来的:

  1. RNN:用一个 state 总结过去;
  2. LSTM / GRU:让这个 state 更容易长期保存重要信息——解决了问题 A;
  3. fixed-vector Seq2Seq:把“整个 source 可以被一个 state 总结”的假设推到极限,问题 B 彻底暴露;
  4. attention:不再要求所有信息提前塞进一个 fixed-size state,保留 \(h_1, \dots, h_T\) 作为 variable-size memory,根据 decoder 当前需求动态读出 \(c_t\)。

真正变化的是 memory access pattern:从 memory compression 走向 memory access——从提前总结,走向按需读取。

而 attention 最重要的创新,并不是“把所有 hidden states 保存下来”。保存 tensor 本身没有任何难度。重要的是,它为 variable-length memory 设计出了一个可学习、content-dependent、differentiable、且每次返回 fixed-size representation 的 read interface:\(h_1, \dots, h_T\) 是 memory bank,\(s_{t-1}\) 是当前读取需求,\(c_t\) 是这一次 read 的结果。

历史定位也要补一句:attention 最早并不是为了消灭 RNN 而发明的。早期模型里 encoder 仍然是 RNN / BiRNN,decoder 仍然是 RNN,attention 夹在中间,只解决一件事——fixed-length context bottleneck。后来人们会进一步发现,这种“根据当前需求去访问一组 representation”的机制,并不一定要依附在 RNN encoder–decoder 上。但那是另一个故事了。

参考资料
#

相关文章