Transformer-XL

Character-Level Language Modeling with Deeper Self-Attention 中,作者提到 LSTM 和 RNN 变体能够在对字符级语言建模有着非常优秀的表现,这得益于它能够建立在学习长距离依赖方面能力比较强。

基于自注意力机制的 Transformer 模型能够建立更长的依赖,但是其输入长度仍然受到限制,例如:Bert 模型的输入为 512。如果我们的输入要求更长的文本,Tansformer-XL(Extra Long)对输入限制提出一些解决方法。

原始 Tansformer 是 Seq2Seq 架构的模型,Tansformer-XL 则是仅仅使用了编码器,它并不是直接直接从 2017 年发的原始 Transformer 演化而来,而是从 vanlina transformer 演化而来,在 vanlina transformer 中对于更长的输入采取了分段方式进行训练,这就带来一些问题:

  1. 分段会导致输入的长文本按照 max_length 强制分割,破坏了输入文本的语义;
  2. 在 vanlina transformer 训练时,一个完整的长输入会被分段单独训练,每个段无法参考前面段的信息。

2019 年 Google 提出了 Transformer-XL,该模型中提到了两种机制用于克服上面提到的一些缺点,它们是:

  1. Recurrence Mechanism 段循环机制
  2. Relative Position Encoding 相对位置编码

1. Recurrence Mechanism

当输入长文本时,Tansformer-XL 使用仍然使用分段的方式的进行学习,不同于 vanlia transformer 的是,Tansformer-XL 会利用前面段的信息。

既然要利用前面段的信息,所以前面段计算完成之后,每一层的隐藏状态输出都是需要缓存下来。假设我们输入的文本非常长,前面已经输出了 N 段,缓存 N 段不是需要更多的内存么?是的。所以,可以根据自己的资源情况来指定需要缓存前面的几个段的输出。

注意:前面段的输出只参与当前段的正向计算,不会参与反向传播计算

那么,它如何利用前面段的信息?如下图所示:

输入的长度为 18,但是模型最多只能处理 9 长度,此时需要分段输入模型中。在输入第 2 个段到模型中时,Tansformer-XL 会参考上一个段的信息。假设:我们要计算第 2 个段的第 3 个时间步的 第 2 层的输出,此时我们需要的输入是:

  1. 第 2 个段,即:当前段的上一个层(第 1 层)的输出
  2. 第 1 个段,即:前一个段的的第 1 层的输出
  3. 最后,将这两个输出 conat 拼接起来

SG 表示 Stop Gradient 表示上一个段只参与当前段的正向计算,不参与反向传播。根据公式,第一个段的输出 shape 为 (9, 1, 768),第二个段第 1 个层输出为 (3, 1, 768),将其拼接起来变成 (12, 1, 768) 。

接下来,我们计算自主力机制中的 q、k、v ,如下公式所示:

然后,经过 Transformer Layer 层计算最终第 2 个段第 2 层第 3 个时间步的输出:

2. Relative Position Encoding

Transformer 中使用绝对位置编码,如果在分段输入的场景下仍然使用绝对位置编码,会导致不同的段中,相同位置的标识所使用的位置编码也是相同的,如下图所示:

在将一个长的输入分成 2 个段,每个段输入时使用绝对位置编码,就如同上图中 2 个段中的第二个位置的 token 所使用的位置编码是一样的。

使用绝对位置编码时,注意力分数计算如下:

上述公式展开之后如下:

为什么只需要考虑注意力计算,这是因为位置编码只在自注意力计算时用到。Transformer-XL 将上述公式中的绝对位置编码换成相对位置编码,相对位置编码为一个 LxD 的 sinusoid 矩阵,该矩阵不需要学习,该矩阵计算过程如下:

import torch

# 相对位置编码的最大长度
L = 128
embedding_dim = 512

# 定义矩阵的位置
position = torch.arange(L - 1, -1, -1.0, dtype=torch.float)
inv_freq = 1 / (10000 ** (torch.arange(0.0, embedding_dim, 2.0) / embedding_dim) )

print("position shape:", position.shape)
print("inv_freq shape:", inv_freq.shape)

# position.unsqueeze(1) @ inv_freq.unsqueeze(0)
# (128, 1) @ (1, 256) = (128, 256)
sinusoid = torch.einsum("i,j->ij", position, inv_freq)
print("sinusoid shape:", sinusoid.shape)

# 相对位置编码矩阵
relative_positional_embeddings = torch.cat([sinusoid.sin(), sinusoid.cos()], dim=-1)[:, None, :]
print('relative_positional_embeddings:', relative_positional_embeddings.shape)

Transformer-XL 对上述公式进行了一些更改,如下公式所示:

原来的注意力计算过程拆分之后的四项分别表示为:

  1. i 的内容对 j 的内容的关注
  2. i 的内容对 j 的位置的关注
  3. i 的位置对 j 的内容的关注
  4. i 的位置对 j 的位置的关注

对公式进行修改之后:

  1. 原来的 \(W_{K}\) 变成了 \(W_{K,E}\) 和 \(W_{K,R}\) 分别表示对内容、对相对位置的参数。这说明了什么呢?原来我们获得 Token 的 Key 向量,只需要一个 \(W_{K}\) 进行一次变换即可,现在需要使用 \(W_{K,E}\) 和 \(W_{K,R}\) 两个参数计算两个 KEY 向量。
  2. 第三项中 i 位置对 j 内容的注意 \(U_{i}W_{q}\) 变成了 \(u\)。这个变换是什么意思呢?这是因为进行相对位置计算时,当前位置是不变的,是别的 Token 相对我的位置,而我的位置信息似乎并不那么重要,但是当前位置需要进行表示,如何表示?让模型去学习吧,所以这里的 u 就是 i 自己的位置,并且该位置的表示是由模型学习得到的。
  3. 第四项中 i 的位置对 j 位置的注意,这里的位置也变成 \(v\) 可学习的参数。也就是说,第三项、第四项中的 i 位置的向量表示都变成了固定的可学习位置表示。并且,对内容注意、对位置注意的 i 位置向量是不同的。
  4. 另外,第四项中 j 相对位置编码变成了不可学习的固定的由 sin+cos 计算得到的矩阵。当然该矩阵在最原始的位置编码中表示的是绝对位置编码,在这里则表示相对位置编码。

transformer-xl 由上面的变换得到了在 self-attention 中引入相对位置信息,并且也引入了 u 和 v 两个可学习的相对位置编码参数。

整理下上面的公式,包含 u 和 v 的两项变成了调整 \(E_{xi}\)的 bias,通过 u 和 v 调整词嵌入和位置,如下公式所示:

这个公式可以理解为:i 的 QUERY 在注意 j 的 KEY 时,引入了相对位置编码信息 u。同理,i 的 QUERY 在注意 j 的位置时,引入了相对位置信息 v。

未经允许不得转载:一亩三分地 » Transformer-XL