第二章 Transformers
很多人将最近一波生成式人工智能的进展追溯到2017年发布称为transformer的模型。其最著名的应用是强大的大语言模型(LLM),如Llama和GPT-4,每天有数亿人使用。transformer已成为现代人工智能应用的核心,推动着聊天机器人、搜索系统乃至机器翻译和内容摘要等各类应用。甚至已超越了文本领域,在计算机视觉、音乐生成和蛋白质折叠等领域引起了巨大反响。本章中,我们将探讨transformer背后的核心概念及其工作原理,重点介绍其中一个最常见的应用:语言模型。
在深入了解transformer之前,我们先退一步,了解什么是语言模型。语言模型(LM)的核心是一个概率模型,它通过前面或周边的词来预测序列中的下一个词(或标记)。这样,它捕获到了语言的基本结构和模式,使其能够生成真实且连贯的文本。例如,给定句子“I began my day eating
”,语言模型可能会以高概率预测下一个词为“breakfast
”。
那么,transformer在这一过程中发挥什么作用呢?与使用固定大小滑动窗口或循环神经网络(RNN)的传统语言模型不同,transformer设计用于更高效、更有表现力地处理长距离依赖关系和复杂的词间关系。例如,假设您想使用语言模型总结一篇新闻文章,这篇文章可能包含数百甚至几千个词。传统的语言模型在处理长上下文时会遇到困难,因此摘要可能会遗漏文章开头的一些重要细节。而基于transformer的语言模型在这一任务上表现出色。除了高质量的生成能力外,transformer还具有其他特性,如训练的高效并行化、可扩展性和知识迁移,使其在多种任务中广受欢迎并且非常适用。这一创新的核心是自注意力机制,它使模型能够在整个序列的上下文中权衡每个词的重要性。
为建立对语言模型如何运作的直观理解,我们将使用与现有模型交互的代码示例,并在出现相关部分时进行描述。一起开干吧。
注:在英文材料中经常可以看到tranformer和transformers的表述,前者表示以自注意力机制为底层的技术,而transformers可能是指基于这一技术的各类模型,也有可能是指Hugging Face的transformers库,这个名称在营销角度非常巧妙,但也经常会让人产生误解。
本文相关代码请见GitHub仓库。
实操语言模型
在本节中,我们会加载并与现存的(预训练的)transformer模型进行互动,以获得对其工作原理的高阶理解。我们将使用2019年因文本生成能力而引发关注的GPT-2模型。尽 管按今天的标准来看,GPT-2显得小而简单,但它仍然是展示这些语言模型如何运作的好例子。同样的原理也适用于自那以后发布的更大(大100多倍!)且更强的模型。
文本分词
下面开始生成基于初始输入的一些文本。例如,给定短语“it was a dark and stormy
”,我们希望模型生成一些词来完善这个句子。模型不能直接接收文本作为输入;其输入必须是以数字表示的数据。为将文本输入到模型中,我们必须首先找到一种将文本序列转换为数字的方法。这个过程称为分词/标记化(tokenization),是所有自然语言处理(NLP)流程中的关键步骤。
一种简单的方法是将文本分割成单个字符,并为每个字符分配一个唯一的数字ID。这个方案对于像中文这样每个字符都包含大量信息的语言可能有用。在像英语这样的语言中,这会创建一个非常小的分词表,并且在运行推理时会有很少的未知标记(训练期间未发现的字符)。然而,这种方法需要许多标记来表示一个字符串,这对性能不利,并且会抹去文本的一些结构和含义——这对准确性是不利的。每个字符携带的信息非常少,使得模型难以学习文本的基本结构。
另一种方法是将文本按单词分割。虽然这可以让每个标记捕捉更多的含义,但它也有缺点:我们需要处理更多的未知词(例如,拼写错误、俚语等),需要处理相同词的不同形式(例如,“run”,“runs”,“running”等),并且我们可能会得到一个非常大的词表,对于像英语这样的语言来说,这个词表可轻松超过五十万个单词。现代的分词策略在这两个极端之间找到了 平衡,将文本分割成既能捕获文本结构和含义又能处理未知词和相同词不同形式的词元。
通常同时出现的字符(如最常见的单词)可以分配一个代表整个单词或词组的单个标记。长或复杂的单词,以及有许多词形变化的单词,可能会被分割成多个标记,每个标记通常代表单词的一个有含义部分。没有什么“最佳”分词器;每个语言模型都有其自己的分词器。分词器之间的差异在于支持的标记数量和分词策略。
我们来看GPT-2的分词器如何处理一个句子。首先加载与GPT-2对应的分词器。然后,我们将输入文本(也称为提示词)通过分词器运行,将字符串编码为表示标记的数字。我们使用decode()
方法将每个ID转换回其对应的标记,以便进行演示。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
input_ids = tokenizer("It was a dark and stormy", return_tensors="pt").input_ids
input_ids
tensor([[1026, 373, 257, 3223, 290, 6388, 88]])
for t in input_ids[0]:
print(t, "\t:", tokenizer.decode(t))
tensor(1026) : It
tensor(373) : was
tensor(257) : a
tensor(3223) : dark
tensor(290) : and
tensor(6388) : storm
tensor(88) : y
可以看到,分词器将输入字符串分割成一系列标记,并为每个标记分配一个唯一的ID。大多数单词由单个标记表示,但“stormy
”被表示为两个标记:一个是“ storm
”(包括单词前的空格),另一个是后缀“y
”。这使得模型能够学习“stormy
”与“storm
”有关,后缀“y
”通常用于将名词变成形容词。GPT-2的词表大约50,000个标记,分词器可以高效地表示几乎所有输入文本,平均每个单词大约1.3个标记。
注: 尽管我们经常谈论训练分词器,它与训练模型没有关系。模型训练本质上是随机的(非确定性的),而训练分词器使用的是统计过程,识别出哪些子词在给定数据集中是最合适的选择。如何选择子词是分词算法的设计决策。因此,标记化训练是确定性的。我们不会深入探讨不同的分词策略,但一些最流行的分词方法包括在GPT-2中使用的字节级BPE、WordPiece和SentencePiece。
预测概率
GPT-2按因果语言模型(也称为自回归模型)训练,这表示它被训练用来在给定前面的标记后预测序列中的下一个标记。transformers库提供了高级工具,让我们能够快速使用这样的模型生成文本或执行其他任务。通过直接检查语言建模任务中的预测,可以帮助我们理解模型如何做出预测。首先来加载模型。
from transformers import AutoModelForCausalLM
gpt2 = AutoModelForCausalLM.from_pretrained("gpt2")
注:请注意
AutoTokenizer
和AutoModelForCausalLM
的使用。transformers库支持几百个模型及相应的分词器。为避免记住每个分词器和模型类的名称,我们使用AutoTokenizer
和AutoModelFor
。对于自动模型,我们需要指定使用模型的任务,例如分类(
AutoModelForSequenceClassification
)或目标检测(AutoModelForObjectDetection
)。对于GPT-2,我们将使用与因果语言建模任务相对应的类。使用自动类时,transformers会根据模型的配置选择适用的默认类。例如,其背后使用GPT2Tokenizer
和GPT2LMHeadModel
。
如果我们将上一节中的分词/词法单元句子输入模型,会得到每个输入字符串标记对应的50,257个值的结果:
outputs = gpt2(input_ids)
outputs.logits.shape # An output for each input token
torch.Size([1, 7, 50257])
输出的第一个维度是批次的数量(1是因为我们只通过模型运行了一个序列)。第二个维度是序列长度,即输入序列中的标记数量(本例中是7)。第三个维度是词汇量大小。我们为原始序列中的每个标记得到大约5万多个数字的列表。这些是模型的原始输出,或称logits,对应于词表中的标记。对于每个输入标记,模型预测词表中每个标记在该点之后延续序列的可能性。以我们的示例句子为例,模型将为“It
”,“It was
”,“It was a
”等预测logits。较高的logits值意味着模型认为相应的标记更可能是序列的延续。下表显示了输入序列、最可能的标记ID及其对应的标记。
Logits是模型的原始输出(如一串数字[0.1, 0.2, 0.01, …])。我们可以使用logits来选择最可能延续序列的标记。然而,我们也可以将logits转换为概率,下面会看到。
输入序列 | 最可能的下一个标记的ID | 对应的token |
---|---|---|
It | 318 | is |
It was | 257 | a |
It was a | 845 | very |
It was a dark | 1755 | night |
It was a dark and | 4692 | cold |
It was a dark and storm | 88 | y |
It was a dark and stormy | 1755 | (let’s figure this one) |
我们来关注整个输入句子的logits,并看看如何预测句子的下一个单词。我们可以使用argmax()
方 法找到具有最高值的标记索引:
final_logits = gpt2(input_ids).logits[0, -1] # The last set of logits
final_logits.argmax() # The position of the maximum
tensor(1755)
1755 对应于模型认为最有可能接输入字符串 "It was a dark and stormy
" 的标记ID。解码这个标记,我们可以看到这个模型还是清楚一些故事套路的:
tokenizer.decode(final_logits.argmax())
' night'
所以” night”是最可能的词元。考虑输入的语句,这是讲得通的。模型学会了使用一种叫做自注意力的算法来关注其他词元,这是transformer的基本构件。直观地说,自注意力让模型能识别每个词元对短语含义的贡献有多大。
注:transformer模型包含了许多这样的注意力层,每一层都专注于输入的某一方面。与启发式系统相反,这些方面或特征是在训练过程中学习到的,而非事先指定的。
下面我们通过选择前10个值来看看其他候选词元:
import torch
top10_logits = torch.topk(final_logits, 10)
for index in top10_logits.indices:
print(tokenizer.decode(index))
night
day
evening
morning
afternoon
summer
time
winter
weekend
,
需要将logits转换成概率值,以查看模型对每次预测的置信度。我们会通过将每个值与所有其他预测值进行比较,并进行归一化,以使所有数字加起来等于1。这正是softmax()
运算的任务。以下代码使用softmax()
打印出模型所得可能性最高的前10个词汇及其相关概率:
top10 = torch.topk(final_logits.softmax(dim=0), 10)
for value, index in zip(top10.values, top10.indices):
print(f"{tokenizer.decode(index):<10} {value.item():.2%}")
night 46.18%
day 23.46%
evening 5.87%
morning 4.42%
afternoon 4.11%
summer 1.34%
time 1.33%
winter 1.22%
weekend 0.39%
, 0.38%
在做进一步操作之前,建议先用以上代码进行实验。以下是一些建议,供读者尝试:
- 更改几个单词:尝试改变输入中的形容词(例如,“
dark
”和“stormy
”),看看模型的预测如何改变。预测的词仍然是“night
”吗?概率如何变化? - 更改输入内容:尝试不同的输入字符串,看看模型的预测如何变化。你赞同模型的预测吗?
- 语法:如果提供的内容是在语法上不正确的句子会怎么样?模型如何处理?看看顶级预测的概率。
生成文本
一旦知道如何获取模型对序列中下一个词元的预测,通过不断地把模型的预测反馈给它自身,就很容易生成文本了。我们可以调用gpt2(ids)
,生成一个新的分词ID,加入到列表中,然后再次调用函数。为了方便生成多个词,transformers自回归模型具有一个合适的方法generate()
。下面探讨一个示例。
output_ids = gpt2.generate(input_ids, max_new_tokens=20)
decoded_text = tokenizer.decode(output_ids[0])
print("Input IDs", input_ids[0])
print("Output IDs", output_ids)
print(f"Generated text: {decoded_text}")
Input IDs tensor([1026, 373, 257, 3223, 290, 6388, 88])
Output IDs tensor([ 1026, 373, 257, 3223, 290, 6388, 88, 1755, 13,
383, 2344, 373, 19280, 11, 290, 262, 15114, 547,
7463, 13, 383, 2344, 373, 19280, 11, 290, 262])
Generated text: It was a dark and stormy night. The wind was blowing,
and the clouds were falling. The wind was blowing, and the
在上一节中运行gpt2()
的方法时,它返回了词汇表(50257)中每个标记的logits列表。然后,我们需要计算概率并选择最可能的标记。generate()
抽象了这个逻辑。它进行多次前向传播,反复预测下一个标记,并将其追加到输入序列中。generate()
为我们提供了最终序列的token ID,包括输入和新标记。然后,我们可以用 tokenizer 的decode()
方法将其转换回文本。
执行生成有许多种策略。我们刚才选择最大可能性的标记,被称为贪婪解码(greedy decoding)。尽管这种方法直截了当,但有时可能导致次优的结果,特别是在生成较长文本序列时。贪婪解码存在问题,因为它不考虑整个句子的总概率,只关注紧接着的下一个词。例如,给定起始词 Sky
和下一个词的选择 blue
和 rockets
,贪婪解码可能会偏向 Sky blue
,因为blue
初看起来更可能紧跟 Sky
。然而,这种方法会忽视更连贯和可能的整体序列,如Sky rockets soar
。因此,贪婪解码有时会错过最可能的整体序列,导致文本生成的效果不理想。
与一次一个token不同,如 Beam Search(束集搜索)这样的技术,会探索序列的多种可能延续,并返回最可能的延续序列。它在生成过程中保持最有可能的 num_beams
假设,并选择最有可能的那个。
beam_output = gpt2.generate(
input_ids,
num_beams=5,
max_new_tokens=30,
)
print(tokenizer.decode(beam_output[0], skip_special_tokens=True))
It was a dark and stormy night.
"It was dark and stormy," he said.
"It was dark and stormy," he said.
可以看到,输出中存在相同序列的多次重复。有一些参数可用于控制执行更好的生成。来看两个例子:
repetition_penalty
:对已生成的词元进行多大程度的惩罚,避免重复。比较好的默认值是1.2。bad_words_ids
:不应生成的词元列表(例如防止生成冒犯性的词汇)。
下面看惩罚重复所得到的效果:
beam_output = gpt2.generate(
input_ids,
num_beams=5,
repetition_penalty=1.2,
max_new_tokens=38,
)
print(tokenizer.decode(beam_output[0], skip_special_tokens=True))
It was a dark and stormy night.
"There was a lot of rain," he said. "It was very cold."
He said he saw a man with a gun in his hand.
效果好很多。该使用哪种生成策略呢?就像机器学习中常说的……这取决于具体情况。当文本的期望长度比较可预测时,束集搜索(beam search)效果较好。它适用于摘要或翻译等任务,但不适用于开放式生成,因为输出长度可能有较大变化,导致重复。尽管我们可以限制模型避免重复,但这样做也可能导致性能下降。还要注意,束集搜索比贪婪搜索(greedy search)要慢,因为它需要同时对 多个束进行推理,这对大型模型来说可能是个问题。
在使用贪婪搜索和束集搜索生成文本时,我们提升模型生成高概率的下一个词的分布。有趣的是,高质量的人类语言并不遵循类似的分布。人类文本往往更不可预测。关于这一反直觉现象的优秀论文有《神经文本退化的奇特案例》。作者推测人类语言不喜欢可预测的词语——人们会避免陈述显而易见的内容。论文提出了一种称为核采样(nucleus sampling)的方法。
采样时,我们通过从下一个词的概率分布中采样来选择下一个词。这意味着采样是一个不确定性的生成过程。如下一个词可能是night(60%)、day(35%)和apple(5%),那么与其选择night(使用贪婪搜索),我们会从分布中采样。换句话说,即使apple是一个低概率的词,有5%的几率被选择,尽管它可能产生不合逻辑的生成内容。采样避免了生成重复文本,因此可以产生更多样化的内容。采样在transformer模型中通过 do_sample
参数来完成。
from transformers import set_seed
# Setting the seed ensures we get the same results every time we run this code
set_seed(70)
sampling_output = gpt2.generate(
input_ids,
do_sample=True,
max_length=34,
top_k=0, # We'll come back to this parameter
)
print(tokenizer.decode(sampling_output[0], skip_special_tokens=True))
It was a dark and stormy day until it broke down the big canvas on my sleep station, making me money dilapidated, and, with a big soothing mug
我们可以在采样之前操控概率分布,通过temperature
参数使其变得“更陡峭”或“更平缓”。temperature
高于1会增加分布的随机性,这样可以鼓励生成可能性较低的词。temperature
在0和1之间会减少随机性,增加可能性较高词的概率,避免过于出乎意料的预测。temperature
为0会将所有概率集中到最可能的下一个词,相当于贪婪解码。对比以下示例中temperature
参数对生成文本的影响。
sampling_output = gpt2.generate(
input_ids,
do_sample=True,
temperature=0.4,
max_length=40,
top_k=0,
)
print(tokenizer.decode(sampling_output[0], skip_special_tokens=True))
It was a dark and stormy night, and I was alone. I was in the middle of the night, and I was suddenly awakened bygoodness, and I was thinking of the old man
sampling_output = gpt2.generate(
input_ids,
do_sample=True,
temperature=0.001,
max_length=40,
top_k=0,
)
print(tokenizer.decode(sampling_output[0], skip_special_tokens=True))
It was a dark and stormy night. The wind was blowing, and the clouds were falling. The wind was blowing, and the clouds were falling. The wind was blowing, and the clouds were
sampling_output = gpt2.generate(
input_ids,
do_sample=True,
temperature=3.0,
max_length=40,
top_k=0,
)
print(tokenizer.decode(sampling_output[0], skip_special_tokens=True))
It was a dark and stormyfleet verteutorial took possession contingt containing Carol Rhino soils titsfastKEY 07 Deaths od paradeCONT WEEK Barclays Reviskish6 EdwingarPosition serv blat Imperial licenseium Bot
第一条测试比第二条连贯得多。第二条测试使用了非常低的temperature,因此文本重复(类似于贪婪解码)。最后,第三个样本temperature极高,生成了无意义的文本。
你可能注意到有一个参数是top_k
。是什么呢?Top-K采样是一种简单的采样方法,其中只考虑K个最有可能的后续词。例如,使用top_k=5
,生成方法会首先筛选出最有可能的五个词,并重新分配概率使其总和为一。
sampling_output = gpt2.generate(
input_ids,
do_sample=True,
max_length=40,
top_k=10,
)
print(tokenizer.decode(sampling_output[0], skip_special_tokens=True))
It was a dark and stormy night and there were some things we didn't understand. I didn't understand the nature of the problem. I was afraid and scared."
In response to the
嗯……还可以更好。Top-K采样的一个问题是实际中相关候选词的数量可能会有很大差异。如果定义top_k=5
,有些分布仍然会包含概率非常低的词,而其他分布则仅包含高概率词。
我们介绍的最后一种生成策略是Top-p采样(也称为核采样)。与采样最高概率的K个词不同,我们使用所有累计概率超过给定值的最可能词。如果我们使用top_p=0.94
,首先会筛选出累计概率达到0.94或更高的词,然后重新分配概率并进行常规采样。下面看看实际效果。
sampling_output = gpt2.generate(
input_ids,
do_sample=True,
max_length=40,
top_p=0.94,
top_k=0,
)
print(tokenizer.decode(sampling_output[0], skip_special_tokens=True))
It was a dark and stormy night from 7PM. Tony attended the Rangers game with his boys. He overheard them singing. He recorded a video with Jasper in the booth. He rented an empty
Top-K和Top-p都是实践中常用的生成方法。它们甚至可以结合起来过滤掉低概率词,同时提供更多的生成控制。使用随机生成方法的问题是生成的文本不一定连贯。
我们已经看到了三种不同的生成方法:贪婪搜索、束集搜索解码和采样(通过temperature、Top-K和Top-p进一步控制)。这类方法很多!如果你想进一步测试生成方法,以下是一些建议:
- 尝试不同的参数值。增加束的数量会如何影响生成质量?减少或增加
top_p
值会发生什么? - 一种减少束集搜索中重复的方法是引入n-gram(n个词的序列)惩罚。可以通过
no_repeat_ngram_size
配置,避免重复相同的n-gram。例如,使用no_repeat_ngram_size=4
,生成的文本就不会包含完全相同的四个连续词。 - 一种较新的方法,反差搜索,可以生成长且连贯的内容,同时避免重复。这是通过考虑模型预测的概率和上下文的相似性来实现的。可以通过
penalty_alpha
和top_k
控制。有关反差搜索可阅读Generating Human-level Text with Contrastive Search一文。
如果这听起来太经验主义,那是因为确实是这么回事。生成式是一个活跃的研究领域,新论文不断提出不同的方案,如更复杂的过滤方法。我们将在最后一章简要讨论这些内容。没有一种方法适用于所有模型,所以对不同的技术做实验很重要。
零样本泛化
生成式语言是transformer模型的一个有趣而令人振奋的应用,但撰写关于独角兽的伪造文章并不 是其如此受欢迎的原因。为了很好地预测后一个词,这些模型必须学到相当多的现实世界的知识。我们可以利用这一点来执行各种任务。例如,无需训练一个专门用于翻译的模型,只需用一个足够强大的语言模型来输入一个提示词,像这样:
Translate the following sentence from English to French:
Input: The cat sat on the mat.
Translation:
我在GitHub Copilot中实时输入了这一示例,它贴心地建议将“Le chat était assis sur le tapis
”作为以上提示词的延续——这是语言模型可以执行未明确训练任务的完美例子。模型越强大,它在无需额外训练的情况下可以执行的任务就越多。这种灵活性使得transformer模型非常强大,并在近年来变得广受欢迎。
为能亲身体验 ,我们使用GPT-2作为分类模型。具体来说,我们会对电影评论进行正面或负面的分类——这是NLP领域的经典基准任务。为增加趣味性,我们使用零样本的方式,也就是我们不会向模型提供任何标记数据。而是向模型提供评论文本,让其预测情感。来看看它的表现。
我们将评论插入一个提示词模板,为模型提供上下文,并帮助它理解我们的要求。将提示词输入模型后,查看它对后一个词的预测,看看哪个词概率更高:“positive
”还是“negative
”?为此,我们需要找出这些词的相应ID。
# Check the token IDs for the words ' positive' and ' negative'
# (note the space before the words)
tokenizer.encode(" positive"), tokenizer.encode(" negative")
([3967], [4633])
有了ID之后,就可以使用模型进行推理,查看哪个词具有更高的概率:
def score(review):
"""Predict whether it is positive or negative
This function predicts whether a review is positive or negative
using a bit of clever prompting. It looks at the logits for the
tokens ' positive' and ' negative' (note the space before the
words), and returns the label with the highest score.
"""
prompt = f"""Question: Is the following review positive or
negative about the movie?
Review: {review} Answer:"""
input_ids = tokenizer(prompt, return_tensors="pt").input_ids # 对提示词进行分词
final_logits = gpt2(input_ids).logits[0, -1] # 从词汇中获取每个词元的logit,注意我们使用的是gpt2()而不是gpt2.generate(),因为前者返回词中的每个分词的logit,而后者仅返回所选定的分词
if final_logits[3967] > final_logits[4633]: # 检测正向分词的logit是否大于负向分词的logit
print("Positive")
else:
print("Negative")
可以对几个假评论使用zero-shot分类器进行尝试:
score("This movie was terrible!")
Negative
score("That was a delight to watch, 10/10 would recommend :)")
Positive
score("A complex yet wonderful film about the depravity of man")
Positive
在补充材料中,有一个已打标记的评论数据集和用于评估这种零样本方法准确性的代码。尝试调整提示词模板以提高模型的性能。看看还能想到其他可以使用类似方法执行的任务吗?
最近模型的零样本能力大大改变了游戏规则。随着模型的改进,它们可以开箱即用地执行更多任务,使其更易于使用,并减少了每个任务需要专用模型的需求。
少样本泛化
虽然有先进的ChatGPT以及对外界完美提示词的追求,零样本泛化(或提示词调优)并不是让强大的语言模型执行各类任务的唯一路径。
零样本泛化是少样本(few-shot )泛化技术的极端应用,在少样本泛化中,我们向语言模型提供一些希望它执行的任务的示例,然后让其提供类似的答案。我们不是在训练模型,而是通过展示一些示例来影响 生成的内容,增加生成文本遵循我们的提示词结构和模式的概率。
来看一个例子。提供示例之外,提供一段简短的描述,例如“Translate English to French
”,也有助于生成更高质量的内容。这次,我们使用一个更强大的模型:GPT-Neo 1.3B。GPT-Neo是由非营利研究实验室EleutherAI开发的一系列transformer模型。这些模型在许多任务中表现优于GPT-2,并且在少样本学习方面表现更好。我们使用13亿参数的变体,虽然在今天看来不算大,但它依然相当强大,是GPT-2(仅有1.24亿参数)的十倍。
model = AutoModelForCausalLM.from_pretrained("EleutherAI/gpt-neo-1.3B")
prompt = """\
Translate English to Spanish:
English: I do not speak Spanish.
Spanish: No hablo español.
English: See you later!
Spanish: ¡Hasta luego!
English: Where is a good restaurant?
Spanish: ¿Dónde hay un buen restaurante?
English: What rooms do you have available?
Spanish: ¿Qué habitaciones tiene disponibles?
English: I like soccer
Spanish:"""
inputs = tokenizer(prompt, return_tensors="pt").input_ids
output = model.generate(
inputs,
do_sample=False,
max_new_tokens=10,
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
Translate English to Spanish:
English: I do not speak Spanish.
Spanish: No hablo español.
English: See you later!
Spanish: ¡Hasta luego!
English: Where is a good restaurant?
Spanish: ¿Dónde hay un buen restaurante?
English: What rooms do you have available?
Spanish: ¿Qué habitaciones tiene disponibles?
English: I like soccer
Spanish: Me gusta el fútbol
我们描述了要实现的任务,并提供了四个示例设置模型的上下文。因此,这是一个4-shot泛化任务。然后,我们要求模型生成更多文本以遵循模式并提供所需的翻译。以下是一些可以探索的想法:
- 更少的示例是否可以?
- 没有任务描述是否可行?
- 其他任务如何?
- 在这一情况下,GPT-2的表现如何?
注: 由于GPT-2的大小和训练过程,它在少样本任务上表现不是很好,在零样本泛化上甚至更差。我们如何在之前的例子中使用它进行情感分类呢?其实做了一点小手脚:我们没有查看模型生成的文本,只是检查了“Positive”的概率是否大于“Negative”的概率。理解模型的底层工作原理,即使是小模型,也能解锁强大的应用。要思考你的问题,不要害怕探索。
GPT-2是一个基础模型的例子。以GPT-2为风格的一些底座模型具有在推理时使用的零样本和少样本能力。另一种方式是模型微调:我们在基础模型之上继续在特定的领域或任务数据上训练。通常很少需要世界上最强大的模型展示的极端泛化能力;如果只要解决一个特定任务,更便宜且更好的是微调并部署一个专门用于单一任务的小模型。同样重要的是要注意基础模型不是对话式的;尽管可以编写一个非常好的提示词来使用基础模型创建一个聊天机器人,但通常更方便的是使用对话数据微调基础模型,从而提高模型的对话能力。在第五章中会进行实践。
Transformer解构
在简单实践语言模型之后,我们准备介绍基于transformer的语言生成模型的架构图。涉及到的高阶组件有:
-
分词:输入文本被分解为单个token(可以是单词和子词)。每个词元都有一个对应的ID,用于索引词嵌入。
-
输入词嵌入:token以称为嵌入的向量进行表示。这些嵌入为数值形式,捕获每个token的语义。可以将向量视为数字列表,每个数字对应token含义的特定方面。在训练过程中,模型学习如何将每个token映射到对应的嵌入。无论token在输入序列中的位置如何,嵌入总是相同的。
-
位置编码:transformer模型没有顺序的概念,因此我们需要用位置信息丰富词嵌入。通过将位置编码添加到词嵌入中来实现。位置编码是一组向量,编码了输入序列中每个token的位置。使得模型能够根据序列中的位置区分标记,这很有用,因为同一个token出现在不同位置可以有不同的含义。
-
Transformer块:transformer模型的核心是transformer块。transformer的强大之处在于堆叠多个块,使模型能够学习输入token之间越来越复杂和抽象的关系。它由两个主要组件组成:
- 自注意义力机制:这种机制允许模型在整个序列的上下文中权衡每个token的重要性。它帮助模型理解输入中不同token之间的关系。自注意力机制是transformer处理长程依赖和复杂词语关系的 关键,可生成连贯且语境恰当的文本。
- 前馈神经网络:自注意力输出通过前馈神经网络进一步细化输入序列的表示。
-
上下文嵌入:transformer块的输出是一组上下文嵌入,捕获输入序列中token之间的关系。与每个token固定的输入嵌入不同,上下文嵌入在transformer模型的每一层中根据token之间的关系进行更新。
-
预测:一个额外的层将最终表示处理为任务相关的最终输出。对于文本生成,这涉及一个线性层将上下文嵌入映射到词空间,随后进行softmax运算以预测序列中的下一个token。
图2-1: 基于transformer的模型的架构
当然,这只是对transformer架构的简化描述。深入研究自注意力机制的工作原理或transformer块的内部机制暂不在我们的讨论范围。但理解transformer模型的顶层架构有助于掌握这些模型的工作原理以及将其应用于各种任务。这种架构使transformer在各种任务和领域中取得了前所未有的表现,你会发现它们不断出现——不仅是在本系列文章,而是在整个学科中。
Transformer模型家族
序列到序列的任务
在本章的开头,我们使用GPT-2进行了自回归 文本生成实验。GPT-2是基于解码器的transformer示例,它具有一组transformer块,用于处理输入序列。这是当今流行的方式,但原始的transformer论文《Attention Is All You Need》中使用了一种更复杂的架构,称为编码器-解码器架构,这种架构至今仍在广泛使用。
transformer论文专注于机器翻译这一序列到序列的任务。当时,机器翻译的最佳结果是通过循环神经网络(RNN)实现的,例如LSTM和GRU(读者如果不熟悉,不必担心)。该论文通过专注于注意力方法展示了更好的结果,并表达了其可扩展性和训练更容易。这些因素——出色的表现、稳定的训练和易于扩展——是transformer取得成功并被应用于多种任务的原因,下一节将更深入地探讨这些方面。
在编码器-解码器模型中,就像论文中描述的原始transformer模型,一组transformer块(称为编码器)将输入序列处理成一组丰富的表示,然后将其传递给另一组transformer块(称为解码器),解码它们为输出序列。这种将一个序列转换为另一个序列的方法称为序列到序列或seq2seq,适用于翻译、摘要或问答等任务。
例如,将一段英文句子喂给翻译模型的编码器,生成一个捕获输入含义的嵌入。然后,解码器使用这个嵌入生成对应的法语句子。生成过程在解码器中按token逐一进行,在本章早些时候生成序列时有演示。然而,对每个后续token的预测不仅受到正在生成的序列中前一个token的影响,还受到来自编码器的输出的影响。
将编码器端的输出合并到解码器堆栈中的机制称为交叉注意力。它类似于自注意力,不同之处在 于输入中的每个token(解码器正在处理的序列)关注的是来自编码器的上下文,而不是其序列中的其他token。交叉注意力层与自注意力交错,让解码器能够使用其序列中的上下文和来自编码器的信息。
在transformer论文发表后,现有的序列到序列模型,如Marian NMT,将这些技术作为其架构的核心部分进行了整合。使用这些思想开发了新的模型。其中一个值得关注的是BART(“Bidirectional and Auto-Regressive Transformers
”的缩写)。在预训练期间,BART会破坏输入序列,并尝试在解码器输出中重建。之后,BART会针对其他生成任务(如翻译或摘要)进行微调,利用预训练期间获得的丰富序列表示。顺便说一下,输入破坏是扩散模型背后的关键思想之一,我们将在第三章中讲到。
另一个值得注意的序列到序列模型是T5。T5以通用的方式处理多种NLP任务,通过将60种任务表述为文本到文本的转换。不同任务不需要自定义层或代码,训练使用相同的超参数,模型从多样化的数据集中学习。
我们刚刚讨论了编码器-解码器和单解码器架构。常见的问题是,为什么像翻译这样的任务需要编码器-解码器模型,而诸如GPT-2的单解码器模型也能表现良好的结果。编码器-解码器模型旨在将整个输入序列翻译为输出序列,使其极其适合翻译任务。而单解码器模型专注于预测序列中的下一个token。最初,像GPT-2这样的单解码器模型在零样本学习场景中的能力不如更先进的模型如GPT-3,但这不仅仅是因为缺少编码器。像GPT-3这样的高级模型在零样本能力上的提升源自更大的训练数据、改进的训练技术和更大的模型规模。尽管seq2seq模型中的编码器在理解输入序列的完整上下文中发挥着 关键作用,但单解码器模型的进步使它们更加有效和多样化,即使对于传统上依赖seq2seq模型的任务也是如此。