查看全课大纲(第 44 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
Seq2Seq 与早期注意力机制
小象实战讲义 · 自然语言处理
在掌握了循环神经网络(RNN)与长短期记忆网络(LSTM)之后,我们面临一个更现实的问题:如何将一个序列(如一句中文)映射为另一个序列(如一句英文)?这正是机器翻译等任务的核心。本节将介绍序列到序列(Seq2Seq)模型,它通过编码器-解码器(Encoder-Decoder)架构解决了变长序列的映射问题。我们还将探讨其核心瓶颈——信息压缩,并引入早期注意力机制(Attention Mechanism)作为解决方案。理解这些思想是通往现代Transformer架构的关键一步。
💡 核心导读
- Seq2Seq架构:学习如何通过一个编码器将输入序列压缩为固定长度的上下文向量,再由解码器逐步生成输出序列。
- 信息瓶颈问题:理解为什么将所有信息压缩到一个向量中会限制模型性能,尤其是在处理长序列时。
- 注意力机制原理:掌握注意力如何让解码器在生成每个词时,动态地“关注”输入序列的不同部分,从而缓解信息瓶颈。
- 注意力公式拆解:学习注意力权重的计算过程,包括对齐分数(alignment score)和上下文向量的加权求和。
- 应用扩展:了解Seq2Seq与注意力机制在机器翻译、序列标注、指令解析等任务中的广泛应用。
Seq2Seq:编码器-解码器架构
序列到序列(Seq2Seq)模型的核心思想借鉴了自编码器(Auto-Encoder),但目标不同。自编码器旨在重建输入,而Seq2Seq旨在将一个序列映射到另一个序列,例如将中文句子“ABC”翻译成英文句子“WXYZ”。
基本架构与工作流程
一个典型的Seq2Seq模型由两部分组成:
- 编码器(Encoder):通常是一个RNN(如LSTM),它按顺序读取输入序列的每个词(如
A, B, C),并将序列信息逐步编码到其最终的隐藏状态中。这个最终的隐藏状态(或细胞状态)被视为对整个输入序列的“总结”,我们称之为上下文向量(Context Vector),记作C。 - 解码器(Decoder):也是一个RNN(如LSTM)。它以上下文向量
C作为其初始隐藏状态,并开始逐步生成输出序列。生成过程是自回归的:- 在初始时刻,解码器接收一个特殊的开始符(如
<SOS>)作为输入,结合C,预测输出序列的第一个词W。 - 接着,将预测出的词
W作为下一时刻的输入,结合更新后的隐藏状态,预测下一个词X。 - 重复此过程,直到预测出特殊的结束符(如
<EOS>),生成过程结束。
- 在初始时刻,解码器接收一个特殊的开始符(如
整个过程如下图所示(概念示意):
编码器 (Encoder LSTM) 解码器 (Decoder LSTM)
输入: A -> B -> C -> <EOS> 初始: <SOS>, C
隐藏: h1 -> h2 -> h3 -> h_final(C) 生成: W -> X -> Y -> Z -> <EOS>关键点:输入和输出序列的长度可以不同,模型通过<EOS>标记来动态决定序列的结束。
双向编码器(Bidirectional Encoder)
在基础Seq2Seq中,编码器通常是单向的(如从左到右)。这意味着编码C主要包含了序列末尾的信息,对序列开头的信息可能捕捉不足。为了获得更丰富的上下文表示,实践中常使用双向RNN(Bidirectional RNN) 或双向LSTM作为编码器。
双向RNN包含两个独立的RNN:一个从左向右处理序列,另一个从右向左处理序列。对于序列中的每个位置,我们将两个方向RNN在该位置的隐藏状态拼接起来,作为该位置的最终表示。编码器最终的上下文向量C则通常取自双向RNN在序列末端的隐藏状态。
# 概念性伪代码,展示双向编码的思想
import numpy as np
# 假设词向量维度为2,隐藏状态维度为3
embedding_dim = 2
hidden_dim = 3
vocab_size = 5 # 假设词汇表大小为5
# 随机初始化词嵌入矩阵和RNN权重(简化版,无偏置,无激活函数)
np.random.seed(42)
embedding_matrix = np.random.randn(vocab_size, embedding_dim)
W_f = np.random.randn(hidden_dim, embedding_dim + hidden_dim) # 前向RNN权重
W_b = np.random.randn(hidden_dim, embedding_dim + hidden_dim) # 后向RNN权重
def simple_rnn_step(x, h_prev, W):
"""一个简化的RNN步计算"""
combined = np.concatenate([x, h_prev])
h_next = np.tanh(np.dot(W, combined))
return h_next
# 输入序列的索引 [A, B, C] -> 假设对应索引 [0, 1, 2]
input_indices = [0, 1, 2]
seq_len = len(input_indices)
# 初始化隐藏状态
h_forward = np.zeros(hidden_dim)
h_backward = np.zeros(hidden_dim)
# 存储每个时间步的双向隐藏状态
hidden_states_forward = []
hidden_states_backward = []
# 前向传播
for i in range(seq_len):
x = embedding_matrix[input_indices[i]]
h_forward = simple_rnn_step(x, h_forward, W_f)
hidden_states_forward.append(h_forward.copy())
# 后向传播
for i in reversed(range(seq_len)):
x = embedding_matrix[input_indices[i]]
h_backward = simple_rnn_step(x, h_backward, W_b)
hidden_states_backward.insert(0, h_backward.copy()) # 按原序插入
# 拼接每个时间步的前向后向状态,得到双向表示
bidirectional_states = []
for hf, hb in zip(hidden_states_forward, hidden_states_backward):
bi_state = np.concatenate([hf, hb])
bidirectional_states.append(bi_state)
print(f"时间步双向状态 (前向+后向): {bi_state}")
# 编码器最终上下文向量 C (例如,取最后一个前向和第一个后向状态的组合)
C_forward_final = hidden_states_forward[-1]
C_backward_final = hidden_states_backward[0] # 后向RNN的第一个输出对应原序列的第一个词
C = np.concatenate([C_forward_final, C_backward_final])
print(f"\n编码器最终上下文向量 C (维度 {C.shape}): {C}")时间步双向状态 (前向+后向): [-0.1642898 -0.13836452 0.64062041 -0.52526598 -0.96296658 -0.27264361]
时间步双向状态 (前向+后向): [-0.95533035 -0.9916545 0.4158945 -0.9030081 0.99420628 -0.47513622]
时间步双向状态 (前向+后向): [ 0.82309228 0.36415647 0.68134298 0.23881516 -0.28470543 0.23262416]
编码器最终上下文向量 C (维度 (6,)): [ 0.82309228 0.36415647 0.68134298 -0.52526598 -0.96296658 -0.27264361]优势:双向编码使模型能同时利用过去和未来的上下文信息。例如,在句子“我爱北京天安门”中,要预测“北京”,从左向右看有“我爱”,从右向左看有“天安门”,双向信息能提供更准确的上下文。
注意力机制:解决信息瓶颈
标准的Seq2Seq模型存在一个显著的信息瓶颈(Information Bottleneck):无论输入序列多长,所有信息都必须压缩进一个固定维度的上下文向量C中。这对于长序列或复杂句子来说非常困难,解码器在生成后续词时,可能无法有效访问编码器前端的信息。
注意力机制的核心思想
注意力机制的灵感来源于人类的视觉注意力:当描述一幅图像时,我们会关注图像的不同区域。将其迁移到序列任务上:在解码器生成每一个输出词时,让它动态地“注意”输入序列中与之最相关的部分,而不是仅仅依赖一个固定的C。
具体来说,对于解码器在时刻 i 要生成的词 y_i,我们计算一个注意力权重分布 α_i1, α_i2, ..., α_iT(T是输入序列长度),其中 α_ij 表示生成 y_i 时对输入词 x_j 的关注程度。然后,我们用这些权重对编码器所有时间步的隐藏状态 h_j 进行加权求和,得到一个动态的上下文向量 c_i: c_i = Σ_{j=1}^{T} α_ij * h_j
这个 c_i 代表了在生成当前输出词时,模型认为最重要的输入信息。随后,c_i 会和解码器当前的隐藏状态 s_i 一起,用于预测输出词 y_i。
注意力权重的计算
那么,注意力权重 α_ij 如何得到?它通常通过一个可训练的小型神经网络(对齐模型)计算,步骤如下:
计算对齐分数(Alignment Score):对于解码器当前隐藏状态
s_{i-1}和编码器第j个隐藏状态h_j,计算一个分数e_{ij},衡量两者的相关性。e_{ij} = v^T * tanh(W1 * h_j + W2 * s_{i-1})其中W1,W2,v是可训练的参数矩阵/向量。归一化为权重:使用 softmax 函数将所有
e_{ij}(j从 1 到 T)归一化,得到注意力权重α_ij,确保所有权重之和为 1。α_{ij} = exp(e_{ij}) / Σ_{k=1}^{T} exp(e_{ik})
这个过程可以理解为:模型根据解码器当前的状态(“我想生成什么”),去查询(query)编码器的所有状态(“输入里有什么”),找出最相关的部分,并分配相应的注意力。
# 注意力机制计算过程的简化演示
import numpy as np
np.random.seed(42)
# 假设编码器有3个时间步,每个隐藏状态维度为4
T = 3
encoder_hidden_dim = 4
encoder_states = np.random.randn(T, encoder_hidden_dim)
print("编码器隐藏状态 h_j:")
for j, h in enumerate(encoder_states):
print(f" h_{j}: {h}")
# 假设解码器上一时刻隐藏状态维度为5
decoder_hidden_dim = 5
s_prev = np.random.randn(decoder_hidden_dim)
print(f"\n解码器上一时刻隐藏状态 s_prev: {s_prev}")
# 注意力参数(简化维度)
attn_dim = 6 # 对齐模型内部维度
W1 = np.random.randn(attn_dim, encoder_hidden_dim)
W2 = np.random.randn(attn_dim, decoder_hidden_dim)
v = np.random.randn(attn_dim)
# 1. 计算对齐分数 e_ij
e_scores = []
for h_j in encoder_states:
# e_ij = v^T * tanh(W1*h_j + W2*s_prev)
score = np.dot(v, np.tanh(np.dot(W1, h_j) + np.dot(W2, s_prev)))
e_scores.append(score)
e_scores = np.array(e_scores)
print(f"\n对齐分数 e_ij: {e_scores}")
# 2. 通过softmax计算注意力权重 alpha_ij
def softmax(x):
exp_x = np.exp(x - np.max(x)) # 数值稳定性处理
return exp_x / exp_x.sum()
alpha_weights = softmax(e_scores)
print(f"注意力权重 alpha_ij: {alpha_weights}")
print(f"权重和: {alpha_weights.sum():.6f}")
# 3. 计算动态上下文向量 c_i
c_i = np.zeros(encoder_hidden_dim)
for j, (alpha, h_j) in enumerate(zip(alpha_weights, encoder_states)):
c_i += alpha * h_j
print(f"\n动态上下文向量 c_i: {c_i}")编码器隐藏状态 h_j:
h_0: [ 0.49671415 -0.1382643 0.64768854 1.52302986]
h_1: [-0.23415337 -0.23413696 1.57921282 0.76743473]
h_2: [-0.46947439 0.54256004 -0.46341769 -0.46572975]
解码器上一时刻隐藏状态 s_prev: [ 0.24196227 -1.91328024 -1.72491783 -0.56228753 -1.01283112]
对齐分数 e_ij: [-1.15431427 -2.03192703 2.94244188]
注意力权重 alpha_ij: [0.01624414 0.0067539 0.97700196]
权重和: 1.000000
动态上下文向量 c_i: [-0.45219015 0.5262549 -0.43157301 -0.42509539]结果解读:计算显示,在给定解码器状态 s_prev 的情况下,对齐分数里 e_3 = 2.9424 明显最大,softmax 之后注意力权重为 [0.0162, 0.0068, 0.9770]——几乎全部落在编码器的第三个隐藏状态 h_2(约 0.977),h_0 只拿到 0.0162。最终的动态上下文向量 c_i 是三个 h_j 的加权和,因此在数值上极度接近 h_2 本身:c_i ≈ 0.0162·h_0 + 0.0068·h_1 + 0.9770·h_2。
这里值得留意的是 softmax 的放大效应:本例最大分 2.9424 与最低分 -2.0319 相差约 4.97,取指数之后强弱被拉开成 60 倍,权重几乎变成“一个赢家全拿”。需要注意:分布有多尖锐取决于相对分差、分数尺度与温度;分差较小时权重仍可能接近均匀(二项 softmax 中分差 1 时最大权重约 0.73,并非赢家全拿),不能笼统说“差一两分就很尖锐”。
从向量视角理解注意力
从词嵌入(Word Embedding)的角度看,一句话可以表示为一组词向量的序列。如果我们简单地对这些词向量求平均,得到的是一个粗略的句子向量。注意力机制所做的,实际上是计算一个更合理的“加权平均”。权重 α 表明了每个词对于当前解码任务的重要性。因此,动态上下文向量 c_i 可以理解为输入序列在当前时刻的“数学期望”向量,它更精准地捕捉了与当前生成词相关的语义。
Seq2Seq与注意力的应用扩展
Seq2Seq加注意力的架构不仅用于机器翻译,还可广泛应用于任何输入输出均为序列的任务:
- 序列标注:如词性标注(POS Tagging)、命名实体识别(NER)。输入是词序列,输出是标签序列。
- 分词:输入是字符序列(如“我爱北京天安门”),输出是表示分词边界的标签序列(如“1 1 0 1 0 0 1”,其中 1 表示当前位置是一个词的结束,0 表示该字仍属于当前词内部;对应切分结果为“我/爱/北京/天安门”)。
- 文本摘要:输入是长文档序列,输出是摘要序列。
- 语音识别:输入是音频帧序列,输出是文字序列。
- 指令解析/语义解析:这是一个有趣的方向。输入是自然语言指令(如“今天很热,把空调调低两度”),输出是机器可执行的逻辑命令序列(如“
[DECREMENT, DECREMENT]”)。这需要大量的(指令,命令)配对数据进行监督学习。
重要提醒:Seq2Seq模型是典型的监督学习(Supervised Learning) 模型,它依赖于大量高质量的输入-输出序列对。其优势在于能够进行端到端(End-to-End)训练,将复杂的映射关系封装在一个“黑箱”神经网络中,避免了传统方法(如统计机器翻译中复杂的特征工程和多个子模型)的设计复杂性。但其性能高度依赖于训练数据的规模和质量。
📝 动手练一练
- 概念辨析:标准Seq2Seq模型和加入了注意力机制的Seq2Seq模型,在解码器预测每个输出词时,所依赖的上下文信息有什么本质区别?
👉 点击查看参考答案
在标准Seq2Seq模型中,解码器在预测每一个输出词时,所依赖的来自编码器的上下文信息是固定不变的,即那个单一的、压缩了整个输入序列的上下文向量 C。无论生成第1个词还是第10个词,它看到的“输入摘要”都是同一个 C。
而在加入了注意力机制的Seq2Seq模型中,解码器在预测第 i 个输出词时,会计算一个动态的上下文向量 c_i。c_i 是编码器所有隐藏状态的加权和,权重 α_ij 表示当前生成步骤对输入第 j 个词的关注程度。因此,c_i 是随着生成步骤 i 变化而变化的,它让解码器在生成不同词时,能够有选择地聚焦于输入序列的不同部分。
- 手算练习:假设一个极简的编码器只有两个时间步,隐藏状态分别为
h1 = [1, 0],h2 = [0, 1](均为 2 维)。解码器上一时刻状态s_prev = [0.5, 0.5](同为 2 维)。注意力对齐模型简化为:e_i = dot(s_prev, h_i)(即点积作为分数)。请计算解码器在当前时刻应对h1和h2分配的注意力权重α1和α2,以及由此得到的动态上下文向量c。
👉 点击查看参考答案
计算过程:
- 计算对齐分数:
e1 = dot([0.5, 0.5], [1, 0]) = 0.5*1 + 0.5*0 = 0.5e2 = dot([0.5, 0.5], [0, 1]) = 0.5*0 + 0.5*1 = 0.5 - 通过softmax计算权重:
exp(e1) = exp(0.5) ≈ 1.6487exp(e2) = exp(0.5) ≈ 1.6487sum = 1.6487 + 1.6487 = 3.2974α1 = 1.6487 / 3.2974 ≈ 0.5α2 = 1.6487 / 3.2974 ≈ 0.5 - 计算动态上下文向量
c:c = α1 * h1 + α2 * h2 = 0.5*[1,0] + 0.5*[0,1] = [0.5, 0.5]
答案:注意力权重 α1 ≈ 0.5, α2 ≈ 0.5。动态上下文向量 c = [0.5, 0.5]。在这个对称的例子中,模型对两个输入状态给予了同等关注。
本章小结
本节我们深入探讨了序列到序列学习的经典解决方案。Seq2Seq模型通过编码器-解码器架构,优雅地处理了变长序列的映射问题。然而,其将长序列信息压缩至单一固定向量的设计带来了信息瓶颈。注意力机制的引入是解决此问题的关键创新,它允许解码器在生成时动态地、有选择地关注输入序列的不同部分,显著提升了模型对长序列和复杂依赖关系的处理能力。
理解编码器-解码器框架和注意力机制的基本原理,是学习现代Transformer架构的基石。在接下来的课程中,我们将看到注意力机制如何演变为自注意力(Self-Attention),并成为Transformer模型的核心组件。
📋 行动清单
- 在脑海中绘制标准Seq2Seq和带注意力Seq2Seq的数据流图,明确
C与c_i的区别。 - 推导一遍注意力权重的计算公式:从
s_{i-1}和h_j到α_{ij}的完整过程。 - 思考一个你熟悉的场景(如智能客服、文档摘要),尝试用Seq2Seq+注意力的思路描述其工作原理。
—— 小象教研组