nlper 成长之路(一)——Transformer理解与深入

最近需要用到Transformer,花了几天时间理解了一下源码,惊叹于其设计的思想之精妙,这会是一系列长篇文章的第一部分。

1. 预备知识

1.1 BLEU

​ biligual evaluation understudy 算法是一种衡量模型生成序列和参考序列之间的N元词组(N-Gram)重合度的算法,最早用来评价机器翻译模型的质量,目前也广泛应用在各种序列生成任务中

​ 令 x x x为从模型分布 p θ p_\theta pθ中生成的一个候选(Candidate)序列, s ( 1 ) , ⋯   , s ( K ) s^{(1)},\cdots,s^{(K)} s(1),,s(K)为从真实数据分布中采集的一组参考序列, W W W为从生成的候选序列中提取所有 N N N元组合的集合,这些 N N N元组合的精度(Precision)
P N ( x ) = ∑ w ∈ W m i n ( c w ( x ) , m a x k = 1 K c w ( s ( k ) ) ) ∑ w ∈ W c w ( x ) P_N(x)=\frac{\sum\limits_{w \in W} min(c_w(x),max^ {K} _ {k=1}c_w(s^{(k)}))}{\sum\limits_{w\in W}c_w(x)} PN(x)=wWcw(x)wWmin(cw(x),maxk=1Kcw(s(k)))
其中 c w ( x ) c_w(x) cw(x)是N元组合 w w w在生成序列 x x x中出现的次数, c w ( s ( k ) ) c_w(s^{(k)}) cw(s(k))是N元组合 w w w在参考序列 s ( k ) s^{(k)} s(k)中出现的次数,N元组合的精度 P N ( x ) P_N(x) PN(x)是计算生成序列中的 N N N元组合有多少比例在参考序列中出现。

