聊聊 Transformer

Attention is all you need 是一篇将 Attention 思想发挥到极致的论文,出自 Google。这篇论文中提出一个全新的模型,叫 Transformer。
论文地址:https://arxiv.org/abs/1706.03762
接下来我想讲讲自己对Transformer模型的理解:
**

1.模型结构

**
Transformer结构图
Transformer模型有传统的seq2seq一样,也是由encoder和decoder组成。
**

1.1 Encoder

**
encoder由 6 层相同的层组成,每一个大模块分别由两部分组成:

第一部分是 multi-head self-attention
第二部分是 position-wise feed-forward network,是一个全连接层
这个两个sub_layer部分,都有一个残差连接(residual connection),然后接着一个 Layer Normalization,故每一个sub_layer输出可表示为:
sub_layer表示

**注意:**Encoder端每个大模块接收的输入是不一样的,第一个大模块(最底下的那个)接收的输入是输入序列的embedding(embedding可以通过word2vec预训练得来),其余大模块接收的是其前一个大模块的输出,最后一个模块的输出作为整个Encoder端的输出。

1.2 Decoder

**
和 encoder 类似,decoder 也是由6个相同的层组成,每一个大模块包括以下3个部分:

第一个部分是 multi-head self-attention mechanism
第二部分是 multi-head context-attention mechanism
第三部分是一个 position-wise feed-forward network和 encoder 一样,上面三个部分的每一个部分,都有一个残差连接,后接一个 Layer Normalization。

decoder 和 encoder 不同的地方在 multi-head context-attention mechanism

同样需要注意的是,Decoder端每个大模块接收的输入也是不一样的,其中第一个大模块(最底下的那个)训练时和测试时的接收的输入是不一样的,并且每次训练时接收的输入也可能是不一样的(也就是模型总览图示中的"shifted right"),其余大模块接收的是同样是其前一个大模块的输出,最后一个模块的输出作为整个Decoder端的输出。
**
对于第一个大模块,简而言之,其训练及测试时接收的输入为:
训练的时候每次的输入为上次的输入加上输入序列向后移一位的ground truth(例如每向后移一位就是一个新的单词,那么则加上其对应的embedding),特别地,当decoder的time step为1时(也就是第一次接收输入),其输入为一个特殊的token,可能是目标序列开始的token(如),也可能是源序列结尾的token(如),也可能是其它视任务而定的输入等等,不同源码中可能有微小的差异,其目标则是预测下一个位置的单词(token)是什么,对应到time step为1时,则是预测目标序列的第一个单词(token)是什么,以此类推;
这里需要注意的是,在实际实现中可能不会这样每次动态的输入,而是一次性把目标序列的embedding通通输入第一个大模块中,然后在多头attention模块对序列进行mask即可(后边说的sentence mask)
而在测试的时候,是先生成第一个位置的输出,然后有了这个之后,第二次预测时,再将其加入输入序列,以此类推直至预测结束。

2.Attention

**

  • scaled dot-product attention

Tansformer模型采用的Attention是scaled dot-product attention,原始论文的描述为:

An attention function can be described as a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. The output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility of the query with the corresponding key.

通过 query 和 key 的相似性程度来确定 value 的权重分布,即:
Transformer公式
caled dot-product attention 和 dot-product attention 唯一的区别就是,scaled dot-product attention 有一个缩放因子, 叫 1 d k \frac{1}{\sqrt d_k} d k1 d k { d_k} dk表示 Key 的维度,默认用 64。

论文里对于 d k { d_k} dk的作用这么来解释:对于 d k { d_k} dk很大的时候,点积得到的结果维度很大,使得结果处于softmax函数梯度很小的区域。这时候除以一个缩放因子,可以一定程度上减缓这种情况。caled dot-product attention结构图如下所示:
caled dot-product attention
现在来说下 K、Q、V 分别代表什么:

在 encoder 的 self-attention 中,Q、K、V 都来自同一个地方,它们是上一层 encoder 的输出。对于第一层 encoder,它们就是 word embedding 和 positional encoding 相加得到的输入。
在 decoder 的 self-attention 中,Q、K、V 也是自于同一个地方,它们是上一层 decoder 的输出。对于第一层 decoder,同样也是 word embedding 和 positional encoding 相加得到的输入。但是对于 decoder,我们不希望它能获得下一个 time step (即将来的信息,不想让他看到它要预测的信息),因此我们需要进行 sequence masking。
在 encoder-decoder attention 中,Q 来自于 decoder 的上一层的输出,K 和 V 来自于 encoder 的输出,K 和 V 是一样的。
Q、K、V 的维度都是一样的,分别用 d q { d_q} dq d k { d_k} dk d v { d_v} dv 来表示
目前可能描述有有点抽象,不容易理解。结合一些应用来说,比如,如果是在自动问答任务中的话,Q 可以代表答案的词向量序列,取 K = V 为问题的词向量序列,那么输出就是所谓的 Aligned Question Embedding。

其pytorch实现为:

import torch
import torch.nn as nn
import torch.functional as F
import numpy as np

