Seq2Seq模型与注意力机制基础
一句话概述
Seq2Seq(Sequence-to-Sequence)模型由Sutskever等和Cho等于2014年分别独立提出,通过编码器-解码器(Encoder-Decoder)架构实现不定长序列到不定长序列的转换(如中文→英文翻译)。注意力机制(Attention Mechanism)由Bahdanau等人在2015年引入,解决了Seq2Seq模型中编码器将所有信息压缩为单一固定长度向量的瓶颈——它让解码器在每个时间步都能"回顾"编码器的所有隐藏状态,并根据相关性动态加权。注意力机制不仅是Seq2Seq的性能突破,更是后来Transformer和大型语言模型的核心技术基石。
💡 核心要点:①Encoder-Decoder架构:编码器将源序列编码为上下文向量,解码器基于此生成目标序列 ②信息瓶颈:固定长度的上下文向量无法充分表示长序列的所有信息 ③注意力机制让解码器在每个时间步动态关注编码器的不同位置 ④注意力本质是"软寻址"——根据查询(Query)和键(Key)的相似度,对值(Value)加权求和
教学与演示
一、Encoder-Decoder框架
是什么(定义):Seq2Seq模型包含两个RNN(或其变体):编码器(Encoder)读取源序列(如中文句子),将其压缩为一个固定长度的上下文向量(通常是编码器最后一个时间步的隐藏状态)。解码器(Decoder)基于这个上下文向量,逐词生成目标序列(如英文翻译)。解码器是自回归的——t时刻的输出y_t会作为t+1时刻的输入。
大白话 Encoder-Decoder就像"翻译官的工作流程"——先把中文整句读完,在脑子里形成一个"意思摘要"(上下文向量),然后基于这个摘要,逐词说出英文翻译。问题是:如果中文句子很长,一个"摘要"装不下所有信息,翻译到后面就"忘词"了。
怎么做(实现):
import numpy as np
# ========================================
# Seq2Seq 模型 —— Encoder-Decoder 架构
# 编码器将源序列压缩为上下文向量
# 解码器基于上下文向量生成目标序列
# ========================================
class Seq2Seq:
"""
简化版 Seq2Seq(无注意力)
编码器: 处理源序列,输出最后一个隐藏状态作为上下文向量
解码器: 基于上下文向量自回归生成目标序列
"""
def __init__(self, src_vocab_size, tgt_vocab_size, hidden_size=16):
"""
参数:
src_vocab_size: 源语言词表大小
tgt_vocab_size: 目标语言词表大小
hidden_size: 隐藏状态维度
"""
# 共享隐藏维度
self.hidden_size = hidden_size
# ---- 编码器 ----
# 将源语言词映射到隐藏空间
self.enc_W_xh = np.random.randn(hidden_size, src_vocab_size) * 0.01
self.enc_W_hh = np.random.randn(hidden_size, hidden_size) * 0.01
self.enc_b = np.zeros(hidden_size)
# ---- 解码器 ----
# 将目标语言词映射到隐藏空间
self.dec_W_xh = np.random.randn(hidden_size, tgt_vocab_size) * 0.01
self.dec_W_hh = np.random.randn(hidden_size, hidden_size) * 0.01
self.dec_b = np.zeros(hidden_size)
# 输出层:隐藏状态 → 词表概率
self.dec_W_out = np.random.randn(tgt_vocab_size, hidden_size) * 0.01
self.dec_b_out = np.zeros(tgt_vocab_size)
def _rnn_step(self, x_t, h_prev, W_xh, W_hh, b):
"""单步RNN: h_t = tanh(W_xh·x_t + W_hh·h_{t-1} + b)"""
return np.tanh(np.dot(W_xh, x_t) + np.dot(W_hh, h_prev) + b)
def encode(self, src_sequence):
"""
编码器:处理源序列,返回最后一个隐藏状态作为上下文向量
参数:
src_sequence: 源序列的 one-hot 编码,shape (src_len, src_vocab_size)
返回:
context: 上下文向量,shape (hidden_size,)
enc_states: 编码器所有时间步的隐藏状态
"""
h = np.zeros(self.hidden_size)
enc_states = []
for t in range(len(src_sequence)):
x_t = src_sequence[t]
h = self._rnn_step(x_t, h, self.enc_W_xh, self.enc_W_hh, self.enc_b)
enc_states.append(h)
# 上下文向量 = 编码器最后的隐藏状态
context = h
return context, np.array(enc_states)
def decode_step(self, y_prev, h_prev_dec):
"""
解码器单步:生成下一个词的概率分布
参数:
y_prev: 上一时间步的输出(one-hot或embedding)
h_prev_dec: 解码器上一时间步的隐藏状态
返回:
probs: 下一个词的概率分布
h_t: 当前时间步的隐藏状态
"""
# RNN 前向
h_t = self._rnn_step(y_prev, h_prev_dec,
self.dec_W_xh, self.dec_W_hh, self.dec_b)
# 输出层
logits = np.dot(self.dec_W_out, h_t) + self.dec_b_out
# Softmax
logits = logits - np.max(logits)
probs = np.exp(logits) / np.sum(np.exp(logits))
return probs, h_t
def decode(self, context, max_len=10):
"""
解码器:自回归生成目标序列(使用贪心解码)
参数:
context: 编码器输出的上下文向量
max_len: 最大生成长度
返回:
generated: 生成的词索引序列
"""
h_dec = context # 解码器初始隐藏状态 = 编码器上下文向量
# 第一个输入是 <SOS> token(这里简化为全零向量)
y_prev = np.zeros(self.dec_W_xh.shape[1])
generated = []
for _ in range(max_len):
probs, h_dec = self.decode_step(y_prev, h_dec)
# 贪心解码:取概率最大的词
next_word = np.argmax(probs)
generated.append(next_word)
# 当前输出 = 下一个时间步的输入(自回归)
y_prev = np.zeros_like(y_prev)
y_prev[next_word] = 1.0 # one-hot
return generated
# --- 演示 ---
print("Seq2Seq 基本架构演示:")
print("=" * 60)
np.random.seed(42)
# 简化:源词表5个词,目标词表5个词
model = Seq2Seq(src_vocab_size=5, tgt_vocab_size=5, hidden_size=16)
# 源序列: 3个词的 one-hot 编码
src_seq = np.eye(5)[[0, 2, 3]] # 词0, 词2, 词3
print(f"源序列长度: {len(src_seq)} 个词, 词表大小: 5")
# 编码
context, enc_states = model.encode(src_seq)
print(f"编码完成:上下文向量维度={len(context)}, 编码器状态数={len(enc_states)}")
# 解码生成
generated = model.decode(context, max_len=5)
print(f"生成的目标序列: {generated}")
print(f" → 编码器将3个词压缩为1个{len(context)}维向量")
print(f" → 解码器基于这个向量生成了{len(generated)}个词")
# --- 信息瓶颈分析 ---
print(f"\n信息瓶颈分析:")
print(f" 源序列信息量: {len(src_seq)} 个词")
print(f" 上下文向量容量: {model.hidden_size} 个实数")
print(f" → 无论源序列多长,都被压缩为固定{model.hidden_size}维向量")
print(f" → 长序列信息可能丢失 ← 注意力机制要解决的问题!")
大白话 Seq2Seq的致命问题:不管输入是5个词还是50个词,都要塞进同一个"摘要"里。摘要的容量是固定的(比如256维向量),短句子能装下,长句子就溢出——翻译到后面,前面的信息已经被"挤出去"了。
二、注意力机制:动态加权访问编码器信息
是什么(定义):注意力机制(Bahdanau Attention/Additive Attention)让解码器在每个生成时间步t,不再仅依赖固定的上下文向量c,而是动态地"关注"编码器的所有隐藏状态。具体来说:计算解码器当前状态与每个编码器状态的"相关性分数"(注意力权重),然后对所有编码器状态加权求和,得到"上下文向量"c_t。c_t随着解码进度动态变化,使模型能够在对齐的位置上"聚焦"。
大白话 注意力机制就是"翻译时随时回头看原文"——不再是翻译前快速扫一遍(固定上下文),而是每翻译一个词,都重新回去看原文的每个词,判断"现在这个翻译步骤和原文的哪个词最相关",然后把相关位置的信息加权提取出来。
怎么做(实现):
import numpy as np
# ========================================
# 注意力机制 —— 解码器动态关注编码器
# Bahdanau Attention (Additive Attention)
# ========================================
def bahdanau_attention(decoder_hidden, encoder_states):
"""
Bahdanau 注意力机制(加性注意力)
参数:
decoder_hidden: 解码器当前隐藏状态,shape (hidden_size,)
encoder_states: 编码器所有时间步的隐藏状态,shape (src_len, hidden_size)
返回:
context_vector: 加权求和后的上下文向量,shape (hidden_size,)
attention_weights: 每个编码器时间步的注意力权重,shape (src_len,)
计算步骤:
1. 计算分数: score_i = v^T · tanh(W_a·[h_dec; h_enc_i])
2. 归一化: α_i = softmax(score_i)
3. 加权求和: c = Σ α_i · h_enc_i
"""
src_len = len(encoder_states)
hidden_size = len(decoder_hidden)
# 简化:用随机参数模拟注意力权重矩阵
# 实际中W_a是学习到的参数矩阵
np.random.seed(42)
W_a = np.random.randn(hidden_size, hidden_size * 2) * 0.1 # 拼接后维度×2
v = np.random.randn(hidden_size) * 0.1 # 分数向量
# 计算每个编码器位置的注意力分数
scores = np.zeros(src_len)
for i in range(src_len):
# 拼接解码器状态和编码器状态
concat = np.concatenate([decoder_hidden, encoder_states[i]])
# 分数 score_i = v^T · tanh(W_a · concat)
scores[i] = np.dot(v, np.tanh(np.dot(W_a, concat)))
# Softmax 得到注意力权重
scores = scores - np.max(scores) # 数值稳定
attention_weights = np.exp(scores) / np.sum(np.exp(scores))
# 加权求和得到上下文向量
context_vector = np.zeros(hidden_size)
for i in range(src_len):
context_vector += attention_weights[i] * encoder_states[i]
return context_vector, attention_weights
# --- 演示 ---
np.random.seed(42)
print("注意力机制演示(Bahdanau Attention):")
print("=" * 60)
# 模拟编码器状态(5个词的隐藏状态)
src_len = 5
hidden_size = 8
encoder_states = np.random.randn(src_len, hidden_size)
# 模拟解码器在不同时间步的隐藏状态
print(f"源序列: 5个词 (如: '我 / 爱 / 吃 / 苹果')")
print(f"编码器隐藏状态: {src_len}×{hidden_size}\n")
decoder_steps = [
("生成 'I' 时", np.random.randn(hidden_size)),
("生成 'love' 时", np.random.randn(hidden_size)),
("生成 'eating' 时",np.random.randn(hidden_size)),
("生成 'apples' 时",np.random.randn(hidden_size)),
]
for desc, dec_h in decoder_steps:
ctx, attn = bahdanau_attention(dec_h, encoder_states)
print(f" {desc}:")
print(f" 注意力权重: {attn}")
max_idx = np.argmax(attn)
src_words = ['我', '爱', '吃', '苹果', '<EOS>']
print(f" 最关注: '{src_words[max_idx]}' (权重={attn[max_idx]:.3f})")
print(f" → 翻译 '{src_words[max_idx]}' 时,注意力集中在对应的原文词上")
print(f"\n 关键: 每个解码步的注意力权重不同,实现动态对齐!")
print(f" 例如: 生成 'I' 时最关注 '我', 生成 'apples' 时最关注 '苹果'")
大白话 注意力 = "相关性打分 + 加权平均"——对于解码器当前想生成的词,去问编码器:"我现在的状态和原文的每个位置有多相关?"得到一个0到1的分数(Softmax归一化后),然后把原文所有位置的信息按分数加权平均。最相关的位置贡献最多。
什么用(AI关联):注意力机制是深度学习历史上最重要的创新之一。它直接催生了Transformer(2017),而Transformer又催生了BERT、GPT等一系列大语言模型。可以说,从Seq2Seq+Attention到Transformer+Self-Attention,注意力机制开启了现代NLP和大模型时代。
哪些坑(缺点):①计算复杂度O(T_src × T_tgt)——源序列和目标序列长度乘积,长序列计算开销大。②注意力是"软"寻址(所有位置都看),可能被不相关信息分散注意力。③加性注意力需要额外的可学习参数(W_a, v),而后来Transformer的点积注意力更简洁高效。
三、注意力机制的两种变体
是什么(定义):除了Bahdanau的加性注意力外,Luong等人提出了乘性注意力(Dot-product Attention)和General注意力。乘性注意力直接计算解码器状态和编码器状态的点积作为分数:score = h_dec^T · h_enc,计算更简单无需额外参数。General注意力在中间加一个可学习的矩阵:score = h_dec^T · W · h_enc。这两种变体是Transformer中缩放点积注意力(Scaled Dot-Product Attention)的前身。
大白话 加性注意力:把两个向量拼起来,通过一个小神经网络算出相关性分数(复杂但准确)。乘性注意力:直接算两个向量的点积(相似度)作为分数(简单高效)。后来Transformer选了乘性注意力+缩放,因为它能更好地利用GPU的矩阵乘法加速。
| 类型 | 分数计算 | 参数 | 计算复杂度 |
|---|---|---|---|
| 加性 (Bahdanau) | v^T·tanh(W·[h_dec;h_enc]) | W, v | O(d²) |
| 乘性 (Luong dot) | h_dec^T·h_enc | 无 | O(d) |
| General (Luong) | h_dec^T·W·h_enc | W | O(d²) |
| 缩放点积 (Transformer) | (Q·K^T)/√d_k | 无 | O(d) |
概念关系图谱
| 概念 | 核心含义 | 与AI的关系 | 关联概念 |
|---|---|---|---|
| Encoder-Decoder | 编码器压缩源序列,解码器生成目标序列 | 机器翻译等Seq2Seq任务的基础 | 自回归、Teacher Forcing |
| 上下文向量 | 编码器最后隐藏状态,承载源序列信息 | 信息瓶颈是注意力机制的动机 | 固定长度表示、信息压缩 |
| 注意力权重 | 每个编码器位置对当前解码的重要性 | 动态对齐,解决信息瓶颈 | Softmax、相关性分数 |
| 加性注意力 | v^T·tanh(W·[h_dec;h_enc]) | Bahdanau 2015提出,引入注意力到NMT | 对齐、神经机器翻译 |
| 乘性注意力 | 直接点积 h_dec^T·h_enc | Transformer的前身,计算更高效 | 缩放点积、自注意力 |
重点答疑
Q1: 注意力机制为什么能解决信息瓶颈?
信息瓶颈指的是编码器将所有源序列信息压缩为一个固定长度的向量c。注意力机制不再使用单一向量c,而是让解码器在每个时间步直接从编码器的所有隐藏状态中提取相关信息——通过注意力权重实现"按需读取"。编码器的每个时间步的隐藏状态都被完整保留,解码器可以在任意时刻回到任意位置获取信息。这使得模型处理长序列的能力大幅提升。
Q2: 为什么需要Teacher Forcing?
在训练Seq2Seq模型时,如果解码器使用自己上一时刻的预测作为输入(自回归),一个早期的错误会导致连锁反应(Exposure Bias)。Teacher Forcing解决方案:训练时不用模型自己的预测,而是用真实的上一时刻标签作为输入。公式:训练时输入y_{t-1}^true,推理时输入y_{t-1}^pred。这加速了收敛但导致训练和推理不匹配。Scheduled Sampling(以一定概率使用模型自己的预测)是折中方案。
Q3: 注意力机制和Transformer的自注意力有什么区别?
注意力机制(Encoder-Decoder Attention)是跨序列的——解码器关注编码器(Query来自解码器,Key/Value来自编码器)。自注意力(Self-Attention)是同一序列内部的——Query、Key、Value都来自同一序列,每个位置关注序列中的所有位置(包括自己)。Transformer同时使用了这两种注意力:编码器用自注意力,解码器用自注意力(带因果掩码)+ 交叉注意力(关注编码器)。
章节单词汇总
| 英文 | 音标 | 术语/释义 |
|---|---|---|
| Seq2Seq | /siːkwəns tuː siːkwəns/ | 序列到序列模型 |
| Encoder | /ɪnˈkoʊdər/ | 编码器,将输入编码为表示 |
| Decoder | /diːˈkoʊdər/ | 解码器,从表示生成输出 |
| Attention Mechanism | /əˈtenʃən ˈmekənɪzəm/ | 注意力机制 |
| Alignment | /əˈlaɪnmənt/ | 对齐,源语言和目标语言词的对应关系 |
| Teacher Forcing | /ˈtiːtʃər ˈfɔːrsɪŋ/ | 教师强制,用真实标签作训练输入 |
| Autoregressive | /ˌɔːtoʊrɪˈɡresɪv/ | 自回归,用已生成的输出作后续输入 |
| Context Vector | /ˈkɒntekst ˈvektər/ | 上下文向量 |
| Additive Attention | /ˈædətɪv əˈtenʃən/ | 加性注意力(Bahdanau) |
| Dot-product Attention | /dɒt ˈprɒdʌkt əˈtenʃən/ | 点积注意力(Luong) |
面试练习
Q1 [单选] Seq2Seq模型中,编码器输出的上下文向量通常是什么?
- A. 编码器最后一个时间步的隐藏状态
- B. 编码器所有隐藏状态的平均值
- C. 编码器第一个时间步的输入
- D. 目标序列的第一个词
解答:基础的Seq2Seq使用编码器最后一个时间步的隐藏状态作为上下文向量(代表整句的"摘要")。
Q2 [单选] 注意力机制解决的核心问题是什么?
- A. 训练速度太慢
- B. 固定长度上下文向量的信息瓶颈
- C. 梯度消失
- D. 词汇量太小
解答:注意力机制让解码器在每一步都能动态关注编码器的所有位置,打破了固定长度上下文向量的信息瓶颈。
Q3 [单选] Bahdanau注意力(加性注意力)的分数计算公式是?
- A. score = v^T · tanh(W_a · [h_dec; h_enc])
- B. score = h_dec^T · h_enc
- C. score = softmax(h_dec · h_enc)
- D. score = ReLU(h_dec + h_enc)
解答:Bahdanau注意力使用一个小型前馈网络(v^T·tanh(W_a·[h_dec;h_enc]))计算分数,因此称为"加性注意力"。
Q4 [多选] 以下关于Seq2Seq模型的描述,哪些正确?
- A. 编码器处理源序列,解码器生成目标序列
- B. 解码器是自回归的
- C. 编码器和解码器必须使用相同的RNN类型
- D. Teacher Forcing加速训练收敛
解答:编码器和解码器可以使用不同类型的RNN(如编码器用双向LSTM,解码器用单向GRU)。A、B、D正确。
Q5 [单选] 在注意力机制中,Softmax的作用是什么?
- A. 增加分数的差异
- B. 将分数归一化为概率分布(总和为1)
- C. 减少计算量
- D. 引入非线性
解答:Softmax将注意力分数转化为(0,1)之间且总和为1的权重分布,确保合理的加权平均。
Q6 [单选] Teacher Forcing在训练时使用什么作为解码器的输入?
- A. 真实的上一时刻标签
- B. 模型自己预测的上一时刻输出
- C. 编码器的隐藏状态
- D. 随机噪声
解答:Teacher Forcing在训练时使用真实的y_{t-1}而非模型预测的ŷ_{t-1}作为解码器输入,加速收敛。
Q7 [单选] 注意力机制中,上下文向量c_t是如何计算的?
- A. 编码器隐藏状态的加权求和(权重=注意力分数)
- B. 编码器隐藏状态的平均值
- C. 编码器最后一个隐藏状态
- D. 编码器隐藏状态的最大值
解答:c_t = Σ α_ti · h_i^enc,是对所有编码器状态的加权求和,权重α_ti由注意力分数Softmax得到。
Q8 [多选] 乘性注意力(Dot-product Attention)相比加性注意力的优势?
- A. 无需额外可学习参数
- B. 计算更高效(矩阵乘法)
- C. 总是产生更准确的注意力权重
- D. 在Transformer中被采用(加入缩放因子)
解答:乘性注意力计算更快、无额外参数,但准确度不一定更高(取决于任务)。Transformer采用缩放点积注意力。
Q9 [单选] 在Seq2Seq+Attention中,解码器每个时间步产生的注意力权重有几个?
- A. 等于源序列长度(每个编码器位置一个权重)
- B. 1个
- C. 等于隐藏状态维度
- D. 等于目标序列长度
解答:每个解码步产生一个长度为源序列长度的注意力权重向量,表示当前解码步对每个源词位置的关注程度。
Q10 [单选] 注意力机制引入前,Seq2Seq面临的主要问题是什么?
- A. 长序列中信息丢失(信息瓶颈)
- B. 梯度爆炸
- C. 词汇量不足
- D. 训练数据不够
解答:固定长度的上下文向量无法充分表示长序列的全部信息,导致翻译长句时质量明显下降。