​ 由于精度只衡量生成序列中的 N N N元组合是否在参考序列中出现,生成序列越短,其精度会越高,因此可以引入长度惩罚因子(Brevity Penalty),如果生成序列的长度短于参考序列,就对其进行惩罚。
b ( x ) = { 1 i f l x > l s e x p ( 1 − l s / l x ) i f l x ≤ l s b(x) = \left \{ \begin{array}{rcl} 1 & & if & l_x > l_s \\ exp(1-l_s/l_x) & & if & l_x \leq l_s \end{array} \right. b(x)={1exp(1ls/lx)ififlx>lslxls
其中 l x l_x lx为生成序列 x x x的长度, l s l_s ls为参考序列的最短长度

BLEU算法是通过计算不同长度的N元组合($N=1,2, \cdots $)的精度,并进行几何加权平均而得到。
B L E U − N ( x ) = b ( x ) × e x p ( ∑ N = 1 N ′ a N l o g P N ) BLEU-N(x) = b(x) \times exp(\sum\limits_{N=1}^{N'}a_NlogP_N) BLEUN(x)=b(x)×exp(N=1NaNlogPN)
其中 N ′ N' N为最长N元组合的长度, a N a_N aN为不同N元组合的权重,一般设为 1 N ′ \frac {1}{N'} N1,BLEU算法的值域是[0,1],越大表明生成的质量越好,但是BLEU算法只计算精度,而不关心召回率(即参考序列的N元组合是否在生成序列中出现).

1.2. Attention is All you need

1.2.1 Transformer历史意义

  • 提出self-attention,拉开了非序列化模型的序幕。

  • 为预训练模型的到来打下了坚实的基础。

1.2.2 基于transformer结构的预训练模型

bert(采用的transformer的encoder部分)

gpt(采用的transformer的decoder部分)

albert等tingbert模型

1.2.3 论文结构

image-20201212100302365

1.2.4 transformer结构

image-20201212100456334image-20201212100535044

输入 --> input embedding --> positional encoding

for i in range(6):

​ self attention --> layer normalization --> feed forward --> layer normalization

for i in range(6):

​ self attention --> layer normalization -->encoder-decoder attention --> layer normalization --> feed forward --> layer normalization

  • 使用了残差网络的思想,每个子层的输出都是 L a y e r N o r m ( x + S u b l a y e r ( x ) ) LayerNorm(x+Sublayer(x)) LayerNorm(x+Sublayer(x)),其中 S u b l a y e r Sublayer Sublayer是要实现的功能。

1.2.5 transformer细节

建议配合源码食用,可以在colab里打开跑一遍
https://github.com/tensorflow/docs/blob/master/site/en/tutorials/text/transformer.ipynb
中文版本:
https://github.com/tensorflow/docs-l10n/blob/master/site/zh-cn/tutorials/text/transformer.ipynb

1.2.5.1 Positional encoding

word embedding 不包含位置信息,因此需要向模型中添加一个句子中各个词的相对位置信息。

计算方法如下:
a n g l e _ r a t e d = ( m i n _ r a t e ) d / d m a x \Large angle\_rate_d = (min\_rate)^{d / d_{max}} angle_rated=(min_rate)d/dmax

angle_rate_exponents = np.linspace(0,1,depth//2)
angle_rates = min_rate**(angle_rate_exponents)

angle rate 的范围从 1 [rads/step]min_rate [rads/step] d m a x d_{max} dmax 个点.
image-20201214000303877

positions = np.arange(num_positions) 
angle_rads = (positions[:, np.newaxis])*angle_rates[np.newaxis, :]

计算angle_rads

做弧度——角度转换作图如下
在这里插入图片描述

但这样的原始角度做模型输入是不好的,不连续(不可导),且无界

因此做如下的转换:

P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) \Large{PE_{(pos, 2i)} = sin(pos / 10000^{2i / d_{model}})} PE(pos,2i)=sin(pos/100002i/dmodel)
P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d m o d e l ) \Large{PE_{(pos, 2i+1)} = cos(pos / 10000^{2i / d_{model}})} PE(pos,2i+1)=cos(pos/100002i/dmodel)

  • positional encoding之所以这么计算的原因是求一个新的词的位置编码时,可以用之前的位置通过线性运算求解,因此可证明位置编码的合理性

s i n ( a + b ) = s i n ( a ) ∗ c o s ( b ) + c o s ( a ) ∗ s i n ( b ) c o s ( a + b ) = c o s ( a ) ∗ c o s ( b ) − s i n ( a ) ∗ s i n ( b ) \Large { sin(a+b) = sin(a)*cos(b)+cos(a)*sin(b)\\ cos(a+b) = cos(a)*cos(b)-sin(a)*sin(b) } sin(a+b)=sin(a)cos(b)+cos(a)sin(b)cos(a+b)=cos(a)cos(b)sin(a)sin(b)

P E ( p o s + s t e p , 2 i ) = s i n ( p o s ∗ a n g l e _ r a t e i ) ∗ c o s ( s t e p ∗ a n g l e _ r a t e i ) + c o s ( p o s ∗ a n g l e _ r a t e i ) ∗ s i n ( s t e p ∗ a n g l e _ r a t e i ) {PE_{(pos + step, 2i)} = sin(pos *angle\_rate_i)*cos(step *angle\_rate_i)+ cos(pos *angle\_rate_i)*sin(step *angle\_rate_i)} PE(pos+step,2i)=sin(posangle_ratei)cos(stepangle_ratei)+cos(posangle_ratei)sin(stepangle_ratei)
P E ( p o s + s t e p , 2 i + 1 ) = c o s ( p o s ∗ a n g l e _ r a t e i ) ∗ c o s ( s t e p ∗ a n g l e _ r a t e i ) − s i n ( p o s ∗ a n g l e _ r a t e i ) ∗ s i n ( s t e p ∗ a n g l e _ r a t e i ) {PE_{(pos+step, 2i+1)} = cos(pos *angle\_rate_i)*cos(step *angle\_rate_i)- sin(pos *angle\_rate_i)*sin(step *angle\_rate_i)} PE(pos+step,2i+1)=cos(posangle_ratei)cos(stepangle_ratei)sin(posangle_ratei)sin(stepangle_ratei)

image-20201214013856319

-10

image-20201214013838151

正弦波关于y轴奇对称,余弦波偶对称

plt.plot(np.dot(pos_encoding,update)[:,60])

image-20201214014633564

每一列都是正弦波

1.2.5.2 Scaled Dot-Product Attention

scaled_dot_product_attention

分步解释
注意力机制需要三个输入,Q(query)、K(key)、V(value)
计算方法如下:
A t t e n t i o n ( Q , K , V ) = s o f t m a x k ( Q K T d k ) V \Large{Attention(Q, K, V) = softmax_k(\frac{QK^T}{\sqrt{d_k}}) V} Attention(Q,K,V)=softmaxk(dk QKT)V

  • Q、K、V是怎么来的?
    假设输入的词向量为x,用来计算Q、K、V、的矩阵分别为 W Q , W K , W V W_Q,W_K,W_V WQ,WK,WV
    Q = x × W Q K = x × W K V = x × W V Q = x \times W_Q\\ K = x \times W_K\\ V = x \times W_V Q=x×WQK=x×WKV=x×WV
    Q:查询向量
    K:表示被查询信息与其他信息的相关性的向量
    V:表示被查询信息的向量
    query shape = (…, seq_len_q, depth)
    key shape = (…, seq_len_k, depth)
    value shape = (…, seq_len_v, depth_v)
    Q,K,V除了最后两个维度要其他都相同,seq_len_k 需要和 seq_len_v 相同

  • scale限幅操作
    考虑softmax的形状,较大的值会引起较小的梯度,因此如果depth( d k d_k dk)较大,会使Q、K相乘的结果较大,算得的梯度较小,不利于梯度传播。
    例如,假设Q和K的平均值为0,方差为1。它们的矩阵乘法的平均值为0,方差为dk。 因此,使用dk的平方根用于缩放(而不是其他任何数字),能够使Q和K的乘积的均值为0,方差为1,可获得更柔和的softmax。

  • mask矩阵的计算

    • look_ahead mask是一个右上三角全为1的矩阵,为1的部分代表不可见部分,用于遮挡一个序列中的后续标记,在解码器部分使用,即在预测一个词时,不应该看见这个词之后的词。
      def create_look_ahead_mask(size):
          mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
          return mask  # (seq_len, seq_len)
      
    • padding mask
      对于那些用来填充词向量的为0的位置,padding mask 对应位置为1, 它用来使输入的填充部分不会用于计算中。
      def create_padding_mask(seq):
      seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
      
      # add extra dimensions to add the padding
      # to the attention logits.
      return seq[:, tf.newaxis, tf.newaxis, :]  # (batch_size, 1, 1, seq_len)
      
  • mask的作用位置
    值得注意的是,mask作用在 Q K T d k \frac{QK^T}{\sqrt{d_k}} dk QKT上,这是一个最后两个维度为 (seq_len, seq_len)的张量。mask的一行代表一个句子,一列代表词在句子中的位置,本来就应当作用在这样一个张量上,运算过程中存在broadcast。

mask中为1的部分会乘-1*e9,考虑softmax的形状,直接会使输出值为0,同时梯度也为0
该部分源码如下:

def scaled_dot_product_attention(q, k, v, mask):
  """计算注意力权重。
  q, k, v 必须具有匹配的前置维度。
  k, v 必须有匹配的倒数第二个维度,例如:seq_len_k = seq_len_v。
  虽然 mask 根据其类型(填充或前瞻)有不同的形状,
  但是 mask 必须能进行广播转换以便求和。
  
  参数:
    q: 请求的形状 == (..., seq_len_q, depth)
    k: 主键的形状 == (..., seq_len_k, depth)
    v: 数值的形状 == (..., seq_len_v, depth_v)
    mask: Float 张量,其形状能转换成
          (..., seq_len_q, seq_len_k)。默认为None。
    
  返回值:
    输出,注意力权重
  """

  matmul_qk = tf.matmul(q, k, transpose_b=True)  # (..., seq_len_q, seq_len_k)
  
  # 缩放 matmul_qk
  dk = tf.cast(tf.shape(k)[-1], tf.float32)
  scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)

  # 将 mask 加入到缩放的张量上。
  if mask is not None:
    scaled_attention_logits += (mask * -1e9)  

  # softmax 在最后一个轴(seq_len_k)上归一化,因此分数
  # 相加等于1。
  attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)  # (..., seq_len_q, seq_len_k)

  output = tf.matmul(attention_weights, v)  # (..., seq_len_q, depth_v)

  return output, attention_weights
1.2.5.3 Multi-Head Attention

在这里插入图片描述

几个问题

  • split与多个Dense层
    在实际写代码时并不需要先做split然后将每个头的输入分别先经过一个Dense,直接对Q、K、V分别经过一个Dense,然后再做split就可以,所谓的split其实就是reshape出一个新的维度num_head,而做Scaled Dot-Product Attention时也无需分开,上述Attention层在写的时候就只是对最后两个维度做转换,因此看上去无论输入还是运算过程中都是一个四维向量整体在运算,但实际上可以达到split的效果。
  • multi head到底有什么用?
    multi head在Transformer中起到了类似CNN中多个feature map的作用,一个矩阵的参数是随机初始化的,初始化的参数不同可能影响模型最终收敛的位置不同,因此可以理解成一套初始化参数就代表一个观察角度。多一个头就多一个调参空间,多一个头就相当于多一个模型做ensemble,所以头的个数既不是越多越好,也不是越少越好。


源码如下:

class MultiHeadAttention(tf.keras.layers.Layer):
  def __init__(self, d_model, num_heads):
    super(MultiHeadAttention, self).__init__()
    self.num_heads = num_heads
    self.d_model = d_model
    
    assert d_model % self.num_heads == 0
    
    self.depth = d_model // self.num_heads
    
    self.wq = tf.keras.layers.Dense(d_model)
    self.wk = tf.keras.layers.Dense(d_model)
    self.wv = tf.keras.layers.Dense(d_model)
    
    self.dense = tf.keras.layers.Dense(d_model)
        
  def split_heads(self, x, batch_size):
    """分拆最后一个维度到 (num_heads, depth).
    转置结果使得形状为 (batch_size, num_heads, seq_len, depth)
    """
    x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
    return tf.transpose(x, perm=[0, 2, 1, 3])
    
  def call(self, v, k, q, mask):
    batch_size = tf.shape(q)[0]
    
    q = self.wq(q)  # (batch_size, seq_len, d_model)
    k = self.wk(k)  # (batch_size, seq_len, d_model)
    v = self.wv(v)  # (batch_size, seq_len, d_model)
    
    q = self.split_heads(q, batch_size)  # (batch_size, num_heads, seq_len_q, depth)
    k = self.split_heads(k, batch_size)  # (batch_size, num_heads, seq_len_k, depth)
    v = self.split_heads(v, batch_size)  # (batch_size, num_heads, seq_len_v, depth)
    
    # scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth)
    # attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
    scaled_attention, attention_weights = scaled_dot_product_attention(
        q, k, v, mask)
    
    scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])  # (batch_size, seq_len_q, num_heads, depth)

    concat_attention = tf.reshape(scaled_attention, 
                                  (batch_size, -1, self.d_model))  # (batch_size, seq_len_q, d_model)

    output = self.dense(concat_attention)  # (batch_size, seq_len_q, d_model)
        
    return output, attention_weights
