GPT-2 (Generative Pre-trained Transformer 2): 是 OpenAI 发布的 GPT 系列大语言模型的第二代。它采用了纯解码器 (decoder only) 的结构,是一种自回归语言模型。

注意力机制是 GPT 的核心,前置文章:https://io.zouht.com/178.html

1 总体结构

1.1 回顾 Encoder-Decoder 结构

GPT-2 采用了 Decoder Only 的结构,在介绍它之前,不妨先看一下最原始的 Encoder-Decoder 结构。在 Attention Is All You Need 论文中介绍的便是 Encoder-Decoder 结构,最初用作机器翻译任务。它的结构图示如下:

图片来源:Attention Is All You Need

要理解这个结构的意义,得结合它的应用——机器翻译。对于翻译任务,给出原语言的文本,第一步便是理解和提取原语言文本中蕴含的信息。然后借助提取得到的信息,生成目标语言的翻译。在生成目标语言的过程中,也要注意前文与后文的关联,遵循目标语言潜在的规则。

Encoder 模块(上图左侧部分)便是负责提取源语言内涵的信息,通过一层又一层的基于多头注意力机制的 Encoder 块,理解原文本中 Token 间的关联,提取源文本中的信息,最后输出一个向量。下面是 Encoder 的注意力可视化,可见 Encoder 只在源文本范围进行注意力计算。

图片来源:Tensor2Tensor Intro

Decoder 模块(上图右侧部分)便是负责依据提取信息生成目标语言,接受从 Encoder 模块传入的向量,同时结合当前已经生成的目标语言的 Token 序列,生成下一个 Token。需要注意的是,Decoder 模块有两种注意力:encoder-decode 注意力和 masked 注意力。对于 encoder-decode 注意力,就是提取源语言和目标语言之间的关联。对于 masked 注意力,它通过遮罩仅允许当前位置利用之前的信息进行预测,可以理解为后侧位置的信息会“泄露答案”给当前位置,这是不利于训练的。

图片来源:https://jalammar.github.io/illustrated-gpt2/,采用:CC BY-NC-SA 4.0

下面是 Decoder 模块的注意力可视化,可以看见 Decoder 输出的 Token 能够利用源语言 Token 序列的信息,这便是 encoder-decode 注意力实现的。同时输出的目标语言 Token 只能利用之前输出的 Token(彩色连线),而不能利用后面的 Token(灰色连线),这便是 masked 注意力实现的。

图片来源:Tensor2Tensor Intro

了解了 Encoder-Decoder 结构后,可以发现该结构和机器翻译任务极为契合,但这也说明这种结构可能不适合其他任务。

1.2 Decoder Only 结构

GPT-2 采用了一种新的结构,在整个模型中只存在 Decoder 模块,称为 Decoder Only 结构。

由于没有 Encoder,Decoder 模块的 encoder-decode 注意力就没有意义了,因此它也被移除了。可以回看本文 Encoder-Decoder 结构的图示,其中把 Decoder 的 Multi-Head Attention 和它的 Add&Norm 删掉,便是 GPT-2 的 Decoder 结构了(其实也可以看作把 Encoder 的 Multi-Head Attention 换成 Masked Multi-Head Attention).

以下便是 GPT-2 的总体结构了:

2 结构细节

通过对比 GPT-2 的源码和上面的结构图,可以更加深入理解它的模型结构。

https://github.com/openai/gpt-2/blob/master/src/model.py

2.1 位置编码

GPT-2 采用了一种非常简单的绝对位置编码方式,即直接从 $0$ 开始顺序给每个 Token 编号。

def expand_tile(value, size):
    """Add a new axis of given size."""
    value = tf.convert_to_tensor(value, name='value')
    ndims = value.shape.ndims
    return tf.tile(tf.expand_dims(value, axis=0), [size] + [1]*ndims)

def positions_for(tokens, past_length):
    batch_size = tf.shape(tokens)[0]
    nsteps = tf.shape(tokens)[1]
    return expand_tile(past_length + tf.range(nsteps), batch_size)

用一个示例来运行,可以看见位置编码的结果如下:

tokens = tf.constant([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]])
print(positions_for(tokens, 0))
# tf.Tensor(
# [[0 1 2 3 4]
#  [0 1 2 3 4]
#  [0 1 2 3 4]], shape=(3, 5), dtype=int32)
print(positions_for(tokens, 2))
# tf.Tensor(
# [[2 3 4 5 6]
#  [2 3 4 5 6]
#  [2 3 4 5 6]], shape=(3, 5), dtype=int32)

2.2 Text Embed & Position Embed

