Loading

transformer之我观

transformer模型学习有感

一个 transformer 模型用自注意力层而非 Rnn 或 Cnn 来处理变长的输入。这种通用架构有一系列的优势:

  • 它不对数据间的时间/空间关系做任何假设。这是处理一组对象(objects)的理想选择。
  • 层输出可以并行计算,而非像 RNN 这样的一步一步的计算。
  • 远距离的两个项可以影响彼此的输出,而无需经过许多 RNN 步骤或卷积层。
  • 它能学习长距离的依赖关系。在Rnn和Cnn中会产生梯度消失,难以更新神经网络浅层的参数。

该架构的缺点是:

  • 对于时间序列,一个单位时间的输出是从整个历史记录计算的,而非仅从输入和当前的隐含状态计算得到。这可能效率较低。
  • 如果输入确实有序列的关系,比如文本,则必须加入一些位置编码,否则模型只会看到一堆单词而不是一个有序列关系的句子。

首先是transformer最经典的architecture架构图。

其中,Encoder负责将输入\((x_1, x_2, ..., x_n)\) 转换为 \((z_1,z_2,...,z_n)\) 的输出,其中 \(x_t\) 是我们原始输入的第t个词,\(z_t\) 是这个词的向量表示。在每个时刻 \(t\) Decoder拿到Encoder的输出 \((z1,...,zn)\) ,通过自回归来生成预测序列 \((y_1, y_2, ...,y_m)\),也就是将过去时刻的输出作为我第t时刻的额外输入。(即步骤3)

Encoder架构

主要分为五个部分,下面将以原论文中的顺序依次讲解。

1. Inputs Embedding

就是我们在处理文本类输入时常用的词嵌入word2vec,用来将一个字表示成一个向量。

不同于One-Hot编码的“没有任何的相对关系,只是一串01向量”;embedding可以看作是将每个单词的One-Hot编码向量乘以一个参数矩阵 \(W_0\),输出就是这个单词的词向量但是输出的embedding向量的维度一般小于One-Hot编码。

transformer将所有词向量的embedding都设置为512

2. Positional Encoding

因为transformer中没有任何的rnn或者cnn,完全依赖于注意力机制来建立输入和输出之间的关系。但是我们的输入是一个句子,会包含序列信息。不同于RNN会按顺序处理每一个字,天然的有序列关系,transformer需要对输入添加位置编码。也就是对输入序列中的每一个字 \(x_i\) ,我们都在其中添加一个关于 \(i\) 的相对位置编码。

def positional_encoding(position, d_model):
  ---算角度
  angle_rads = get_angles(np.arange(position)[:, np.newaxis],
                          np.arange(d_model)[np.newaxis, :],
                          d_model)

  # 将 sin 应用于数组中的偶数索引(indices);2i
  angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])

  # 将 cos 应用于数组中的奇数索引;2i+1
  angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
  pos_encoding = angle_rads[np.newaxis, ...]
  ---
  return tf.cast(pos_encoding, dtype=tf.float32)#将位置信息转换为float

pos_encoding = positional_encoding(50, 512)
print (pos_encoding.shape)

>>>(1, 50, 512)

Input Tensor 的 size 为 [batch_size, seq_length, 512]

batch_size 指的是定义的批量大小 batch_size
seq_length 指的是 sequence 的长度,(比如“我爱你”,seq_length = 3)
512 指的是 embedding 的 dimension

3.多头+自注意力机制

3.1 Scaled Dot-Product Attention

注意力函数就是通过矩阵乘法,求得query矩阵和key矩阵的向量乘法,得到注意力矩阵。经过一层softmax之后,再跟value矩阵相乘。

注意力矩阵的第i行的含义是:第i个q跟所有的k的相似度,可以理解为向量做内积。所以我们对每行做softmax之后,就会得到对于这个query向量对于所有key向量的相似度,并且相加为1。

