Transformer

一、引言

Transformer 是 Google 的团队在 2017 年提出的一种 NLP 经典模型,现在比较火热的 Bert 也是基于 Transformer。Transformer 模型使用了 Self-Attention 机制,不采用 RNN 的顺序结构,使得模型可以并行化训练,而且能够拥有全局信息。

2020年Transformer火爆了整个CV界,像DETR直接使用CNN+Transformer的混合结构,还有Vision Transformer直接舍弃了CNN结构,单纯使用Transformer来做图像分类,这些模型都取得了比较好的效果。

二、Transformer核心思想

Transformer中抛弃了传统的CNN和RNN,整个网络结构完全是由Attention机制组成。作者采用Attention机制的原因是考虑到RNN(或者LSTM,GRU等)的计算是顺序的,RNN相关算法只能从左向右依次计算或者从右向左依次计算,这种机制带来了两个问题: 

  • 时间片 t 的计算依赖 t−1t−1 时刻的计算结果,这样限制了模型的并行能力;
  • 顺序计算的过程中信息会丢失,尽管LSTM等门机制的结构一定程度上缓解了长期依赖的问题,但是对于特别长期的依赖现象,LSTM依旧无能为力

Transformer的提出就是为了解决RNN中存在的问题:

  • 首先它使用了Attention机制,将序列中的任意两个位置之间的距离是缩小为一个常量;
  • 其次它不是类似RNN的顺序结构,因此具有更好的并行性,符合现有的GPU框架

三、Transformer结构

 Transformer的模型本质上就是一个Encoder-Decoder结构,输入序列先进行Embedding,经过Encoder之后结合上一次output再输入Decoder,最后用softmax计算序列下一个单词的概率。

3.1 Input Embedding

Transformer的输入为Input Embedding + Positional Embedding,其中Input Embedding在pytorch中用torch.nn.Embedding包实现,将每个词转化为词向量Input Embedding,一般dim=256或512,代码中用d_model变量来表示dim,即词向量的维度。

torch.nn.Embedding(num_embeddingsembedding_dim, padding_idx=None, max_norm=None, norm_type=2.0, scale_grad_by_freq=False, sparse=False, _weight=None)

torch.nn.Embedding 模块可以看做一个字典,字典中每个索引对应一个词和词的embedding形式。利用这个模块,可以给词做embedding的初始化操作。

主要参数介绍:

  • num_embeddings :字典中词的个数
  • embedding_dim:embedding词向量的维度

代码如下:

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        super(Embeddings, self).__init__()
        # d_model表示embedding的维度,即词向量的维度;vocab表示词汇表的数量。
        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model  #表示embedding的维度
 
    def forward(self, x):
        return self.lut(x) * math.sqrt(self.d_model)

3.2 Positional Embedding

在RNN中,对句子的处理是一个个word按顺序输入的。但在 Transformer 中,输入句子的所有word是同时处理的,没有考虑词的排序和位置信息。因此,Transformer 的作者提出了加入 “positional encoding” 的方法来解决这个问题。“positional encoding“”使得 Transformer 可以衡量 word 位置有关的信息。

如何实现具有位置信息的encoding呢?作者提供了两种思路:

  • 通过训练学习 positional encoding 向量;
  • 使用公式来计算 positional encoding向量。

试验后发现两种选择的结果是相似的,所以采用了第2种方法,优点是不需要训练参数,而且即使在训练集中没有出现过的句子长度上也能用(意思是说假设在训练集的句子最长有100个单词,而测试集来了个200个单词的句子,公式法Positional Encodding照样能解决)。

Positional Encoding的公式如下:

公式参数解析:

  • pos指这个word在这个句子中的位置,如“I am Chinese”这句话中,pos=0指代的是I,pos=1指代的是am;
  • 2i指的是embedding词向量的偶数维度(d_model = 512);
  • 2i+1指的是embedding词向量的奇数维度(d_model = 512);i的取值范围为[0,\frac{dmodel}{2}];

# Positional Encoding
class PositionalEncoding(nn.Module):
    "实现PE功能"
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
         
        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)           # [1, max_len, d_model]
        self.register_buffer('pe', pe)
         
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)

注意:"x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)" 这行代码表示;输入模型的整个EmbeddingWord EmbeddingPositional Embedding直接相加之后的结果。

四、Encoder

Encoder部分是由N个相同的Encoder Layer串联而成。Encoder Layer可以简化为两个部分:(1)Multi-Head Self Attention (2) Feed-Forward network。示意图如下:

4.1 Self Attention

Multi-Head Self Attention 实际上是由h个Self Attention 层并行组成,原文中h=8。我们先来介绍Self Attention。

Self Attention的输入是词向量,记为X=[x_{1}, x_{2}, x_{3}...]X经过一个线性变换得到query(Q),经过第二个线性变换得到key(K),  经过第三个线性变换得到value(V)。也就是:

  • query = linear_q(x)
  • key = linear_k(x)
  • value = linear_v(x)