1.2.5.4 Point wise feed forward network

比较简单,只是经过两个Dense,中间有个Relu。
在这里插入图片描述

1.2.6 Transformer的编解码器

在这里插入图片描述
Transformer 模型与标准的具有注意力机制的序列到序列模型(sequence to sequence with attention model),遵循相同的一般模式。

输入语句经过 N 个编码器层,为序列中的每个词/标记生成一个输出。
解码器关注编码器的输出以及它自身的输入(自注意力)来预测下一个词。
1.2.6.1 Encoder Layer

每个编码器层包括以下子层:

多头注意力(有填充遮挡)
点式前馈网络(Point wise feed forward networks)。

每个子层在其周围有一个残差连接,然后进行层归一化。残差连接有助于避免深度网络中的梯度消失问题。

每个子层的输出是 LayerNorm(x + Sublayer(x))。归一化是在 d_model(最后一个)维度完成的。Transformer 中有 N 个编码器层。

  • LayerNorm 与 BatchNorm
    LN是和BN非常近似的一种归一化方法,不同的是BN取的是不同样本的同一个特征,而LN取的是同一个样本的不同特征。在BN和LN都能使用的场景中,BN的效果一般优于LN,原因是基于不同数据,同一特征得到的归一化特征更不容易损失信息。

但是有些场景是不能使用BN的,例如batchsize较小或者在RNN中,这时候可以选择使用LN,LN得到的模型更稳定且起到正则化的作用。RNN能应用到小批量和RNN中是因为LN的归一化统计量的计算是和batchsize没有关系的。