class ScaledDotProductAttention(nn.Module):
    """Scaled dot-product attention mechanism."""

    def __init__(self, attention_dropout=0.0):
        super(ScaledDotProductAttention, self).__init__()
        self.dropout = nn.Dropout(attention_dropout)
        self.softmax = nn.Softmax(dim=2)

    def forward(self, q, k, v, scale=None, attn_mask=None):
        """
        前向传播.
        Args:
        	q: Queries张量,形状为[B, L_q, D_q]
        	k: Keys张量,形状为[B, L_k, D_k]
        	v: Values张量,形状为[B, L_v, D_v],一般来说就是k
        	scale: 缩放因子,一个浮点标量
        	attn_mask: Masking张量,形状为[B, L_q, L_k]

        Returns:
        	上下文张量和attention张量
        """
        attention = torch.bmm(q, k.transpose(1, 2))
        if scale:
            attention = attention * scale
        if attn_mask:
            # 给需要 mask 的地方设置一个负无穷
            attention = attention.masked_fill_(attn_mask, -np.inf)
	# 计算softmax
        attention = self.softmax(attention)
	# 添加dropout
        attention = self.dropout(attention)
	# 和V做点积
        context = torch.bmm(attention, v)
        return context, attention

- Multi-head attention

Multi-Head Attention相当于多个不同的self-attention的集成,论文中h=8。论文提到,他们发现将 Q、K、V 通过一个线性映射之后,分成 h 份,对每一份进行 scaled dot-product attention 效果更好。然后,把各个部分的结果合并起来,再次经过线性映射,得到最终的输出。Multi-Head Attention图如下所示:
Multi-head Attention
论文中的具体实现为:
multi-head Attention

**

3.Layer normalization

Normalization 有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为 0 方差为 1 的数据。我们在把数据送入激活函数之前进行 normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。
说到 normalization,那就肯定得提到 Batch Normalization。

BN 的主要思想就是:在每一层的每一批数据上进行归一化。我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。

BN 的具体做法就是对每一小批数据,在批这个方向上做归一化。
什么是 Layer normalization 呢?它也是归一化数据的一种方式,不过 LN 是在每一个样本上计算均值和方差,而不是 BN 那种在批方向计算均值和方差!
在这里插入图片描述
下面看一下 LN 的公式:
在这里插入图片描述
**

4.Mask

**
mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。

其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。

  • Padding Mask

每个批次输入序列长度是不一样的,要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。因为这些填充的位置,其实是没什么意义的,所以我们的 attention 机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。

具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!

def padding_mask(seq_k, seq_q):
    # seq_k 和 seq_q 的形状都是 [B,L]
    len_q = seq_q.size(1)
    # `PAD` is 0
    pad_mask = seq_k.eq(0)
    pad_mask = pad_mask.unsqueeze(1).expand(-1, len_q, -1)  # shape [B, L_q, L_k]
    return pad_mask
  • Sequence mask

Decoder端多头self-attention模块与Encoder端的一致,但是需要注意的是Decoder端的多头self-attention需要做mask,因为它在预测时,是“看不到未来的序列的”,所以要将当前预测的单词(token)及其之后的单词(token)全部mask掉。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。

那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为 1,下三角的值权威0,对角线也是 0。把这个矩阵作用在每一个序列上,就可以达到我们的目的了。

def sequence_mask(seq):
    batch_size, seq_len = seq.size()
    mask = torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8),
                    diagonal=1)
    mask = mask.unsqueeze(0).expand(batch_size, -1, -1)  # [B, L, L]
    return mask

注意:
对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个 mask 相加作为attn_mask。
其他情况,attn_mask 一律等于 padding mask。

5.Positional Embedding

我们对每一个 word 进行 embedding 作为 input 表达。但是还有问题,embedding 本身不包含在句子中的相对位置信息。

那 RNN 为什么在任何地方都可以对同一个 word 使用同样的向量呢?因为 RNN 是按顺序对句子进行处理的,一次一个 word。但是在 Transformer 中,输入句子的所有 word 是同时处理的,没有考虑词的排序和位置信息。

对此,Transformer 的作者提出了加入 ”positional encoding“ 的方法来解决这个问题。”positional encoding“ 使得 Transformer 可以衡量 word 位置有关的信息。论文中的具体实现为:
在这里插入图片描述
pos 指的是这个 word 在这个句子中的位置
i指的是 embedding 维度。比如选择 d_model=512,那么i就从1数到512

为什么选择 sin 和 cos ?positional encoding 的每一个维度都对应着一个正弦曲线,作者假设这样可以让模型相对轻松地通过对应位置来学习。

其具体实现为:

class PositionalEncoding(nn.Module):
    
    def __init__(self, d_model, max_seq_len):
        """初始化。
        Args:
            d_model: 一个标量。模型的维度,论文默认是512
            max_seq_len: 一个标量。文本序列的最大长度
        """
        super(PositionalEncoding, self).__init__()
        
        # 根据论文给的公式,构造出PE矩阵
        position_encoding = np.array([
          [pos / np.power(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)]
          for pos in range(max_seq_len)])
        # 偶数列使用sin,奇数列使用cos
        position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
        position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])

        # 在PE矩阵的第一行,加上一行全是0的向量,代表这`PAD`的positional encoding
        # 在word embedding中也经常会加上`UNK`,代表位置单词的word embedding,两者十分类似
        # 那么为什么需要这个额外的PAD的编码呢?很简单,因为文本序列的长度不一,我们需要对齐,
        # 短的序列我们使用0在结尾补全,我们也需要这些补全位置的编码,也就是`PAD`对应的位置编码
        pad_row = torch.zeros([1, d_model])
        position_encoding = torch.cat((pad_row, position_encoding))
        
        # 嵌入操作,+1是因为增加了`PAD`这个补全位置的编码,
        # Word embedding中如果词典增加`UNK`,我们也需要+1。看吧,两者十分相似
        self.position_encoding = nn.Embedding(max_seq_len + 1, d_model)
        self.position_encoding.weight = nn.Parameter(position_encoding,
                                                     requires_grad=False)
    def forward(self, input_len):
        """神经网络的前向传播。

        Args:
          input_len: 一个张量,形状为[BATCH_SIZE, 1]。每一个张量的值代表这一批文本序列中对应的长度。

        Returns:
          返回这一批序列的位置编码,进行了对齐。
        """
        
        # 找出这一批序列的最大长度
        max_len = torch.max(input_len)
        tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
        # 对每一个序列的位置进行对齐,在原序列位置的后面补上0
        # 这里range从1开始也是因为要避开PAD(0)的位置
        input_pos = tensor(
          [list(range(1, len + 1)) + [0] * (max_len - len) for len in input_len])
        return self.position_encoding(input_pos)

**

6.Position-wise Feed-Forward network

**
这是一个全连接网络,包含两个线性变换和一个非线性函数(实际上就是 ReLU)。公式如下:
在这里插入图片描述
具体实现为:

class PositionalWiseFeedForward(nn.Module):

    def __init__(self, model_dim=512, ffn_dim=2048, dropout=0.0):
        super(PositionalWiseFeedForward, self).__init__()
        self.w1 = nn.Conv1d(model_dim, ffn_dim, 1)
        self.w2 = nn.Conv1d(ffn_dim, model_dim, 1)
        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(model_dim)

    def forward(self, x):
        output = x.transpose(1, 2)
        output = self.w2(F.relu(self.w1(output)))
        output = self.dropout(output.transpose(1, 2))

        # add residual and norm layer
        output = self.layer_norm(x + output)
        return output

对于Transformer的一些常见问题总结:

1.Transformer为什么需要进行Multi-head Attention?这样做有什么好处?Multi-head Attention的计算过程?各方论文的观点是什么?

Multi-head Attention的原因是将模型分为多个头,形成多个子空间,可以让模型去关注不同方面的信息,最后再将各个方面的信息综合起来。其实直观上也可以想到,如果自己设计这样的一个模型,必然也不会只做一次attention,多次attention综合的结果至少能够起到增强模型的作用,也可以类比CNN中同时使用多个卷积核的作用,直观上讲,多头的注意力有助于网络捕捉到更丰富的特征/信息

2.Transformer相比于RNN/LSTM,有什么优势?为什么?

(1).RNN系列的模型,并行计算能力很差

RNN系列的模型T时刻隐层状态的计算,依赖两个输入,一个是T时刻的句子输入单词X_t,另一个是T−1时刻的隐层状态的输出,这是最能体现RNN本质特征的一点,RNN的历史信息是通过这个信息传输渠道往后传输的。而RNN并行计算的问题就出在这里,因为T时刻的计算依赖T−1时刻的隐层计算结果,而T−1时刻的计算依赖T-2T−2时刻的隐层计算结果,如此下去就形成了所谓的序列依赖关系。

(2).Transformer的特征抽取能力比RNN系列的模型要好
相关对比实验可见:
放弃幻想,全面拥抱Transformer:自然语言处理三大特征抽取器(CNN/RNN/TF)比较

3.Transformer是如何训练的?测试阶段如何进行测试呢?
Transformer训练过程与seq2seq类似,首先Encoder端得到输入的encoding表示,并将其输入到Decoder端做交互式attention,之后在Decoder端接收其相应的输入,经过多头self-attention模块之后,结合Encoder端的输出,再经过FFN,得到Decoder端的输出之后,最后经过一个线性全连接层,就可以通过softmax来预测下一个单(token),然后根据softmax多分类的损失函数,将loss反向传播即可,所以从整体上来说,Transformer训练过程就相当于一个有监督的多分类问题。
需要注意的是,Encoder端可以并行计算,一次性将输入序列全部encoding出来,但Decoder端不是一次性把所有单词(token)预测出来的,而是像seq2seq一样一个接着一个预测出来的。
而对于测试阶段,其与训练阶段唯一不同的是Decoder端最底层的输入,具体地前边讲过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值