用矩阵表示如下:


假设我们的输入X是下图中的x_{1}, x_{2}, x_{3}, x_{4},每个x_{i}是一个512维的词向量,将每个词向量先乘上一个矩阵W得到embedding,即向量a_{1}, a_{2}, a_{3}, a_{4}。接着这个embedding进入self-attention层,每一个向量a_{i}分别乘以3个不同的矩阵W^{q},W^{k},W^{v},以向量a_{1}为例,分别得到3个不同的向量q_{1},k_{1},v_{1}

接下来使用每个query  q去对每个key  k做attention,attention就是匹配这2个向量有多接近,比如我现在要对 q_{1} 和 k_{1} 做attention,我就可以把这2个向量做scaled Dot-product,得到a_{1,1}。接下来再拿q_{1} 和 k_{2 } 做attention,得到a_{1, 2}....。计算公式见下图:

上式中,d是q和k的维度,因为q\times k的数值会随着dimension的增大而增大,所以要除以根号dimension。

接下来,把计算得到的所有a_{1, i}值取softmax操作。

取完 softmax 操作以后,就得到了\large \hat{a}_{1, i},我们用它和所有对应的v^{i}相乘,再求和,得到\large b^{1},如下图:

用上述同样的方法,也可以计算出\large b^{2},b^{3},b^{4},如下图,\large b^{2}就是用\large q^{2}去对其它的k做attention,得到\large \hat{a}_{2, i},再与相对应的value值\large v^{i}相乘再求和得到的。


以上过程是计算的详细过程,但一般我们是用代码实现的,那么我们就得把上述过程简化为矩阵(张量)运算

注:self-attention就是一堆矩阵乘法,可以实现GPU加速

4.2 Multi-Head Self Attention

Multi-Head-Attention 就是将embedding之后的X按维度d_model=512 切割成h=8个,分别做self-attention之后再合并在一起。

以两个head得情况为例,由\large a^{i}乘以\large W^{q},W^{k},W^{v}生成的\large q^{i}, k^{i}, v^{i},而\large q^{i}, k^{i}, v^{i}再分别进一步乘以2个转移矩阵变成\large q^{i,1},q^{i,2}\large k^{i,1},k^{i,2}\large v^{i,1},v^{i,2}。接下来\large q^{i,1}再与其它所有的\large k^{i,1}做attention,然后再与\large v^{i,1}相乘,求和得到\large b^{i,1}。(\large i = 1,2,...n_{head},本例中n_head为2)

同理可以得到\large b^{i,2}

\large b^{i,1}\large b^{i,2}执行concat操作,再通过一个transformation matrix调整维度,使之与刚才的\large b^{i}维度一致。

从下图14可以看到 Multi-Head Attention 包含多个 Self-Attention 层,首先将输入 X 分别传递到 2个不同的 Self-Attention 中,计算得到 2 个输出结果。得到2个输出矩阵之后,Multi-Head Attention 将它们拼接在一起 (Concat),然后传入一个Linear层,得到 Multi-Head Attention 最终的输出Z  。可以看到 Multi-Head Attention 输出的矩阵 Z 与其输入的矩阵 X 的维度是一样的。

Multi-Head Self Attention代码如下:

class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        "Take in model size and number of heads."
        super(MultiHeadedAttention, 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):
        """
        实现MultiHeadedAttention。
           输入的q,k,v是形状 [batch, L, d_model]。
           输出的x 的形状同上。
        """
        if mask is not None:
            # Same mask applied to all h heads.
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)
         
        # 1) 这一步qkv变化:[batch, L, d_model] ->[batch, h, L, d_model/h]
        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))]
         
        # 2) 计算注意力attn 得到attn*v 与attn
        # qkv :[batch, h, L, d_model/h] -->x:[b, h, L, d_model/h], attn[b, h, L, L]
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        # 3) 上一步的结果合并在一起还原成原始输入序列的形状
        x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
        # 最后再过一个线性层
        return self.linears[-1](x)

4.3 Add & Norm

x 序列经过multi-head-self-attention 之后实际经过一个“add+norm”层,再进入feed-forward network(后面简称FFN),在FFN之后又经过一个norm再输入下一个encoder layer。

其中BatchNorm和LayerNorm的区别如下:

Batch Normalization强行让一个batch的数据的某个channel\mu =0,\sigma =1  ,而Layer Normalization让一个数据的所有channel\mu =0,\sigma =1  。

还是云里雾里,那再给张图,如下所示,一列代表一条数据x,行代表channel的索引:

几乎每个sub layer之后都会经过一个归一化,然后再加在原来的输入上,进行残差连接。(残差连接,防止网络退化,梯度消失)

 

4.4 Feed-Forward Network

