ibcadmin 发表于 2019-10-24 09:45:44

深入理解Transformer及其源码解读

<p></p>
  深度学习广泛应用于各个范畴。基于transformer的预训练模型(gpt/bertd等)根本已统治NLP深度学习范畴,可见transformer的紧张性。本文联合《Attention is all you need》与Harvard的代码《Annotated Transformer》深入明白transformer模型。 Harvard的代码在python3.6 torch 1.0.1 上跑不通,本文做了很多修改。修改后的代码地点:Transformer。
<h2>1 模型的头脑</h2>
<p>  Transformer中扬弃了传统的CNN和RNN,整个网络布局完满是由Attention机制构成。 作者接纳Attention机制的原因是考虑到RNN(或者LSTM,GRU等)的盘算限定为是序次的,也就是说RNN相关算法只能从左向右依次盘算或者从右向左依次盘算,这种机制带来了两个问题: </p>

  <strong>(1) 时间片 $t$ 的盘算依赖 $t-1$ 时候的盘算结果,如许限定了模型的并行本领;</strong>
  (2) 序次盘算的过程中信息会丢失,只管LSTM等门机制的布局肯定程度上缓解了长期依赖的问题,但是对于特别<strong>长期的依赖现象,LSTM依旧无能为力</strong>。
<p>  Transformer的提出解决了上面两个问题:</p>
  (1) 起首它使用了<strong>Attention机制</strong>,将序列中的任意两个位置之间的隔断是缩小为一个常量;
  (2) 其次它不是类似RNN的序次布局,因此具有<strong>更好的并行性,符合现有的GPU框架</strong>。

<h2>2 模型的架构</h2>
<div align="center"></div>
<p>  如上图,transformer模型本质上是一个Encoder-Decoder的布局。输入序列先辈行Embedding,颠末Encoder之后联合上一次output再输入Decoder,最后用softmax盘算序列下一个单词的概率。</p>

<h2>3 Embedding</h2>
<p>  transformer的输入是<strong>Word Embedding + Position Embedding</strong>。</p>
<h2>3.1 Word Embedding</h2>
  Word embedding在pytorch中通常用 nn.Embedding 实现,其权重矩阵通常有两种选择:
  (1)使用 Pre-trained的<strong>Embeddings并固化</strong>,这种环境下现实就是一个 Lookup Table。
  (2)对其进行随机初始化(固然也可以选择 Pre-trained 的结果),但<strong>设为 Trainable</strong>。如许在 training 过程中不停地对 Embeddings 进行改进。
  transformer选择后者,代码实现如下:


class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
      super(Embeddings, self).__init__()
      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)


  其中d_model表示embedding的维度,即词向量的维度;vocab表示词汇表的数目。

<h2>3.2 Positional Embedding</h2>

<p>  在RNN中,对句子的处理是一个个word按序次输入的。但在 Transformer 中,输入句子的全部word是同时处理的,没有考虑词的排序和位置信息。因此,Transformer 的作者提出了加入 “positional encoding” 的方法来解决这个问题。“positional encoding“”使得 Transformer 可以衡量 word 位置有关的信息。</p>
<p>  <strong>如何实现具有位置信息的encoding呢?</strong>作者提供了两种思绪:</p>
<ul>
<li>通过训练学习 positional encoding 向量;</li>
<li>使用公式来盘算 positional encoding向量。</li>
</ul>
<p>  试验后发现两种选择的结果是相似的,所以接纳了第2种方法,优点是不必要训练参数,而且纵然在训练会合没有出现过的句子长度上也能用。</p>
<p></p>
  <strong>Positional Encoding的公式如下:</strong>
<strong>$$PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}})$$</strong>
<strong>$$PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})$$</strong>
  其中,$pos$指的是这个 word 在这个句子中的位置;$2i$指的是 embedding 词向量的偶数维度,$2i+1$指的是embedding 词向量的奇数维度。
具体实现如下:

# 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)         #
      self.register_buffer('pe', pe)
      
    def forward(self, x):
      x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
      return self.dropout(x)


  <strong>注意:</strong>"x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)" 这行代码表示;输入模型的<strong>整个Embedding是Word Embedding与Positional Embedding直接相加之后的结果。</strong><br />
  <strong>为什么上面的两个公式能体现单词的相对位置信息呢?</strong>
  我们写一段代码取词向量的4个维度看下:

# 在位置编码下方,将基于位置添加正弦波。对于每个维度,波的频率和偏移都不同。
plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)
y = pe.forward(Variable(torch.zeros(1, 100, 20)))
plt.plot(np.arange(100), y.data.numpy())
plt.legend(["dim %d"%p for p in ])



<p>  输出图像:</p>
<p><div align="center"></div></p>
<p>  可以看到某个序列中不同位置的单词,在某一维度上的位置编码数值不一样,即同一序列的不同单词在单个纬度符合某个正弦或者余弦,可认为他们的具有相对关系。</p>


<h2>4 Encoder</h2>
  Encoder部分是由个层类似小Encoder Layer串联而成。小Encoder Layer可以简化为两个部分:(<strong>1)Multi-Head Self Attention</strong> <strong>(2) Feed-Forward network</strong>。示意图如下:
<div align="center"></div>
  究竟上multi head self attention 和feed forward network之后都接了一层add 和norm这里先不讲,后面4.1.2再讲。
<h2>4.1 Muti-Head-Attention</h2>
  Multi-Head Self Attention 现实上是<strong>由h个Self Attention 层并行构成,原文中h=8</strong>。接下来我们先先容Self Attention。
<h2>4.1.1 Self-Attention</h2>
  self-attention的输入是序列词向量,此处记为x。x颠末一个线性变换得到query(Q), x颠末第二个线性变换得到<code >key(K)</code>,x颠末第三个线性变换得到<code >value(V)</code>。
也就是:

<ul>
<li>key = linear_k(x)</li>
<li>query = linear_q(x)</li>
<li>value = linear_v(x)</li>
</ul>
<p>用矩阵表示即:</p>
<p><div align="center"></div></p>
<p>  <strong>注意:这里的linear_k, linear_q, linear_v是相互独立、权重($W^Q$, $W^K$, $<strong>W^V</strong>$)是不同的,通过训练可得到。</strong>得到query(Q),key(K),value(V)之后按照下面的公式盘算attention(Q, K, V):</p>

