当前位置: 首页 > article >正文

seq2seq以及注意力机制

讲解seq2seq之前,首先讲解一下注意力机制

注意力机制

1. 注意力机制的核心思想

注意力机制的核心思想可以用一句话概括:根据当前任务的需要,动态地为输入序列中的每个部分分配不同的权重。这些权重决定了模型在生成输出时,应该更多地关注输入的哪些部分。

1.1 关键概念
  • 查询(Query):表示当前任务的需求。例如,在机器翻译中,查询是当前要生成的词。
  • 键(Key):表示输入序列的每个部分。例如,输入句子中的每个词。
  • 值(Value):表示输入序列的实际内容。通常,键和值可以是相同的。
  • 注意力分数(Attention Score):表示查询和键之间的相关性。
  • 注意力权重(Attention Weight):通过softmax函数将注意力分数归一化,表示每个输入部分的重要性。
  • 上下文向量(Context Vector):根据注意力权重对值进行加权求和,得到的结果。

2. 注意力机制的详细步骤

2.1 输入和输出
  • 输入序列(英文):["I", "love", "you"]
  • 输出序列(中文):["我", "爱", "你"]
2.2 具体步骤
  1. 编码输入序列

    • 使用一个编码器(如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"
  2. 生成查询向量

    • 在解码器的每一步,生成一个查询向量 q i q_i qi,表示当前任务的需求。例如,在生成 "我" 时,查询向量 q 1 q_1 q1 表示需要关注输入序列中与 "我" 相关的部分。
  3. 计算注意力分数

    • 对于输入序列中的每个编码向量 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 是可学习的参数。
  4. 计算注意力权重

    • 使用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)
      这些权重表示每个输入部分的重要性。
  5. 计算上下文向量

    • 使用注意力权重对编码向量进行加权求和,得到上下文向量 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 包含了模型当前需要关注的信息。
  6. 生成输出

    • 将上下文向量 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. 注意力机制的类型

  1. 加性注意力(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)
  2. 点积注意力(Dot-Product Attention)

    • 直接计算查询和键的点积。
    • 公式: e i j = q i T h j e_{ij} = q_i^T h_j eij=qiThj
  3. 自注意力(Self-Attention)

    • 在同一个序列内部计算注意力,用于捕捉序列内部的依赖关系。
    • 广泛应用于Transformer模型。
  4. 多头注意力(Multi-Head Attention)

    • 使用多个注意力头并行计算,捕捉不同的特征表示。
    • 每个头独立计算注意力,最后将结果拼接或加权求和。

seq2seq

讲解seq2seq模型时,以英文译法文为例子

1.模型架构

在这里插入图片描述

  • 思想:将输入序列映射为一个中间表示,然后再从这个中间表示生成目标序列
  • 包括三部分,分别是encoder(编码器)decoder(解码器)中间语义张量c
  • 编码流程:1个时间步1个时间步的编码,每个时间步有输出,最终组合成中间语义张量C
  • 解码流程:1个时间步1个时间步的解码

2.构建编码器

构建编码器比较简单,可以使用RNN、GRU、LSTM等。这里构建的编码器将不采用注意力机制。
在这里插入图片描述
编码器的构建就是一个简单的GRU模型,最终会有一个outputhidden。假设输入为"欢迎来北京",经分词、文本数值化、数值张量化后的结果为:

  • 欢迎–>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 ibmm 计算:
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
计算过程:
  1. 计算 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=[(11+23+35)(41+53+65)(12+24+36)(42+54+66)]
    结果:
    C 1 = [ 22 28 49 64 ] C_1 = \begin{bmatrix} 22 & 28 \\ 49 & 64 \end{bmatrix} C1=[22492864]

  2. 计算 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=[(77+89+911)(107+119+1211)(78+810+912)(108+1110+1212)]
    结果:
    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} KR21×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= k11k21k21,1k12k22k21,2k13k23k21,3
  • V(Value):句子中每个单词的词向量表示,形状为 (21, 3)

    • 假设 V ∈ R 21 × 3 V \in \mathbb{R}^{21 \times 3} VR21×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= v11v21v21,1v12v22v21,2v13v23v21,3

