Transformer代码从零解读【Pytorch官方版本】

news2024/11/19 2:28:38

文章目录

      • 1、Transformer大致有3大应用
      • 2、Transformer的整体结构图
      • 3、如何处理batch-size句子长度不一致问题
      • 4、MultiHeadAttention(多头注意力机制)
      • 5、前馈神经网络
      • 6、Encoder中的输入masked
      • 7、完整代码
      • 补充知识:

1、Transformer大致有3大应用

1、机器翻译类应用:Encoder和Decoder共同使用,
2、只使用Encoder端:文本分类BERT和图片分类VIT,
3、只使用Decoder端:生成类模型,

2、Transformer的整体结构图

Transformer整体结构有2个输入,1个输出,具体过程可参考这个链接:详情请点击,
如下图,左边是Encoder,右边是Decoder,2个输入分别是Encoder的输入,Decoder的输入,
先看左边的Encoder,输入经过词向量层和位置编码层,得到最终的输入,通过多头注意力机制和前馈神经网络得到Encoder的输出,该输出会与Decoder进行交互,
再看右边的Decoder,输入经过词向量层和位置编码层,得到最终的输入,通过掩码注意力机制,然后交互注意力机制与Encoder的输出做交互,Encoder的输出做K矩阵、V矩阵,Decoder的值做Q矩阵,再经过前馈神经网络层,得到Decoder的输出,

在这里插入图片描述

如下图一共有2个输入,分别是“我爱你”和“S I LOVE YOU”,“我爱你”这个句子是3个token,token翻译成词元,“S I LOVE YOU”中的 S 是特殊字符,“I LOVE YOU E”是解码端的真实标签,与输出结果计算损失,
解码端是没法并行的,因为输入【S】,输出【I】,然后输出的【I】作为下一阶段的输入,这一次的输入取决于上一次的输出,所以解码端无法并行,
但是为了加快训练速度和收敛速度,我们使用Teacher forcing,就是把真实标签作为一种输入,把当前输入单词后面所有的单词全部 mask 掉,

在这里插入图片描述

“ich mochte ein bier P”是编码端的德语输入,“S i want a beer"是解码端的英语输入,“i want a beer E”是解码端的真实标签,一般在训练时为了加快训练速度,需要增加batch-size,

在这里插入图片描述

3、如何处理batch-size句子长度不一致问题

以中文为例,batch-size为4,如下图所示每一行句子代表每个batch-size中的第一个句子,代表Encoding的输入,Decoding的输入和标签值下图已省略,
1个batch在被模型处理的时候,为了加快速度常使用矩阵的方式来计算,但是如果一个batch中句子长度不一致,就组不成一个有效的矩阵,为了解决这个问题,一个常规的操作就是给每个句子设置 max-length,

在这里插入图片描述

假设设置max-length为8,句子的长度大于8的删除,小于8的用P替换,如下图,
需要注意的是,PAD这种方法不仅用在在Encoder的输入,也用在Decoder的输入,

在这里插入图片描述

位置编码公式:
P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) PE(pos,2i)=sin(pos/10000^{2i/d_{model}}) PE(pos,2i)=sin(pos/100002i/dmodel)
P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d m o d e l ) PE(pos,2i+1)=cos(pos/10000^{2i/d_{model}}) PE(pos,2i+1)=cos(pos/100002i/dmodel)
两个共有的部分: e − ( 2 i ) / d m o d e l ∗ l o g ( 10000 ) = 1 / 1000 0 2 i / d m o d e l e^{-(2i)/d_{model}*log(10000)}=1/10000^{2i/d_{model}} e(2i)/dmodellog(10000)=1/100002i/dmodel,这里POS代表的是每个字符在整个句子中的索引,512是整个句子最大长度,和2i对应的Embedding维度512要区分开,位置编码和Embedding相加即可得到整个输出的内容,

