Transformer讲解以及代码(学习版)

##纯小白记录 主要是跟着帖子练习代码以及整理思路 不做任何商用##有问题可以不断更新

参考内容包括但不限于:

三万字最全解析!从零实现Transformer(小白必会版😃) - 知乎

Transformer解读(论文 + PyTorch源码)_pytorch的transformer论文-优快云博客

保姆级分析self Attention为何除根号d,看不懂算我的 - 知乎

Softmax函数及其导数-优快云博客

The Annotated Transformer(访问可能需要魔法)

一.Transformer的架构

1.1编码器

首先来看Encoder部分,它是由N层方框里面的内容堆叠起来的。对于每一层来说,都由两部分构成:一部分是multi-head self-attention机制,另一部分是一个简单的全连接前馈网络。在每一部分上,都使用残差+layer normalization来进行处理。论文中,这样的方框有6个,即N = 6,模型的隐层单元数 d_{model}= 512

Encoder(编码器)部分示意图

1.2自注意力机制

Encoder内部没有使用RNN,取而代之的是一种self-attention(自注意力)机制。

一般我们用的attention机制,可以抽象为输入一个查询(query),去查询键值对(key-value pair)中的key,然后得到一个概率分布,再据此对value进行加权相加,获取当前query下的注意力表征。而我们的query,往往是Decoder中某一个step的输出,key-value pair往往是encoder的输出。

论文里面使用的也是这种attention机制,只不过其query、key、value都是由encoder的输出经过不同的变换而来,也即self-attention,所有的东西都是自己。他们定义了一种叫“Scaled Dot-Product Attention”的计算方式,用于计算给定query、key和value下的注意力表征,如下图(左)所示:

这里的QKV分别表示querykeyvalue矩阵,它们的维度分别为L_q * d_k、 L_k * d_kL_k * d_v​。计算公式为:

一般我们经常使用的attention计算方式有两种:一种是乘性attention,即使用内积的方式;另一种是加性attention,即使用额外一层隐藏层来计算。这两种计算方式理论上复杂度是差不多的,但乘性attention因为可以用矩阵运算,会更节省时间和空间。对照着上图(左)和公式来看,这个公式与乘性attention计算方式的唯一不同就在于使用了一个缩放因子\frac{1}{\sqrt{d_k}}。这里为何要进行缩放呢?论文中给出了解释:在d_k​比较小的时候,不加缩放的效果和加性attention的效果差不多,但当d_k​比较大的时候,不加缩放的效果就明显比加性attention的效果要差,怀疑是当d_k​增长的时候,内积的量级也会增长,导致softmax函数会被推向梯度较小的区域,为了缓解这个问题,加上了这个缩放项进行量级缩小。

这里补充一下关于缩放因子\frac{1}{\sqrt{d_k}}

保姆级分析self Attention为何除根号d,看不懂算我的 - 知乎

Softmax函数及其导数-优快云博客

首先是softmax函数的求导:

所以如果不加缩放会出现维度的影响会使得数值很大,输出的S_i或者S_j倾向于接近1或者0,不管哪种方式都会导致导函数的值很小,会造成梯度消失。

在transformer网络中,softmax函数的输入由key向量和query向量之间的点积组成,key向量和query向量的维度 越大,点积往往越大, 原文论中12个head对应的大小是64,作者在原论文中采用的补救措施,是将点积除以key和query维度的平方根,这个计算过程如下:我们假设key和query服从均值为0,方差为1的均匀分布, 即D(query)=D(key)=1, 维度大小为64,(其实不用假设,现在基本上都符合)那么点积后的,我们可以计算他的方差变化。

所以我们将每个分数都除以64, 将方差降到1。

ok继续:

论文里面还提到,只使用一个attention的计算方式未免太过单薄,所以他们提出了multi-head(多头)注意力机制。将注意力的计算分散到不同的子空间进行,以期能从多方面进行注意力的学习,具体做法如上图(右)所示。并行地将QKV通过不同的映射矩阵映射到不同的空间(每个空间是一个头),再分别在这些空间中对应着进行单个“Scaled Dot-Product Attention”的学习,最后将得到的多头注意力表征进行拼接,经过一个额外的映射层映射到原来的空间。其公式如下:

这里的W_i^Q \in R^{d_{model} * d_k}W_i^K \in R^{d_{model} * d_k}W_i^V \in R^{d_{model} * d_v}W^O \in R^{hd_v * d_{model}}。表示第i个头的变换矩阵,h表示头的个数。

在论文里面,h = 8,并且d_k = d_v = d_{model} / h = 64。可见这里虽然分了很多头去计算,但整体的维度还是不变的,因此计算量还是一样的。

1.3前馈网络