class EncoderLayer(tf.keras.layers.Layer):
  def __init__(self, d_model, num_heads, dff, rate=0.1):
    super(EncoderLayer, self).__init__()

    self.mha = MultiHeadAttention(d_model, num_heads)
    self.ffn = point_wise_feed_forward_network(d_model, dff)

    self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    
    self.dropout1 = tf.keras.layers.Dropout(rate)
    self.dropout2 = tf.keras.layers.Dropout(rate)
    
  def call(self, x, training, mask):

    attn_output, _ = self.mha(x, x, x, mask)  # (batch_size, input_seq_len, d_model)
    attn_output = self.dropout1(attn_output, training=training)
    out1 = self.layernorm1(x + attn_output)  # (batch_size, input_seq_len, d_model)
    
    ffn_output = self.ffn(out1)  # (batch_size, input_seq_len, d_model)
    ffn_output = self.dropout2(ffn_output, training=training)
    out2 = self.layernorm2(out1 + ffn_output)  # (batch_size, input_seq_len, d_model)
    
    return out2
1.2.6.2 Decoder Layer

每个解码器层包括以下子层:

遮挡的多头注意力(前瞻遮挡和填充遮挡)
多头注意力(用填充遮挡)。V(数值)和 K(主键)接收编码器输出作为输入。Q(请求)接收遮挡的多头注意力子层的输出。
点式前馈网络

