Transformers具有学习长期依赖的潜力,但在语言模型的设置中受到固定长度上下文的限制。Transformer-XL引入了两点创新——循环机制(Recurrence Mechanism)和相对位置编码(Relative Positional Encoding),不仅可以捕获长距离依赖性,还可以解决上下文碎片问题。Transformer-XL学习的依赖性比RNN长80%,比vanilla Transformers长450%,在短序列和长序列上都能获得更好的性能,并且在评估过程中比vanilla Transformers快1800倍。
循环机制(Segment-Level Recurrence with State Reuse)
循环机制的目标是通过利用之前段的信息来实现长期依赖性。与vanilla Transformer类似,Transformer-XL处理第一个标记段,但它会保留隐藏层的输出。处理后面的段时,每个隐藏层都会接收两个输入:
- 该段的前一个隐藏层的输出,和vanilla Transformer相同(如下图中的灰色箭头所示)。
- 上一个段的隐藏层的输出(如绿色箭头所示),可以使模型创建长期依赖关系。
其中,表示第几段,表示第几层,表示隐层的输出。表示停止计算梯度,表示在长度维度上的两个隐层的拼接,是模型参数。与Transformer唯一关键的不同就在于Key和Value矩阵的计算上,即和,它们基于的是扩展后的上下文隐层状态进行计算,是之前段的缓存。
从技术上讲,这两个输入会被拼接,然后用于计算当前段的Key和Value矩阵。该步骤为网络提供了更多关于每个表征的权重(重要性)的信息,但它不会更改Value矩阵。
图:Transformer-XL语言模型的训练和测试示意。来源:Transformer-XL
该概念可以扩展到更长的依赖上。使用相同的方法,利用前面多个段的信息,只要GPU内存允许,在测试阶段也可以获得更长的依赖。
循环机制的另一个优点是其测试速度快。在每个步骤中,它可以一次前进一整个段(而不是像在vanilla Transformer中一次只能前进一个表征),并使用先前段的数据来预测当前段的表征。
相对位置编码
循环机制引入了新的挑战——原始位置编码将每个段分开处理,因此,来自不同段的表征会具有相同的位置编码。例如,第一和第二段的第一个表征将具有相同的编码,虽然它们的位置和重要性并不相同(比如第一个段中的第一个表征可能重要性低一些)。这种混淆可能会错误地影响网络。
针对此问题,论文提出了一种新的位置编码方式。这种位置编码是每个注意力模块的一部分。它不会仅在第一层之前编码位置,而且会基于表征之间的相对距离而非绝对位置进行编码。
在Transformer中,第一层的计算查询和键之间的attention分数的方式为:
其中,是词的embedding,是词的embedding,和是位置向量,这个式子实际上是的展开,就是Transformer中的标准格式。
在Transformer-XL中,对上述的attention计算方式进行了变换,转为相对位置的计算,而且不仅仅在第一层这么计算,在每一层都是这样计算。
从技术上讲,它对注意力头分数(Attention Head’s Score)的计算方式不再是简单的乘法(Qi⋅Kj),而是包括四个部分:
- 内容权重——没有添加原始位置编码的原始分数。
- 相对于当前内容的位置偏差(Qi)。该项使用正弦类函数来计算表征之间的相对距离(例如i-j),用以替代当前表征的绝对位置。
- 可学习的全局内容偏差u——用于调整其他表征内容(Kj)的重要性。
- 可学习的全局位置偏差v——仅根据表征之间的距离调整重要性(例如,最后一个词可能比前一段中的词更重要)。
Transformer-XL模型的整体计算公式整理如下
multihead attention 代码如下
def rel_multihead_attn(w, r, r_w_bias, r_r_bias, attn_mask, mems, d_model,
n_head, d_head, dropout, dropatt, is_training,
kernel_initializer, scope='rel_attn'):
scale = 1 / (d_head ** 0.5)
with tf.variable_scope(scope):
qlen = tf.shape(w)[0]
rlen = tf.shape(r)[0]
bsz = tf.shape(w)[1]
cat = tf.concat([mems, w],
0) if mems is not None and mems.shape.ndims > 1 else w
w_heads = tf.layers.dense(cat, 3 * n_head * d_head, use_bias=False,
kernel_initializer=kernel_initializer, name='qkv')
r_head_k = tf.layers.dense(r, n_head * d_head, use_bias=False,
kernel_initializer=kernel_initializer, name='r')
w_head_q, w_head_k, w_head_v = tf.split(w_heads, 3, -1)
w_head_q = w_head_q[-qlen:]
klen = tf.shape(w_head_k)[0]
w_head_q = tf.reshape(w_head_q, [qlen, bsz, n_head, d_head])
w_head_k = tf.reshape(w_head_k, [klen, bsz, n_head, d_head])
w_head_v = tf.reshape(w_head_v, [klen, bsz, n_head, d_head])
r_head_k = tf.reshape(r_head_k, [rlen, n_head, d_head])
rw_head_q = w_head_q + r_w_bias
rr_head_q = w_head_q + r_r_bias
AC = tf.einsum('ibnd,jbnd->ijbn', rw_head_q, w_head_k)
BD = tf.einsum('ibnd,jnd->ijbn', rr_head_q, r_head_k)
BD = rel_shift(BD)
attn_score = (AC + BD) * scale
attn_mask_t = attn_mask[:, :, None, None]
attn_score = attn_score * (1 - attn_mask_t) - 1e30 * attn_mask_t
attn_prob = tf.nn.softmax(attn_score, 1)
attn_prob = tf.layers.dropout(attn_prob, dropatt, training=is_training)
attn_vec = tf.einsum('ijbn,jbnd->ibnd', attn_prob, w_head_v)
size_t = tf.shape(attn_vec)
attn_vec = tf.reshape(attn_vec, [size_t[0], size_t[1], n_head * d_head])
attn_out = tf.layers.dense(attn_vec, d_model, use_bias=False,
kernel_initializer=kernel_initializer, name='o')
attn_out = tf.layers.dropout(attn_out, dropout, training=is_training)
output = tf.contrib.layers.layer_norm(attn_out + w, begin_norm_axis=-1)
return output