• 【NLP Learning】Transformer Encoder续集之网络结构源码解读


    从源码解读Encoder内部原理机制

    上一篇文章中我们详细了解了Encoder内部三种重要机制的原理,包括mask、Embedding、scaled。这篇文章我们主要从Transformer的Encoder源码入手,读懂Encoder的结构。

    在这里插入图片描述

    1 模拟数据及超参数设定

    考虑到本次编码的主要目的在于对Transformer的编码器结构进行全面理解,所以对于模拟数据和超参数进行了比较简单的设计,模型参数解释及对应设置如表1.1所示

    编码器端的模拟数据输入主要是字典序的三句话,并且输入时就已经对输入序列进行了对齐(使用P占位),其主体内容如代码片段1.1所示。

    # 介绍:将文本数据及转化为字典序列
    def make_data(src_vocab,sentences):
        enc_inputs = []
        for i in range(len(sentences)):
            enc_input = [[src_vocab[n] for n in sentences[i].split()]]
            enc_inputs.extend(enc_input)
        return torch.LongTensor(enc_inputs)
    
    # 构造输入
    sentences = ['我 是 学 生 P',
                 '我 喜 欢 学 习',
                 '我 是 男 生 P']
    src_vocab = {'P': 0, '我': 1, '是': 2, '学': 3, '生': 4, '喜': 5, '欢': 6, '习': 7, '男': 8}  # 词源字典  字:索引
    
    # 将输入转为字典索引
    enc_inputs=make_data(src_vocab,sentences)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    于是,我们可以利用原始的sentences、src_vocab以make_data()函数构造出输入序列,其内容与其在字典中的顺序有关。随后便可以利用Dataset函数定义模拟出的数据,使其可被DataLoader加载,其内容如代码片段1.2所示。

    # 介绍:将模拟数据转化为可被Dataloader加载的格式
    # 定义Dataset
    class MyDataSet(Data.Dataset):
        def __init__(self, enc_inputs):
            super(MyDataSet, self).__init__()
            self.enc_inputs = enc_inputs
    
        def __len__(self):
            return self.enc_inputs.shape[0]
    
        def __getitem__(self, idx):
            return self.enc_inputs[idx]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    使用Dataloader加载,其输出如下:
    在这里插入图片描述

    2 构造Position Embedding和Padding Mask

    Position embedding主要目的是为了弥补Attension机制中词序列信息丢失的问题,把词序信号加到词向量上帮助模型学习这些序列信息。
    Padding mask作用是掩盖不参与运算的元素,还有一个作用是在softmax之前将填充元素mask掉使其不参与权重分配(还有另一种mask方式:seq mask,是为了在decoder阶段保证模型看不到答案而设置的,其表现形式为三角矩阵)。

    2.1 Position Embedding

    对于任何一门语言,单词在句子中的位置以及排列顺序是非常重要的,它们不仅是一个句子的语法结构的组成部分,更是表达语义的重要概念。一个单词在句子的位置或排列顺序不同,可能整个句子的意思就发生了偏差,例如:

    I do not like the story of the movie, but I do like the cast.
    I do like the story of the movie, but I do not like the cast.

    上面两句话所使用的的单词完全一样,但是所表达的句意却截然相反,因此考虑引入词序信息来区别这两句话的意思。
    我们知道,循环神经网络本身就是一种顺序结构,天生就包含了词在序列中的位置信息。而Transformer模型抛弃了RNN、CNN作为序列学习的基本模型,,完全采用Attention取而代之,这些词序信息就会丢失,模型就没有办法知道每个词在句子中的相对和绝对的位置信息。因此,有必要把词序信号加到词向量上帮助模型学习这些信息,位置编码(Positional Encoding)就是用来解决这种问题的方法,其数学表达式如下:
    在这里插入图片描述
    其中,𝑝𝑜𝑠是词在词表中出现的位置序号,𝑖是维度序号。我们可以先生成相同维度的用0填充的张量𝑝𝑒_𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔,再用上述规则进行填充,其实现如下所示。

    # 定义Position Embedding
    class PositionalEncoding(nn.Module):
        
        def __init__(self, d_model, dropout=0.1, max_len=5000):
            super(PositionalEncoding, self).__init__()
            self.dropout = nn.Dropout(p=dropout)
            pos_table = np.array([
                [pos / np.power(10000, 2 * i / d_model) for i in range(d_model)]
                if pos != 0 else np.zeros(d_model) for pos in range(max_len)])
            pos_table[1:, 0::2] = np.sin(pos_table[1:, 0::2])# 字嵌入维度为偶数时
            pos_table[1:, 1::2] = np.cos(pos_table[1:, 1::2])# 字嵌入维度为奇数时
            self.pos_table = torch.FloatTensor(pos_table).cuda()# enc_inputs: [seq_len, d_model]
    
        def forward(self, enc_inputs):# enc_inputs: [batch_size, seq_len, d_model]
            enc_inputs += self.pos_table[:enc_inputs.size(1), :]
            return self.dropout(enc_inputs.cuda())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.2 Padding Mask

    前面已经提到,padding mask的主要目的是让非定长序列对齐,但其实这一步在构造模拟数据时就考虑到了,所以这里padding mask主要就是生成训练时用的矩阵张量,其实现如下所示。

    # 定义padding mask函数
    def get_attn_pad_mask(seq_q, seq_k):# seq_q: [batch_size, seq_len] ,seq_k: [batch_size, seq_len]
        batch_size, len_q = seq_q.size()
        batch_size, len_k = seq_k.size()
        pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)# 判断 输入那些含有P(=0),用1标记 ,[batch_size, 1, len_k]
        return pad_attn_mask.expand(batch_size, len_q, len_k)# 扩展成多维度
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3 构造Multi-Head Attention模块

    MHA是Transformer的核心部分,主要目的是获取序列不同位置上的权重。由图3.1MHA的网络结构图可以看出,其主要步骤分为两步:QKV的切分映射与自注意力计算。这里自注意力(self-attention)引入了Query、Key、Value的概念:Query是查询对象;Key是键,用来和要查询的Query计算相关性,得到一个权重(相关性或者相似度);Value是操作的值,将QK计算后的权重乘以Value得到最终的结果。

    3.1 Multi-head输入生成

    首先,利用自注意力机制通过对原始输入X进行变换得到QKV,也就是将原始输入X进行不同的线性映射来得到Q、K、V,其计算过程如下:
    Q = X ∗ W Q K = X ∗ W k V = X ∗ W v Q=X*W_Q\\ K=X*W_k\\ V=X*W_v Q=XWQK=XWkV=XWv
    得到QKV后,接下来就要进行multi-head切分,本质上就是在embedding的维度上将矩阵切分为多个张量切分完后的维度应该是:
    b a t c h S i z e ∗ s e q L e n ∗ h e a d s ∗ ( e m b e d d i n g D i m / h e a d s ) batchSize*seqLen*heads*(embeddingDim/heads) batchSizeseqLenheads(embeddingDim/heads)
    每一个张量对应一个head的输入,如图下图所示。
    在这里插入图片描述

    3.2 Scaled Dot-Product Attention计算

    前面说MHA是Transformer的核心部分,而Scaled Dot-Product Attention则是MHA的核心部分。得到切分后的QKV后,每个heads进行注意力的计算,如图3.1左图所示。假设Q、K、V为切分完后的矩阵(其中一个头),根据两个向量的点积越大越相似,我们通过 Q K T QK^T QKT求出注意力矩阵,再根据注意力矩阵来给V进行加权,即:
    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
    其中 d k \sqrt{d_k } dk 是为了把注意力矩阵变成标准正态分布, s o f t m a x softmax softmax进行归一化,使每个字与其他字的注意力权重之和为1,这一操作使得每一个字的嵌入都包含当前句子内所有字的信息,并且 A t t e n t i o n ( Q , K , V ) Attention(Q,K,V) Attention(Q,K,V)的维度和 V V V的维度保持一致。上述过程的主体代码如下所示。

    class ScaledDotProductAttention(nn.Module):
        def __init__(self):
            super(ScaledDotProductAttention, self).__init__()
    
        def forward(self, Q, K, V, attn_mask):                              
            scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) 
            scores.masked_fill_(attn_mask, -1e9)
            attn = nn.Softmax(dim=-1)(scores)
            context = torch.matmul(attn, V)
            return context, attn
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.2 Multi-Head Attention

    基于前面的多头划分和自注意力,可以很自然地理解Transformer的MHA部分,其主体代码如下所示。

    class MultiHeadAttention(nn.Module):
        def __init__(self):
            super(MultiHeadAttention, self).__init__()
            self.W_Q = nn.Linear(d_model, d_k * n_heads, bias=False)
            self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)
            self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)
            self.fc = nn.Linear(n_heads * d_v, d_model, bias=False)
    
        def forward(self, input_Q, input_K, input_V, attn_mask):
    
            residual, batch_size = input_Q, input_Q.size(0)
            Q = self.W_Q(input_Q).view(batch_size, -1, n_heads, d_k).transpose(1, 2)    # Q: [batch_size, n_heads, len_q, d_k]
            K = self.W_K(input_K).view(batch_size, -1, n_heads, d_k).transpose(1, 2)    # K: [batch_size, n_heads, len_k, d_k]
            V = self.W_V(input_V).view(batch_size, -1, n_heads, d_v).transpose(1,2)     # V: [batch_size, n_heads, len_v(=len_k), d_v]
            attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1,1)                  # attn_mask : [batch_size, n_heads, seq_len, seq_len]
            context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)             # context: [batch_size, n_heads, len_q, d_v]
                                                                                        # attn: [batch_size, n_heads, len_q, len_k]
            context = context.transpose(1, 2).reshape(batch_size, -1,n_heads * d_v)     # context: [batch_size, len_q, n_heads * d_v]
            output = self.fc(context)                                                   # [batch_size, len_q, d_model]
            return nn.LayerNorm(d_model).cuda()(output + residual), attn
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4 构造前馈传播Feed Forward模块

    前馈网络也就是简单的两层线性映射再利用激活函数做非线性映射,没有太复杂的地方,其主体代码如下所示

    class PoswiseFeedForwardNet(nn.Module):
        def __init__(self):
            super(PoswiseFeedForwardNet, self).__init__()
            self.fc = nn.Sequential(
                nn.Linear(d_model, d_ff, bias=False),
                nn.ReLU(),
                nn.Linear(d_ff, d_model, bias=False))
    
        def forward(self, inputs):
            residual = inputs
            output = self.fc(inputs)
            return nn.LayerNorm(d_model).cuda()(output + residual)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5 Add&Norm

    这一步主要是进行跳连接Shortcut和层归一化LayerNorm。
    首先是跳连接,将原始输入X和经过MHA的输入进行sum,得到融合后的特征,其作用类似残差连接,能够缓解梯度消失问题。
    然后是LayerNorm(作用是把神经网络中隐藏层归一为标准正态分布,加速收敛),具体操作是将每一行的每一个元素减去这行的均值, 再除以这行的标准差, 从而得到归一化后的数值,其公式如下:
    y = x − E [ x ] V a r [ x ] + ε ∗ γ + β y=\frac{x-E[x]}{\sqrt{Var[x]+ε}}*γ+β y=Var[x]+ε xE[x]γ+β

    6 构造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, src_len, d_model]
            # 输入3个enc_inputs分别与W_q、W_k、W_v相乘得到Q、K、V       
            enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,
                                enc_self_attn_mask)# attn: [batch_size, n_heads, src_len, src_len]
            enc_outputs = self.pos_ffn(enc_outputs)# enc_outputs: [batch_size, src_len, d_model]
            return enc_outputs, attn
    
    
    class Encoder(nn.Module):
        def __init__(self):
            super(Encoder, self).__init__()
            self.src_emb = nn.Embedding(src_vocab_size, d_model)# 把字转换字向量
            self.pos_emb = PositionalEncoding(d_model)# 加入位置信息
            self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])
    
        def forward(self, enc_inputs):# enc_inputs: [batch_size, src_len]
            enc_outputs = self.src_emb(enc_inputs)# enc_outputs: [batch_size, src_len, d_model]
            enc_outputs = self.pos_emb(enc_outputs)# enc_outputs: [batch_size, src_len, d_model]
            enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
            enc_self_attns = []
            for layer in self.layers:
                enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)                                                    
                enc_self_attns.append(enc_self_attn)
            return enc_outputs
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    7 输出结果

    loader = Data.DataLoader(MyDataSet(enc_inputs), 2, True)
    for encoder_input in loader:
        enc_input=encoder_input.cuda()
        encoder=Encoder().cuda()
        output=encoder(enc_input)
        print(output)
        print(output.shape)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

  • 相关阅读:
    Expression与Func的区别(Expression与反射的结合使用)
    免费的visual studio智能代码插件——CodeGeeX
    Dubbo架构概览:服务注册与发现、远程调用、监控与管理
    【雕爷学编程】Arduino动手做(109)---3路电压转换模块
    【k8s管理--集群日志管理elk】
    Google play 应用下架、封号常见原因:8.3/10.3分发协议及恶意软件政策问题浅析
    【机器学习】朴素贝叶斯概率模型
    红队渗透靶场之SickOs1.1
    「学习笔记」gdb 调试的简单操作
    Three.js 这样写就有阴影效果啦
  • 原文地址:https://blog.csdn.net/weixin_43427721/article/details/127897138