每个子层在其周围有一个残差连接,然后进行层归一化。每个子层的输出是 LayerNorm(x + Sublayer(x))。归一化是在 d_model(最后一个)维度完成的。

Transformer 中共有 N 个解码器层。

当 Q 接收到解码器的第一个注意力块的输出,并且 K 接收到编码器的输出时,注意力权重表示根据编码器的输出赋予解码器输入的重要性。换一种说法,解码器通过查看编码器输出和对其自身输出的自注意力,预测下一个词。参看按比缩放的点积注意力部分的演示。

  • Decoder Layer中有两层multi-head attention,其中一层是计算输入的自注意力,作为第二层的 Q , 另一层的 V 和 K 是encoder的输出
class DecoderLayer(tf.keras.layers.Layer):
  def __init__(self, d_model, num_heads, dff, rate=0.1):
    super(DecoderLayer, self).__init__()

    self.mha1 = MultiHeadAttention(d_model, num_heads)
    self.mha2 = MultiHeadAttention(d_model, num_heads)

    self.ffn = point_wise_feed_forward_network(d_model, dff)
 
    self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    
    self.dropout1 = tf.keras.layers.Dropout(rate)
    self.dropout2 = tf.keras.layers.Dropout(rate)
    self.dropout3 = tf.keras.layers.Dropout(rate)
    
    
  def call(self, x, enc_output, training, 
           look_ahead_mask, padding_mask):
    # enc_output.shape == (batch_size, input_seq_len, d_model)

    attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask)  # (batch_size, target_seq_len, d_model)
    attn1 = self.dropout1(attn1, training=training)
    out1 = self.layernorm1(attn1 + x)
    
    attn2, attn_weights_block2 = self.mha2(
        enc_output, enc_output, out1, padding_mask)  # (batch_size, target_seq_len, d_model)
    attn2 = self.dropout2(attn2, training=training)
    out2 = self.layernorm2(attn2 + out1)  # (batch_size, target_seq_len, d_model)
    
    ffn_output = self.ffn(out2)  # (batch_size, target_seq_len, d_model)
    ffn_output = self.dropout3(ffn_output, training=training)
    out3 = self.layernorm3(ffn_output + out2)  # (batch_size, target_seq_len, d_model)
    
    return out3, attn_weights_block1, attn_weights_block2
