
深度学习架构Seq2Seq-添加并理解注意力机制(二)
使用 **“我喜欢吃鱼”** 翻译为 **“I like eating fish”** 的简单例子,逐步推演 注意力机制 的实现步骤
第一章:人工智能之不同数据类型及其特点梳理
第二章:自然语言处理(NLP):文本向量化从文字到数字的原理
第三章:循环神经网络RNN:理解 RNN的工作机制与应用场景(附代码)
第四章:循环神经网络RNN、LSTM以及GRU 对比(附代码)
第五章:理解Seq2Seq的工作机制与应用场景中英互译(附代码)
第六章:深度学习架构Seq2Seq-添加并理解注意力机制(一)
第七章:深度学习架构Seq2Seq-添加并理解注意力机制(二)
第八章:深度学习模型Transformer初步认识整体架构
第九章:深度学习模型Transformer核心组件—自注意力机制
第十章:理解梯度下降、链式法则、梯度消失/爆炸
第十一章:Transformer核心组件—残差连接与层归一化
承接上一篇《深度学习架构Seq2Seq-添加并理解注意力机制(一)》介绍普通 Seq2Seq 的局限性、输入序列太长,为什么会导致信息丢失、注意力机制的核心思想等内容之后,这一篇使用一个中文翻译英文的简单例子,逐步推演 注意力机制 的实现步骤。
五、推演注意力机制
以下是使用 “我喜欢吃鱼” 翻译为 “I like eating fish” 的简单例子,逐步推演 注意力机制 的实现步骤。假设我们已经训练好模型,此处仅展示 单步推理过程,以生成英文单词 “eating” 为例,展示注意力机制的计算步骤。
步骤1:编码器处理输入
输入序列(中文)
["我", "喜欢", "吃", "鱼"]
假设编码器,输出每个词的隐藏状态(为简化计算,隐藏状态维度为 2):
中文词 | 编码器隐藏状态 h i h_i hi |
---|---|
我 | h 1 = [ 0.9 , 0.1 ] h_1 = [0.9, 0.1] h1=[0.9,0.1] |
喜欢 | h 2 = [ 0.7 , 0.3 ] h_2 = [0.7, 0.3] h2=[0.7,0.3] |
吃 | h 3 = [ 0.2 , 0.8 ] h_3 = [0.2, 0.8] h3=[0.2,0.8] |
鱼 | h 4 = [ 0.1 , 0.9 ] h_4 = [0.1, 0.9] h4=[0.1,0.9] |
步骤2:解码器状态与注意力分数计算
假设解码器当前隐藏状态(生成“eating”前的状态)为:
s
t
−
1
=
[
0.6
,
0.4
]
s_{t-1} = [0.6, 0.4]
st−1=[0.6,0.4]
计算注意力分数(点积)
e
t
1
=
s
t
−
1
⋅
h
1
=
0.6
×
0.9
+
0.4
×
0.1
=
0.54
+
0.04
=
0.58
e
t
2
=
s
t
−
1
⋅
h
2
=
0.6
×
0.7
+
0.4
×
0.3
=
0.42
+
0.12
=
0.54
e
t
3
=
s
t
−
1
⋅
h
3
=
0.6
×
0.2
+
0.4
×
0.8
=
0.12
+
0.32
=
0.44
e
t
4
=
s
t
−
1
⋅
h
4
=
0.6
×
0.1
+
0.4
×
0.9
=
0.06
+
0.36
=
0.42
\begin{aligned} e_{t1} &= s_{t-1} \cdot h_1 = 0.6 \times 0.9 + 0.4 \times 0.1 = 0.54 + 0.04 = 0.58 \\ e_{t2} &= s_{t-1} \cdot h_2 = 0.6 \times 0.7 + 0.4 \times 0.3 = 0.42 + 0.12 = 0.54 \\ e_{t3} &= s_{t-1} \cdot h_3 = 0.6 \times 0.2 + 0.4 \times 0.8 = 0.12 + 0.32 = 0.44 \\ e_{t4} &= s_{t-1} \cdot h_4 = 0.6 \times 0.1 + 0.4 \times 0.9 = 0.06 + 0.36 = 0.42 \\ \end{aligned}
et1et2et3et4=st−1⋅h1=0.6×0.9+0.4×0.1=0.54+0.04=0.58=st−1⋅h2=0.6×0.7+0.4×0.3=0.42+0.12=0.54=st−1⋅h3=0.6×0.2+0.4×0.8=0.12+0.32=0.44=st−1⋅h4=0.6×0.1+0.4×0.9=0.06+0.36=0.42
归一化为注意力权重(Softmax)
α
t
i
=
exp
(
e
t
i
)
∑
j
=
1
4
exp
(
e
t
j
)
\alpha_{ti} = \frac{\exp(e_{ti})}{\sum_{j=1}^4 \exp(e_{tj})}
αti=∑j=14exp(etj)exp(eti)
计算具体值:
exp
(
0.58
)
≈
1.79
exp
(
0.54
)
≈
1.72
exp
(
0.44
)
≈
1.55
exp
(
0.42
)
≈
1.52
∑
=
1.79
+
1.72
+
1.55
+
1.52
=
6.58
\begin{aligned} \exp(0.58) &\approx 1.79 \\ \exp(0.54) &\approx 1.72 \\ \exp(0.44) &\approx 1.55 \\ \exp(0.42) &\approx 1.52 \\ \sum &= 1.79 + 1.72 + 1.55 + 1.52 = 6.58 \\ \end{aligned}
exp(0.58)exp(0.54)exp(0.44)exp(0.42)∑≈1.79≈1.72≈1.55≈1.52=1.79+1.72+1.55+1.52=6.58
α t 1 = 1.79 / 6.58 ≈ 0.272 α t 2 = 1.72 / 6.58 ≈ 0.261 α t 3 = 1.55 / 6.58 ≈ 0.236 α t 4 = 1.52 / 6.58 ≈ 0.231 \begin{aligned} \alpha_{t1} &= 1.79 / 6.58 \approx 0.272 \\ \alpha_{t2} &= 1.72 / 6.58 \approx 0.261 \\ \alpha_{t3} &= 1.55 / 6.58 \approx 0.236 \\ \alpha_{t4} &= 1.52 / 6.58 \approx 0.231 \\ \end{aligned} αt1αt2αt3αt4=1.79/6.58≈0.272=1.72/6.58≈0.261=1.55/6.58≈0.236=1.52/6.58≈0.231
注意力权重分布:
α
t
=
[
0.272
,
0.261
,
0.236
,
0.231
]
\alpha_{t} = [0.272, 0.261, 0.236, 0.231]
αt=[0.272,0.261,0.236,0.231]
步骤3:生成上下文向量
c
t
=
0.272
×
[
0.9
,
0.1
]
+
0.261
×
[
0.7
,
0.3
]
+
0.236
×
[
0.2
,
0.8
]
+
0.231
×
[
0.1
,
0.9
]
=
[
0.272
×
0.9
,
0.272
×
0.1
]
+
[
0.261
×
0.7
,
0.261
×
0.3
]
+
[
0.236
×
0.2
,
0.236
×
0.8
]
+
[
0.231
×
0.1
,
0.231
×
0.9
]
=
[
0.2448
,
0.0272
]
+
[
0.1827
,
0.0783
]
+
[
0.0472
,
0.1888
]
+
[
0.0231
,
0.2079
]
=
[
0.2448
+
0.1827
+
0.0472
+
0.0231
,
0.0272
+
0.0783
+
0.1888
+
0.2079
]
=
[
0.4978
,
0.5022
]
(
≈
[
0.50
,
0.50
]
)
\begin{aligned} \mathbf{c}_t &= 0.272 \times [0.9, 0.1] + 0.261 \times [0.7, 0.3] + 0.236 \times [0.2, 0.8] + 0.231 \times [0.1, 0.9] \\ &= [0.272 \times 0.9, 0.272 \times 0.1] \\ &\quad + [0.261 \times 0.7, 0.261 \times 0.3] \\ &\quad + [0.236 \times 0.2, 0.236 \times 0.8] \\ &\quad + [0.231 \times 0.1, 0.231 \times 0.9] \\ &= [0.2448, 0.0272] + [0.1827, 0.0783] + [0.0472, 0.1888] + [0.0231, 0.2079] \\ &= [0.2448 + 0.1827 + 0.0472 + 0.0231, \quad 0.0272 + 0.0783 + 0.1888 + 0.2079] \\ &= [0.4978, \quad 0.5022] \quad (\approx [0.50, 0.50]) \end{aligned}
ct=0.272×[0.9,0.1]+0.261×[0.7,0.3]+0.236×[0.2,0.8]+0.231×[0.1,0.9]=[0.272×0.9,0.272×0.1]+[0.261×0.7,0.261×0.3]+[0.236×0.2,0.236×0.8]+[0.231×0.1,0.231×0.9]=[0.2448,0.0272]+[0.1827,0.0783]+[0.0472,0.1888]+[0.0231,0.2079]=[0.2448+0.1827+0.0472+0.0231,0.0272+0.0783+0.1888+0.2079]=[0.4978,0.5022](≈[0.50,0.50])
步骤4:解码器生成输出
拼接解码器状态与上下文向量
假设参数
W
c
W_c
Wc 和
b
c
b_c
bc 将拼接后的向量
[
s
t
−
1
;
c
t
]
=
[
0.6
,
0.4
,
0.50
,
0.50
]
[s_{t-1}; \mathbf{c}_t] = [0.6, 0.4, 0.50, 0.50]
[st−1;ct]=[0.6,0.4,0.50,0.50] 映射到新的隐藏状态:
s
~
t
=
tanh
(
[
0.6
,
0.4
,
0.50
,
0.50
]
⋅
W
c
+
b
c
)
≈
[
0.65
,
0.35
]
\tilde{s}_t = \tanh\left( [0.6, 0.4, 0.50, 0.50] \cdot W_c + b_c \right) \approx [0.65, 0.35]
s~t=tanh([0.6,0.4,0.50,0.50]⋅Wc+bc)≈[0.65,0.35]
预测输出概率分布
假设词汇表为 ["I", "like", "eating", "fish"]
,通过全连接层计算:
Logits
=
[
0.65
×
w
1
+
0.35
×
w
2
,
…
]
\text{Logits} = [0.65 \times w_1 + 0.35 \times w_2, \quad \dots]
Logits=[0.65×w1+0.35×w2,…]
假设最终概率分布为:
p
(
y
t
∣
y
<
t
,
X
)
=
[
0.05
,
0.15
,
0.70
,
0.10
]
p(y_t | y_{<t}, \mathbf{X}) = [0.05, 0.15, 0.70, 0.10]
p(yt∣y<t,X)=[0.05,0.15,0.70,0.10]
模型选择 “eating”(概率最高)。
步骤5:注意力权重可视化
解码时间步 | 生成单词 | 注意力权重(我, 喜欢, 吃, 鱼) |
---|---|---|
t = 1 t=1 t=1 | I | [0.8, 0.1, 0.05, 0.05] |
t = 2 t=2 t=2 | like | [0.1, 0.7, 0.1, 0.1] |
t = 3 t=3 t=3 | eating | [0.05, 0.1, 0.8, 0.05] |
t = 4 t=4 t=4 | fish | [0.05, 0.05, 0.1, 0.8] |
- 生成“eating”时,模型显著关注输入词 “吃”(权重 0.8),而非均匀分配。
关键结论
- 注意力权重非均匀:模型根据当前解码状态,动态分配对输入词的关注度。
- 上下文向量聚焦关键信息:生成“eating”时,上下文向量主要包含“吃”的信息。
- 数学可解释性:通过点积和 Softmax 实现权重分配,逻辑清晰。
六、为什么要归一化处理
在注意力机制中,对注意力分数进行 归一化处理(通常使用 Softmax 函数)是核心步骤之一。它的目的是将原始的注意力分数转化为 概率分布,使模型能够明确地决定在不同位置分配多少“注意力权重”。
6.1 归一化的数学必要性
(1) 概率化权重分配
注意力分数(如点积或加性分数)的原始值可能是任意实数(正负均可),而权重需要满足:
- 非负性:权重 α t i ≥ 0 \alpha_{ti} \geq 0 αti≥0
- 归一性:权重总和为 1(即 ∑ i = 1 T α t i = 1 \sum_{i=1}^T \alpha_{ti} = 1 ∑i=1Tαti=1)
通过 Softmax 函数可以将任意实数映射到 [0, 1] 区间,并确保权重总和为 1,形成有效的概率分布。
(2) 放大显著信号
Softmax 的指数操作会 放大高分数、抑制低分数。例如:
- 若某位置的注意力分数远高于其他位置,其权重将接近 1,其他位置接近 0。
- 若所有位置分数相近,权重趋于均匀分布。
6.2 归一化的实际意义
(1) 稳定梯度
- 未归一化的分数可能因输入尺度不同导致梯度爆炸或消失。
- Softmax 的归一化操作通过约束输出范围([0,1]),使梯度更稳定,模型更容易训练。
(2) 可解释性
- 权重总和为 1,直观表示每个输入位置对当前解码步骤的 相对重要性。
- 例如,在生成英文单词 “eating” 时,权重若为
[0.1, 0.1, 0.7, 0.1]
,可明确看出模型关注输入词 “吃”。
(3) 动态聚焦能力
- 归一化后,模型可以在不同时间步灵活调整注意力焦点(如翻译时先关注主语,再关注动词)。
6.3 反例:如果不做归一化
假设注意力分数为 [5, 3, 1, -2]
:
- 直接使用原始分数:
- 正负值共存,无法直接表示权重。
- 总和不为 1,无法反映相对重要性。
- 简单归一化(如除以总和):
- 总和为
5 + 3 + 1 - 2 = 7
→ 权重为[5/7, 3/7, 1/7, -2/7]
。 - 存在负权重,逻辑上不合理(注意力权重应为非负)。
- 总和为
6.4 实际案例:翻译 “我喜欢吃鱼” → “I like eating fish”
输入词与注意力分数(未归一化)
输入词 | 注意力分数 e t i e_{ti} eti |
---|---|
我 | 8.0 |
喜欢 | 5.0 |
吃 | 12.0 |
鱼 | 3.0 |
步骤1:计算每个分数的指数
exp
(
8.0
)
≈
2980.958
exp
(
5.0
)
≈
148.413
exp
(
12.0
)
≈
162754.791
exp
(
3.0
)
≈
20.086
\begin{aligned} \exp(8.0) &\approx 2980.958 \\ \exp(5.0) &\approx 148.413 \\ \exp(12.0) &\approx 162754.791 \\ \exp(3.0) &\approx 20.086 \\ \end{aligned}
exp(8.0)exp(5.0)exp(12.0)exp(3.0)≈2980.958≈148.413≈162754.791≈20.086
步骤2:计算分母(所有指数和)
分母
=
2980.958
+
148.413
+
162754.791
+
20.086
=
165
,
904.248
\text{分母} = 2980.958 + 148.413 + 162754.791 + 20.086 = 165,904.248
分母=2980.958+148.413+162754.791+20.086=165,904.248
步骤3:计算每个词的归一化权重
α
我
=
2980.958
165904.248
≈
0.018
(
1.8
%
)
α
喜欢
=
148.413
165904.248
≈
0.0009
(
0.09
%
)
α
吃
=
162754.791
165904.248
≈
0.981
(
98.1
%
)
α
鱼
=
20.086
165904.248
≈
0.0001
(
0.01
%
)
\begin{aligned} \alpha_{\text{我}} &= \frac{2980.958}{165904.248} \approx 0.018 \quad (1.8\%) \\ \alpha_{\text{喜欢}} &= \frac{148.413}{165904.248} \approx 0.0009 \quad (0.09\%) \\ \alpha_{\text{吃}} &= \frac{162754.791}{165904.248} \approx 0.981 \quad (98.1\%) \\ \alpha_{\text{鱼}} &= \frac{20.086}{165904.248} \approx 0.0001 \quad (0.01\%) \\ \end{aligned}
α我α喜欢α吃α鱼=165904.2482980.958≈0.018(1.8%)=165904.248148.413≈0.0009(0.09%)=165904.248162754.791≈0.981(98.1%)=165904.24820.086≈0.0001(0.01%)
权重分配结果
输入词 | 归一化权重 α t i \alpha_{ti} αti |
---|---|
我 | ≈ 1.8% |
喜欢 | ≈ 0.09% |
吃 | ≈ 98.1% |
鱼 | ≈ 0.01% |
关键结论
-
Softmax 的放大效应:
- 尽管“吃”的分数(12.0)仅比“我”(8.0)高 4 分,但其权重占比超过 98%。
- 原因是指数函数对高分值的放大效应( exp ( 12 ) ≫ exp ( 8 ) \exp(12) \gg \exp(8) exp(12)≫exp(8))。
-
归一化的实际意义:
- 模型几乎完全聚焦于 “吃”,生成对应的英文词 “eating”。
- 其他词的权重被抑制到接近 0,说明模型认为它们与当前解码步骤无关。
为何要设计高分差异?
- 模拟真实场景:在训练好的模型中,注意力分数差异通常较大,模型会明确学习到某些位置的关键性(如“吃”与“eating”的强关联)。
- 突出 Softmax 特性:通过极端值展示 Softmax 如何将显著信号转化为接近 1 的权重,抑制噪声。
6.5 总结:为什么必须归一化?
原因 | 说明 |
---|---|
数学合理性 | 将任意分数转化为概率分布,满足权重的基本数学性质(非负、和为1)。 |
梯度稳定性 | 约束输出范围,避免梯度爆炸或消失,提升训练稳定性。 |
可解释性 | 直观展示模型关注的重点位置,便于调试和分析。 |
动态聚焦 | 通过 Softmax 的指数特性,让模型灵活聚焦关键位置,抑制无关信息。 |
七、代码实现
定义一个简单的带有注意力机制的Seq2Seq模型,使用LSTM作为编码器,GRU作为解码器。
class AttentionSeq2Seq(nn.Module):
def __init__(self, enc_hid_dim, dec_hid_dim, vocab_size):
super().__init__()
# 一个双向LSTM,输入维度为100(假设是词嵌入的维度),隐藏层维度为enc_hid_dim。
self.encoder = nn.LSTM(input_size=100, hidden_size=enc_hid_dim, bidirectional=True)
# 解码器 一个GRU,输入大小为enc_hid_dim * 2(因为编码器是双向的),隐藏层大小为dec_hid_dim。
self.decoder = nn.GRU(input_size=enc_hid_dim * 2, hidden_size=dec_hid_dim)
# 注意力权重计算(加性注意力)
# 注意力机制:由线性层构成,用于计算加性注意力权重。
self.attn = nn.Linear(enc_hid_dim * 2 + dec_hid_dim, dec_hid_dim)
self.v = nn.Linear(dec_hid_dim, 1, bias=False)
# 输出层:将上下文向量和解码器输出拼接后通过全连接层映射到词汇表大小。
self.fc = nn.Linear(enc_hid_dim * 2 + dec_hid_dim, vocab_size)
# 添加一个线性层用于调整拼接后向量的维度
self.concat_linear = nn.Linear(100 + enc_hid_dim * 2, enc_hid_dim * 2)
# 定义前向传播的过程
def forward(self, src, trg):
# 首先运行编码器,获取所有时间步的隐藏状态enc_outputs
# 以及最后的隐藏状态和细胞状态(hidden, cell)。
# 编码器输出 [src_seq_len, batch_size, 2*enc_hid_dim]
enc_outputs, (hidden, cell) = self.encoder(src)
# 调整 enc_outputs 维度为 [batch_size, src_seq_len, 2*enc_hid_dim]
enc_outputs = enc_outputs.permute(1, 0, 2)
# 初始化解码器状态 [1, batch_size, dec_hid_dim]
# 初始化解码器的隐藏状态dec_hidden为编码器最后一层的隐藏状态。
dec_hidden = hidden[-1].unsqueeze(0)
# 对于目标序列中的每个时间步t
outputs = []
for t in range(trg.size(0)):
# 调用_attention方法计算当前解码器隐藏状态与所有编码器隐藏状态之间的注意力权重。
# 计算注意力权重 [batch_size, src_seq_len]
attn_weights = self._attention(dec_hidden, enc_outputs)
# 扩展维度 → [batch_size, 1, src_seq_len]
attn_weights = attn_weights.unsqueeze(1)
# torch.bmm 代表批量矩阵乘法
# 计算上下文向量 [batch_size, 1, 2*enc_hid_dim]
context = torch.bmm(attn_weights, enc_outputs)
context = context.squeeze(1) # [batch_size, 2*enc_hid_dim]
# trg[t]: torch.Size([5, 100])
# context: torch.Size([5, 1024])
# 将当前目标词嵌入与上下文向量拼接,作为解码器的输入。
dec_input = torch.cat([trg[t], context], dim=1)
# dec_input : torch.Size([5, 1124])
# 使用线性层调整维度以适应解码器输入
adjusted_dec_input = torch.relu(self.concat_linear(dec_input)) # [batch_size, dec_hid_dim]
# adjusted_dec_input: torch.Size([5, 1024])
# 更新解码器的状态,并通过全连接层预测下一个单词的概率分布。
dec_output, dec_hidden = self.decoder(adjusted_dec_input.unsqueeze(0), dec_hidden)
# 预测输出
output = self.fc(torch.cat([dec_output.squeeze(0), context], dim=1))
outputs.append(output)
return torch.stack(outputs)
def _attention(self, dec_hidden, enc_outputs):
# dec_hidden: [1, batch_size, dec_hid_dim]
# enc_outputs: [batch_size, src_seq_len, 2*enc_hid_dim]
# 扩展解码器隐藏状态以匹配编码器输出的时间长度。
dec_hidden = dec_hidden.repeat(enc_outputs.size(1), 1, 1) # [src_seq_len, batch_size, dec_hid_dim]
dec_hidden = dec_hidden.permute(1, 0, 2) # [batch_size, src_seq_len, dec_hid_dim]
# 通过线性层和激活函数计算能量值energy。
# 计算能量值 [batch_size, src_seq_len]
energy = torch.tanh(self.attn(torch.cat((dec_hidden, enc_outputs), dim=2)))
# 通过另一个线性层self.v和Softmax函数计算最终的注意力权重。
attention = self.v(energy).squeeze(2) # [batch_size, src_seq_len]
return F.softmax(attention, dim=1)
验证模型:
import torch
# 假设源序列和目标序列的最大长度为10,批次大小为5,词汇表大小为1000
src = torch.randn(10, 5, 100) # 源序列 [seq_len, batch_size, embed_dim]
trg = torch.randint(0, 1000, (10, 5)) # 目标序列 [seq_len, batch_size]
#src: torch.Size([10, 5, 100])
#trg: torch.Size([10, 5])
# 转换为目标词嵌入
embedding = nn.Embedding(1000, 100) # 假设词嵌入维度为100
trg_embedded = embedding(trg)
# trg_embedded: torch.Size([10, 5, 100])
# trg_embedded[:-1]: torch.Size([9, 5, 100])
# 实例化模型并测试
model = AttentionSeq2Seq(enc_hid_dim=512, dec_hid_dim=512, vocab_size=1000)
output = model(src, trg_embedded)
print(output.shape)
# 应输出 torch.Size([10, 5, 1000])
关键点
- 注意力机制: 在每个解码步骤中,该模型不仅依赖于前一时刻的隐藏状态来生成下一个词,还会根据当前解码状态与所有编码状态的相关性动态地聚焦于源句子的不同部分。
- 上下文向量: 通过注意力权重对编码器的所有隐藏状态进行加权求和得到,代表了当前解码步骤最相关的信息。
- 输出预测: 拼接解码器输出和上下文向量并通过全连接层映射到词汇表大小,以预测下一个词的概率分布。
八、总结
引入注意力机制的 Seq2Seq 模型通过动态上下文向量,会显著提升长序列任务的表现。注意力机制的核心在于 灵活的信息筛选与融合,这一思想后来被 Transformer 发扬光大,成为现代深度学习的基石之一。
更多推荐
所有评论(0)