这部分是整体架构图中的Feed Forward模块,其实就是一个简单的全连接前馈网络。它由两层全连接及ReLU激活函数构成,计算公式如下:

这里的全连接是Position-wise逐位置的,即设前面的attention输出的维度为B * Length * d_{model},则变换时,实际上是只针对d_{model}进行变换,对于每个位置(Length维度)上,都使用同样的变换矩阵。

在论文中,这里的d_{model}仍然是512,两层全连接的中间隐层单元数为d_{ff} = 2048

1.4 add&norm

在整体架构图中,还有一个部分是add&norm,这其实是借鉴了图像中的残差思想。在self-attention和feed forward计算之后都会加上一个残差变换,同时也会加上Layer Normalization(参见: https://arxiv.org/pdf/1607.06450.pdf ,用在有循环机制的网络里面效果较好)。设输入为x,则输出为LayerNorm(x+SubLayer(x)),这里的SubLayer即是self-attention或feed forward层。

1.5 解码器

接着来看Decoder部分(右半部分),它同样也是由N层(在论文中,仍取N = 6)堆叠起来,对于其中的每一层,除了与Encoder中相同的self-attention及Feed Forward之外,还在中间插入了一层传统encoder-decoder框架中的attention层,即将decoder的输出作为query去查询encoder的输出,同样用的是multi-head attention,使得在decode的时候能看到encoder的所有输出。

同时,作为decoder,在预测当前步的时候,是不能知道后面的内容的,即attention需要加上mask,将当前步之后的分数全部置为-\infty,然后再计算softmax,以防止发生数据泄露。

1.6 位置编码层

位置编码的意义:Positional Encoding就是将位置信息添加(嵌入)到Embedding词向量中,让Transformer保留词向量的位置信息,可以提高模型对序列的理解能力。

Transformer虽然摒弃了RNN的循环结构和CNN的局部相关性,但对于序列来说,最重要的其实还是先后顺序。看前面self-attention的处理方式,实际上与“词袋”模型没什么区别,这样忽略了位置信息的缺陷肯定是要通过一定的手段来弥补。

论文中提出了一个非常“smart”的方式来加入位置信息,就是这里的Positional Encoding,它对于每个位置pos进行编码,然后与相应位置的word embedding进行相加,构成当前位置的新word embedding。它采用如下的公式为每个pos进行编码:

其中,i表示embedding向量中的位置,即d_{model}中的每一维。选择这种sin函数有两种好处:1)可以不用训练,直接编码即可,而且不管什么长度,都能直接得到编码结果;2)能表示相对位置,根据sin(\alpha+\beta)=sin\alpha cos\beta + cos\alpha sin\betaPE_{pos+k}可以表示为PE_{pos}的线性变换,这样的线性组合意味着位置向量中蕴含了相对位置信息。

这里插个眼,看完再来补充。