1.2.6.3 Encoder

包括:

输入嵌入(Input Embedding)
位置编码(Positional Encoding)
N 个编码器层(encoder layers)

输入经过嵌入(embedding)后,该嵌入与位置编码相加。该加法结果的输出是编码器层的输入。编码器的输出是解码器的输入。

class Encoder(tf.keras.layers.Layer):
  def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
               maximum_position_encoding, rate=0.1):
    super(Encoder, self).__init__()

    self.d_model = d_model
    self.num_layers = num_layers
    
    self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
    self.pos_encoding = positional_encoding(maximum_position_encoding, 
                                            self.d_model)
    
    
    self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate) 
                       for _ in range(num_layers)]
  
    self.dropout = tf.keras.layers.Dropout(rate)
        
  def call(self, x, training, mask):

    seq_len = tf.shape(x)[1]
    
    # 将嵌入和位置编码相加。
    x = self.embedding(x)  # (batch_size, input_seq_len, d_model)
    x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
    x += self.pos_encoding[:, :seq_len, :]

    x = self.dropout(x, training=training)
    
    for i in range(self.num_layers):
      x = self.enc_layers[i](x, training, mask)
    
    return x  # (batch_size, input_seq_len, d_model)
1.2.6.4 Decoder

包括:

输出嵌入(Output Embedding)
位置编码(Positional Encoding)
N 个解码器层(decoder layers)

目标(target)经过一个嵌入后,该嵌入和位置编码相加。该加法结果是解码器层的输入。解码器的输出是最后的线性层的输入。

class Decoder(tf.keras.layers.Layer):
  def __init__(self, num_layers, d_model, num_heads, dff, target_vocab_size,
               maximum_position_encoding, rate=0.1):
    super(Decoder, self).__init__()

    self.d_model = d_model
    self.num_layers = num_layers
    
    self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
    self.pos_encoding = positional_encoding(maximum_position_encoding, d_model)
    
    self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate) 
                       for _ in range(num_layers)]
    self.dropout = tf.keras.layers.Dropout(rate)
    
  def call(self, x, enc_output, training, 
           look_ahead_mask, padding_mask):

    seq_len = tf.shape(x)[1]
    attention_weights = {}
    
    x = self.embedding(x)  # (batch_size, target_seq_len, d_model)
    x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
    x += self.pos_encoding[:, :seq_len, :]
    
    x = self.dropout(x, training=training)

    for i in range(self.num_layers):
      x, block1, block2 = self.dec_layers[i](x, enc_output, training,
                                             look_ahead_mask, padding_mask)
      
      attention_weights['decoder_layer{}_block1'.format(i+1)] = block1
      attention_weights['decoder_layer{}_block2'.format(i+1)] = block2
    
    # x.shape == (batch_size, target_seq_len, d_model)
    return x, attention_weights

1.2.7 完整的transformer

除了经过编码器解码器还要走一个带 softmax 的Dense

class Transformer(tf.keras.Model):
  def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size, 
               target_vocab_size, pe_input, pe_target, rate=0.1):
    super(Transformer, self).__init__()

    self.encoder = Encoder(num_layers, d_model, num_heads, dff, 
                           input_vocab_size, pe_input, rate)

    self.decoder = Decoder(num_layers, d_model, num_heads, dff, 
                           target_vocab_size, pe_target, rate)

    self.final_layer = tf.keras.layers.Dense(target_vocab_size)
    
  def call(self, inp, tar, training, enc_padding_mask, 
           look_ahead_mask, dec_padding_mask):

    enc_output = self.encoder(inp, training, enc_padding_mask)  # (batch_size, inp_seq_len, d_model)
    
    # dec_output.shape == (batch_size, tar_seq_len, d_model)
    dec_output, attention_weights = self.decoder(
        tar, enc_output, training, look_ahead_mask, dec_padding_mask)
    
    final_output = self.final_layer(dec_output)  # (batch_size, tar_seq_len, target_vocab_size)
    
    return final_output, attention_weights