位置编码公式代码如下:

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()

        # 位置编码的实现其实很简单,直接对照着公式去敲代码就可以,下面这个代码只是其中一种实现方式;
        # 从理解上来讲,需要注意的就是偶数和奇数在公式上有一个共同部分,我们使用log函数把次方拿下来,方便计算;
        # pos代表的是单词在句子中的索引,这点需要注意;比如max_len是128个,那么索引就是从0,1,2,...,127
        # 假设我的d_model是512,2i那个符号中i从0取到了255,那么2i对应取值就是0,2,4,...,510
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)  # shape:[max_len,1]
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))  # shape:[d_model/2]
        pe[:, 0::2] = torch.sin(position * div_term)  # 这里需要注意的是pe[:, 0::2]这个用法,就是从0开始到最后面,步长为2,其实代表的就是偶数位置
        pe[:, 1::2] = torch.cos(position * div_term)  # 这里需要注意的是pe[:, 1::2]这个用法,就是从1开始到最后面,步长为2,其实代表的就是奇数位置
        # 上面代码获取之后得到的pe.shape:[max_len, d_model]

        # 下面这个代码之后,我们得到的pe形状是:[max_len, 1, d_model]
        pe = pe.unsqueeze(0).transpose(0, 1)

        self.register_buffer('pe', pe)  # 定一个缓冲区,其实简单理解为这个参数不更新就可以

    def forward(self, x):
        """
        x: [src_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

为什么需要告诉后面模型哪些位置被PAD填充

注意力机制公式:

A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dk QKT)V

下图是 Q K T QK^T QKT相乘后的矩阵,还没有经过softmax计算,表示每个单词和其他所有单词的相似性,应该能看到不应该把PAD参与计算,

在这里插入图片描述

如何去掉PAD信息?利用符号矩阵,不是PAD置为0,是PAD的置为1,

在这里插入图片描述

代码:

把PAD为0的元素置为True,

# 比如说,我现在的句子长度是5,在后面注意力机制的部分,我们在计算出来QK转置除以根号之后,softmax之前,我们得到的形状
# len_input * len*input  代表每个单词对其余包含自己的单词的影响力

# 所以这里我需要有一个同等大小形状的矩阵,告诉我哪个位置是PAD部分,之后在计算计算softmax之前会把这里置为无穷大;

# 一定需要注意的是这里得到的矩阵形状是batch_size x len_q x len_k,我们是对k中的pad符号进行标识,并没有对k中的做标识,因为没必要

# seq_q 和 seq_k 不一定一致,在交互注意力,q来自解码端,k来自编码端,所以告诉模型编码这边pad符号信息就可以,解码端的pad信息在交互注意力层是没有用到的;

def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k, one is masking
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k

4、MultiHeadAttention(多头注意力机制)

如下图,batch-size为1,src_len为2(即有2个单词), d m o d e l d_{model} dmodel为4,

在这里插入图片描述
代码:

class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        # 输入进来的QKV是相等的,我们会使用映射linear做一个映射得到参数矩阵Wq, Wk, Wv
        self.W_Q = nn.Linear(d_model, d_k * n_heads)
        self.W_K = nn.Linear(d_model, d_k * n_heads)
        self.W_V = nn.Linear(d_model, d_v * n_heads)
        self.linear = nn.Linear(n_heads * d_v, d_model)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, Q, K, V, attn_mask):
        # 这个多头分为这几个步骤,首先映射分头,然后计算atten_scores,然后计算atten_value;
        # 输入进来的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model],👇
        # V: [batch_size x len_k x d_model], attn_mask.shape:[batch_size, src_len_q, src_len_k]
        residual, batch_size = Q, Q.size(0)
        # (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)

        # 下面这个就是先映射,后分头;一定要注意的是q和k分头之后维度是一致的,所以一看这里都是dk
        q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,
                                                                       2)  # q_s: [batch_size x n_heads x len_q x d_k]
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,
                                                                       2)  # k_s: [batch_size x n_heads x len_k x d_k]
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,
                                                                       2)  # v_s: [batch_size x n_heads x len_k x d_v]

        # 输入进行的attn_mask形状是 batch_size x len_q x len_k,👇
        # 然后经过下面这个代码得到 新的attn_mask : [batch_size x n_heads x len_q x len_k],
        # 就是把pad信息重复了n个头上
        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)

        # 然后我们计算 ScaledDotProductAttention 这个函数,去7.看一下
        # 得到的结果有两个:context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q x len_k]
        context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
        # context: [batch_size x len_q x n_heads * d_v]
        context = context.transpose(1, 2).contiguous().view(batch_size, -1,
                                                            n_heads * d_v)
        output = self.linear(context)
        return self.layer_norm(output + residual), attn  # output: [batch_size x len_q x d_model]

Encoder代码:

class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, enc_inputs, enc_self_attn_mask):
        # 下面这个就是做自注意力层,输入是enc_inputs,形状是[batch_size x seq_len_q x d_model] 需要注意的是最初始的QKV矩阵是等同于这个输入的,
        # 去看一下enc_self_attn函数 6.
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,
                                               enc_self_attn_mask)  # enc_inputs to same Q,K,V
        enc_outputs = self.pos_ffn(enc_outputs)  # enc_outputs: [batch_size x len_q x d_model]
        return enc_outputs, attn


def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k, one is masking
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k


class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()

        # 位置编码的实现其实很简单,直接对照着公式去敲代码就可以,下面这个代码只是其中一种实现方式;
        # 从理解上来讲,需要注意的就是偶数和奇数在公式上有一个共同部分,我们使用log函数把次方拿下来,方便计算;
        # pos代表的是单词在句子中的索引,这点需要注意;比如max_len是128个,那么索引就是从0,1,2,...,127
        # 假设我的d_model是512,2i那个符号中i从0取到了255,那么2i对应取值就是0,2,4,...,510
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)  # 这里需要注意的是pe[:, 0::2]这个用法,就是从0开始到最后面,补长为2,其实代表的就是偶数位置
        pe[:, 1::2] = torch.cos(position * div_term)  # 这里需要注意的是pe[:, 1::2]这个用法,就是从1开始到最后面,补长为2,其实代表的就是奇数位置
        # 上面代码获取之后得到的pe:[max_len*d_model]

        # 下面这个代码之后,我们得到的pe形状是:[max_len*1*d_model]
        pe = pe.unsqueeze(0).transpose(0, 1)

        self.register_buffer('pe', pe)  # 定一个缓冲区,其实简单理解为这个参数不更新就可以

    def forward(self, x):
        """
        x: [seq_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)


