seq2seq以及注意力机制
讲解seq2seq之前,首先讲解一下注意力机制
注意力机制
1. 注意力机制的核心思想
注意力机制的核心思想可以用一句话概括:根据当前任务的需要,动态地为输入序列中的每个部分分配不同的权重。这些权重决定了模型在生成输出时,应该更多地关注输入的哪些部分。
1.1 关键概念
- 查询(Query):表示当前任务的需求。例如,在机器翻译中,查询是当前要生成的词。
- 键(Key):表示输入序列的每个部分。例如,输入句子中的每个词。
- 值(Value):表示输入序列的实际内容。通常,键和值可以是相同的。
- 注意力分数(Attention Score):表示查询和键之间的相关性。
- 注意力权重(Attention Weight):通过softmax函数将注意力分数归一化,表示每个输入部分的重要性。
- 上下文向量(Context Vector):根据注意力权重对值进行加权求和,得到的结果。
2. 注意力机制的详细步骤
2.1 输入和输出
- 输入序列(英文):
["I", "love", "you"]
- 输出序列(中文):
["我", "爱", "你"]
2.2 具体步骤
-
编码输入序列:
- 使用一个编码器(如RNN、LSTM或Transformer)将输入序列编码成一组向量:
h = [ h 1 , h 2 , h 3 ] h = [h_1, h_2, h_3] h=[h1,h2,h3],其中 h 1 h_1 h1 对应"I"
, h 2 h_2 h2 对应"love"
, h 3 h_3 h3 对应"you"
。
- 使用一个编码器(如RNN、LSTM或Transformer)将输入序列编码成一组向量:
-
生成查询向量:
- 在解码器的每一步,生成一个查询向量
q
i
q_i
qi,表示当前任务的需求。例如,在生成
"我"
时,查询向量 q 1 q_1 q1 表示需要关注输入序列中与"我"
相关的部分。
- 在解码器的每一步,生成一个查询向量
q
i
q_i
qi,表示当前任务的需求。例如,在生成
-
计算注意力分数:
- 对于输入序列中的每个编码向量
h
j
h_j
hj,计算它与查询向量
q
i
q_i
qi 的相关性分数
e
i
j
e_{ij}
eij。
公式: e i j = f ( q i , h j ) e_{ij} = f(q_i, h_j) eij=f(qi,hj),其中 f f f 是计算相关性的函数(如点积或加性注意力)。- 点积注意力: e i j = q i T h j e_{ij} = q_i^T h_j eij=qiThj
- 加性注意力: e i j = v T tanh ( W q q i + W k h j ) e_{ij} = v^T \tanh(W_q q_i + W_k h_j) eij=vTtanh(Wqqi+Wkhj),其中 W q W_q Wq、 W k W_k Wk 和 v v v 是可学习的参数。
- 对于输入序列中的每个编码向量
h
j
h_j
hj,计算它与查询向量
q
i
q_i
qi 的相关性分数
e
i
j
e_{ij}
eij。
-
计算注意力权重:
- 使用softmax函数将注意力分数归一化为权重:
α i j = exp ( e i j ) ∑ k = 1 n exp ( e i k ) \alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^n \exp(e_{ik})} αij=∑k=1nexp(eik)exp(eij)
这些权重表示每个输入部分的重要性。
- 使用softmax函数将注意力分数归一化为权重:
-
计算上下文向量:
- 使用注意力权重对编码向量进行加权求和,得到上下文向量
c
i
c_i
ci:
c i = ∑ j = 1 n α i j h j c_i = \sum_{j=1}^n \alpha_{ij} h_j ci=∑j=1nαijhj
上下文向量 c i c_i ci 包含了模型当前需要关注的信息。
- 使用注意力权重对编码向量进行加权求和,得到上下文向量
c
i
c_i
ci:
-
生成输出:
- 将上下文向量
c
i
c_i
ci 与解码器的隐藏状态结合,生成当前输出。例如,生成
"我"
。
- 将上下文向量
c
i
c_i
ci 与解码器的隐藏状态结合,生成当前输出。例如,生成
3. 具体例子:机器翻译中的注意力机制
我们以英文句子 "I love you"
翻译成中文 "我爱你"
为例,详细说明每一步的计算过程。
3.1 编码输入序列
- 输入序列:
["I", "love", "you"]
- 编码后的向量: h = [ h 1 , h 2 , h 3 ] h = [h_1, h_2, h_3] h=[h1,h2,h3]
3.2 生成查询向量
- 在生成
"我"
时,解码器生成查询向量 q 1 q_1 q1。
3.3 计算注意力分数
- 计算
q
1
q_1
q1 与
h
1
h_1
h1、
h
2
h_2
h2、
h
3
h_3
h3 的相关性分数:
e 11 = f ( q 1 , h 1 ) e_{11} = f(q_1, h_1) e11=f(q1,h1)
e 12 = f ( q 1 , h 2 ) e_{12} = f(q_1, h_2) e12=f(q1,h2)
e 13 = f ( q 1 , h 3 ) e_{13} = f(q_1, h_3) e13=f(q1,h3)
3.4 计算注意力权重
- 使用softmax函数归一化分数:
α 11 = exp ( e 11 ) exp ( e 11 ) + exp ( e 12 ) + exp ( e 13 ) \alpha_{11} = \frac{\exp(e_{11})}{\exp(e_{11}) + \exp(e_{12}) + \exp(e_{13})} α11=exp(e11)+exp(e12)+exp(e13)exp(e11)
α 12 = exp ( e 12 ) exp ( e 11 ) + exp ( e 12 ) + exp ( e 13 ) \alpha_{12} = \frac{\exp(e_{12})}{\exp(e_{11}) + \exp(e_{12}) + \exp(e_{13})} α12=exp(e11)+exp(e12)+exp(e13)exp(e12)
α 13 = exp ( e 13 ) exp ( e 11 ) + exp ( e 12 ) + exp ( e 13 ) \alpha_{13} = \frac{\exp(e_{13})}{\exp(e_{11}) + \exp(e_{12}) + \exp(e_{13})} α13=exp(e11)+exp(e12)+exp(e13)exp(e13)
3.5 计算上下文向量
- 加权求和:
c 1 = α 11 h 1 + α 12 h 2 + α 13 h 3 c_1 = \alpha_{11} h_1 + \alpha_{12} h_2 + \alpha_{13} h_3 c1=α11h1+α12h2+α13h3
3.6 生成输出
- 使用
c
1
c_1
c1 生成
"我"
。
4. 注意力机制的类型
-
加性注意力(Additive Attention):
- 使用一个神经网络计算查询和键的相关性。
- 公式: e i j = v T tanh ( W q q i + W k h j ) e_{ij} = v^T \tanh(W_q q_i + W_k h_j) eij=vTtanh(Wqqi+Wkhj)
-
点积注意力(Dot-Product Attention):
- 直接计算查询和键的点积。
- 公式: e i j = q i T h j e_{ij} = q_i^T h_j eij=qiThj
-
自注意力(Self-Attention):
- 在同一个序列内部计算注意力,用于捕捉序列内部的依赖关系。
- 广泛应用于Transformer模型。
-
多头注意力(Multi-Head Attention):
- 使用多个注意力头并行计算,捕捉不同的特征表示。
- 每个头独立计算注意力,最后将结果拼接或加权求和。
seq2seq
讲解seq2seq模型时,以英文译法文
为例子
1.模型架构
- 思想:将输入序列映射为一个中间表示,然后再从这个中间表示生成目标序列
- 包括三部分,分别是
encoder(编码器)
、decoder(解码器)
、中间语义张量c
- 编码流程:1个时间步1个时间步的编码,每个时间步有输出,最终组合成中间语义张量C
- 解码流程:1个时间步1个时间步的解码
2.构建编码器
构建编码器比较简单,可以使用RNN、GRU、LSTM等。这里构建的编码器将不采用注意力机制。
编码器的构建就是一个简单的GRU模型,最终会有一个output
和hidden
。假设输入为"欢迎来北京",经分词、文本数值化、数值张量化后的结果为:
- 欢迎–>1–>[0.1,0.1,0.1]
- 来–>2–>[0.2,0.3,0.4]
- 北京–>3–>[0.4,0.5,0.6]
- pre_hidden.shape = [1,1,256]
那么output.shape = (1,3,256)(batch_first = True
情况下)
hidden.shape = (1,1,256)
这里output
将作为中间语义张量c
(也就是后面注意力机制的Value
),hidden将作为解码器的第一个key
。
2.构建解码器
这里的解码器采用注意力机制的策略。
2.1代码解释
假设有一个数据集:每一行有2列,第1列为英文
,第2列为法文
,中间用制表符tab
分割
- 数据一共有10599条
要求: 给出一个英文,翻译成法文,最终每个时间步相当于一个分类问题
- 其中source单词-英文数目:2803;target单词-法文数目:4345
- 探索source文本(eg:英文)和target文本(eg:法文)之间的语义关系
'''
MAX_LENGTH :这里是为了规范“中间语义张量c”的形状,比如若['欢迎','来','北京']最后经过encoder得到的是(1,3,32),然后变为(3,32),\
但是如果输入['欢迎','你','来','北京'],那么经过encoder得到的是(1,4,32)-->(4,32)。\
因此需要有一个统一的规范来规定其大小。如MAX_LENGTH表示最后的中间语义张量c为(1,MAX_LENGTH,32)。
dropout_p=0.1:为了让神经元随机失活(经验模型)
'''
class AttnDecoderRNN(nn.Module):
def __init__(self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.output_size = output_size # 法文单词数量 4345
self.hidden_size = hidden_size # 隐藏层大小 256
self.dropout_p = dropout_p # 随机失活层
self.max_length = max_length
# 实例化 法文的 词向量层 -->得到(4345,256)
self.embedding = nn.Embedding(output_size, hidden_size)
# 实例化gru对象
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
# 实例化全连接层 256-->4345
self.out = nn.Linear(hidden_size, output_size)
# 实例化softmax层
self.softmax = nn.LogSoftmax(dim=-1)
# 用于注意力机制
self.attn = nn.Linear(self.hidden_size + self.hidden_size, self.max_length) # add
# 线性层2:注意力结果表示 按照指定维度进行输出层 nn.Linear(32+32, 32)
self.attn_combine = nn.Linear(self.hidden_size + self.hidden_size, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
def attentionQKV(self, Q, K, V): ### 返回加强的Q,和权重矩阵参数值
# 1 求查询张量q的注意力权重分布, attn_weights[1,32]
tmp1 = torch.cat((Q[0], K[0]), dim=-1 ) # [1,1,32],[1,1,32] -->[1,32],[1,32]-->[1,64]
# 计算Q、K之间的关联性(使用线性层学习)
# 需要把(批次,n,m1)转化为(n,m2),然后组合为(n,m1+m2),这里要根据实际情况来选择矩阵形状
tmp2 = self.attn(tmp1) # [1,64] ---> [1,10] 10个单词(这里就是上面的"MAX_LENGTH")
attn_weights = F.softmax(tmp2, dim=-1) # [1,10]--->[1,10]
#attn_weights = F.softmax(self.attn(torch.cat((Q[0], K[0]), dim=-1)), dim=-1) 也可按照这个计算
# 2 求查询张量q的注意力结果表示 bmm运算, attn_applied[1,1,32]
# [1,10]-->[1,1,10] [1,10,32] ===> [1,1,32]
attn_applied = torch.bmm(attn_weights.unsqueeze(0), V) #可以不加下面的一层,但是实验证明效果更好
# 3-1 q 与 attn_applied 融合
# [1,32],[1,32] ===> [1,64]
output = torch.cat((Q[0],attn_applied[0]),dim=-1)
#3-2 再按照指定维度输出 output[1,1,32]
# [1,64]-->[1,32]
output = self.attn_combine(output).unsqueeze(0)
# 返回注意力结果表示output:[1,1,32], 注意力权重分布attn_weights:[1,10]
return output, attn_weights
def forward(self, input, hidden, encoder_outputs):
# 三个参数分别代表:“Q”、“K”、“V”
# 数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
input = self.embedding(input)
input = self.dropout(input)
input, attn_weights = self.attentionQKV(input, hidden, encoder_outputs.unsqueeze(0))
# 数据经过relu()层 input = F.relu(input)
input = F.relu(input)
# 数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
output, hidden = self.gru(input, hidden)
# 数据结果out层 形状变化 [1,1,256]->[1,256]-->[1,4345]
output = self.out(output[0])
output = self.softmax(output)
# 返回 解码器分类output[1,4345],最后隐层张量hidden[1,1,256]
return output, hidden, attn_weights
def inithidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
3.bmm矩阵运算
假设有两个三维张量:
- 张量 A 的形状为
(batch_size, n, m)
- 张量 B 的形状为
(batch_size, m, p)
对于每个批次索引
i
i
i,bmm
计算:
C
i
=
A
i
×
B
i
C_i = A_i \times B_i
Ci=Ai×Bi
其中:
-
A
i
A_i
Ai 是张量 A 的第
i
i
i 个矩阵,形状为
(n, m)
-
B
i
B_i
Bi 是张量 B 的第
i
i
i 个矩阵,形状为
(m, p)
-
C
i
C_i
Ci 是输出张量的第
i
i
i 个矩阵,形状为
(n, p)
2. 具体计算示例
假设:
- 批次大小
batch_size = 2
- 张量 A 的形状为
(2, 2, 3)
,即每个矩阵是 2 × 3 2 \times 3 2×3 - 张量 B 的形状为
(2, 3, 2)
,即每个矩阵是 3 × 2 3 \times 2 3×2
输入张量:
- 张量 A:
A 1 = [ 1 2 3 4 5 6 ] , A 2 = [ 7 8 9 10 11 12 ] A_1 = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}, \quad A_2 = \begin{bmatrix} 7 & 8 & 9 \\ 10 & 11 & 12 \end{bmatrix} A1=[142536],A2=[710811912] - 张量 B:
B 1 = [ 1 2 3 4 5 6 ] , B 2 = [ 7 8 9 10 11 12 ] B_1 = \begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix}, \quad B_2 = \begin{bmatrix} 7 & 8 \\ 9 & 10 \\ 11 & 12 \end{bmatrix} B1= 135246 ,B2= 791181012
计算过程:
-
计算 C 1 = A 1 × B 1 C_1 = A_1 \times B_1 C1=A1×B1:
C 1 = [ 1 2 3 4 5 6 ] × [ 1 2 3 4 5 6 ] C_1 = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} \times \begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix} C1=[142536]× 135246
计算:
C 1 = [ ( 1 ⋅ 1 + 2 ⋅ 3 + 3 ⋅ 5 ) ( 1 ⋅ 2 + 2 ⋅ 4 + 3 ⋅ 6 ) ( 4 ⋅ 1 + 5 ⋅ 3 + 6 ⋅ 5 ) ( 4 ⋅ 2 + 5 ⋅ 4 + 6 ⋅ 6 ) ] C_1 = \begin{bmatrix} (1 \cdot 1 + 2 \cdot 3 + 3 \cdot 5) & (1 \cdot 2 + 2 \cdot 4 + 3 \cdot 6) \\ (4 \cdot 1 + 5 \cdot 3 + 6 \cdot 5) & (4 \cdot 2 + 5 \cdot 4 + 6 \cdot 6) \end{bmatrix} C1=[(1⋅1+2⋅3+3⋅5)(4⋅1+5⋅3+6⋅5)(1⋅2+2⋅4+3⋅6)(4⋅2+5⋅4+6⋅6)]
结果:
C 1 = [ 22 28 49 64 ] C_1 = \begin{bmatrix} 22 & 28 \\ 49 & 64 \end{bmatrix} C1=[22492864] -
计算 C 2 = A 2 × B 2 C_2 = A_2 \times B_2 C2=A2×B2:
C 2 = [ 7 8 9 10 11 12 ] × [ 7 8 9 10 11 12 ] C_2 = \begin{bmatrix} 7 & 8 & 9 \\ 10 & 11 & 12 \end{bmatrix} \times \begin{bmatrix} 7 & 8 \\ 9 & 10 \\ 11 & 12 \end{bmatrix} C2=[710811912]× 791181012
计算:
C 2 = [ ( 7 ⋅ 7 + 8 ⋅ 9 + 9 ⋅ 11 ) ( 7 ⋅ 8 + 8 ⋅ 10 + 9 ⋅ 12 ) ( 10 ⋅ 7 + 11 ⋅ 9 + 12 ⋅ 11 ) ( 10 ⋅ 8 + 11 ⋅ 10 + 12 ⋅ 12 ) ] C_2 = \begin{bmatrix} (7 \cdot 7 + 8 \cdot 9 + 9 \cdot 11) & (7 \cdot 8 + 8 \cdot 10 + 9 \cdot 12) \\ (10 \cdot 7 + 11 \cdot 9 + 12 \cdot 11) & (10 \cdot 8 + 11 \cdot 10 + 12 \cdot 12) \end{bmatrix} C2=[(7⋅7+8⋅9+9⋅11)(10⋅7+11⋅9+12⋅11)(7⋅8+8⋅10+9⋅12)(10⋅8+11⋅10+12⋅12)]
结果:
C 2 = [ 202 232 247 284 ] C_2 = \begin{bmatrix} 202 & 232 \\ 247 & 284 \end{bmatrix} C2=[202247232284]
输出张量:
C
=
[
[
22
28
49
64
]
,
[
202
232
247
284
]
]
C = \begin{bmatrix} \begin{bmatrix} 22 & 28 \\ 49 & 64 \end{bmatrix}, \begin{bmatrix} 202 & 232 \\ 247 & 284 \end{bmatrix} \end{bmatrix}
C=[[22492864],[202247232284]]
形状为 (2, 2, 2)
。
3. 代码验证
以下是使用 PyTorch 验证上述计算的代码:
import torch
# 定义输入张量
A = torch.tensor([[[1, 2, 3], [4, 5, 6]],
[[7, 8, 9], [10, 11, 12]]], dtype=torch.float32)
B = torch.tensor([[[1, 2], [3, 4], [5, 6]],
[[7, 8], [9, 10], [11, 12]]], dtype=torch.float32)
# 使用 bmm 计算
C = torch.bmm(A, B)
print("A:", A)
print("B:", B)
print("Output C:", C)
输出:
A: tensor([[[ 1., 2., 3.],
[ 4., 5., 6.]],
[[ 7., 8., 9.],
[10., 11., 12.]]])
B: tensor([[[ 1., 2.],
[ 3., 4.],
[ 5., 6.]],
[[ 7., 8.],
[ 9., 10.],
[11., 12.]]])
Output C: tensor([[[ 22., 28.],
[ 49., 64.]],
[[202., 232.],
[247., 284.]]])
为什么bmm矩阵可以达到效果?
下面是一个案例:
- 注意力机制的实现步骤:
- 用查询张量q 与key 进行运算(相似度运算)得到一个注意力机制的权重分布(就是一个权重系数)
- 用权重分布乘以内容得到注意机制的结果表示
- 最终的目标如下:attention(Q, K, V) —> attention_weights, attention_q
- attention是关于qkv的函数,通过qkv运算,普通的q升级成一个更加强大的q
问题:it
指代的是谁?
在机器人第2定律中有这样一句话:
- A robot must obey the orders given
it
by human beings except where such orders would conflict with the First Law. - (机器人必须服从人类给它的命令,除非这些命令与第一定律相冲突。);机器人第一定律:机器人不能伤害人类。
对上面一句话要形成:qkv
- q:要查询的问题既it代指谁?
- k:就是每个单词的权重
- v:每个单词的词向量表示
看一下下面的图形操作
- :q和k进行计算关系(比如相识度),得到注意力机制的权重分布
attention_weight [0.001, 0.3, 0.5, 0.002, 0.001, 0.00003 …..]
- 注意:这个权重分布指
“it”单词的向量表示(也是这个问题的q)
和文本序列21个单词形成的权重分布。数据形状是(1, 21)
- :用权重分布 @ 内容进行加权求和,得到注意力机制的结果表示。
<s>3个特征,a3个特征 ; 用0.001*<s>词向量 + 0.3*a的词向量 + 0.5*robot词向量 +.. = attention_q(它也是三个特征)
- :值向量加权混合得到的结果也是一个向量, 它将其50%的注意力放在了单词"robot"上, 30%的注意力放在了"a"上, 还有0.3%的注意力放在了"it"上…
下面解释为什么bmm矩阵可以实现这个效果
句子有 21 个单词,每个单词用一个 (3,)
向量表示。
构建 Q、K、V
-
Q(Query):
it
的词向量表示,形状为(1, 3)
。- 假设
it
的词向量为:
q = [ q 1 q 2 q 3 ] q = \begin{bmatrix} q_1 & q_2 & q_3 \end{bmatrix} q=[q1q2q3]
- 假设
-
K(Key):句子中每个单词的词向量表示,形状为
(21, 3)
。- 假设 K ∈ R 21 × 3 K \in \mathbb{R}^{21 \times 3} K∈R21×3。
- 例如:
K = [ k 11 k 12 k 13 k 21 k 22 k 23 ⋮ ⋮ ⋮ k 21 , 1 k 21 , 2 k 21 , 3 ] K = \begin{bmatrix} k_{11} & k_{12} & k_{13} \\ k_{21} & k_{22} & k_{23} \\ \vdots & \vdots & \vdots \\ k_{21,1} & k_{21,2} & k_{21,3} \end{bmatrix} K= k11k21⋮k21,1k12k22⋮k21,2k13k23⋮k21,3
-
V(Value):句子中每个单词的词向量表示,形状为
(21, 3)
。- 假设 V ∈ R 21 × 3 V \in \mathbb{R}^{21 \times 3} V∈R21×3。
- 例如:
V = [ v 11 v 12 v 13 v 21 v 22 v 23 ⋮ ⋮ ⋮ v 21 , 1 v 21 , 2 v 21 , 3 ] V = \begin{bmatrix} v_{11} & v_{12} & v_{13} \\ v_{21} & v_{22} & v_{23} \\ \vdots & \vdots & \vdots \\ v_{21,1} & v_{21,2} & v_{21,3} \end{bmatrix} V= v11v21⋮v21,1v12v22⋮v21,2v13v23⋮v21,3
计算注意力权重
-
使用矩阵乘法计算 Q ⋅ K T Q \cdot K^T Q⋅KT,得到相似度矩阵(这个也可以使用线性层训练得到分数)。
-
公式:
similarity = Q ⋅ K T \text{similarity} = Q \cdot K^T similarity=Q⋅KT-
Q
Q
Q 的形状为
(1, 3)
。 -
K
K
K 的形状为
(21, 3)
。 -
K
T
K^T
KT 的形状为
(3, 21)
。 - 结果形状为
(1, 21)
。
-
Q
Q
Q 的形状为
-
具体计算:
similarity = [ q 1 q 2 q 3 ] ⋅ [ k 11 k 21 … k 21 , 1 k 12 k 22 … k 21 , 2 k 13 k 23 … k 21 , 3 ] \text{similarity} = \begin{bmatrix} q_1 & q_2 & q_3 \end{bmatrix} \cdot \begin{bmatrix} k_{11} & k_{21} & \dots & k_{21,1} \\ k_{12} & k_{22} & \dots & k_{21,2} \\ k_{13} & k_{23} & \dots & k_{21,3} \end{bmatrix} similarity=[q1q2q3]⋅ k11k12k13k21k22k23………k21,1k21,2k21,3 - 结果是一个
(1, 21)
的向量,表示it
与句子中每个单词的相似度。
- 结果是一个
计算注意力权重:
-
对相似度矩阵进行 softmax 操作,得到注意力权重分布。
-
公式:
attention_weights = softmax ( similarity d k ) \text{attention\_weights} = \text{softmax}\left(\frac{\text{similarity}}{\sqrt{d_k}}\right) attention_weights=softmax(dksimilarity)-
similarity
\text{similarity}
similarity 的形状为
(1, 21)
。 -
attention_weights
\text{attention\_weights}
attention_weights 的形状为
(1, 21)
。
-
similarity
\text{similarity}
similarity 的形状为
-
具体计算:
attention_weights = softmax ( similarity 3 ) \text{attention\_weights} = \text{softmax}\left(\frac{\text{similarity}}{\sqrt{3}}\right) attention_weights=softmax(3similarity) -
示例:
attention_weights = [ 0.001 , 0.3 , 0.5 , 0.002 , 0.001 , 0.00003 , … ] \text{attention\_weights} = [0.001, 0.3, 0.5, 0.002, 0.001, 0.00003, \dots] attention_weights=[0.001,0.3,0.5,0.002,0.001,0.00003,…]
加权求和
-
使用矩阵乘法计算 attention_weights ⋅ V \text{attention\_weights} \cdot V attention_weights⋅V,得到加权后的值。
-
公式:
attention_output = attention_weights ⋅ V \text{attention\_output} = \text{attention\_weights} \cdot V attention_output=attention_weights⋅V-
attention_weights
\text{attention\_weights}
attention_weights 的形状为
(1, 21)
。 -
V
V
V 的形状为
(21, 3)
。 - 结果形状为
(1, 3)
。
-
attention_weights
\text{attention\_weights}
attention_weights 的形状为
-
具体计算:
attention_output = [ w 1 w 2 … w 21 ] ⋅ [ v 11 v 12 v 13 v 21 v 22 v 23 ⋮ ⋮ ⋮ v 21 , 1 v 21 , 2 v 21 , 3 ] \text{attention\_output} = \begin{bmatrix} w_1 & w_2 & \dots & w_{21} \end{bmatrix} \cdot \begin{bmatrix} v_{11} & v_{12} & v_{13} \\ v_{21} & v_{22} & v_{23} \\ \vdots & \vdots & \vdots \\ v_{21,1} & v_{21,2} & v_{21,3} \end{bmatrix} attention_output=[w1w2…w21]⋅ v11v21⋮v21,1v12v22⋮v21,2v13v23⋮v21,3 - 结果是一个
(1, 3)
的向量,表示注意力机制的输出。
- 结果是一个
-
示例:
attention_output = 0.001 ⋅ <s> + 0.3 ⋅ a + 0.5 ⋅ robot + … \text{attention\_output} = 0.001 \cdot \text{<s>} + 0.3 \cdot \text{a} + 0.5 \cdot \text{robot} + \dots attention_output=0.001⋅<s>+0.3⋅a+0.5⋅robot+…
- 模型将 50% 的注意力放在
robot
上,30% 的注意力放在a
上,0.3% 的注意力放在it
上。 - 这表明模型认为
it
指代的是robot
。
代码实现
实现训练的代码分为两个主要的路线,一个是外层业务训练,一个是内层具体训练。
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size ):
super(EncoderRNN, self).__init__()
self.input_size = input_size # 这个不是输入维度,而是单词个数
self.hidden_size = hidden_size
# 实例化词嵌入层对象
self.embedding = nn.Embedding(num_embeddings=input_size, embedding_dim=hidden_size)
# 实例化gru对象
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
def forward(self, input, hidden):
# 数据经过词嵌入层 数据形状 [1,6] --> [1,6,256]
output = self.embedding(input)
# 数据经过gru层 形状变化 gru([1,6,256],[1,1,256]) --> [1,6,256] [1,1,256]
output, hidden = self.gru(output, hidden)
return output, hidden
def inithidden(self):
return torch.zeros(1, 1, self.hidden_size)
class AttnDecoderRNN(nn.Module):
def __init__(self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.output_size = output_size # 4345
self.hidden_size = hidden_size # 256
self.dropout_p = dropout_p # add
self.max_length = max_length # add
# 实例化 法文的 词向量层
self.embedding = nn.Embedding(output_size, hidden_size)
# 实例化gru对象
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
# 实例化全连接层
self.out = nn.Linear(hidden_size, output_size)
# 实例化softmax层
self.softmax = nn.LogSoftmax(dim=-1)
self.attn = nn.Linear(self.hidden_size + self.hidden_size, self.max_length) # add
# 线性层2:注意力结果表示 按照指定维度进行输出层 nn.Linear(32+32, 32)
self.attn_combine = nn.Linear(self.hidden_size + self.hidden_size, self.hidden_size) #add
self.dropout = nn.Dropout(self.dropout_p) # add
def attentionQKV(self, Q, K, V): ### addd
# 1 求查询张量q的注意力权重分布, attn_weights[1,32]
# tmp1 = torch.cat((Q[0], K[0]), dim=-1 ) # [1,1,32],[1,1,32] -->[1,32],[1,32]-->[1,64]
# tmp2 = self.attn(tmp1) # [1,64] ---> [1,10] 10个单词
# tmp3 = F.softmax(tmp2, dim=-1) # [1,10]--->[1,10]
# print('tmp3-->', tmp3)
attn_weights = F.softmax(self.attn(torch.cat((Q[0], K[0]), dim=-1)), dim=-1)
# 2 求查询张量q的注意力结果表示 bmm运算, attn_applied[1,1,32]
# [1,10]-->[1,1,10] [1,10,32] ===> [1,1,32]
attn_applied = torch.bmm(attn_weights.unsqueeze(0), V)
# 3-1 q 与 attn_applied 融合
# [1,32],[1,32] ===> [1,64]
output = torch.cat((Q[0],attn_applied[0]),dim=-1)
#3-2 再按照指定维度输出 output[1,1,32]
# [1,64]-->[1,32]
output = self.attn_combine(output).unsqueeze(0)
# 返回注意力结果表示output:[1,1,32], 注意力权重分布attn_weights:[1,10]
return output, attn_weights
def forward(self, input, hidden, encoder_outputs):
# 数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
input = self.embedding(input)
input = self.dropout(input)
input, attn_weights = self.attentionQKV(input, hidden, encoder_outputs.unsqueeze(0))
# 数据经过relu()层 input = F.relu(input)
input = F.relu(input)
# 数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
output, hidden = self.gru(input, hidden)
# 数据结果out层 形状变化 [1,1,256]->[1,256]-->[1,4345]
output = self.out(output[0])
output = self.softmax(output)
# 返回 解码器分类output[1,4345],最后隐层张量hidden[1,1,256]
return output, hidden, attn_weights
def inithidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
# 内部迭代训练函数Train_Iters
# 1 编码 encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 数据形状 eg [1,6],[1,1,256] --> [1,6,256],[1,1,256]
# 2 解码参数准备和解码
# 解码参数1 固定长度C encoder_outputs_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
# 解码参数2 decode_hidden # 解码参数3 input_y = torch.tensor([[SOS_token]], device=device)
# 数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
# output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算损失 target_y = y[0][idx].view(1)
# 每个时间步处理 for idx in range(y_len): 处理三者之间关系input_y output_y target_y
# 3 训练策略 use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
# teacher_forcing 把样本真实值y作为下一次输入 input_y = y[0][idx].view(1, -1)
# not teacher_forcing 把预测值y作为下一次输入
# topv,topi = output_y.topk(1) # if topi.squeeze().item() == EOS_token: break input_y = topi.detach()
# 4 其他 # 计算损失 # 梯度清零 # 反向传播 # 梯度更新 # 返回 损失列表myloss.item()/y_len
def Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss):
# 1 编码 encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 数据形状 eg [1,6],[1,1,256] --> [1,6,256],[1,1,256]
encode_hihden = my_encoderrnn.inithidden()
encode_output, encode_hihden= my_encoderrnn(x, encode_hihden)
# 2 解码参数准备和解码
# 解码参数1 固定长度C encoder_outputs_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
for idx in range(encode_output.shape[1]):
encode_output_c[idx] = encode_output[0, idx]
# 解码参数2 decode_hidden
decode_hidden = encode_hihden
# 解码参数3
input_y = torch.tensor([[SOS_token]], device=device)
myloss = 0.0
y_len = y.shape[1]
for idx in range(y_len):
# 数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算真实值
target_y = y[0][idx].view(1) # [1,]
myloss += mycrossentropyloss(output_y, target_y) # output_y预测值[1,4345]和target_y真实值[1,] 做分类损失时, 要查一个维度
# 把样本真实值y作为下一次输入
input_y = y[0][idx].view(1, -1)
# 计算损失 target_y = y[0][idx].view(1)
# 每个时间步处理 for idx in range(y_len): 处理三者之间关系input_y output_y target_y
# 梯度清零
myadam_encode.zero_grad(); myadam_decode.zero_grad()
# 反向传播 计算梯度
myloss.backward()
# 梯度更新
myadam_encode.step(); myadam_decode.step()
return myloss.item() /y_len
# Train_seq2seq() 思路分析
# 实例化 mypairsdataset对象 实例化 mydataloader
# 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
# 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
# 实例化损失函数 mycrossentropyloss = nn.NLLLoss()
# 定义模型训练的参数
# epoches mylr=1e4 teacher_forcing_ratio print_interval_num plot_interval_num (全局)
# plot_loss_list = [] (返回) print_loss_total plot_loss_total starttime (每轮内部)
# 外层for循环 控制轮数 for epoch_idx in range(1, 1+epochs)
# 内层for循环 控制迭代次数 # for item, (x, y) in enumerate(mydataloader, start=1)
# 调用内部训练函数 Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss)
# 计算辅助信息
# 计算打印屏幕间隔损失-每隔1000次 # 计算画图间隔损失-每隔100次
# 每个轮次保存模型 torch.save(my_encoderrnn.state_dict(), PATH1)
# 所有轮次训练完毕 画损失图 plt.figure() .plot(plot_loss_list) .save('x.png') .show()
def Train_seq2seq():
# 实例化 mypairsdataset对象 实例化 mydataloader
mypairsdataset = MyPairsDataset(my_pairs)
mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)
# 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
my_encoderrnn = EncoderRNN(2803, 256) # 英文单词个数 2803,encoder_hidden.shze为256维
my_attndecoderrnn = AttnDecoderRNN (4345, 256) # 法文单词个数 4345 ,decoder_hidden为256维
# 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
myadam_encode = optim.Adam(my_encoderrnn.parameters(), lr=mylr)
myadam_decode = optim.Adam(my_attndecoderrnn.parameters(), lr=mylr)
# 实例化损失函数
mycrossentropyloss = nn.NLLLoss()
# 定义模型训练的参数
plot_loss_list = [] # 用于画图
# 外层for循环 控制轮数
for epoch_idx in range(1, 1+epochs):
print_loss_total, plot_loss_total = 0.0, 0.0
starttime = time.time()
# 内层for循环 控制迭代次数
for item, (x, y) in enumerate(mydataloader, start=1):
# 调用内部训练函数
myloss = Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss) # 返回得到的损失
print_loss_total += myloss
plot_loss_total += myloss
# 计算辅助信息
# 计算打印屏幕间隔损失-每隔1000次 # 计算画图间隔损失-每隔100次
if item % print_interval_num == 0:
print_loss_avg = print_loss_total / print_interval_num
# 将总损失归0
print_loss_total = 0
# 打印日志,日志内容分别是:训练耗时,当前迭代步,当前进度百分比,当前平均损失
print('轮次%d 损失%.6f 时间:%d' % (epoch_idx, print_loss_avg, time.time() - starttime))
if item % plot_interval_num == 0:
# 通过总损失除以间隔得到平均损失
plot_loss_avg = plot_loss_total / plot_interval_num
# 将平均损失添加plot_loss_list列表中
plot_loss_list.append(plot_loss_avg)
# 总损失归0
plot_loss_total = 0
if item == 10:
break
# 每个轮次保存模型 torch.save(my_encoderrnn.state_dict(), PATH1)
torch.save(my_encoderrnn.state_dict(), './my_encoderrnn_%d.pth' % epoch_idx)
torch.save(my_attndecoderrnn.state_dict(), './my_attndecoderrnn_%d.pth' % epoch_idx)
# 所有轮次训练完毕 画损失图
# 所有轮次训练完毕 画损失图 plt.figure() .plot(plot_loss_list) .save('x.png') .show()
plt.figure()
plt.plot(plot_loss_list)
plt.savefig('./s2sq_loss.png')
plt.show()
上面代码是使用真实的y作为下一个输入,这种方式称为Teacher Forcing
以下是 Teacher Forcing、Free-Running 和 Scheduled Sampling 的具体数学公式和代码实现的详细解释。
1. Teacher Forcing
数学公式
在 Teacher Forcing 中,解码器在每一步的输入是真实标签(ground truth)。假设解码器的输入序列为 y = ( y 1 , y 2 , … , y T ) y = (y_1, y_2, \dots, y_T) y=(y1,y2,…,yT),则解码器的输入和输出关系如下:
-
输入:
- 初始输入是起始符 y 0 = SOS y_0 = \text{SOS} y0=SOS。
- 第 t t t 步的输入是真实标签 y t − 1 y_{t-1} yt−1。
-
输出:
- 解码器在第 t t t步的输出是 y ^ t \hat{y}_t y^t,即对 y t y_t yt的预测。
代码实现
def Train_Iters_TeacherForcing(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss):
# 1 编码
encode_hidden = my_encoderrnn.inithidden()
encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 2 解码参数准备
encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
for idx in range(encode_output.shape[1]):
encode_output_c[idx] = encode_output[0, idx]
decode_hidden = encode_hidden
input_y = torch.tensor([[SOS_token]], device=device) # 初始输入是 SOS_token
myloss = 0.0
y_len = y.shape[1]
for idx in range(y_len):
# 解码器前向传播
output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算真实值
target_y = y[0][idx].view(1) # [1,]
myloss += mycrossentropyloss(output_y, target_y) # 计算损失
# Teacher Forcing: 使用真实标签作为下一步的输入
input_y = y[0][idx].view(1, -1)
# 梯度清零
myadam_encode.zero_grad()
myadam_decode.zero_grad()
# 反向传播
myloss.backward()
# 梯度更新
myadam_encode.step()
myadam_decode.step()
return myloss.item() / y_len
2. Free-Running
数学公式
在 Free-Running 中,解码器在每一步的输入是上一步的预测结果。假设解码器的输入序列为 y = ( y 1 , y 2 , … , y T ) y = (y_1, y_2, \dots, y_T) y=(y1,y2,…,yT),则解码器的输入和输出关系如下:
-
输入:
- 初始输入是起始符 y 0 = SOS y_0 = \text{SOS} y0=SOS。
- 第 t t t步的输入是上一步的预测结果 y ^ t − 1 \hat{y}_{t-1} y^t−1。
-
输出:
- 解码器在第 t t t步的输出是 y ^ t \hat{y}_t y^t,即对 y t y_t yt的预测。
代码实现
def Train_Iters_FreeRunning(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss):
# 1 编码
encode_hidden = my_encoderrnn.inithidden()
encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 2 解码参数准备
encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
for idx in range(encode_output.shape[1]):
encode_output_c[idx] = encode_output[0, idx]
decode_hidden = encode_hidden
input_y = torch.tensor([[SOS_token]], device=device) # 初始输入是 SOS_token
myloss = 0.0
y_len = y.shape[1]
for idx in range(y_len):
# 解码器前向传播
output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算真实值
target_y = y[0][idx].view(1) # [1,]
myloss += mycrossentropyloss(output_y, target_y) # 计算损失
# Free-Running: 使用解码器的预测结果作为下一步的输入
topv, topi = output_y.topk(1) # 取概率最高的 token
input_y = topi.detach() # 分离计算图,避免梯度传播到上一步
# 梯度清零
myadam_encode.zero_grad()
myadam_decode.zero_grad()
# 反向传播
myloss.backward()
# 梯度更新
myadam_encode.step()
myadam_decode.step()
return myloss.item() / y_len
3. Scheduled Sampling
数学公式
在 Scheduled Sampling 中,解码器在每一步的输入是真实标签或预测结果的动态组合。假设解码器的输入序列为 y = ( y 1 , y 2 , … , y T ) y = (y_1, y_2, \dots, y_T) y=(y1,y2,…,yT),则解码器的输入和输出关系如下:
-
输入:
- 初始输入是起始符 y 0 = SOS y_0 = \text{SOS} y0=SOS。
- 第 t 步的输入以概率 p 使用真实标签 y t − 1 y_{t-1} yt−1,以概率 ( 1 − p ) (1-p) (1−p)使用预测结果 y ^ t − 1 \hat{y}_{t-1} y^t−1。
-
输出:
- 解码器在第 t 步的输出是 y ^ t \hat{y}_t y^t ,即对 y t y_t yt 的预测。
代码实现
def Train_Iters_ScheduledSampling(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss, teacher_forcing_ratio):
# 1 编码
encode_hidden = my_encoderrnn.inithidden()
encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 2 解码参数准备
encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
for idx in range(encode_output.shape[1]):
encode_output_c[idx] = encode_output[0, idx]
decode_hidden = encode_hidden
input_y = torch.tensor([[SOS_token]], device=device)
myloss = 0.0
y_len = y.shape[1]
for idx in range(y_len):
# 解码器前向传播
output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算真实值
target_y = y[0][idx].view(1) # [1,]
myloss += mycrossentropyloss(output_y, target_y) # 计算损失
# Scheduled Sampling: 动态选择输入
use_teacher_forcing = random.random() < teacher_forcing_ratio
if use_teacher_forcing:
input_y = y[0][idx].view(1, -1) # 使用真实标签
else:
topv, topi = output_y.topk(1) # 使用预测结果
input_y = topi.detach()
# 梯度清零
myadam_encode.zero_grad()
myadam_decode.zero_grad()
# 反向传播
myloss.backward()
# 梯度更新
myadam_encode.step()
myadam_decode.step()
return myloss.item() / y_len
总结
- Teacher Forcing:训练稳定,但训练和推理行为不一致。
- Free-Running:训练和推理行为一致,但训练可能不稳定。
- Scheduled Sampling:结合两者的优点,动态调整输入来源。