AGI 之 【Hugging Face】 的【文本生成】的 [生成连贯文本 ] / [ 贪婪搜素解码 ] / [束搜索解码] / [ top-k和采样 ] 的简单整理
一、简单介绍
AGI,即通用人工智能(Artificial General Intelligence),是一种具备人类智能水平的人工智能系统。它不仅能够执行特定的任务,而且能够理解、学习和应用知识于广泛的问题解决中,具有较高的自主性和适应性。AGI的能力包括但不限于自我学习、自我改进、自我调整,并能在没有人为干预的情况下解决各种复杂问题。
- AGI能做的事情非常广泛:
跨领域任务执行:AGI能够处理多领域的任务,不受限于特定应用场景。
自主学习与适应:AGI能够从经验中学习,并适应新环境和新情境。
创造性思考:AGI能够进行创新思维,提出新的解决方案。
社会交互:AGI能够与人类进行复杂的社会交互,理解情感和社会信号。
- 关于AGI的未来发展前景,它被认为是人工智能研究的最终目标之一,具有巨大的变革潜力:
技术创新:随着机器学习、神经网络等技术的进步,AGI的实现可能会越来越接近。
跨学科整合:实现AGI需要整合计算机科学、神经科学、心理学等多个学科的知识。
伦理和社会考量:AGI的发展需要考虑隐私、安全和就业等伦理和社会问题。
增强学习和自适应能力:未来的AGI系统可能利用先进的算法,从环境中学习并优化行为。
多模态交互:AGI将具备多种感知和交互方式,与人类和其他系统交互。
Hugging Face作为当前全球最受欢迎的开源机器学习社区和平台之一,在AGI时代扮演着重要角色。它提供了丰富的预训练模型和数据集资源,推动了机器学习领域的发展。Hugging Face的特点在于易用性和开放性,通过其Transformers库,为用户提供了方便的模型处理文本的方式。随着AI技术的发展,Hugging Face社区将继续发挥重要作用,推动AI技术的发展和应用,尤其是在多模态AI技术发展方面,Hugging Face社区将扩展其模型和数据集的多样性,包括图像、音频和视频等多模态数据。
- 在AGI时代,Hugging Face可能会通过以下方式发挥作用:
模型共享:作为模型共享的平台,Hugging Face将继续促进先进的AGI模型的共享和协作。
开源生态:Hugging Face的开源生态将有助于加速AGI技术的发展和创新。
工具和服务:提供丰富的工具和服务,支持开发者和研究者在AGI领域的研究和应用。
伦理和社会责任:Hugging Face注重AI伦理,将推动负责任的AGI模型开发和应用,确保技术进步同时符合伦理标准。
AGI作为未来人工智能的高级形态,具有广泛的应用前景,而Hugging Face作为开源社区,将在推动AGI的发展和应用中扮演关键角色。
(注意:以下代码运行,可能需要科学上网)
二、文本生成
文本生成(Text Generation)是自然语言处理(NLP)中的一项任务,其目标是根据给定的输入(例如一个开头的句子、一个问题、一个主题等)生成连贯、符合语法且具有语义意义的文本片段。这项任务可以应用于多种场景,如自动写作、对话系统、代码生成、翻译等。
- 文本生成的基本原理
文本生成的核心在于构建一个能够预测下一个词或序列的模型。这些模型通过学习大量的文本数据,捕捉语言的语法和语义结构,从而生成具有高质量的自然语言文本。
- 文本生成的难点
- 连贯性:生成的文本需要逻辑连贯,前后文要保持一致。
- 语法正确:生成的文本必须符合目标语言的语法规则。
- 上下文相关性:生成的内容要与给定的上下文或输入相关。
- 多样性:避免生成重复或千篇一律的内容,提升文本的多样性。
- 文本生成的实现方式
文本生成可以通过多种方式实现,下面是几种主要的方法:
1. 语言模型(Language Models)
语言模型是文本生成的核心工具,通过学习大量的文本数据来预测下一个词的概率。主要有以下几种模型:
1.1 自回归模型(Autoregressive Models)
自回归模型通过依次生成每个词来生成文本,每一步都依赖于之前生成的词。
代表模型:
- GPT 系列(GPT, GPT-2, GPT-3):这些模型通过大量的数据进行训练,能够生成非常自然和连贯的文本。
示例代码(使用 GPT-2):
from transformers import GPT2LMHeadModel, GPT2Tokenizer model_name = 'gpt2' model = GPT2LMHeadModel.from_pretrained(model_name) tokenizer = GPT2Tokenizer.from_pretrained(model_name) input_text = "Once upon a time" input_ids = tokenizer.encode(input_text, return_tensors='pt') output_ids = model.generate(input_ids, max_length=50) output_text = tokenizer.decode(output_ids[0], skip_special_tokens=True) print(output_text)
1.2 自编码模型(Autoencoder Models)
自编码模型通过将输入文本编码为固定长度的向量,然后解码生成新的文本。这类模型通常用于生成与输入相关的文本。
代表模型:
- BERT(Bidirectional Encoder Representations from Transformers):虽然 BERT 主要用于理解任务,但也可以用于生成任务。
1.3 生成对抗网络(GANs)
GANs 包括一个生成器和一个判别器,生成器生成文本,判别器判断文本的真实性。这类模型在文本生成中较少使用,但在某些特定场景下表现出色。
2. 条件生成模型(Conditional Generation Models)
这类模型在生成文本时考虑额外的条件信息,如特定的上下文、问题等。
代表模型:
- T5(Text-To-Text Transfer Transformer):T5 将所有任务转化为文本到文本的形式,通过给定的条件生成目标文本。
示例代码(使用 T5):
from transformers import T5ForConditionalGeneration, T5Tokenizer model_name = 't5-small' model = T5ForConditionalGeneration.from_pretrained(model_name) tokenizer = T5Tokenizer.from_pretrained(model_name) input_text = "translate English to German: The house is wonderful." input_ids = tokenizer.encode(input_text, return_tensors='pt') output_ids = model.generate(input_ids, max_length=40) output_text = tokenizer.decode(output_ids[0], skip_special_tokens=True) print(output_text)
3. 控制生成(Controlled Generation)
控制生成通过特定的机制或条件来控制生成文本的内容、风格或格式。这类方法在需要生成特定风格或遵循特定规则的文本时非常有用。
代表技术:
- Ctrl(Conditional Transformer Language Model):通过条件控制生成过程。
4. 强化学习(Reinforcement Learning)
在文本生成中使用强化学习,可以通过奖励机制优化生成文本的质量。例如,通过用户反馈或预定义的评价指标来调整生成策略。
文本生成是自然语言处理中的一个重要领域,涉及多种模型和技术。通过语言模型(如 GPT 系列)、条件生成模型(如 T5)、控制生成和强化学习等方法,我们可以在不同的应用场景中生成高质量的文本。Hugging Face 提供了强大的工具和预训练模型,使我们能够方便地实现文本生成任务。
基于Transformer的语言模型有一种非常神奇的特性,它们能够生成几乎与人类写作的文本难以区分的文本。一个著名的例子是OpenAI的GPT-2,当给定以下提示时 :
In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.
能够生成一篇引人入胜的关于独角兽的新闻文章:
The scientist named the population, after their distinctive horn, Ovid's Unicorn. These four-horned, silver-white unicorns were previously unknown to science. Now, after almost two centuries, the mystery of what sparked this odd phenomenon is finally solved. Dr. Jorge Pérez, an evolutionary biologist from the University of La Paz, and several companions, were exploring the Andes Mountains when they found a small valley, with no other animals or humans. Pérez noticed that the valley had what appeared to be a natural fountain, surrounded by two peaks of rock and silver snow. Pérez and the others then ventured further into the valley.“By the time we reached the top of one peak, the water looked blue, with some crystals on top, ”said Pérez. Pérez and his friends were astonished to see the unicorn herd. These creatures could be seen from the air without having to move too much to see them—they were so close they could touch their horns. While examining these bizarre creatures the scientists discovered that the creatures also spoke some fairly regular English...
这个例子如此引人注目的原因在于它是在没有任何明确监督的情况下生成的!通过简单地学习预测数百万个网页文本中下一个单词,像GPT-2及其更强大的后继者GPT-3这样的语言模型能够获得广泛的技能和模式识别能力,并可以通过不同类型的输入提示来激活这些能力。图5-1展示了语言模型有时会在预训练期间接触到需要根据上下文预测以下词元的任务序列,如加法、单词拼写纠正和翻译。如果模型足够大,这使它们能够在微调期间或在推理时有效地迁移这些知识。这些任务并没有被预先选择,而是在用于训练百亿参数语言模型的大型语料库中自然发生的。
然而,正如Delip Rao指出的那样(https://oreil.ly/mOM3V),Meena自己并不知道在讲冷笑话。
Transformer模型生成逼真文本的能力已经催生了各种各样的应用,例如InferKit(https://oreil.ly/I4adh)、Write WithTransformer(https://oreil.ly/ipkap)、AI Dungeon(https://oreil.ly/8ubC1),以及像Google的Meena(https://oreil.ly/gMegC)这样的对话代理。
三、生成连贯文本的挑战
到目前为止,本节的重点是通过预训练和监督微调的组合来处理NLP任务。正如我们所看到的,对于像序列或词元分类这样的特定任务,生成预测是相当简单的。模型产生一些logit,我们要么取最大值以获得预测类,要么应用softmax函数以获得每个类的预测概率。相比之下,将模型的概率输出转换为文本需要一种解码方法,这在文本生成中引入了一些独特的挑战:
●解码是迭代进行的,因此涉及的计算量比仅通过模型前向传递的一次传输入要大得多。
●生成的文本的质量和多样性取决于解码方法和相关的超参数的选择。
现在我们从研究GPT-2是如何预训练的以及随后如何应用于生成文本来开始理解这个解码过程的工作原理。
像其他自回归或因果语言模型一样,GPT-2被预训练用于在给定一些初始提示或上下文序列x=x,x,…,xk的情况下,估计文本中出现词元序列y=y,y,…,yt的概率P(y|x),这个过程的核心在于解码方法,它决定了每个时间步选择哪个词元。由于获取足够的训练数据以直接估计P(y|x)是不切实际的,因此通常使用概率的链式规则将其分解为条件概率的乘积:
1212
其中,y是序列y,...,yt的简写符号。我们从这些条件概率中获得了这样的直觉,即自回归语言模型等同于给定一个句子中前面的单词预测后面的单词。这正是上述方程右侧的概率所描述的。请注意,这种预训练目标与BERT的预训练目标非常不同,BERT利用过去和未来的上下文来预测掩码词元。
<t1-1
现在你可能已经猜到了我们如何将下一个词元预测任务调整为生成任意长度的文本序列。如图5-3所示,我们从像“Transformers are the”这样的提示开始,使用模型预测下一个词元。一旦我们确定了下一个词元,我们将其附加到提示上,然后使用新的输入序列生成另一个词元。如此循环,直到达到特殊的序列结尾词元或预定义的最大长度。
由于输出序列取决于输入提示的选择,因此这种类型的文本生成通常被称为条件文本生成。
这个过程的核心是决定在每个时间步长选择哪个词元的解码方法。由于语言模型头在每个步骤为词表中的每个词元生成一个logit zt,我们可以通过使用softmax获得下一个可能词元wi的概率分布:
,i
<t,i
大多数解码方法的目标是通过选择一个来搜索概率最大的、满足以下条件的整体序列:
直接找到 将涉及使用语言模型评估每个可能的序列。由于没有一个算法能够在合理的时间内完成这个任务,因此我们转而依赖近似方法。在本章中,我们将探讨其中的一些近似方法,并逐渐构建出更加智能和复杂的算法,以生成高质量的文本。
四、贪婪搜索解码
从模型的连续输出中获取离散词元的最简单解码方法是在每个时间步中贪婪地选择具有最高概率的词元:
如果你的机器内存不足,你可以通过将model_name="gpt-xl"替换为model_name="gpt"来加载一个更小的GPT-2版本。
这里我们通过15亿个参数的版本的GPT-2来了解贪婪搜索的工作原理,我们先加载模型:
# 导入 PyTorch 库
import torch
# 从 transformers 库中导入自动分词器和因果语言模型
from transformers import AutoTokenizer, AutoModelForCausalLM
# 检查当前设备是否有 GPU 可用,如果有则使用 GPU,否则使用 CPU
device = "cuda" if torch.cuda.is_available() else "cpu"
# 指定要使用的预训练模型的名称
model_name = "gpt2-xl"
# 加载指定名称的预训练分词器
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 加载指定名称的预训练因果语言模型,并将其移动到指定设备(GPU 或 CPU)
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
运行结果:
现在让我们生成一些文本!虽然Hugging Face Transformers库为像GPT-2这样的自回归模型提供了现成的generate()函数,但为了了解底层原理,我们将自己实现这种解码方法。下面我们将采用之前图片所示的相同迭代方法,使用“Transformers are the”作为输入提示,在8个时间步内运行解码。在每个时间步中,我们挑选出模型在提示中最后一个词元的logit,并用softmax包装它们以获得概率分布。然后我们挑选出具有最高概率的下一个词元,将其添加到输入序列中,并再次运行该过程。代码在每个时间步中存储5个最有可能的词元,以便我们可以可视化它们的选择,全部代码如下:
# 导入 pandas 库,用于数据处理
import pandas as pd
# 输入文本
input_txt = "Transformers are the"
# 使用分词器将输入文本转换为模型输入格式,并将其移动到指定设备(GPU 或 CPU)
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
# 创建一个空列表,用于存储每次迭代的结果
iterations = []
# 定义生成文本的步数
n_steps = 8
# 每步选择的候选词数量
choices_per_step = 5
# 禁用梯度计算(在推理时通常不需要计算梯度)
with torch.no_grad():
for _ in range(n_steps):
# 创建一个字典,用于存储当前迭代的信息
iteration = dict()
# 将当前输入的 token 解码为文本,存储到字典中
iteration["Input"] = tokenizer.decode(input_ids[0])
# 使用模型生成输出
output = model(input_ids=input_ids)
# 选择第一个 batch 的最后一个 token 的 logits,并应用 softmax 计算概率
next_token_logits = output.logits[0, -1, :]
next_token_probs = torch.softmax(next_token_logits, dim=-1)
# 对下一个 token 的概率进行排序,按降序排列
sorted_ids = torch.argsort(next_token_probs, dim=-1, descending=True)
# 存储概率最高的几个候选词
for choice_idx in range(choices_per_step):
# 获取候选词的 token id
token_id = sorted_ids[choice_idx]
# 获取候选词的概率,并转换为 numpy 格式
token_prob = next_token_probs[token_id].cpu().numpy()
# 将候选词的 token id 解码为文本,并格式化为 "词汇 (概率)" 的形式
token_choice = f"{tokenizer.decode(token_id)} ({100 * token_prob:.2f}%)"
# 将候选词存储到字典中
iteration[f"Choice {choice_idx+1}"] = token_choice
# 将预测的下一个 token 附加到输入序列中
input_ids = torch.cat([input_ids, sorted_ids[None, 0, None]], dim=-1)
# 将当前迭代的结果添加到列表中
iterations.append(iteration)
# 将迭代结果转换为 pandas DataFrame 格式,以便更方便地查看和分析
pd.DataFrame(iterations)
运行结果:
Input | Choice 1 | Choice 2 | Choice 3 | Choice 4 | Choice 5 | |
0 | Transformers are the | most (8.53%) | only (4.96%) | best (4.65%) | Transformers (4.37%) | ultimate (2.16%) |
1 | Transformers are the most | popular (16.78%) | powerful (5.37%) | common (4.96%) | famous (3.72%) | successful (3.20%) |
2 | Transformers are the most popular | toy (10.63%) | toys (7.23%) | Transformers (6.60%) | of (5.46%) | and (3.76%) |
3 | Transformers are the most popular toy | line (34.38%) | in (18.20%) | of (11.71%) | brand (6.10%) | line (2.69%) |
4 | Transformers are the most popular toy line | in (46.29%) | of (15.09%) | , (4.94%) | on (4.40%) | ever (2.72%) |
5 | Transformers are the most popular toy line in | the (65.99%) | history (12.42%) | America (6.91%) | Japan (2.44%) | North (1.40%) |
6 | Transformers are the most popular toy line in the | world (69.27%) | United (4.55%) | history (4.29%) | US (4.23%) | U (2.30%) |
7 | Transformers are the most popular toy line in ... | , (39.73%) | . (30.64%) | and (9.87%) | with (2.32%) | today (1.74%) |
使用这种简单的方法,我们能够生成句子 “Transformers are the most popular toy line in the world” 。有趣的是,这表明GPT-2已经内化了一些关于变形金刚媒体特许经营权的知识:两家玩具公司(Hasbro和Takara Tomy)创建了变形金刚这一品牌并拥有其特许经营权。我们还可以在每个时间步看到其他词元选项及其概率,这体现了文本生成的迭代性质。其他任务(如序列分类)的单个前向传递足以生成预测,而文本生成则需要逐个解码输出词元。
可见实现贪婪搜索并不太难,接下来我们将使用Hugging Face Transformers库内置的generate()函数来探索更复杂的解码方法。为了重现我们的简单示例,我们先确保采样已关闭(除非你从checkpoint加载的特定模型配置另有规定,否则默认情况下已关闭),并通过max_new_tokens来指定新生成的词元数量:
# 使用分词器将输入文本转换为模型输入格式(张量),并将其移动到指定设备(GPU 或 CPU)
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
# 使用模型生成文本,指定生成的最大新 token 数量为 n_steps,且不进行采样(即选择最高概率的 token)
output = model.generate(input_ids, max_new_tokens=n_steps, do_sample=False)
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output[0]))
运行结果:
Transformers are the most popular toy line in the world,
现在让我们尝试一些更有趣的东西:我们能否重现本章一开头的OpenAI的独角兽故事?和之前一样,我们将使用词元分析器对提示进行编码,并指定一个更大的max_length值来生成更长的文本序列:
# 定义生成文本的最大长度
max_length = 128
# 输入文本
input_txt = """In a shocking finding, scientists discovered \
a herd of unicorns living in a remote, previously unexplored \
valley, in the Andes Mountains. Even more surprising to the \
researchers was the fact that the unicorns spoke perfect English.\n\n
"""
# 使用分词器将输入文本转换为模型输入格式(张量),并将其移动到指定设备(GPU 或 CPU)
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
# 使用模型生成文本,指定生成的最大长度为 max_length,不进行采样(即选择最高概率的 token)
output_greedy = model.generate(input_ids, max_length=max_length, do_sample=False)
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_greedy[0]))
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. The researchers, from the University of California, Davis, and the University of Colorado, Boulder, studied the valley's vegetation and found that the unicorns were living in a valley that was once covered by a forest. The valley was once a lush, green forest, but it was destroyed by a landslide in the 1980s. The researchers believe that the unicorns were living in the valley before the
前几句与本章一开头的OpenAI例子有很大不同,发现者是不同大学和不同的科学家!我们也可以看到贪婪搜索解码的一个主要缺点:它往往会产生重复的输出序列(最后两段是重复的),在新闻文章中这显然是不可取的。这是贪婪搜索算法的一个常见问题,它们可能无法给出最优解。在解码的上下文中,它们可能会错过整体概率更高的词序列,只因为高概率的单词恰好在低概率的单词之前出现。
幸运的是,还有更好的方法——束搜索解码,这也是一种很流行的方法。
虽然贪婪搜索解码很少用于需要多样性的文本生成任务,但对于生成像算术这样需要确定性和事实正确输出的短序列时 ,它可能很有用。对于这些任务,你可以通过用几个行分隔符来对GPT-2进行条件设置,例如"5+8=>13 \n 7+2=>9 \n 1+0=>"。
五、束搜索解码
不同于每次都选择概率最高的词元,束搜索会跟踪最有可能的下一个top-b个词元,其中b称为束的数量或部分假设。下一个束是通过考虑现有集合的所有可能的下一个词元扩展并选择最有可能的b个扩展而选择的。该过程会一直重复,直到达到最大长度或遇到EOS词元,然后根据它们的对数概率对b个束进行排名,选择最有可能的序列。束搜索的示例如图所示。
为什么我们使用对数概率而不是概率本身来评分序列?其中一个原因是,计算序列P(y,y,...,yt|x)的整体概率需要计算条件概率的乘积P(yt|y,x)而每个条件概率通常是在[0,1]范围内的小的数字,将它们相乘可能会导致整体概率很容易下溢。这意味着计算机无法精确表示计算结果。例如,假设我们有一个长度t=1024的词元序列,并慷慨地假设每个词元的概率为0.5。这个序列的整体概率将会是一个非常小的数字:
12<t
0.5 ** 1024
运行结果:
5.562684646268003e-309
使用对数概率进行计算是为了避免数字不稳定性问题,因为我们会遇到下溢的情况。我们可以通过计算相关项——对数概率来避免这个问题。如果我们对联合概率和条件概率取对数,然后利用对数的乘法规则,那么我们可以得到:
换句话说,我们之前提到的概率乘积变成了对数概率的总和,这样就不太可能遇到数值不稳定的情况。例如,对于前面提到的相同例子,计算对数概率如下:
# 导入 numpy 库,用于科学计算
import numpy as np
# 计算自然对数 np.log(0.5) 的值,并将其乘以 1024,然后求和
# 这里使用列表生成式创建一个包含 1024 个 np.log(0.5) 的列表,再使用 sum() 函数对其求和
result = sum([np.log(0.5)] * 1024)
# 打印结果
print(result)
运行结果:
-709.7827128933695
这是一个我们可以轻松处理的数字,这种方法也适用于更小的数字。由于我们只想比较相对概率,因此我们可以直接使用对数概率进行比较。
让我们计算并比较贪婪搜索和束搜索生成的文本的对数概率,以查看束搜索是否可以提高总体概率。由于Hugging Face Transformers库返回给定输入词元的下一个词元的非规范化logit,因此我们首先需要规范化logit,以创建序列中每个词元在整个词表的概率分布。然后我们需要选择仅存在于序列中的词元的概率。实现这些步骤的具体函数如下:
# 导入 PyTorch 的功能模块,包括用于计算 log softmax 的函数
import torch.nn.functional as F
# 定义一个函数,用于从 logits 中计算标签的对数概率
def log_probs_from_logits(logits, labels):
# 对 logits 应用 log softmax,计算每个类别的对数概率
logp = F.log_softmax(logits, dim=-1)
# 使用 torch.gather 从 log softmax 结果中提取对应标签的对数概率
# labels.unsqueeze(2) 将标签张量扩展为与 logp 相同的维度
# squeeze(-1) 用于去除多余的维度
logp_label = torch.gather(logp, 2, labels.unsqueeze(2)).squeeze(-1)
# 返回对应标签的对数概率
return logp_label
以上函数给出了单个词元的对数概率,如果要获得序列的总对数概率,我们只需将每个词元的对数概率相加。
# 定义一个函数,用于计算给定标签序列的对数概率
def sequence_logprob(model, labels, input_len=0):
# 禁用梯度计算(在推理时通常不需要计算梯度)
with torch.no_grad():
# 使用模型生成输出(logits)
output = model(labels)
# 从 logits 中计算标签的对数概率
log_probs = log_probs_from_logits(
output.logits[:, :-1, :], # 忽略最后一个 token 的 logits
labels[:, 1:] # 从第二个 token 开始对齐标签
)
# 计算序列从 input_len 开始的对数概率和
seq_log_prob = torch.sum(log_probs[:, input_len:])
# 返回对数概率和,并将其转换为 numpy 格式
return seq_log_prob.cpu().numpy()
请注意,我们忽略了输入序列的对数概率,因为它们不是由模型生成的。我们还可以看到,将logit与标注对齐非常重要。由于模型预测下一个词元,因此我们不会得到第一个标注的logit,而且我们不需要最后一个logit,因为我们没有与之对应的真实词元。
我们首先使用这些函数来计算对OpenAI提示使用贪婪解码器的序列对数概率:
# 计算生成序列的对数概率,传递模型、生成的序列和输入长度
logp = sequence_logprob(model, output_greedy, input_len=len(input_ids[0]))
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_greedy[0]))
# 打印生成序列的对数概率值
print(f"\nlog-prob: {logp:.2f}")
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. The researchers, from the University of California, Davis, and the University of Colorado, Boulder, studied the valley's vegetation and found that the unicorns were living in a valley that was once covered by a forest. The valley was once a lush, green forest, but it was destroyed by a landslide in the 1980s. The researchers believe that the unicorns were living in the valley before the log-prob: -102.97
现在我们来比较一下使用束搜索生成的序列。使用generate()函数并指定num_beams参数的数量即可激活束搜索。我们选择的束数越多,结果就可能越好。但是,生成过程变得更慢,因为我们要为每个束生成并行序列:
# 使用模型生成文本,指定生成的最大长度为 max_length
# 使用 beam search 的方式生成文本,beam 数量为 5,不进行采样(即选择最高概率的 token)
output_beam = model.generate(input_ids, max_length=max_length, num_beams=5, do_sample=False)
# 计算生成序列的对数概率,传递模型、生成的序列和输入长度
logp = sequence_logprob(model, output_beam, input_len=len(input_ids[0]))
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_beam[0]))
# 打印生成序列的对数概率值
print(f"\nlog-prob: {logp:.2f}")
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. The discovery of the unicorns was made by a team of scientists from the University of California, Davis, and the University of Colorado, Boulder. According to the researchers, the discovery of the unicorns was made by a team of scientists from the University of California, Davis, and the University of Colorado, Boulder. According to the researchers, the discovery of the unicorns was made by a team log-prob: -50.35
我们可以看到,使用束搜索相较于简单的贪婪解码,得到了更好的对数概率值(数值越高越好)。然而,我们可以看到束搜索一样存在重复文本的问题。解决这个问题的一种方式是通过no_repeat_ngram_size参数施加n-gram惩罚,该参数会跟踪已经出现的n-gram并将下一个词元的概率设置为零,从而避免产生先前出现过的n-gram:
# 使用模型生成文本,指定生成的最大长度为 max_length
# 使用 beam search 的方式生成文本,beam 数量为 5,不进行采样(即选择最高概率的 token)
# 添加 no_repeat_ngram_size 参数,防止重复的 n-gram(这里 n=2)
output_beam = model.generate(input_ids, max_length=max_length, num_beams=5, do_sample=False, no_repeat_ngram_size=2)
# 计算生成序列的对数概率,传递模型、生成的序列和输入长度
logp = sequence_logprob(model, output_beam, input_len=len(input_ids[0]))
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_beam[0]))
# 打印生成序列的对数概率值
print(f"\nlog-prob: {logp:.2f}")
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. The discovery was made by a team of researchers from the University of California, Santa Cruz, and the National Geographic Society. The team, led by Dr. John Marzluff, a professor of integrative biology at UCSC, discovered the unicorn herd by accident. "We were looking for a place where we could study the effects of climate change on animals and plants," said Marzluff log-prob: -93.33
现在结果不错了!我们成功避免了重复的文本,而且我们可以看到,尽管得分较低,但文本仍然是连贯的。带有n-gram惩罚的束搜索是一种在关注高概率词元(使用束搜索)的同时减少重复(使用n-gram惩罚)之间获得平衡的好方法,常用于需要确保事实正确性的应用,如文本摘要或机器翻译。当事实正确性不如生成输出的多样性重要时,例如在开放域的闲聊或故事生成中,另一种减少重复并提高多样性的选择是使用采样。现在我们探讨一些最常用的采样方法来完成我们对文本生成的探索。
六、采样方法
最简单的采样方法是在每个时间步从模型输出的概率分布中随机采样,采样的范围为整个词表:
其中|V|表示词表的基数。我们可以通过添加温度参数T来控制输出的多样性,该参数在进行softmax之前重新缩放logit。
如果你了解一些物理知识,你可能会发现它与玻尔兹曼分布(https://oreil.ly/ZsMmx)有惊人的相似之处。
通过调整T,我们可以控制概率分布的形状 。当T≪1时,分布会在原点附近峰值化,罕见的词元会被压制。另一方面,当T≫1时,分布变得平坦,每个词元变得同等可能。如下图展示了温度对词元概率的影响。
# 导入 matplotlib 库用于绘图
import matplotlib.pyplot as plt
# 导入 numpy 库用于科学计算
import numpy as np
# 定义 softmax 函数,带有温度参数 T,用于调整分布的平滑度
def softmax(logits, T=1):
# 计算经过温度调节的指数值
e_x = np.exp(logits / T)
# 归一化,使所有概率之和为 1
return e_x / e_x.sum()
# 生成 1000 个随机 logits 值,并对其取指数
logits = np.exp(np.random.random(1000))
# 对 logits 值进行排序,并按降序排列
sorted_logits = np.sort(logits)[::-1]
# 创建一个包含 1000 个元素的数组,表示索引
x = np.arange(1000)
# 遍历不同的温度值,计算相应的 token 概率并绘制图形
for T in [0.5, 1.0, 2.0]:
# 使用 softmax 函数计算经过温度调节后的概率分布
plt.step(x, softmax(sorted_logits, T), label=f"T={T}")
# 添加图例,位置为最佳位置
plt.legend(loc="best")
# 设置 x 轴标签
plt.xlabel("Sorted token probabilities")
# 设置 y 轴标签
plt.ylabel("Probability")
# 保存图像到文件
plt.savefig('images/token_probabilities.png')
# 显示图形
plt.show()
运行结果:
为了展示如何使用温度参数来影响生成的文本,让我们通过在generate()函数中设置温度参数为2来进行采样:
# 设置随机种子以确保结果的可重复性
torch.manual_seed(42)
# 使用模型生成文本,指定生成的最大长度为 max_length
# 启用采样(do_sample=True),并设置温度参数 temperature=2.0,未设置 top_k 筛选(top_k=0)
output_temp = model.generate(input_ids, max_length=max_length, do_sample=True, temperature=2.0, top_k=0)
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_temp[0]))
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. Feed Boost Year Hampe Eagle Rouse Symbol Steal Therefore inappropriate Sprite 69 151 Hill-James Golfne Castle j Runningespantry Spy legislative Trueverlife cone Hermes mark Central Bombsaver democracy Civil|RosSkill livesvedesc Init scan buggy metroDunults micro brightly Byrne unusually BBs Radiustool toddlers CAP contriv itself turtle four 32 Democraticfanson idea cheeserrors Dargon morphache Umb robot Palestrog carrotsinvest
我们可以清晰地看到,高温度导致大部分无意义的语句。通过强调稀有的词元,我们让模型创建了奇怪的语法和许多虚构的单词!我们看看如果降低温度会发生什么:
# 设置随机种子以确保结果的可重复性
torch.manual_seed(42)
# 使用模型生成文本,指定生成的最大长度为 max_length
# 启用采样(do_sample=True),并设置温度参数 temperature=0.5,未设置 top_k 筛选(top_k=0)
output_temp = model.generate(input_ids, max_length=max_length, do_sample=True, temperature=0.5, top_k=0)
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_temp[0]))
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. The researchers, from the University of Bristol and the University of California, Berkeley, found the unicorns while conducting a GPS survey of the area. They discovered the herd of unicorns in the Andes Mountains, in the Peruvian Andes. The area is known for its unique flora and fauna. The researchers believe that the unicorns are descendants of the original herds that were brought to this
这样生成的文本明显更加连贯,甚至包括了另一个被认为发现了这个结论的大学的引用!我们可以从温度中得到的主要教训是,它允许我们控制样本的质量,但在连贯性(低温度)和多样性(高温度)之间总是存在一个权衡,需要根据具体应用场景进行调整。
另一种调整连贯性和多样性之间权衡的方法是截断词汇的分布。这允许我们自由地使用温度来调整多样性,但在一个更有限的范围内排除在上下文中过于奇怪的词汇(即低概率词)。有两种主要方法:top-k和核(或top-p)采样。让我们看一下。
七、top-k和核采样
top-k和核(top-p)采样是使用温度的两种流行的替代或扩展方法。在这两种情况下,基本思想是限制每个时间步可以采样的可能词元数量。为了了解其工作原理,让我们先在图5-6中查看T=1时模型输出的累积概率分布。
让我们分解一下这些图,因为它们包含了很多信息。在图5-6的上图中,我们可以看到一个词元概率的直方图。它在10-8左右有一个峰值,其次是在10-4左右有一个较小的峰值,然后急剧下降,只有少量词元的概率在10-2到10-1之间。从这张图中可以看出,选择具有最高概率的词元的概率(在10-1处的独立条)是1/10。
在图5-6的下图中,我们按降序对词元进行了排序,并计算了前10 000个词元的累积总和(在GPT-2的词表中总共有50 257个词元)。曲线表示选择任何前面词元的概率。例如,从具有最高概率的1000个词元中选择任何一个的概率大约为96%。我们可以看到概率迅速上升到90%以上,但只有在几千个词元之后才饱和接近100%。该图表显示,有1/100的概率不会选择不在前2000个词元中的任何词元。
尽管这些数字一开始可能看起来很小,但它们变得重要,因为我们在生成文本时每个词元只采样一次。因此,即使只有百分之一或千分之一的概率,如果我们采样数百次,那么在某个时刻挑选一个不太可能的词元的机会是相当大的。在采样时选择这些词元会严重影响生成文本的质量,因此我们通常希望避免这些非常不太可能的词元。这就是top-k和top-p采样发挥作用的地方。
top-k采样的思想是通过仅从具有最高概率的k个词元中进行采样来避免低概率选择。这对分布的长尾部分施加了固定的截断,并确保我们只从可能的选择中进行采样。回到下图,top-k采样等价于定义一条竖线,然后从竖线左侧的词元中进行采样。
# 设置随机种子以确保结果的可重复性
torch.manual_seed(42)
# 定义输入文本
input_txt = """In a shocking finding, scientists discovered \
a herd of unicorns living in a remote, previously unexplored \
valley, in the Andes Mountains. Even more surprising to the \
researchers was the fact that the unicorns spoke perfect English.\n\n
"""
# 使用分词器将输入文本转换为模型输入格式(张量),并将其移动到指定设备(GPU 或 CPU)
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
# 导入 PyTorch 的功能模块
import torch.nn.functional as F
# 禁用梯度计算(在推理时通常不需要计算梯度)
with torch.no_grad():
# 使用模型生成输出(logits)
output = model(input_ids=input_ids)
# 选择最后一个 token 的 logits
next_token_logits = output.logits[:, -1, :]
# 对 logits 应用 softmax,计算概率分布,并将结果从 GPU 移动到 CPU,再转换为 numpy 格式
probs = F.softmax(next_token_logits, dim=-1).detach().cpu().numpy()
#id distribution
#alt Probability distribution of next token prediction.
#caption Probability distribution of next token prediction (left) and cumulative distribution of descending token probabilities
# 导入 matplotlib 库用于绘图
import matplotlib.pyplot as plt
# 导入 numpy 库用于科学计算
import numpy as np
# 创建一个包含两个子图的图形,尺寸为 (10, 3.5)
fig, axes = plt.subplots(1, 2, figsize=(10, 3.5))
# 绘制第一个子图:下一个 token 预测的概率分布
# 使用对数刻度绘制直方图,bins 参数用于设置直方图的刻度范围
axes[0].hist(probs[0], bins=np.logspace(-10, -1, 100), color="C0", edgecolor="C0")
axes[0].set_xscale("log") # 设置 x 轴为对数刻度
axes[0].set_yscale("log") # 设置 y 轴为对数刻度
axes[0].set_title("Probability distribution") # 设置图形标题
axes[0].set_xlabel("Probability") # 设置 x 轴标签
axes[0].set_ylabel("Count") # 设置 y 轴标签
# 绘制第二个子图:降序 token 概率的累计分布
# 使用 numpy 的 cumsum 和 sort 函数计算累计概率
axes[1].plot(np.cumsum(np.sort(probs[0])[::-1]), color="black")
axes[1].set_xlim([0, 10000]) # 设置 x 轴范围
axes[1].set_ylim([0.75, 1.01]) # 设置 y 轴范围
axes[1].set_title("Cumulative probability") # 设置图形标题
axes[1].set_ylabel("Probability") # 设置 y 轴标签
axes[1].set_xlabel("Token (descending probability)") # 设置 x 轴标签
axes[1].minorticks_on() # 启用次刻度
# 添加 top-k 阈值和 nucleus 阈值的线条和标签
top_k_label = 'top-k threshold (k=2000)'
top_p_label = 'nucleus threshold (p=0.95)'
axes[1].vlines(x=2000, ymin=0, ymax=2, color='C0', label=top_k_label) # 添加垂直线
axes[1].hlines(y=0.95, xmin=0, xmax=10000, color='C1', label=top_p_label, linestyle='--') # 添加水平线
axes[1].legend(loc='lower right') # 添加图例,位置为右下角
# 保存图表为文件,并设置自适应大小
plt.savefig('images/Probability_distribution.png', bbox_inches='tight')
# 调整布局以避免重叠
plt.tight_layout()
运行结果:
同样,generate()函数提供了一个使用top k参数轻松实现这一目的的方法:
# 设置随机种子以确保结果的可重复性
torch.manual_seed(42)
# 使用模型生成文本,指定生成的最大长度为 max_length
# 启用采样(do_sample=True),并设置 top_k 参数为 50(从最高概率的前 50 个 token 中进行采样)
output_topk = model.generate(input_ids, max_length=max_length, do_sample=True, top_k=50)
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_topk[0]))
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. To get to the Valley of the Unicorns—the first discovery of its kind in the world—the team traveled to Argentina to meet with local farmers, who were interested in the unusual finding. "We were told it was the highest point in the world, over 6,000 meters," said lead researcher Professor Robert Berlowitz in the statement. "At the summit, they thought it was
这可能是目前为止我们生成的最像人类语言的文本。但是我们如何选择k呢?k的值是手动选择的,并且在序列中的每个选择中都是相同的,与实际的输出分布无关。我们可以通过查看一些文本质量指标来找到一个好的k值,我们将在后面探讨这些指标,但是这个固定的截止值可能不能非常令人满意。
一种替代方法是使用动态截断。在核(top-p)采样中,我们不选择固定的截断值,而是设定一个当达到一定概率质量时的截断条件。假设我们将该值设定为95%。然后,我们按概率降序排序所有词元,并从列表顶部逐个添加词元,直到所选词元的概率之和达到95%。回到上图,p的值定义了概率累积总和图上的水平线,我们仅从线下词元中进行采样。根据输出分布的不同,可能只有一个(非常可能)词元或一百个(等可能)词元。讲到这里,generate()函数还提供了一个激活top-p采样的参数。让我们试一下:
# 设置随机种子以确保结果的可重复性
torch.manual_seed(42)
# 使用模型生成文本,指定生成的最大长度为 max_length
# 启用采样(do_sample=True),并设置 top_p 参数为 0.90(从累计概率为 0.90 的 token 中进行采样,即 nucleus sampling)
output_topp = model.generate(input_ids, max_length=max_length, do_sample=True, top_p=0.90)
# 将生成的 token 序列解码为文本并打印出来
print(tokenizer.decode(output_topp[0]))
运行结果:
In a shocking finding, scientists discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. To get to the Valley of the Unicorns, the researchers took a helicopter with a special camera to an area that was previously unexplored, so that they could get close to the unicorns. They then shot videos of the animals and discovered that the animals could not see objects moving far away, only at close distances. In addition, the unicorns could tell the direction of light, and were aware of
由上可见,top-p采样方法也产生了一个连贯的故事,这次的新变化是这些独角兽从澳大利亚迁徙到南美洲。你甚至可以结合这两种采样方法,以兼顾两者的优点。设置top_k=50和top_p=0.9,表示从最多50个词元中选择概率质量为90%的词元。
当我们使用采样时,也可以应用束搜索。不同于贪婪地选择下一个候选词汇批量,我们可以对它们进行采样,然后以相同的方式构建出束。
八、哪种解码方法最好
不幸的是,目前没有一种通用的最佳解码方法。最好的方法将取决于你生成文本的任务性质。如果你想让你的模型执行像算术或提供特定问题答案这样的精确任务,那么你应该降低温度或使用确定性方法,如将贪婪搜索与束搜索相结合,以确保获得最可能的答案。如果你想让模型生成更长的文本,甚至有点创造性,那么你应该切换采样方法,增加温度或使用top-k和核采样的混合方法。