class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        # nn.Embedding:https://blog.csdn.net/raelum/article/details/125462028?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171029892316800211559494%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=171029892316800211559494&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-125462028-null-null.142^v99^pc_search_result_base8&utm_term=nn.Embedding&spm=1018.2226.3001.4187
        self.src_emb = nn.Embedding(src_vocab_size, d_model)  # 这个其实就是去定义生成一个矩阵,大小是 src_vocab_size * d_model,👇
        # src_vocab_size=src_len,d_model是词向量的维度,
        self.pos_emb = PositionalEncoding(d_model)  # 位置编码情况,这里是固定的正余弦函数,也可以使用类似词向量的nn.Embedding获得一个可以更新学习的位置编码
        self.layers = nn.ModuleList(
            [EncoderLayer() for _ in range(n_layers)])  # 使用ModuleList对多个encoder进行堆叠,因为后续的encoder并没有使用词向量和位置编码,所以抽离出来;

    def forward(self, enc_inputs):
        # 这里我们的 enc_inputs 形状是: [batch_size * src_len]

        # 下面这个代码通过src_emb进行索引定位,enc_outputs输出形状是[batch_size, src_len, d_model]
        enc_outputs = self.src_emb(enc_inputs)

        # 这里就是位置编码,把两者相加放入到了这个函数里面,从这里可以去看一下位置编码函数的实现;3.
        enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)

        # get_attn_pad_mask是为了得到句子中pad的位置信息,给到模型后面,在计算自注意力和交互注意力的时候去掉pad符号的影响,去看一下这个函数 4.
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
        enc_self_attns = []
        for layer in self.layers:
            # 去看EncoderLayer 层函数 5.
            enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
            enc_self_attns.append(enc_self_attn)
        return enc_outputs, enc_self_attns

5、前馈神经网络

代码:

class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)
        self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, inputs):
        residual = inputs  # inputs : [batch_size, len_q, d_model]
        output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))
        output = self.conv2(output).transpose(1, 2)
        return self.layer_norm(output + residual)

6、Encoder中的输入masked

如果当前输入为【S】,则后面的【卷起来】被遮挡,当输入为【S卷】时,后面的【起来】被遮挡,形成上三角矩阵为1的矩阵,

在这里插入图片描述

7、完整代码