<strong>$$Attention(Q, K, V) = Softmax(\frac{QK^T}{\sqrt{d_k}})V$$</strong>
<strong>用矩阵表示上面的公式即:</strong>
<strong><strong><strong><div align="center"></div></strong></strong></strong>
<p>   这里Z就是attention(Q, K, V)。</p>
<p>  (1) 这里$d_k=d_{model}/h = 512/8 = 64$。</p>
<p>  <strong>(</strong><strong>2) 为什么要用$\sqrt{d_k}$ 对 $QK^T$进行缩放呢?</strong></p>
<p>  $d_k$现实上是Q/K/V的最后一个维度,当$d_k$越大,$QK^T$就越大,大概会<strong>将</strong><strong>softmax函数推入梯度极小的地区</strong>。</p>
<p>  (3) softmax之后值都介于0到1之间,可以明白成得到了 attention weights。然后基于这个 attention weights 对 V 求 weighted sum 值 Attention(Q, K, V)。 </p>
<p>  <strong>Multi-Head-Attention 就是将embedding之后的X按维度$d_{model}=512$ 切割成$h=8$个,分别做self-attention之后再合并在一起。</strong></p>
<p><strong>源码如下:</strong></p>

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是形状 。
         输出的x 的形状同上。
      """
      if mask is not None:
            # Same mask applied to all h heads.
            mask = mask.unsqueeze(1)
      nbatches = query.size(0)
      
      # 1) 这一步qkv变革: ->
      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 : -->x:, attn
      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)

<p> </p>

<h2>4.1.2 Add & Norm</h2>
  x 序列颠末multi-head-self-attention 之后现实颠末一个“add+norm”层,再进入feed-forward network(后面简称FFN),在FFN之后又颠末一个norm再输入下一个encoder layer。

class LayerNorm(nn.Module):
    """构造一个layernorm模块"""
    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):
      "Norm"
      mean = x.mean(-1, keepdim=True)
      std = x.std(-1, keepdim=True)
      return self.a_2 * (x - mean) / (std + self.eps) + self.b_2


class SublayerConnection(nn.Module):
    """Add+Norm"""
    def __init__(self, size, dropout):
      super(SublayerConnection, self).__init__()
      self.norm = LayerNorm(size)
      self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
      "add norm"
      return x + self.dropout(sublayer(self.norm(x)))


<p>  <strong>注意:几乎每个sub layer之后都会颠末一个归一化,然后再加在原来的输入上。这里叫残余毗连。</strong></p>

<h2>4.2 Feed-Forward Network</h2>
  Feed-Forward Network可以细分为有两层,第一层是一个线性激活函数,第二层是激活函数是ReLU。可以表示为:
<strong>$$FFN=max(0, xW_1+b_1)W_2 + b_2$$</strong>
  这层比力简单,就是实现上面的公式,直接看代码吧:

# 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))))

<p>  总的来说Encoder 是由上述小encoder layer 6个串行叠加构成。encoder sub layer重要包罗两个部分:</p>


<ul>
<li>SubLayer-1 做 Multi-Headed Attention</li>
<li>SubLayer-2 做 Feed Forward Neural Network</li>
</ul>
<p>  来看下Encoder主架构的代码:</p>

def clones(module, N):
    "产生N个类似的层"
    return nn.ModuleList()

class Encoder(nn.Module):
    """N层堆叠的Encoder"""
    def __init__(self, layer, N):
      super(Encoder, self).__init__()
      self.layers = clones(layer, N)
      self.norm = LayerNorm(layer.size)
      
    def forward(self, x, mask):
      "每层layer依次通过输入序列与mask"
      for layer in self.layers:
            x = layer(x, mask)
      return self.norm(x)


<h2>5 Decoder</h2>
  Decoder与Encoder有所不同,Encoder与Decoder的关系可以用下图形貌(以机器翻译为例):
<div align="center"></div>
<p>Decoder的代码重要布局:</p>

# Decoder部分
class Decoder(nn.Module):
    """带mask功能的通用Decoder布局"""
    def __init__(self, layer, N):
      super(Decoder, self).__init__()
      self.layers = clones(layer, N)
      self.norm = LayerNorm(layer.size)
      
    def forward(self, x, memory, src_mask, tgt_mask):
      for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
      return self.norm(x)

<p>Decoder子布局(Sub layer):</p>
<p><div align="center"></div> </p>
<p>  Decoder 也是N=6层堆叠的布局。被分为3个 SubLayer,Encoder与Decoder有<strong>三大重要的不同</strong>:</p>
<p>  (1)Decoder SubLayer-1 使用的是 “Masked” Multi-Headed Attention 机制,防止为了模型看到要猜测的数据,防止泄露。</p>
<p>  (2)SubLayer-2 是一个 Encoder-Decoder Multi-head Attention。</p>
<p>  (3)LinearLayer 和 SoftmaxLayer 作用于 SubLayer-3 的输出后面,来猜测对应的 word 的 probabilities 。</p>

<h2>5.1 Mask-Multi-Head-Attention</h2>
  Mask 的目标是防止 Decoder “seeing the future”,就像防止考生偷看考试答案一样。这里mask是一个下三角矩阵,对角线以及对角线左下都是1,别的都是0。下面是个10维度的下三角矩阵:


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


Mask的代码实现:


def subsequent_mask(size):
    """
    mask后续的位置,返回尺寸下三角Tensor
    对角线及其左下角满是1,右上角满是0
    """
    attn_shape = (1, size, size)
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    return torch.from_numpy(subsequent_mask) == 0


  当mask不为空的时候,attention盘算必要将x做一个操作:scores = scores.masked_fill(mask == 0, -1e9)。即将mask==0的替换为-1e9,别的稳固。
<h2>5.2 Encoder-Decoder Multi-head Attention</h2>
  这部分和Multi-head Attention的区别是该层的输入<strong>来自encoder和上一次decoder的结果</strong>。具体实现如下:


class DecoderLayer(nn.Module):
    "Decoder is made of self-attn, src-attn, and feed forward (defined below)"
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
      super(DecoderLayer, self).__init__()
      self.size = size
      self.self_attn = self_attn
      self.src_attn = src_attn
      self.feed_forward = feed_forward
      self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
      "将decoder的三个Sublayer串联起来"
      m = memory
      x = self.sublayer(x, lambda x: self.self_attn(x, x, x, tgt_mask))
      x = self.sublayer(x, lambda x: self.src_attn(x, m, m, src_mask))
      return self.sublayer(x, self.feed_forward)


<p>  注意:self.sublayer(x, lambda x: self.src_attn(x, m, m, src_mask)) 这行就是Encoder-Decoder Multi-head Attention。</p>
<p>  query = x,key = m, value = m, mask = src_mask,<strong>这里x来自上一个 DecoderLayer,m来自 Encoder的输出</strong>。</p>
<h2>5.3 Linear and Softmax to Produce Output Probabilities</h2>
  Decoder的最后一个部分是过一个linear layer将decoder的输出扩展到与vocabulary size一样的维度上。颠末softmax 后,选择概率最高的一个word作为猜测结果。假设我们有一个已经训练好的网络,<strong>在做猜测时,步调如下:</strong>
  (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>,翻译完成。<br />  这部分的代码实现:

class Generator(nn.Module):
    """
    Define standard linear + softmax generation step。
    界说尺度的linear + softmax 天生步调。
    """
    def __init__(self, d_model, vocab):
      super(Generator, self).__init__()
      self.proj = nn.Linear(d_model, vocab)

    def forward(self, x):
      return F.log_softmax(self.proj(x), dim=-1)


<p>  <strong>在训练过程中,</strong>模型没有收敛得很好时,Decoder猜测产生的词很大概不是我们想要的。这个时候假如再把错误的数据再输给Decoder,就会越跑越偏。这个时候怎么办?</p>
<p>  (1)在训练过程中可以使用 “teacher forcing”。由于我们知道应该猜测的word是什么,那么可以给Decoder喂一个正确的结果作为输入。</p>
<p>  (2)除了选择最高概率的词 (greedy search),还可以选择是比如 “Beam Search”,可以保存topK个猜测的word。 Beam Search 方法不再是只得到一个输出放到下一步去训练了,我们可以设定一个值,拿多个值放到下一步去训练,这条路径的概率即是每一步输出的概率的乘积。</p>

<h2>6 Transformer的优缺点</h2>
<h2>6.1 优点</h2>

<p>  (1)每层盘算<strong>复杂度比RNN要低</strong>。</p>
<p>  (2)可以进行<strong>并行盘算</strong>。</p>
<p>  (3)从盘算一个序列长度为n的信息要颠末的路径长度来看, CNN必要增加卷积层数来扩大视野,RNN必要从1到n逐个进行盘算,而Self-attention只必要一步矩阵盘算就可以。Self-Attention可以比RNN<strong>更好地解决长时依赖问题</strong>。固然假如盘算量太大,比如序列长度N大于序列维度D这种环境,也可以用窗口限定Self-Attention的盘算数目。</p>
<p>  (4)从作者在附录中给出的栗子可以看出,Self-Attention<strong>模型更可表明,Attention结果的分布表明了该模型学习到了一些语法和语义信息</strong>。</p>
<h2>6.2 缺点</h2>
<p>  在原文中没有提到缺点,是后来在Universal Transformers中指出的,重要是两点:</p>
<p>  (1)实践上:有些RNN轻易可以解决的问题transformer没做到,比如<strong>复制string</strong>,或者推理时碰到的sequence长度比训练时更长(由于碰到了没见过的position embedding)。</p>
<p>  (2)理论上:transformers不是computationally universal(图灵完备),这种非RNN式的模型黑白图灵完备的的,<strong>无法单独完成NLP中推理、决议等盘算问题</strong>(包罗使用transformer的bert模型等等)。</p>
<h2>7 References</h2>
<p>  1 http://jalammar.github.io/illustrated-transformer/</p>
<p>  2 https://zhuanlan.zhihu.com/p/48508221</p>
<p>  3 https://zhuanlan.zhihu.com/p/47063917</p>
<p>  4 https://zhuanlan.zhihu.com/p/80986272</p>
<p>  5 https://arxiv.org/abs/1706.03762</p>

<br><br/><br/><br/><br/><br/>来源:<a href="https://www.cnblogs.com/zingp/p/11696111.html" target="_blank">https://www.cnblogs.com/zingp/p/11696111.html</a>
页: [1]
查看完整版本: 深入理解Transformer及其源码解读