长序列建模中的问题
#2026/01/02 #ai
本节是理解为什么我们需要 Transformer 和“注意力机制”的基石。
在 Transformer 出现之前,我们主要靠 RNN(循环神经网络)来处理文本。
这一节的核心就是通过机器翻译这个任务,揭示了老一辈架构(RNN 编码器-解码器)的致命弱点:
记忆瓶颈。
目录
- 1. 背景:机器翻译为什么难?
- 2. 以前的架构:编码器-解码器 RNN
- 3. 核心痛点:信息瓶颈 (The Information Bottleneck)
- 4. 解决方案:注意力机制 (Attention)
- 总结
1. 背景:机器翻译为什么难?
翻译不是简单的 HashMap(键值对)查找替换。不同语言的语法结构完全不同。
- 直译(错误):
- 逐词替换,语序混乱。
- 意译(正确):
- 需要读完整句话,理解上下文,重新组织语言。
代码演示:直译 vs. 意译
# 假设我们有一个简单的德语到英语的字典
dictionary = {
"Kannst": "Can", "du": "you", "mir": "me", "helfen": "help",
"diesen": "this", "Satz": "sentence", "zu": "to", "uebersetzen": "translate"
}
german_sentence = ["Kannst", "du", "mir", "helfen", "diesen", "Satz", "zu", "uebersetzen"]
# 1. 错误的逐词翻译 (Word-for-word)
english_literal = [dictionary[word] for word in german_sentence]
print(f"直译结果: {' '.join(english_literal)}")
# 输出: Can you me help this sentence to translate
# 评价: 语法完全是错的,像个机器人。
# 2. 正确的翻译 (需要调整语序)
# 德语动词 "uebersetzen" 在最后,英语 "translate" 要提前
english_correct = "Can you help me to translate this sentence"
print(f"正确结果: {english_correct}")
结论: 翻译模型必须 “记住” 整个句子的内容,特别是像德语这种动词放在最后的语言,模型必须读完最后的一个词,才能生成开头正确的英语翻译。
2. 以前的架构:编码器-解码器 RNN
为了解决这个问题,以前的主流架构是 Seq2Seq(序列到序列)模型,通常由两个 RNN 组成。