# from https://github.com/graykode/nlp-tutorial/tree/master/5-1.Transformer

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import math


def make_batch(sentences):
    input_batch = [[src_vocab[n] for n in sentences[0].split()]]
    output_batch = [[tgt_vocab[n] for n in sentences[1].split()]]
    target_batch = [[tgt_vocab[n] for n in sentences[2].split()]]
    return torch.LongTensor(input_batch), torch.LongTensor(output_batch), torch.LongTensor(target_batch)


# 10
def get_attn_subsequent_mask(seq):
    """
    seq: [batch_size, tgt_len]
    """
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
    # attn_shape: [batch_size, tgt_len, tgt_len]
    subsequence_mask = np.triu(np.ones(attn_shape), k=1)  # 生成一个上三角矩阵
    subsequence_mask = torch.from_numpy(subsequence_mask).byte()
    return subsequence_mask  # [batch_size, tgt_len, tgt_len]


# 7. ScaledDotProductAttention
class ScaledDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()

    def forward(self, Q, K, V, attn_mask):
        # 输入进来的维度分别是 [batch_size x n_heads x len_q x d_k]  K: [batch_size x n_heads x len_k x d_k]👇
        # V: [batch_size x n_heads x len_k x d_v], attn_mask : [batch_size x n_heads x len_q x len_k]
        # 首先经过matmul函数得到的scores形状是 : [batch_size x n_heads x len_q x len_k]
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)

        # 然后关键词地方来了,下面这个就是用到了我们之前重点讲的attn_mask,把被mask的地方置为无限小,softmax之后基本就是0,对q的单词不起作用
        scores.masked_fill_(attn_mask, -1e9)  # Fills elements of self tensor with value where mask is one.
        attn = nn.Softmax(dim=-1)(scores)
        context = torch.matmul(attn, V)
        return context, attn


# 6. MultiHeadAttention
class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        # 输入进来的QKV是相等的,我们会使用映射linear做一个映射得到参数矩阵Wq, Wk, Wv
        self.W_Q = nn.Linear(d_model, d_k * n_heads)
        self.W_K = nn.Linear(d_model, d_k * n_heads)
        self.W_V = nn.Linear(d_model, d_v * n_heads)
        self.linear = nn.Linear(n_heads * d_v, d_model)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, Q, K, V, attn_mask):
        # 这个多头分为这几个步骤,首先映射分头,然后计算atten_scores,然后计算atten_value;
        # 输入进来的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model],👇
        # V: [batch_size x len_k x d_model], attn_mask.shape:[batch_size, src_len_q, src_len_k]
        residual, batch_size = Q, Q.size(0)
        # (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)

        # 下面这个就是先映射,后分头;一定要注意的是q和k分头之后维度是一致的,所以一看这里都是dk
        q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,
                                                                       2)  # q_s: [batch_size x n_heads x len_q x d_k]
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,
                                                                       2)  # k_s: [batch_size x n_heads x len_k x d_k]
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,
                                                                       2)  # v_s: [batch_size x n_heads x len_k x d_v]

        # 输入进行的attn_mask形状是 batch_size x len_q x len_k,👇
        # 然后经过下面这个代码得到 新的attn_mask : [batch_size x n_heads x len_q x len_k],
        # 就是把pad信息重复了n个头上
        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)

        # 然后我们计算 ScaledDotProductAttention 这个函数,去7.看一下
        # 得到的结果有两个:context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q x len_k]
        context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
        # context: [batch_size x len_q x n_heads * d_v]
        context = context.transpose(1, 2).contiguous().view(batch_size, -1,
                                                            n_heads * d_v)
        output = self.linear(context)
        return self.layer_norm(output + residual), attn  # output: [batch_size x len_q x d_model]


# 8. PoswiseFeedForwardNet
class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)
        self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, inputs):
        residual = inputs  # inputs : [batch_size, len_q, d_model]
        output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))
        output = self.conv2(output).transpose(1, 2)
        return self.layer_norm(output + residual)


# 4. get_attn_pad_mask

# 比如说,我现在的句子长度是5,在后面注意力机制的部分,我们在计算出来QK转置除以根号之后,softmax之前,我们得到的形状
# len_input * len*input  代表每个单词对其余包含自己的单词的影响力