GPT-2 使用两个可训练矩阵来做 Text Embed 和 Position Embed,模型将会自学习文本 (Token) 和位置编码嵌入为向量的方式:

wpe = tf.get_variable('wpe', [hparams.n_ctx, hparams.n_embd], initializer=tf.random_normal_initializer(stddev=0.01))
wte = tf.get_variable('wte', [hparams.n_vocab, hparams.n_embd], initializer=tf.random_normal_initializer(stddev=0.02))

最终的嵌入向量由词嵌入向量和位置嵌入向量相加得到:

past_length = 0 if past is None else tf.shape(past)[-2]
h = tf.gather(wte, X) + tf.gather(wpe, positions_for(X, past_length))

2.3 Decoder

下面便是 GPT-2 的核心,即 Decoder 块的实现:

def block(x, scope, *, past, hparams):
    with tf.variable_scope(scope):
        nx = x.shape[-1].value
        a, present = attn(norm(x, 'ln_1'), 'attn', nx, past=past, hparams=hparams)
        x = x + a
        m = mlp(norm(x, 'ln_2'), 'mlp', nx*4, hparams=hparams)
        x = x + m
        return x, present

和下图进行对比,可以发现 Attention、MLP 和残差连接都能对应上。

不过发现 Layer Norm 的位置似乎不一样,在代码实现中,Layer Norm 是在 Attention 和 MLP 之前的。

Decoder 块要堆叠多层,对应的便是代码的这个循环:

for layer, past in enumerate(pasts):
    h, present = block(h, 'h%d' % layer, past=past, hparams=hparams)
    presents.append(present)

2.4 输出

最后一个 Decoder 的输出向量便是预测下一个 Token 需要用到的信息,首先把它还原成二维张量:

h_flat = tf.reshape(h, [batch*sequence, hparams.n_embd])

接下来用 Text Embed 矩阵把它从 Embed 空间还原到 Token 空间,输出一个预测概率的张量:

logits = tf.matmul(h_flat, wte, transpose_b=True)
logits = tf.reshape(logits, [batch, sequence, hparams.n_vocab])

其中每一行代表一个单词的预测,即模型预测每个词是词汇表中每个其他词的概率。

2.5 超参数

def default_hparams():
    return HParams(
        n_vocab=0,
        n_ctx=1024,
        n_embd=768,
        n_head=12,
        n_layer=12,
    )

3 运行流程

3.1 一次推理

首先需要输入一段 Token 序列,可以是一段待补全的句子如 robot must obey ...,如果想要从零生成,也可以直接输入一个特别的起始 Token <s> 来开始运行。获得输入的 Token 序列后,显然它是不满足 Context 长度的,因此在序列后加上 <pad> 填充到目标长度。该 Token 序列经过词汇表编码,送入模型进行推理,最后会得到一个预测结果的概率分布,下面就需要依据概率分布选取预测的结果了。

3.2 选取结果

Top-k 和 temperature 是两种在 GPT 中常用的策略,用于控制模型生成文本的随机性和多样性。这两种策略在模型生成文本时,会影响模型选择下一个词的方式。

Top-k

在生成模型中,每个词的生成都是基于一个概率分布的。模型会为每个可能的下一个词分配一个概率,然后从这个分布中抽取一个词。抽取过程如下:

  1. 模型会计算出每个可能的下一个词的概率。
  2. 然后,模型会选择概率最高的 k 个词。
  3. 最后,模型会从这 k 个词中随机抽取一个。

Temperature

Temperature 抽样的公式可以用 softmax 函数表示。假设模型的原始输出是一个向量 $z$,其中 $z_i$ 是第 i 个词的原始输出。那么,经过 temperature 抽样后,第 $i$ 个词被选中的概率 $p_i$ 可以用下面的公式计算:

$$ p_i = \frac{e^{z_i / T}}{\sum_j e^{z_j / T}} $$

其中,$T$ 是 temperature 参数,$sum_j$ 是对所有可能的词求和。这个公式说明:

  • 当 $T$ 越小,高概率的词会变得更有可能被抽到,低概率的词会变得更不可能被抽到,模型更加固定。
  • 当 $T$ 越大,高概率和低概率的词之间的差距会变得更小,模型生成的文本会更加随机和多样。

3.3 连续推理

GPT-2 是一种自回归语言模型,它预测下一个 Token 时需要基于它以前的 Token 以及当前的输入,即:

$$ h_i=\mathrm{LM}_{\phi}(z_i,h_{<i}) $$

因此,它的运行过程实际上是一个循环的过程。经过 3.1 的一次推理流程后,用 3.2 的方式选取预测的 Token,然后将预测 Token 尾接到输入,再进行推理。循环直到达到需要的长度为止。

文章目录