动手学CV-Pytorch计算机视觉 Transformer介绍和实战案例

动手学CV-Pytorch计算机视觉 Transformer介绍和实战案例

2017年谷歌在一篇名为《Attention Is All You Need》的论文中,提出了一个基于attention(自注意力机制)结构来处理序列相关的问题的模型,名为Transformer。

Transformer在很多不同nlp任务中获得了成功,例如:文本分类、机器翻译、阅读理解等。在解决这类问题时,Transformer模型摒弃了固有的定式,并没有用任何CNN或者RNN的结构,而是使用了Attention注意力机制,自动捕捉输入序列不同位置处的相对关联,善于处理较长文本,并且该模型可以高度并行地工作,训练速度很快。

本文将大致按照以下几个模块进行讲解:

  • 模型结构概览
  • 模型输入
  • Encoder
  • Decoder
  • 模型输出
  • 模型构建
  • 实战案例

每个模块会配合代码+注释+讲解的方式来进行介绍,最后会有一个玩具级别的序列预测任务进行实战。

通过本文,希望可以帮助大家,初探Transformer的原理和用法,下面直接进入正式内容:


6.1.1 模型结构概览

如下是Transformer的两个结构示意图:

上图是从一篇英文博客中截取的Transformer的结构简图,下图是原论文中给出的结构简图,更细粒度一些,可以结合着来看。

模型大致分为Encoder(编码器)和Decoder(解码器)两个部分,分别对应上图中的左右两部分。

其中编码器由N个相同的层堆叠在一起(我们后面的实验取N=6),每一层又有两个子层。

第一个子层是一个Multi-Head Attention(多头的自注意机制),第二个子层是一个简单的Feed Forward(全连接前馈网络)。两个子层都添加了一个残差连接+layer normalization的操作。

模型的解码器同样是堆叠了N个相同的层,不过和编码器中每层的结构稍有不同。对于解码器的每一层,除了编码器中的两个子层Multi-Head AttentionFeed Forward,解码器还包含一个子层Masked Multi-Head Attention,如图中所示每个子层同样也用了residual以及layer normalization。

模型的输入由Input EmbeddingPositional Encoding(位置编码)两部分组合而成,模型的输出由Decoder的输出简单的经过softmax得到。

结合上图,我们对Transformer模型的结构做了个大致的梳理,只需要先有个初步的了解,下面对提及的每个模块进行详细介绍。

6.1.2 模型输入

首先我们来看模型的输入是什么样的,先明确模型输入,后面的模块理解才会更直观。

输入部分包含两个模块,EmbeddingPositional Encoding

1. Embedding层

Embedding层的作用是将某种格式的输入数据,例如文本,转变为模型可以处理的向量表示,来描述原始数据所包含的信息。

Embedding层输出的可以理解为当前时间步的特征,如果是文本任务,这里就可以是Word Embedding,如果是其他任务,就可以是任何合理方法所提取的特征。

构建Embedding层的代码很简单,核心是借助torch提供的nn.Embedding,如下:

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        """
        类的初始化函数
        d_model:指词嵌入的维度
        vocab:指词表的大小
        """
        super(Embeddings, self).__init__()
        #之后就是调用nn中的预定义层Embedding,获得一个词嵌入对象self.lut
        self.lut = nn.Embedding(vocab, d_model)
        #最后就是将d_model传入类中
        self.d_model =d_model
    def forward(self, x):
        """
        Embedding层的前向传播逻辑
        参数x:这里代表输入给模型的单词文本通过词表映射后的one-hot向量
        将x传给self.lut并与根号下self.d_model相乘作为结果返回
        """
        embedds = self.lut(x)
        return embedds * math.sqrt(self.d_model)

2. 位置编码:

Positional Encodding位置编码的作用是为模型提供当前时间步的前后出现顺序的信息。因为Transformer不像RNN那样的循环结构有前后不同时间步输入间天然的先后顺序,所有的时间步是同时输入,并行推理的,因此在时间步的特征中融合进位置编码的信息是合理的。

位置编码可以有很多选择,可以是固定的,也可以设置成可学习的参数。

这里,我们使用固定的位置编码。具体地,使用不同频率的sin和cos函数来进行位置编码,如下所示:

$$ PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) $$

$$ PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}}) $$

其中pos代表时间步的下标索引,向量 P E p o s PE_{pos} PEpos 也就是第pos个时间步的位置编码,编码长度同Embedding层,这里我们设置的是512。上面有两个公式,代表着位置编码向量中的元素,奇数位置和偶数位置使用两个不同的公式。

思考:为什么上面的公式可以作为位置编码?

我的理解:在上面公式的定义下,时间步p和时间步p+k的位置编码的内积,即 P E p ⋅ P E p + k PE_{p} \cdot PE_{p+k} PEpPEp+k 是与p无关,只与k有关的定值(不妨自行证明下试试)。也就是说,任意两个相距k个时间步的位置编码向量的内积都是相同的,这就相当于蕴含了两个时间步之间相对位置关系的信息。此外,每个时间步的位置编码又是唯一的,这两个很好的性质使得上面的公式作为位置编码是有理论保障的。

下面是位置编码模块的代码实现:

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        """
        位置编码器类的初始化函数
        
        共有三个参数,分别是
        d_model:词嵌入维度
        dropout: dropout触发比率
        max_len:每个句子的最大长度
        """
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        
        # Compute the positional encodings
        # 注意下面代码的计算方式与公式中给出的是不同的,但是是等价的,你可以尝试简单推导证明一下。
        # 这样计算是为了避免中间的数值计算结果超出float的范围,
        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)

因此,可以认为,最终模型的输入是若干个时间步对应的embedding,每一个时间步对应一个embedding,可以理解为是当前时间步的一个综合的特征信息,即包含了本身的语义信息,又包含了当前时间步在整个句子中的位置信息。

3. Encoder和Decoder都包含输入模块

此外有一个点刚刚接触Transformer的同学可能不太理解,编码器和解码器两个部分都包含输入,且两部分的输入的结构是相同的,只是推理时的用法不同,编码器只推理一次,而解码器是类似RNN那样循环推理,不断生成预测结果的。

怎么理解?假设我们现在做的是一个法语-英语的机器翻译任务,想把Je suis étudiant翻译为I am a student

那么我们输入给编码器的就是时间步数为3的embedding数组,编码器只进行一次并行推理,即获得了对于输入的法语句子所提取的若干特征信息。

而对于解码器,是循环推理,逐个单词生成结果的。最开始,由于什么都还没预测,我们会将编码器提取的特征,以及一个句子起始符传给解码器,解码器预期会输出一个单词I。然后有了预测的第一个单词,我们就将I输入给解码器,会再预测出下一个单词am,再然后我们将I am作为输入喂给解码器,以此类推直到预测出句子终止符完成预测。

6.1.3 Encoder

这一小节介绍编码器部分的实现

1. 编码器

编码器作用是用于对输入进行特征提取,为解码环节提供有效的语义信息

整体来看编码器由N个编码器层简单堆叠而成,因此实现非常简单,代码如下:

# 定义一个clones函数,来更方便的将某个结构复制若干份
def clones(module, N):
    "Produce N identical layers."
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])


class Encoder(nn.Module):
    """
    Encoder
    The encoder is composed of a stack of N=6 identical layers.
    """
    def __init__(self, layer, N):
        super(Encoder, self)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值