# 所以这里我需要有一个同等大小形状的矩阵,告诉我哪个位置是PAD部分,之后在计算计算softmax之前会把这里置为无穷大;

# 一定需要注意的是这里得到的矩阵形状是batch_size x len_q x len_k,我们是对k中的pad符号进行标识,并没有对k中的做标识,因为没必要

# seq_q 和 seq_k 不一定一致,在交互注意力,q来自解码端,k来自编码端,所以告诉模型编码这边pad符号信息就可以,解码端的pad信息在交互注意力层是没有用到的;

def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # shape:[batch_size, 1, len_k], one is masking
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # shape:[batch_size, len_q, len_k]


# 3. PositionalEncoding 代码实现
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()

        # 位置编码的实现其实很简单,直接对照着公式去敲代码就可以,下面这个代码只是其中一种实现方式;
        # 从理解上来讲,需要注意的就是偶数和奇数在公式上有一个共同部分,我们使用log函数把次方拿下来,方便计算;
        # pos代表的是单词在句子中的索引,这点需要注意;比如max_len是128个,那么索引就是从0,1,2,...,127
        # 假设我的d_model是512,2i那个符号中i从0取到了255,那么2i对应取值就是0,2,4,...,510
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)  # shape:[max_len,1]
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))  # shape:[d_model/2]
        pe[:, 0::2] = torch.sin(position * div_term)  # 这里需要注意的是pe[:, 0::2]这个用法,就是从0开始到最后面,步长为2,其实代表的就是偶数位置
        pe[:, 1::2] = torch.cos(position * div_term)  # 这里需要注意的是pe[:, 1::2]这个用法,就是从1开始到最后面,步长为2,其实代表的就是奇数位置
        # 上面代码获取之后得到的pe.shape:[max_len, d_model]

        # 下面这个代码之后,我们得到的pe形状是:[max_len, 1, d_model]
        pe = pe.unsqueeze(0).transpose(0, 1)

        self.register_buffer('pe', pe)  # 定一个缓冲区,其实简单理解为这个参数不更新就可以

    def forward(self, x):
        """
        x: [src_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)


# 5. EncoderLayer :包含两个部分,多头注意力机制和前馈神经网络
class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, enc_inputs, enc_self_attn_mask):
        # 下面这个就是做自注意力层,输入是enc_inputs,形状是[batch_size, seq_len_q, d_model], 需要注意的是最初始的QKV矩阵是等同于这个输入的,
        # 去看一下enc_self_attn函数 6.
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,
                                               enc_self_attn_mask)  # enc_inputs to same Q,K,V
        enc_outputs = self.pos_ffn(enc_outputs)  # enc_outputs: [batch_size x len_q x d_model]
        return enc_outputs, attn


# 2. Encoder 部分包含三个部分:词向量embedding,位置编码部分,注意力层及后续的前馈神经网络
class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        # nn.Embedding:https://blog.csdn.net/raelum/article/details/125462028?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171029892316800211559494%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=171029892316800211559494&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-125462028-null-null.142^v99^pc_search_result_base8&utm_term=nn.Embedding&spm=1018.2226.3001.4187
        self.src_emb = nn.Embedding(src_vocab_size, d_model)  # 这个其实就是去定义生成一个矩阵,大小是 src_vocab_size * d_model,👇
        # src_vocab_size=src_len,d_model是词向量的维度,
        self.pos_emb = PositionalEncoding(d_model)  # 位置编码情况,这里是固定的正余弦函数,也可以使用类似词向量的nn.Embedding获得一个可以更新学习的位置编码
        self.layers = nn.ModuleList(
            [EncoderLayer() for _ in range(n_layers)])  # 使用ModuleList对多个encoder进行堆叠,因为后续的encoder并没有使用词向量和位置编码,所以抽离出来;

    def forward(self, enc_inputs):
        # 这里我们的 enc_inputs 形状是: [batch_size, src_len]

        # 下面这个代码通过src_emb进行索引定位,enc_outputs输出形状是[batch_size, src_len, d_model]
        enc_outputs = self.src_emb(enc_inputs)

        # 这里就是位置编码,把两者相加放入到了这个函数里面,从这里可以去看一下位置编码函数的实现;3.
        # enc_outputs.shape:[batch_size, src_len, d_model]
        enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)

        # get_attn_pad_mask是为了得到句子中pad的位置信息,给到模型后面,在计算自注意力和交互注意力的时候去掉pad符号的影响,去看一下这个函数 4.
        # enc_self_attn_mask.shape:[batch_size, src_len_q, src_len_k]
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
        enc_self_attns = []
        for layer in self.layers:
            # 去看EncoderLayer 层函数 5.
            enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
            enc_self_attns.append(enc_self_attn)
        return enc_outputs, enc_self_attns


# 10.
class DecoderLayer(nn.Module):
    def __init__(self):
        super(DecoderLayer, self).__init__()
        self.dec_self_attn = MultiHeadAttention()
        self.dec_enc_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):
        dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask)
        dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask)
        dec_outputs = self.pos_ffn(dec_outputs)
        return dec_outputs, dec_self_attn, dec_enc_attn


# 9. Decoder
class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
        self.pos_emb = PositionalEncoding(d_model)
        self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

    def forward(self, dec_inputs, enc_inputs, enc_outputs):  # dec_inputs : [batch_size x target_len]
        dec_outputs = self.tgt_emb(dec_inputs)  # [batch_size, tgt_len, d_model]
        dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1)  # [batch_size, tgt_len, d_model]

        # get_attn_pad_mask 自注意力层的时候的pad部分
        dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)

        # get_attn_subsequent_mask 这个做的是自注意层的mask部分,就是当前单词之后看不到,使用一个上三角为1的矩阵
        dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)

        # 两个矩阵相加,大于0的为1,不大于0的为0,为1的在之后就会被fill到无限小
        dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequent_mask), 0)

        # 这个做的是交互注意力机制中的mask矩阵,enc的输入是k,我去看这个k里面哪些是pad符号,给到后面的模型;注意哦,我q肯定也是有pad符号,但是这里我不在意的,👇
        # 之前说了好多次了哈
        dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)

        dec_self_attns, dec_enc_attns = [], []
        for layer in self.layers:
            dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask,
                                                             dec_enc_attn_mask)
            dec_self_attns.append(dec_self_attn)
            dec_enc_attns.append(dec_enc_attn)
        return dec_outputs, dec_self_attns, dec_enc_attns


# 1. 从整体网路结构来看,分为三个部分:编码层,解码层,输出层
class Transformer(nn.Module):
    def __init__(self):
        super(Transformer, self).__init__()
        self.encoder = Encoder()  # 编码层
        self.decoder = Decoder()  # 解码层
        self.projection = nn.Linear(d_model, tgt_vocab_size,
                                    bias=False)  # 输出层d_model是我们解码层每个token输出的维度大小,之后会做一个tgt_vocab_size大小的softmax

    def forward(self, enc_inputs, dec_inputs):
        # 这里有两个数据进行输入,一个是enc_inputs,形状为[batch_size, src_len],主要是作为编码段的输入,一个是dec_inputs,形状为[batch_size, tgt_len],👇
        # 主要是作为解码端的输入;
        # enc_inputs作为输入,形状为[batch_size, src_len],输出由自己的函数内部指定,想要输出什么就可以指定输出什么,可以是全部tokens的输出,可以是特定每一层的输出;👇
        # 也可以是中间某些参数的输出;
        # enc_outputs就是主要的输出,enc_self_attns这里没记错的是QK转置相乘之后softmax之后的矩阵值,代表的是每个单词和其他单词相关性;
        enc_outputs, enc_self_attns = self.encoder(enc_inputs)

        # dec_outputs 是decoder主要输出,用于后续的linear映射; dec_self_attns类比于enc_self_attns,是查看每个单词对decoder中输入的其余单词的相关性;
        # dec_enc_attns是decoder中每个单词对encoder中每个单词的相关性;
        dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)

        # dec_outputs做映射到词表大小
        dec_logits = self.projection(dec_outputs)  # dec_logits : [batch_size x src_vocab_size x tgt_vocab_size]
        return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns


if __name__ == '__main__':

    # 句子的输入部分,P是指PAD
    # 这3个句子分别代表Encoding的输入,Decoding的输入,Decoding的真实标签,
    sentences = ['ich mochte ein bier P', 'S i want a beer', 'i want a beer E']

    # Transformer Parameters
    # Padding Should be Zero
    # 构建Encoding词表(Vocabulary)
    src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4}
    src_vocab_size = len(src_vocab)

    # 构建Decoding词表(Vocabulary)
    tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}
    tgt_vocab_size = len(tgt_vocab)

    src_len = 5  # length of source
    tgt_len = 5  # length of target

    # 模型参数
    d_model = 512  # Embedding Size,每个字符转换为Embedding的大小
    d_ff = 2048  # FeedForward dimension
    d_k = d_v = 64  # dimension of K(=Q), V
    n_layers = 6  # number of Encoder of Decoder Layer
    n_heads = 8  # number of heads in Multi-Head Attention

    model = Transformer()

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    enc_inputs, dec_inputs, target_batch = make_batch(sentences)

    for epoch in range(20):
        optimizer.zero_grad()
        outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
        loss = criterion(outputs, target_batch.contiguous().view(-1))
        print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
        loss.backward()
        optimizer.step()

补充知识:

torch.repeat和torch.expand的区别,

代码:

import torch
a=torch.arange(0, 9).view(3, 3)
print(a)
b=a.eq(0)
print(b)
c=b.unsqueeze(1)
print(c)
d=c.expand(3, 3, 3)
print(d)
e=c.repeat(1, 3, 1)
print(e)

输出:

tensor([[0, 1, 2],
        [3, 4, 5],
        [6, 7, 8]])
tensor([[ True, False, False],
        [False, False, False],
        [False, False, False]])
tensor([[[ True, False, False]],

        [[False, False, False]],

        [[False, False, False]]])
tensor([[[ True, False, False],
         [ True, False, False],
         [ True, False, False]],

        [[False, False, False],
         [False, False, False],
         [False, False, False]],

        [[False, False, False],
         [False, False, False],
         [False, False, False]]])
tensor([[[ True, False, False],
         [ True, False, False],
         [ True, False, False]],

        [[False, False, False],
         [False, False, False],
         [False, False, False]],

        [[False, False, False],
         [False, False, False],
         [False, False, False]]])

参考:
1、nn.Embedding:CSDN链接,
2、哔哩哔哩视频,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1521417.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

由浅到深认识C语言(6):变量的存储类型

该文章Github地址:https://github.com/AntonyCheng/c-notes 在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn…

代码随想录算法训练营第40天 | 343. 整数拆分 ,96.不同的二叉搜索树

动态规划章节理论基础: https://programmercarl.com/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 343. 整数拆分 题目链接:https://leetcode.cn/problems/integer-break/ 思路: 动规五部曲&#xf…

【复现】通天星CMS 安全监控云平台 SQL注入漏洞_64

目录 一.概述 二 .漏洞影响 三.漏洞复现 1. 漏洞一: 四.修复建议: 五. 搜索语法: 六.免责声明 一.概述 通天星CMSV6拥有以位置服务、无线3G/4G视频传输、云存储服务为核心的研发团队,专注于为定位、无线视频终端产品提供平…

sqllab第三关通关笔记

知识点: 通过回显的信息判断原始语句的组成猜测该语句为 select 1,2,3 from 表名 where id (输入) limit 0,1 首先通过测试判断存在什么类型的sql注入 构造id1/0 发现正常输出;说明是字符型的sql注入 好了,下面就测试有什么限制条件 构造…

论文阅读——MoCo

Momentum Contrast for Unsupervised Visual Representation Learning 动量在数学上理解为加权移动平均: yt-1是上一时刻输出,xt是当前时刻输入,m是动量,不想让当前时刻输出只依赖于当前时刻的输入,m很大时&#xff0…

使用Laravel开发项目

如何使用Laravel框架开发项目 一、安装Laravel框架 1.在安装Laravel框架钱我们需要先查看要安装的Laravel框架版本以及版本所需要的安装运行条件。 2.配置好安装环境后再安装Laravel框架 2.1.配置安装环境 1)PHP版本 2)PHP OpenSSL扩展 3&#xff…

Ubuntu查看ros版本-linux查看ros版本

使用ros带的rosversion命令即可查看自己的ros版本: rosversion -d

C语言葵花宝典之——文件操作

前言: 在之前的学习中,我们所写的C语言程序总是在运行结束之后,就会自动销毁,那如果我们想将一个结果进行长期存储应该如何操作呢?这时候就需要我们用文件来操作。 目录 1、什么是文件? 1.1 程序文件 1.2…

C语言 之 结构体超详细总结

🌈 个人主页:白子寰 🔥 分类专栏:魔法指针,进阶C,C语言,C语言题集,C语言实现游戏👈 希望得到您的订阅和支持~ 💡 坚持创作博文(平均质量分78.8),分…

docker容器镜像管理

目录 一、 Docker的基本组成 二、 容器和镜像的关系 2.1 面向对象角度 2.2 从镜像容器角度 三、镜像命令 3.1 查看当前已有镜像 3.2 查看已有的全部镜像 3.3 查看镜像ID 3.4 镜像删除 四、 容器命令 4.1 下载镜像 4.2 新建和启动容器 run 4.3 交互式 4.…

【体验有奖】用 AI 画春天,函数计算搭建 Stable Diffusion WebUI

人工智能生成内容 AIGC(Artificial Intelligence Generated Content)是当下备受关注的概念之一,是继 PGC 和 UGC 之后的新型生产方式。AIGC 技术的核心思想是利用人工智能算法生成具有一定创意和质量的内容。例如,根据用户的描述或…

字符分类函数(iscntrl、i是space.....)---c语言

目录 一、定义二、字符分类函数2.1 -iscntrl()2.1.1定义2.1.2使用举例 2.2 -isspace()2.2.1描述2.2.2使用举例 2.3-isdigit()2.3.1描述2.3.2使用举例 2.4-isxdigit()2.4.1描述 2.5-islower()2.5.1描述2.5.2使用举例 2.6-isupper()…

jenkins容器中安装python遇到问题

在Jenkins容器中安装配置Python时遇到问题 执行./configure --prefix/opt/python3/时遇到configure: error: no acceptable C compiler found in $PATH 这个问题就是缺少gcc编译环境。将gcc安装上即可: yum install -y gcc##前提是容器里的系统是cenos才可以&#…

专升本 C语言笔记-06 常用的3种输入输出函数

1.scanf() 与 printf() 的使用 scanf() 格式化输入数据 格式:scanf("格式控制字符串",参数地址列表) scanf("%d,%d,%d",&a,&b,&c); printf("a %d\n",a); printf("b %d\n",b); printf("c %d\n",c); 注意 注…

(网络安全)一款强大的逆向分析工具,开源!

工具介绍 Ghidra 是由美国国家安全局(NSA)研究部门开发的软件逆向工程(SRE)套件,用于支持网络安全任务。包括一套功能齐全的高端软件分析工具,使用户能够在各种平台(Windows、Mac OS和Linux)分析编译后的代…

详解VXLAN

海翎光电的小编今天为大家介绍了什么是VXLAN,以及VXLAN的基本概念和工作原理。 什么是VXLAN VXLAN(Virtual eXtensible Local Area Network,虚拟扩展局域网),是由IETF定义的NVO3(Network Virtualization ov…

深入解析FastAPI多线程:加速代码执行效率

在现代网络应用中,高性能和快速响应是至关重要的,Python 的 FastAPI 框架以其出色的性能和简单易用的特点,成为了许多开发者的首选。然而,在某些场景下,单线程运行可能无法满足需求,这时候就需要考虑使用多…

RPC通信原理(一)

RPC通信原理 RPC的概念 如果现在我有一个电商项目,用户要查询订单,自然而然是通过Service接口来调用订单的实现类。 我们把用户模块和订单模块都放在一起,打包成一个war包,然后再tomcat上运行,tomcat占有一个进程&am…

vue模板语法介绍及内置指令用法

1、文本差值(大括号、v-text、v-htm指令) 最基本的数据绑定就是文本差值,格式为“Mustache”语法(双大括号); 双大括号只能解析文本不能解析html,如需解析html则需使用v-html指令;…

【四 (2)数据可视化之 Matplotlib 常用图表及代码实现 】

目录 文章导航一、介绍二、安装Matplotlib三、导入Matplotlib四、设置可以中文显示四、常用图形1、散点图(Scatter Plot)2.1、线性图(Line Plot)2.2、堆叠折线图2.3、多图例折线图3.1、柱状图/条形图(Bar Chart&#x…