1.2.8 源码中的其他部分

1.2.8.1 optimizer

采用Adam,学习率是下面这个函数,它使学习率在初始训练时线性增长,后面再非线性减小

class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
  def __init__(self, d_model, warmup_steps=4000):
    super(CustomSchedule, self).__init__()
    
    self.d_model = d_model
    self.d_model = tf.cast(self.d_model, tf.float32)

    self.warmup_steps = warmup_steps
    
  def __call__(self, step):
    arg1 = tf.math.rsqrt(step)
    arg2 = step * (self.warmup_steps ** -1.5)
    
    return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)

在这里插入图片描述

1.2.8.2 loss metric

在计算loss时,由于预测出的结果是做了padding的,因此不能直接算,先把做padding的部分屏蔽掉再算。
padding是由于句长不一样,要使他们长度一样,需要填0

def loss_function(real, pred):
  mask = tf.math.logical_not(tf.math.equal(real, 0))
  loss_ = loss_object(real, pred)

  mask = tf.cast(mask, dtype=loss_.dtype)
  loss_ *= mask
  
  return tf.reduce_sum(loss_)/tf.reduce_sum(mask)


def accuracy_function(real, pred):
  accuracies = tf.equal(real, tf.argmax(pred, axis=2))
  
  mask = tf.math.logical_not(tf.math.equal(real, 0))
  accuracies = tf.math.logical_and(mask, accuracies)

  accuracies = tf.cast(accuracies, dtype=tf.float32)
  mask = tf.cast(mask, dtype=tf.float32)
  return tf.reduce_sum(accuracies)/tf.reduce_sum(mask)
1.2.8.3 训练

目标(target)被分成了 tar_inp 和 tar_real。tar_inp 作为输入传递到解码器。tar_real 是位移了 1 的同一个输入:在 tar_inp 中的每个位置,tar_real 包含了应该被预测到的下一个标记(token)。

例如,sentence = “SOS A lion in the jungle is sleeping EOS”

tar_inp = “SOS A lion in the jungle is sleeping”

tar_real = “A lion in the jungle is sleeping EOS”

Transformer 是一个自回归(auto-regressive)模型:它一次作一个部分的预测,然后使用到目前为止的自身的输出来决定下一步要做什么。

在训练过程中,本示例使用了 teacher-forcing 的方法(就像文本生成教程中一样)。无论模型在当前时间步骤下预测出什么,teacher-forcing 方法都会将真实的输出传递到下一个时间步骤上。

当 transformer 预测每个词时,自注意力(self-attention)功能使它能够查看输入序列中前面的单词,从而更好地预测下一个单词。

为了防止模型在期望的输出上达到峰值,模型使用了前瞻遮挡(look-ahead mask)。

  • 训练时解码器的输入直接把整个完整的翻译后的句子做了输入,然后在 multi-head 部分做look-ahead mask遮挡住模型不该看到的部分,训练时的输出也是直接一个完整的句子。
# The @tf.function trace-compiles train_step into a TF graph for faster
# execution. The function specializes to the precise shape of the argument
# tensors. To avoid re-tracing due to the variable sequence lengths or variable
# batch sizes (the last batch is smaller), use input_signature to specify
# more generic shapes.

train_step_signature = [
    tf.TensorSpec(shape=(None, None), dtype=tf.int64),
    tf.TensorSpec(shape=(None, None), dtype=tf.int64),
]

@tf.function(input_signature=train_step_signature)
def train_step(inp, tar):
  tar_inp = tar[:, :-1]
  tar_real = tar[:, 1:]
  
  enc_padding_mask, combined_mask, dec_padding_mask = create_masks(inp, tar_inp)
  
  with tf.GradientTape() as tape:
    predictions, _ = transformer(inp, tar_inp, 
                                 True, 
                                 enc_padding_mask, 
                                 combined_mask, 
                                 dec_padding_mask)
    loss = loss_function(tar_real, predictions)

  gradients = tape.gradient(loss, transformer.trainable_variables)    
  optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
  
  train_loss(loss)
  train_accuracy(accuracy_function(tar_real, predictions))