计算注意力权重
  • 使用矩阵乘法计算 Q ⋅ K T Q \cdot K^T QKT,得到相似度矩阵(这个也可以使用线性层训练得到分数)。

  • 公式:
    similarity = Q ⋅ K T \text{similarity} = Q \cdot K^T similarity=QKT

    • Q Q Q 的形状为 (1, 3)
    • K K K 的形状为 (21, 3)
    • K T K^T KT 的形状为 (3, 21)
    • 结果形状为 (1, 21)
  • 具体计算:
    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] k11k12k13k21k22k23k21,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(dk similarity)

    • similarity \text{similarity} similarity 的形状为 (1, 21)
    • attention_weights \text{attention\_weights} attention_weights 的形状为 (1, 21)
  • 具体计算:
    attention_weights = softmax ( similarity 3 ) \text{attention\_weights} = \text{softmax}\left(\frac{\text{similarity}}{\sqrt{3}}\right) attention_weights=softmax(3 similarity)

  • 示例:
    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_weightsV,得到加权后的值。

  • 公式:
    attention_output = attention_weights ⋅ V \text{attention\_output} = \text{attention\_weights} \cdot V attention_output=attention_weightsV

    • attention_weights \text{attention\_weights} attention_weights 的形状为 (1, 21)
    • V V V 的形状为 (21, 3)
    • 结果形状为 (1, 3)
  • 具体计算:
    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=[w1w2w21] v11v21v21,1v12v22v21,2v13v23v21,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.3a+0.5robot+


  • 模型将 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 ForcingFree-RunningScheduled 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),则解码器的输入和输出关系如下:

  1. 输入

    • 初始输入是起始符 y 0 = SOS y_0 = \text{SOS} y0=SOS
    • t t t 步的输入是真实标签 y t − 1 y_{t-1} yt1
  2. 输出

    • 解码器在第 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),则解码器的输入和输出关系如下:

  1. 输入

    • 初始输入是起始符 y 0 = SOS y_0 = \text{SOS} y0=SOS
    • t t t步的输入是上一步的预测结果 y ^ t − 1 \hat{y}_{t-1} y^t1
  2. 输出

    • 解码器在第 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),则解码器的输入和输出关系如下:

  1. 输入

    • 初始输入是起始符 y 0 = SOS y_0 = \text{SOS} y0=SOS
    • 第 t 步的输入以概率 p 使用真实标签 y t − 1 y_{t-1} yt1,以概率 ( 1 − p ) (1-p) (1p)使用预测结果 y ^ t − 1 \hat{y}_{t-1} y^t1
  2. 输出

    • 解码器在第 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:结合两者的优点,动态调整输入来源。

http://www.kler.cn/a/517968.html

相关文章:

  • Docker快速部署高效照片管理系统LibrePhotos搭建私有云相册
  • Vue.js组件开发-如何实现带有搜索功能的下拉框
  • SQL基础、函数、约束(MySQL第二期)
  • 【FreeRTOS 教程 四】队列创建与发布项目到队列
  • Ansible入门学习之基础元素介绍
  • Unity——从共享文件夹拉取资源到本地
  • js小游戏---2048(附源代码)
  • 支持大功率输出高速频闪的图像处理用光源控制器
  • 亿坊软件前端命名规范
  • windows在命令行中切换盘符
  • springboot中DTO、VO、Entity相互转换
  • 低代码系统-产品架构案例介绍、得帆云(九)
  • 如何用VSCODE配置C++多文件编译
  • three.js+WebGL踩坑经验合集(2):3D场景被相机裁切后,被裁切的部分依然可以被鼠标碰撞检测得到(射线检测)
  • 豆包MarsCode:小C的类二进制拼图
  • ansible自动化运维实战--yaml的使用和配置(7)
  • http请求获取客户端ip
  • Flink(十一): DataStream API (八) Checkpointing
  • Arduino大师练成手册 -- 读取DS18B20
  • MacOS安装Docker battery-historian
  • 编译安装PaddleClas@openKylin(失败,安装好后报错缺scikit-learn)
  • 知识体系_统计学_03_描述性统计_概括性度量
  • 2025数学建模美赛|B题成品论文
  • GraphRAG 简介
  • 「全网最细 + 实战源码案例」设计模式——原型模式
  • 使用 Docker Compose 一键启动 Redis、MySQL 和 RabbitMQ