def scaled_dot_product_attention(q, k, v, mask):
  """计算注意力权重。
  q, k, v 必须具有匹配的第一个维度,也就是batch_size样本容量
  k, v 必须有匹配的倒数第二个维度,也就是seq_len_k = seq_len_v,因为key矩阵和value矩阵一定有相同的
  虽然 mask 根据其类型(填充或前瞻)有不同的形状,
  但是 mask 必须能进行广播转换以便求和。
  
  参数:
    q: 请求的形状 == (batch_size, seq_len_q, depth)
    k: 主键的形状 == (batch_size, seq_len_k, depth)
    v: 数值的形状 == (batch_size, 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)  再把注意力矩阵乘V

  return output, attention_weights

3.2 多头注意力

对于多头注意力机制,也就是我们与其做一次注意力机制,不如我们把 q,k,v 投影到一个低维的空间,其中每个头的输入维度中,embedding减少到之前的 \(1 / h\) ,再分别做h次注意力机制。将得到的h个结果并起来,并重新将结果映射回高维,就得到我们需要的注意力输出了。

可以类比CNN中的多卷积核的作用——每次抽取不同特征,得到多个不同的特征通道。

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

4.残差连接和layernorm

4.1残差连接

在经过多头注意力处理后,我们的输出 \(Attention(Q,K,V)\) 的维度就是 \((batch\_size,seq\_len, d\_model)\) ,跟我们的原始属于 \(x\) 维度完全相等。因此我们这里可以直接使用残差连接,将他们加起来就得到 \((X + Attention(Q,K,V))\)

解决的问题:

  • 网络退化并不是过拟合,过拟合是指~,网络退化是模型在训练数据集和测试集上面的表现都不好。

  • 通常网络退化是由神经网络层数过多引起的(会造成梯度爆炸、消失;过拟合等问题)

  • 解决方法:resnet中提出的残差连接的概念,首先我们知道,我们在训练网络时,网络层数越高理论上效果至少不会比层数较低时表现得更差(因为最后面的几层神经网络可以全都是identity mapping)。但事实上网络层数过深就会造成网络退化。为了解决这个问题,resnet中显式的构造出了一个identity mapping—让每层神经网络学习\(f(x):= H(x) - x\),相当于在反向传播求导的时候加了一个恒等项。(其中X是上一层神经网络的输出,H(x)是这一层要学习的函数)最后该层神经网络的输出等价于上一层神经网络的输出x和这一层神经网络学习残差的输出之和。

4.2 layernorm

作用:跟batchnorm一样,加速模型收敛

  • 不同的是LN是操作一个样本的所有特征,让各个特征的分布变为均值为0,方差为1。并且引入可训练系数 \(\alpha, \beta\) ,弥补归一化过程中损失的信息。

  • BN是对一个batch_size内的所有所有样本的同一特征进行归一化操作。

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)  三个x代表上一层的输出,N个encoder是串联的
    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

5.FFN

就是一个两层的MLP模型。

相当于将多头注意力之后的输出 \(Attention(Q,K,V)\) 做一个非线性变换。因为我们之前的操作包括attention机制都是线性的,这里引入非线性因素,使模型更好收敛。

#@save
class PositionWiseFFN(tf.keras.layers.Layer):
    """基于位置的前馈网络"""
    def __init__(self, ffn_num_hiddens, ffn_num_outputs, **kwargs):
        super().__init__(*kwargs)
        self.dense1 = tf.keras.layers.Dense(ffn_num_hiddens)
        self.relu = tf.keras.layers.ReLU()
        self.dense2 = tf.keras.layers.Dense(ffn_num_outputs)

    def call(self, X):
        return self.dense2(self.relu(self.dense1(X)))

Decoder架构

针对于Encoder的不同来阐述:

1. Decoder的mask多注意力机制

这是因为不同于encoder的多头注意力机制用来抓取全局序列的信息,我Decoder是要用来预测下一个输出的,因此也就无法看到 \(t\) 时刻以及之后的语句序列。

2. Decoder中间的多头注意力

  • 这里不再是自注意力机制,而是将encoder的输出拿过来作为 \(Key-Value\) 矩阵,与自己的第一层注意力输出矩阵做注意力运算。通过这种方式有效的提取encoder的输出。

与RNN的异同

  1. 从输入的处理来看:RNN的输入序列中每个字都天然的按时间排序。transformer中则是用positional encoding给每个字附加上了位置信息。
  2. 从使用序列上一时刻的信息来看:RNN将上一时刻的隐藏状态作为下一时刻的输入,从而传递了之前时刻的信息。transformer是拿到整个句子的注意力输出,然后通过MLP找到我们想要的东西。
  3. 附上邱锡鹏老师书中的插图:几种不同的序列到序列模型
posted @ 2022-03-30 19:49  王子春  阅读(69)  评论(0编辑  收藏  举报