1.2.8.4 评估

以下步骤用于评估:

用葡萄牙语分词器(tokenizer_pt)编码输入语句。此外,添加开始和结束标记,这样输入就与模型训练的内容相同。这是编码器输入。
解码器输入为 start token == tokenizer_en.vocab_size。
计算填充遮挡和前瞻遮挡。
解码器通过查看编码器输出和它自身的输出(自注意力)给出预测。
选择最后一个词并计算它的 argmax。
将预测的词连接到解码器输入,然后传递给解码器。
在这种方法中,解码器根据它预测的之前的词预测下一个。

def evaluate(inp_sentence):
  start_token = [tokenizer_pt.vocab_size]
  end_token = [tokenizer_pt.vocab_size + 1]
  
  # inp sentence is portuguese, hence adding the start and end token
  inp_sentence = start_token + tokenizer_pt.encode(inp_sentence) + end_token
  encoder_input = tf.expand_dims(inp_sentence, 0)
  
  # as the target is english, the first word to the transformer should be the
  # english start token.
  decoder_input = [tokenizer_en.vocab_size]
  output = tf.expand_dims(decoder_input, 0)
    
  for i in range(MAX_LENGTH):
    enc_padding_mask, combined_mask, dec_padding_mask = create_masks(
        encoder_input, output)
  
    # predictions.shape == (batch_size, seq_len, vocab_size)
    predictions, attention_weights = transformer(encoder_input, 
                                                 output,
                                                 False,
                                                 enc_padding_mask,
                                                 combined_mask,
                                                 dec_padding_mask)
    
    # select the last word from the seq_len dimension
    predictions = predictions[: ,-1:, :]  # (batch_size, 1, vocab_size)

    predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)
    
    # return the result if the predicted_id is equal to the end token
    if predicted_id == tokenizer_en.vocab_size+1:
      return tf.squeeze(output, axis=0), attention_weights
    
    # concatentate the predicted_id to the output which is given to the decoder
    # as its input.
    output = tf.concat([output, predicted_id], axis=-1)

  return tf.squeeze(output, axis=0), attention_weights

一个翻译的例子

def plot_attention_weights(attention, sentence, result, layer):
  fig = plt.figure(figsize=(16, 8))
  
  sentence = tokenizer_pt.encode(sentence)
  
  attention = tf.squeeze(attention[layer], axis=0)
  
  for head in range(attention.shape[0]):
    ax = fig.add_subplot(2, 4, head+1)
    
    # plot the attention weights
    ax.matshow(attention[head][:-1, :], cmap='viridis')

    fontdict = {'fontsize': 10}
    
    ax.set_xticks(range(len(sentence)+2))
    ax.set_yticks(range(len(result)))
    
    ax.set_ylim(len(result)-1.5, -0.5)
        
    ax.set_xticklabels(
        ['<start>']+[tokenizer_pt.decode([i]) for i in sentence]+['<end>'], 
        fontdict=fontdict, rotation=90)
    
    ax.set_yticklabels([tokenizer_en.decode([i]) for i in result 
                        if i < tokenizer_en.vocab_size], 
                       fontdict=fontdict)
    
    ax.set_xlabel('Head {}'.format(head+1))
  
  plt.tight_layout()
  plt.show()


def translate(sentence, plot=''):
  result, attention_weights = evaluate(sentence)
  
  predicted_sentence = tokenizer_en.decode([i for i in result 
                                            if i < tokenizer_en.vocab_size])  

  print('Input: {}'.format(sentence))
  print('Predicted translation: {}'.format(predicted_sentence))
  
  if plot:
    plot_attention_weights(attention_weights, sentence, result, plot)

在这里插入图片描述

1.2.9 参考与推荐

https://blog.csdn.net/qq_22795223/article/details/105676186
https://github.com/huggingface/transformers
https://huggingface.co/transformers/index.html
https://jalammar.github.io/illustrated-transformer/
https://www.nowcoder.com/discuss/258321
https://blog.csdn.net/longxinchen_ml/article/details/86533005

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值