在 Transformer 模型出现之前,编码器-解码器结构的 RNN 是机器翻译的常见选择。编码器将源语言的一串词元序列作为输入,并通过隐藏状态(一个中间神经网络层)编码整个输入序列的压缩表示。然后,解码器利用其当前的隐藏状态开始逐个词元进行翻译
构建大语言模型不需要深入了解 RNN。只需记住,编码器-解码器 RNN 存在的缺陷对注意力机制的设计起到了促进作用。
代码模拟
我们可以把这个架构看作两个类:Encoder 负责压缩信息,Decoder 负责解压生成。
import torch
class MockEncoderRNN:
def __init__(self, hidden_size):
self.hidden_size = hidden_size
# 模拟一个全零的初始隐藏状态 (记忆单元)
self.state = torch.zeros(1, hidden_size)
def process_word(self, word_vector):
"""
RNN 的核心逻辑:
读取当前单词 + 上一步的记忆 -> 更新记忆
"""
# 这里用简单的加法模拟复杂的矩阵运算
# 实际 RNN 是: state = tanh(W_h * state + W_x * input)
self.state = self.state + word_vector
return self.state
# 模拟输入:假设我们将单词变成了向量 (Embedding)
# 句子: "A", "long", "story"
inputs = [
torch.tensor([0.1, 0.1]), # "A"
torch.tensor([0.2, 0.2]), # "long"
torch.tensor([0.5, 0.5]) # "story"
]
# --- 编码过程 (Encoder) ---
encoder = MockEncoderRNN(hidden_size=2)
print("--- 编码器开始工作 ---")
for i, word_vec in enumerate(inputs):
current_state = encoder.process_word(word_vec)
print(f"步骤 {i+1}, 处理单词后,隐藏状态更新为: {current_state}")
final_context_vector = encoder.state
print(f"\n【最终压缩包】上下文向量 (Hidden State): {final_context_vector}")
# 输出: tensor([0.8000, 0.8000](/post/9b667bgzz0.html#08000,-08000))
关键点: 编码器读完所有单词后,输出了一个 final_context_vector。在没有注意力机制的年代,这个向量就是解码器拥有的唯一信息来源。
3. 核心痛点:信息瓶颈 (The Information Bottleneck)
问题就出在这个 final_context_vector 上。
- 对于短句子: 比如 “Hello world”,把信息压缩成一个向量没问题。
- 对于长句子: 比如一篇 1000 字的文章。RNN 每读一个新词,都要更新一次隐藏状态。当我们读到第 1000 个词时,第 1 个词的信息(比如主语是谁)可能已经在不断的数学运算中被“覆盖”或“稀释”得微乎其微了。
这就是“长序列建模中的问题”:
模型强行把整个句子的含义塞进一个固定大小的向量里。这就像让你读完《红楼梦》,然后只允许你在手心里写 5 个关键词,以此来复述整本书。你肯定会丢失大量细节。
代码演示:模拟“遗忘”
让我们用代码模拟一下,当序列变长时,早期的信息是如何被淹没的。
import torch
# 假设隐藏状态容量有限,我们用简单的加权平均来模拟 RNN 的更新
# 新状态 = 0.9 * 新输入 + 0.1 * 旧状态 (模拟模型更关注最近的输入)
def rnn_update_simulation(current_state, new_input):
return 0.1 * current_state + 0.9 * new_input
# 初始化状态
hidden_state = torch.tensor([0.0])
# 关键信息在最开始!
inputs = [torch.tensor([100.0])] # 第1个词:非常重要的信息 (值很大)
# 后面跟了 20 个无关紧要的词 (噪音)
for _ in range(20):
inputs.append(torch.tensor([0.01]))
print(f"输入序列长度: {len(inputs)}")
print(f"关键信息(第1个词): {inputs}")
# --- 模拟编码过程 ---
for x in inputs:
hidden_state = rnn_update_simulation(hidden_state, x)
print(f"\n最终隐藏状态: {hidden_state.item():f}")
执行结果分析:
输入序列长度: 21
关键信息(第1个词): tensor([100.])
最终隐藏状态: 0.0011
解读: 虽然第一个词是 100.0(重要信息),但经过 20 次迭代后,最终状态变成了 0.0011。解码器拿到这个状态时,根本不知道开头曾经有过一个 100.0。 这就是梯度消失或信息丢失。
4. 解决方案:注意力机制 (Attention)
为了解决这个问题,研究人员提出了注意力机制(Bahdanau Attention, 2014)。
它的核心思想是:解码器在生成翻译时,不要只盯着那个“最终状态”看,而是允许它“回头看”编码器的所有历史状态。
- 没有注意力: 解码器只看
final_state。 - 有注意力: 解码器在生成第 1 个词时,可以看 Encoder 的第 1 个词;生成第 2 个词时,可以看 Encoder 的第 3 个词。它拥有了随机访问(Random Access) 记忆的能力。
架构对比图 (ASCII Art)
旧架构 (RNN Encoder-Decoder):
[Input 1] -> [State 1]
[Input 2] -> [State 2]
[Input 3] -> [State 3] -----> [唯一上下文向量] -----> [Decoder]
(瓶颈!)
新架构 (Attention):
[Input 1] -> [State 1] -----------------------\
[Input 2] -> [State 2] ------------------------+--> [Context based on Attention] -> [Decoder]
[Input 3] -> [State 3] -----------------------/ (Decoder 可以直接访问所有状态)
总结
这一节告诉我们:
- 翻译很难: 需要完整的上下文理解。
- 老办法不行: 传统的 RNN 把所有信息压缩成一个向量,导致长句子的开头被遗忘(信息瓶颈)。
- 引出主角: 为了解决瓶颈,我们需要一种机制,让模型在解码时能动态地、有选择地关注输入序列的不同部分。这就是注意力机制,也是下一节我们要实现的核心内容。