Feed-Forward Network可以细分为有两层,第一层激活函数为ReLU,第二层为线性激活函数。可以表示为:

FFN=max(0,xW1+b1)W2+b2

代码如下:

# Position-wise Feed-Forward Networks
class PositionwiseFeedForward(nn.Module):
    "实现FFN函数"
    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))))

 

五、Decoder

Decoder输入包括2部分,下方是前一个time step的输出的embedding,再加上一个表示位置的Positional Encoding,得到一个张量。

首先是Masked Multi-Head Self-attention,masked的意思是使attention只会attend on已经产生的sequence。因为还没有产生出来的东西不存在,就无法做attention。

输入:Encoder的输出和 对应 i-1 位置decoder的输出。所以中间的attention(上图中上面一个self attention)不是self-attention,它的Key和Value来自encoder,Query来自上一位置 Decoder 的输出。

输出:对应 i 位置的输出词的概率分布。

解码注意,Encoder可以并行计算,一次性全部Encoding出来,但Decoder不是一次把所有序列解出来的,而是像 RNN 一样一个一个解出来的,因为要用上一个位置的输入当作attention的query

Decoder结构:

  • 包含两个 Multi-Head Attention 层,第一个 Multi-Head Attention 层采用了 Masked 操作,确保预测第 i 个位置时不会接触到未来的信息,即防止模型看到要预测的数据,防止泄露;第二个 Multi-Head Attention 层的Key,Value矩阵使用 Encoder 的编码信息矩阵  进行计算,而Query使用上一个 Decoder block 的输出计算。
  • 最后有一个 Softmax 层计算下一个翻译单词的概率。

5.1 Masked操作

Masked在Scale操作之后,softmax操作之前。

因为在翻译的过程中是顺序翻译的,即翻译完第i个单词,才可以翻译第i+1个单词。通过 Masked 操作可以防止第i个单词知道第 i+1 个单词之后的信息。

Mask的目的是防止 Decoder “seeing the future”,就像防止考生偷看考试答案一样。这里mask是一个下三角矩阵,对角线以及对角线左下都是1,其余都是0。下面是个10维度的下三角矩阵:

tensor([[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         [1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
         [1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
         [1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
         [1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
         [1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
         [1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
         [1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
         [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]], dtype=torch.uint8)

当mask不为空的时候,attention计算需要将x做一个操作:scores = scores.masked_fill(mask == 0, -1e9),即把mask==0的替换为-1e9,其余不变。

5.2 Decoder Multi-head Attention

这部分和Multi-head Attention的区别是该层的输入来自encoder和上一次decoder的结果。其中k和v来自Encoder的输出,q来自上一个上一个位置的Decoder输出。

5.3 Linear and Softmax to Produce Output Probabilities

Decoder的最后一个部分是过一个linear layer将decoder的输出扩展到与vocabulary size一样的维度上。经过softmax 后,选择概率最高的一个word作为预测结果。假设我们有一个已经训练好的网络,在做预测时,步骤如下:(假设输入的句子为“我是一个学生”)

       (1)给 decoder 输入 encoder 对整个句子 embedding 的结果 和一个特殊的开始符号 </s>。decoder 将产生预测,在我们的例子中应该是 ”I”。

  (2)给 decoder 输入 encoder 的 embedding 结果和 “</s>I”,在这一步 decoder 应该产生预测 “am”。

  (3)给 decoder 输入 encoder 的 embedding 结果和 “</s>I am”,在这一步 decoder 应该产生预测 “a”。

  (4)给 decoder 输入 encoder 的 embedding 结果和 “</s>I am a”,在这一步 decoder 应该产生预测 “student”。

  (5)给 decoder 输入 encoder 的 embedding 结果和 “</s>I am a student”, decoder应该生成句子结尾的标记,decoder 应该输出 ”</eos>”。

  (6)然后 decoder 生成了 </eos>,翻译完成。

六、Transformer总结

6.1 优点:

  1. 每层计算复杂度比RNN要低,并且在self attention中可以并行计算;
  2. 从计算一个序列长度为n的信息要经过的路径长度来看, CNN需要增加卷积层数来扩大视野,RNN需要从1到n逐个进行计算,而Self-attention只需要一步矩阵计算就可以。Self-Attention可以比RNN更好地解决长时依赖问题。当然如果计算量太大,比如序列长度N大于序列维度D这种情况,也可以用窗口限制Self-Attention的计算数量。

6.2 缺点:

  1. 实践上:有些RNN轻易可以解决的问题transformer没做到,比如复制string,或者推理时碰到的sequence长度比训练时更长(因为碰到了没见过的position embedding);
  2. 理论上:transformers不是computationally universal(图灵完备),这种非RNN式的模型是非图灵完备的的,无法单独完成NLP中推理、决策等计算问题(包括使用transformer的bert模型等等)

七、References

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值