1.7 一些Tricks和技巧

  • Embedding和Softmax:论文中将embedding层的参数与最后的Softmax层之前的变换层参数进行了共享(参见:https://arxiv.org/pdf/1608.05859.pdf ),并且在embedding层,将嵌入的结果乘上\sqrt{d_{model}}​​。

  • 初始化:看代码里面的初始化方式采用的是PyTorch里面的nn.xavier_uniform,不知道是不是必须的,这个还是要具体问题具体尝试?

  • 优化器:论文里面提到了他们用的优化器,是以 \beta_1=0.9\beta_2=0.98\epsilon=10^{-9}的Adam为基础,而后使用一种warmup的学习率调整方式来进行调节。具体公式如下:基本上就是先用一个固定的warmup_steps进行学习率的线性增长(热身),而后到达warmup_steps之后会随着step_num的增长而逐渐减小,他们用的warmup\_steps = 4000,这个可以针对不同的问题自己尝试。
    l_{rate} = d_{model}^{-0.5}·min(step\_num^{-0.5}, step\_num \cdot warmup\_steps^{-1.5})

  • 正则化:论文在训练的时候采用了两种正则化的方式。1)dropout:主要用在每个SubLayer计算结束之后,比如self-attention或feed forward,然后再与输入进行add & norm,同时也作用在经过了位置编码后的embedding上,他们取的P_{drop}=0.1;2)标签平滑:即Label Smoothing(参见: https://arxiv.org/pdf/1512.00567.pdf ),其实还是从图像上搬过来的,具体操作可以看下一节的代码实现。这里论文取的\epsilon_{ls}=0.1,他们发现会损失困惑度,但能提升准确率和BLEU值!

二、Pytorch实现

参考:

The Annotated TransformerTransformer解读(论文 + PyTorch源码)_pytorch的transformer论文-优快云博客

2.1前置操作

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import math,copy,time
from torch.autograd import Varaiable
import matplotlib.pyplot as plt
import seaborn
seaborn.set_context(context="talk")
%matplotlib inline

2.2编码器解码器

class EncoderDecoder(nn.Module):
    def __init__(self,encoder,decoder,src_embed,tgt_embed,generator):
        super(EncoderDecoder,self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.generator = generator

    def forward(self,src,tgt,src_mask,tgt_mask):
        return self.decode(self.encode(src,src_mask),src_mask,tgt,tgt_mask)

    def encode(selfself,src,src_mask):
        return self.encoder(self.src_embed(src),src_mask)

    def decode(self,memory,src_mask,tgt,tgt_mask):
        return self.decoder(self.tgt_embed(tgt),memory,src_mask,tgt_mask)

2.3多头注意力机制

multi-head attention可用于三个地方,分别是Encoder和Decoder中各自的self-attention部分,还有Encoder-Decoder之间的attention部分。但其实这三个地方的不同仅仅在于query、key、value和mask的不同,因此当将这4部分作为参数传入时,模型的计算方式便可抽象为如下的形式:

class MultiHeadAttention(nn.Module):
    def __init__(self,h,d_model,dropout=0.1):
        super(MultiHeadAttention,self).__init__()
        assert d_model % h == 0
        self.d_k = d_model//h
        self.h = h
        self.linears = clones(nn.Linear(d_model,d_model),4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self,query,key,value,mask=None):
        if mask is not None:
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)

        query,key,value = [l(x).view(nbatches,-1,self.h,self.d_k).transpose(1,2) for l,x in zip(self.linears,(query,key,value))]
        x,self.attn = attention(query,key,value,mask=mask,dropout=self.dropout)
        x = x.transpose(1,2).contiguous().view(nbatched,-1,self.h*self.d_k)
        return self.linears[-1](x)
    
def attention(query,key,value,mask=None,dropout=None):
    d_k = query.size(-1)
    scores = torch.matmul(query,key.transpose(-2,-1))/math.sqrt(d_k)
    if mask is not None:
        scores = scores.mask_fill(mask ==0,-1e9)
    p_attn = F.softmax(scores,dim = -1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn,value),p_attn

2.4前馈网络

#前馈网络
class PositionwiseFeedForward(nn.Module):
    def __init__(self,d_model,d_ff,dropout=0.1):
        super(PositionwiseFeedForward,self).__init__()
        self.w_1 = nn.Linear(d_model,d_ff)
        self.w_2 = nn.Linear(d_ff,d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self,x):
        return self.w_2(self.dropout(F.relu(self.w_1(x))))
    

2.5 add&norm

残差模块+LayerNormalization的实现方式:

class SublayerConnecrion(nn.Module):
    def __init__(self,size,dropout):
        super(SublayerConnecrion,self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self,x,sublayer):
        return x + self.dropout(sublayer(self.norm(x)))
    
class LayerNorm(nn.Module):
    def __init__(self,features,eps=1e-6):
        super(LayerNorm,self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps
      
    def forward(self,x):
        mean = x.mean(-1,keepdim=True)
        std = x.std(-1,keepdim=True)
        return self.a_2 * (x-mean) / (std + self.eps) + self.b_2
    

2.6 位置编码

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        
        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) *
                             -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
        
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], 
                         requires_grad=False)
        return self.dropout(x)

当谈到Transformer模型的代码讲解时,我们可以从以下几个方面来介绍: 1. 数据预处理:首先需要对输入数据进行预处理,通常包括分词、构建词汇表、将文本转换为索引等操作。这些预处理步骤可以使用现有的NLP库(如NLTK、spaCy等)或自定义函数来完成。 2. 模型架构:Transformer模型的核心是多头自注意力机制和前馈神经网络。在代码中,我们需要定义Transformer模型的架构,包括输入嵌入层、多层编码器和解码器、位置编码等。可以使用PyTorch或TensorFlow等深度学习框架来实现模型的架构。 3. 训练过程:在训练过程中,我们需要定义损失函数和优化器。常用的损失函数是交叉熵损失函数,优化器可以选择Adam或SGD等。训练过程包括前向传播、计算损失、反向传播和参数更新等步骤。还可以使用学习率调度器来动态调整学习率。 4. 推理过程:推理过程是使用训练好的模型对新的输入进行预测。在推理过程中,需要对输入进行与训练时相同的预处理,并进行前向传播计算,得到输出结果。可以使用Beam Search等算法来生成多个候选结果,并选择最优的结果。 这些是Transformer模型的基本代码讲解方面,具体实现细节会因不同的框架和任务而有所不同。在实际开发中,可以参考论文《Attention is All You Need》中的伪代码,以及现有的开源实现(如fairseq、transformers等)来进行代码编写。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值