… … 接上篇博文 (第九章-Transformers (I))
一个Transformer Block是指Transformer 中应用了一些跳跃连接,前向(全连)层,以及围绕多头注意力层归一化的单个component。Transformer Block的一个框图如图9-6所示。
首先,注意 Query 是怎么传给多头注意力层以添加到输出 — 这是一个跳跃连接,在现代深度学习架构中很常见。这意味着我们可以构建非常深的神经网络而无需遭受梯度消失问题,因为跳跃连接提供了一个 gradient-free 的高速通路 (highway) 以无干扰的向前传输信息。
其次,在Transformer Block中应用了层归一化 (layer normalization), 从而为训练过程提供稳定性。在本书前面,我们已经看到了batch normalization,BN 中每个通道的输出均值为0,标准差为1。归一化统计是沿着 batch 和 空间维度来计算的。
相反,Transformer Block中的层归一化通过(沿着通道计算归一化统计量)来实现对 batch中每个序列的每个位置进行归一化。这与batch normalization在计算归一化统计量上是完全相反的。两者的区别如下图9-7所示。
译者注: 关于两者差异的图,原书不是很直观,这里重新找了个更清晰的图。
Layer Normalization vs Batch Normalization |
---|
层归一化在原始的GPT论文中使用,通常用于基于文本的任务以避免产生沿着batch中序列的归一化依赖。然而,近期的一些工作(例如 Shen 等) 挑战了这一假设,并表明通过对batch normalization 做一些修改,BN也可以在Transformer中进行应用,超过了传统的层归一化。 |
最后,Transformer Block也包含了 一组前向 (也即 全连) 层,以允许 component 随着网络的加深提取高层次特征。
Transformer Block 的 Keras 实现如样例 9-4所示。
# Keras 中的TransformerBlock
class TransformerBlock(layers.Layer):
# 组成TransformerBlock的子层在初始化函数中定义
def __init__(self, num_heads, key_dim, embed_dim, ff_dim, dropout_rate=0.1):
super(TransformerBlock, self).__init__()
self.num_heads = num_heads
self.key_dim = key_dim
self.embed_dim = embed_dim
self.ff_dim = ff_dim
self.dropout_rate = dropout_rate
self.attn = layers.MultiHeadAttention(
num_heads, key_dim, output_shape = embed_dim
)
self.dropout_1 = layers.Dropout(self.dropout_rate)
self.ln_1 = layers.LayerNormalization(epsilon=1e-6)
self.ffn_1 = layers.Dense(self.ff_dim, activation="relu")
self.ffn_2 = layers.Dense(self.embed_dim)
self.dropout_2 = layers.Dropout(self.dropout_rate)
self.ln_2 = layers.LayerNormalization(epsilon=1e-6)
def call(self, inputs):
input_shape = tf.shape(inputs)
batch_size = input_shape[0]
seq_len = input_shape[1]
# 创建因果掩膜帮助query遮掩未来的key
causal_mask = causal_attention_mask(
batch_size, seq_len, seq_len, tf.bool
)
# 使用注意力掩膜创建多头注意力层
attention_output, attention_scores = self.attn(
inputs,
inputs,
attention_mask=causal_mask,
return_attention_scores=True
)
attention_output = self.dropout_1(attention_output)
# 第一个 add + normalization 层
out1 = self.ln_1(inputs + attention_output)
# 前向层
ffn_1 = self.ffn_1(out1)
ffn_2 = self.ffn_2(ffn_1)
ffn_output = self.dropout_2(ffn_2)
# 第二个 add + normalization 层
return (self.ln_2(out1 + ffn_output), attention_scores)
在我们把所有东西放在一起来训练自己的GPT模型之前,还有最后一个步骤。你应该已经注意到在多头注意力层中,没有哪部分是关注 keys 的顺序的。每个 key 和 query 的点积计算是并行,而非如RNN中一样是序列的。这是一个优点(因为并行的效率增益),但也是一个问题,因为我们明确的需要注意力层来预测下面两个句子的不同输出:
为了解决这一问题,在为最开始的Transformer Block创建输入时,我们使用了一项名为 位置编码 (positional encoding) 的技术。除了对每个token 使用 token 嵌入之外,我们还利用位置编码对token的位置进行编码。
token嵌入是使用标准嵌入层将每个token转换为一个学习到的向量来构建。 我们可以用相同的方式来构建 位置编码,使用一个标准嵌入层来把每个整数位置转换为一个学习到的向量。
小贴士 |
---|
GPT使用嵌入层来嵌入位置,原始的Transformer论文则利用三角函数 — 在第11章中当我们探索音乐生成时,我们会介绍这个替代。 |
为了构建联合的 token-position 嵌入,如图9-8所示,我们把token嵌入加到 positional嵌入中。通过这种方式,句子中每个单词的意思和位置都在单一向量中得以捕获。
如下样例 9-5 所示,定义我们 TokenAndPositionEmbedding 层的代码如下样例 9-5 所示。
# TokenAndPositionEmbedding 层
class TokenAndPositionEmbedding(layers.Layer):
def __init__(self, maxlen, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.maxlen = maxlen
self.vocab_size =vocab_size
self.embed_dim = embed_dim
# tokens 使用 Embedding 层来嵌入
self.token_emb = layers.Embedding(
input_dim=vocab_size, output_dim=embed_dim
)
# tokens的位置也使用嵌入层嵌入
self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
def call(self, x):
maxlen = tf.shape(x)[-1]
positions = tf.range(start=0, limit=maxlen, delta=1)
positions = self.pos_emb(positions)
x = self.token_emb(x)
# 层输出是 token 和 postion 嵌入之和
return x + positions
现在,我们已经准备好来训练自己的GPT模型了!为了把所有的东西放到一起,我们需要把输入文本通过 token 和 position 嵌入层传输,然后通过我们的 Transformer Block。网络的最终输出是一个简单的带softmax激活函数的全连层,节点数等于词典的单词总数。
小贴士 |
---|
简单起见,我们仅仅使用一个 Transformer Block,而非原始论文中的12个。 |
# 样例9-6 Keras中的一个GPT模型
MAX_LEN = 80
VOCAB_SIZE = 10000
EMBEDDING_DIM = 256
N_HEADS = 2
KEY_DIM = 256
FEED_FORWARD_DIM = 256
# input 用0补齐
inputs = layers.Input(shape=(None,), dtype=tf.int32)
# 文本用 TokenAndPositionEmbedding 层编码
x = TokenAndPositionEmbedding(MAX_LEN, VOCAB_SIZE, EMBEDDING_DIM)(inputs)
# 嵌入传入一个 TransformerBlock
x, attention_scores = TransformerBlock(
N_HEADS, KEY_DIM, EMBEDDING_DIM, FEED_FORWARD_DIM
)(x)
# 变换后的输出传入一个带 softmax 的全连层,以预测下一个词的分布
outputs = layers.Dense(VOCAB_SIZE, activation = 'softmax')(x)
# Model接收词tokens的序列作为输入,并输出预测的后续词分布。Transformer Block的输出也返回以确保我们可以检查模型如何施加注意力。
gpt = models.Model(inputs=inputs, outputs=[outputs, attention])
# 模型在预测词分布上使用 SparseCategoricalCrossentropy 损失编译
gpt.compile("adam", loss=[losses.SparseCategoricalCrossentropy(), None])
gpt.fit(train_ds, epochs=5)
既然我们已经编译并训练了自己的GPT模型,我们可以开始用它来生成长的文本字符串。我们可以审视 TransformerBlock输出的注意力权重,以理解Transformer在生成过程的不同点上到底从哪里寻找信息。
我们可以使用下面的过程生成新的文本:
网络将为我们可以采样的每个单词输出一组概率,因此我们可以使得文本生成过程随机,而非确定。
我们将使用第五章中为LSTM文本生成引入的相同TextGenerator类,包括温度参数来指定我们希望采样的过程有确定。让我们看看两个不同温度值下的过程,如图9-10所示。
这两个段落有几个事情值得注意。首先,两者都和原始训练集中的酒评风格类似。它们都是以区域和酒的品类开始,在整个段落中酒的品类保持一致 (例如,它没有突然改变酒的颜色)。如我们在第五章中所见,以温度 1.0 生成的文本更加毛线,因而相较于温度为0.5的文本精确度更低。使用温度1.0生成多个样本将导致更多的发散性,因为模型是从具有更大方差的概率分布采样得到。
我们也可以让模型来告诉我们它在预测句子中下一个词时赋予每个单词的权重。TransformerBlock为每个注意力头输出了注意力权重,该权重是句子中前序词的 softmax 分布。
为了展示这点,针对三个不同输入提示词,图9-11 展示了最高概率的前五个tokens,以及在两个注意力头上对每个前序词的平均注意力。前序词根据(两个注意力头上平均得到的)注意力分数使用不同着色。更深的蓝色意味着在该词上倾注了更强的注意力。
在第一个例子中,模型为了决定区域相关的单词在国家(Germany)上倾注了更多注意力。这完全合理!为了选定一个区域,我们需要从国家相关的词语上抽取更多信息,以此来确保二者匹配。我们无需在头两个tokens (wine review) 上关注太多,因为它们不包含任何跟区域相关的有用信息。
在第二个例子中,我们需要回溯到葡萄 (resling),因此我们需要更多关注第一次提到它的地方。无论在整个句子中需要回溯多远 (上限为80个单词),我们都可以直接从这个词拉取信息。注意这与RNN网络非常不同,RNN依赖一个隐状态以维护整个序列的所有有用信息使得在需要时可以被随时拉起 — 这实际上是个更低效的方法。
最终的序列展示了我们的GPT模型如何基于信息聚合选取合适形容词的一个例子。这里注意力是关于葡萄的(riesling),但同时也依赖于包含残余糖分(residual sugar) 这个事实。因为 Riesling 一般是甜酒,同时也提到了糖分,因此,我们更可能把它形容为 slightly sweet 而非 slightly earthy。
对于充分理解模型如何抽取信息以对于精准预测下一个单词来说,能够如此审视一个网络无疑信息量巨大,我非常推荐你试试输入提示词,看看你的模型是否可以关注到句子中真正远的那些词,这样可以帮助你了解基于注意力模型相对于传统循环模型的能力优势。
我们的GPT模型是解码器 Transformer — 它以每次生成一个token的方式生成文本字符串,并使用因果掩膜来只关注输入字符串中的前序词。当然也存在编码器Transformer, 它并不会使用因果掩膜 — 相反,它们关注整个输入字符串以抽取输入的有意义上下文表示。对于其他任务,例如语言翻译,也存在 编码器-解码器 Transformer,它能够将一个文本字符串翻译到另一个。这种类型的模型既包含编码器Transformer Blocks,也包含解码器Transformer Blocks。
表9-1总结了三类不同的Transformers,其中包含了每个架构和典型用例的最优样例。
类型 | 样例 | 用例 |
---|---|---|
编码器 | BERT(谷歌) | 句子分类,命名实体识别,抽取性问答 |
解码器-解码器 | T5(谷歌) | 文本摘要,翻译,问答 |
解码器 | GPT(OpenAI) | 文本生成 |
编码器Transformer的一个知名例子是 基于Transformer的双向编码器表示 (Bidirectional Encoder Representations from Transformers, BERT) 模型,由谷歌开发(Devlin等,2018年),以在所有层给定缺失词前后上下文时来预测句子中的缺失单词。
编码器Transformers |
---|
编码器Transformers一般用于需要把输入作为整体进行理解的任务,例如句子分类,命名实体识别,以及抽取性问答。它们不用于文本生成任务,因此我们在本书中不会在细节上对其进行讨论 — 感兴趣的话可以参考 Lewis Tunstall 等的 Natural Language Processing with Transformers (O’Reilly出版社) 获取更多信息。 |
在下面的章节里,我们将探索编码器-解码器transformers如何工作,并讨论OpenAI对原始GPT模型架构的拓展,包括专门为对话应用设计的ChatGPT。
现代transformer中一个使用编码器-解码器结构的例子是 谷歌公司开发的 T5 模型。该模型将一些列任务重构为 文本-到-文本框架,包括翻译,语言可接受性,句子相似度,以及文本摘要,如图9-12所示。
T5模型架构与原始transformer论文中的编码器-解码器架构非常匹配,如图9-13所示。关键的差异在于: T5是在750GB 的巨量文本语料(the Colossal Clean Crawled Corpus, C4)上训练 , 而原始transformer论文仅仅只关注语言翻译,因此它只是在1.4GB的英语-德语句对上进行训练。
该框图的大部分我们都很熟悉 — 我们可以看到重复的 Transformer Blocks 以及用于捕获输入序列次序的位置编码。该模型和本章前文我们构建的GPT模型之两个关键差异在于:
图 9-14 展示了一个互参考注意力的样例。解码器层的两个注意力头可以协同工作以提供单词 the 在 street上下文中使用时的正确德语翻译。在德语中,依据名词性别,有三个确定的定冠词(der, die, das), 但是transformer知道选择die,因为一个注意力头可以关注到词语 street (德语中是女性化的词),另一个注意力头关注到需要翻译的词 (the)。
小贴士 |
---|
该示例来自 Tensor2Tensor Github repository, ,它包含了一个 Colab Notebook,允许你摆弄一个训练好的编码器-解码器Transformer模型,并且查看编码器和解码器的注意力机制是如何影响一个给定句子翻译到德语。 |
自2018年原始的GPT论文发表以来,OpenAI在原模型基础上发布了多个更新版本,如表9-2所示。
模型 | 日期 | 层数 | 注意力头 | 词嵌入尺寸 |
---|---|---|---|---|
GPT | 2018年6月 | 12 | 12 | 768 |
GPT-2 | 2019年2月 | 48 | 48 | 1600 |
GPT-3 | 2020年5月 | 96 | 96 | 12888 |
GPT-4 | 2023年3月 | - | - | - |
GPT-3的模型架构与原始的GPT模型相似,只是它更大,并且在更多数据上训练。在写作本书时,GPT-4还处在beta版本 — OpenAI尚未公开模型结构和尺寸的细节,尽管我们知道它可以接收图像作为输入,因而肯定是GPT系列的第一个多模态模型。经模型可通过商业工具和API调用,但GPT-3和GPT-4的模型权重并不开源。
GPT-3也可以基于你自己的训练数据精调— 这允许你提供多个样本来教它通过更新网络的权重来学会面对特定风格的提示词如何响应。在很多情况下这并不需要,因为GPT-3也可以通过在提示词本身提供几个样例(这被称为 few-shot learning) 来学会如何对特定风格的提示词做出反应。精调的有点是,你不再需要在每个特定的提示词输入中提供这些样例了,能够有效减少长期运行的代价。
给定一个系统提示词句子,GPT-3模型输出的一个示例如下图9-15所示。
像GPT这样的语言模型从规模上获益巨大 — 既包括模型权重的数目,也包括数据库规模。语言模型能力的天花板远未达到,因此研究者们正在用更大模型和数据集不断推进其能力边界。
在GPT-4 beta版本发布之前数月,OpenAI官宣了 ChatGPT — 这是一个允许用户以对话接口与大语言模型交互的工具。2022年11月的原始发布基于 GPT-3.5,这是一个比GPT-3更强大的模型,并且面向对话响应做了精调。
一个对话的示例如下图 9-16所示。注意智能体是如何维护输入间的状态,理解第二个问题中提到的注意力指的是 Transformers上下文中的注意力,而非人类关注的能力。
在本书写作时,并没有官方的论文在细节上描述ChatGPT是如何工作的,但是从
中我们知道它使用了一种称为"基于人类反馈的强化学习(RLHF)"技术来精调GPT-3.5模型。这一技术之前曾在ChatGP项目组介绍InstructGPT模型的论文中使用过,这是一个精调的GPT-3模型,准们为更精准执行手写指令的目的设计。
ChatGPT的训练过程如下:
强化学习 |
---|
强化学习的介绍我们将在第12章谈及,我们将探索生成式模型如何在强化学习的设定下使用。 |
RLHF过程如下图9-17所示。
尽管ChatGPT还有诸多限制(例如优势会幻想事实上错误的信息),它仍然是一个强有力的范例,它展示了如何利用Transformers构建生成式模型来产生复杂的,长时间跨度的,与人工生成文本难以区分的新输出。像ChatGPT这类模型目前取得的进展是AI潜力和其对世界变革性影响的有力证明。
进一步的,AI驱动的交流和交互将在未来得到持续演进。像Visual ChatGPT这样的技术正在把ChatGPT的语言能力和Stable Diffusion这样的视觉基础模型结合起来,使得用户不仅可以通过文本与ChatGPT交流,还可以通过图像。语言和视觉能力在Visual ChatGPT和GPT-4这样项目中的融合将有望开启人机交互的新纪元。
本章中,我们探索了Transformer模型架构并构建了一个GPT版本 — 一个经典的文本生成模型。
GPT使用了一种称为注意力的机制,它移除了循环层的限制(如LSTMs)。在工作机理上,它有点像信息检索系统,它利用了queries,keys,和 values 来决定它想要从每个输入token上提取多少信息。
注意力头可以聚合起来形成多头注意力层。这些都封装在Transformer Block里,其包含层归一化,围绕注意力层的跳跃连接。Transformer Blocks可以堆叠起来创建极深的神经网络。
因果掩膜用于确保GPT不会泄露下游tokens的信息到当前的预测。另外,一种名为 位置编码的技术被用以确保输入句子的次序信息不被丢弃,而是像传统的词嵌入一样融合进输入。
当分析GPT的输出时,我们看到它不仅有可能生成新的文本段落,也有可能审视网络的注意力层对于句子中哪里能够聚合信息以提升预测的理解。GPT可以在无信号损失的情况下获取远距离信息,因为注意力分数是并行计算的,并不像RNN那样依赖于一个序列计算的隐状态。
我们看到有三类Transformers (编码器,解码器,编码器-解码器) 以及各自能够完成的不同任务。最后,我们探索了其他语言模型如谷歌T5和OpenAI ChatGPT的解构和训练过程。