使用 LLMs 生成¶
大语言模型(LLMs)是文本生成背后的关键组成部分。简单来说,它们是经过大规模预训练的 transformer 模型,用于根据给定的输入文本预测下一个词(下一个token)。由于它们一次只预测一个token,因此除了调用模型之外,还需要执行自回归生成来生成新的句子。
自回归生成是在给定一些初始输入,通过迭代调用模型及其自身的生成输出来生成文本的推理过程。在🤗 Transformers中,这由 generate() 方法处理,所有具有生成能力的模型都可以使用该方法。
本教程将向你展示如何:
- 使用 LLM 生成文本
- 避免常见的陷阱
- 帮助你充分利用LLM进行下一步指导
在开始之前,请确保已安装所有必要的库:
pip install transformers bitsandbytes>=0.39.0 -q
生成文本¶
一个用于因果语言建模训练的语言模型,将文本的 tokens 序列作为输入,并返回下一个 token 的概率分布。
LLM的前向传递:
使用LLM进行自回归生成的一个关键方面是如何从这个概率分布中选择下一个 token。这个步骤是取决于你的需求的,可以是简单地从概率分布中选择最可能的 token,也可以是复杂地对结果分布应用多种变换后再作选择,只要最终能够得到下一个迭代的 token。
自回归生成迭代地从概率分布中选择下一个token以生成文本:
上述过程是可以迭代重复的,直到达到某个停止条件。在理想情况下,停止条件是由模型决定的,该模型需要学会在何时输出一个结束序列(EOS)标记。如果不是这种情况,生成将在达到某个预定义的最大长度时停止。
正确设置下一个 token 的选择步骤和停止条件对于让你的模型按照预期的方式执行任务至关重要。这就是为什么每个模型都要有一个generation.GenerationConfig文件,它默认配置了一个效果不错的生成参数,会和你的模型一起加载。
如果你对基本的LLM使用感兴趣,我们高级的Pipeline接口是一个很好的起点。然而,LLMs通常需要像quantization和对token选择步骤的精细控制等高级功能,所以最好通过generate()来完成。使用LLM进行自回归生成也是资源密集型的操作,需要在GPU上执行才能获得足够的吞吐量。
首先,你需要加载模型。
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1", device_map="auto", load_in_4bit=True
)
在from_pretrained调用中的有两个参数:
device_map="auto"能够确保模型被移动到你的GPU(s)上。load_in_4bit表示应用4位动态量化来极大地减少资源需求。
接下来,你需要使用一个tokenizer来预处理你的文本输入。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1", padding_side="left")
model_inputs = tokenizer(["A list of colors: red, blue"], return_tensors="pt").to("cuda")
model_inputs变量保存着分词后的文本输入以及注意力掩码。注意力掩码能够确保模型只关注有效的输入 tokens,而忽略填充 tokens,从而保证生成的准确性和质量。
尽管generate()在未传递注意力掩码时会尽其所能推断出注意力掩码,但建议尽可能地传递它以获得最佳结果。
在对输入进行分词后,可以调用generate()方法来返回生成的 tokens。生成的 tokens 需要在打印之前转换为我们能够理解的文本。
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
在最后,你不要一次处理只一个序列!你可以批量输入,这会在小延迟和低内存成本下显著提高吞吐量,你只需要确保能够正确地填充你的输入(详见下文)。
tokenizer.pad_token = tokenizer.eos_token # 大多数LLM默认情况下没有设置填充标记
model_inputs = tokenizer(
["A list of colors: red, blue", "Portugal is"], return_tensors="pt", padding=True
).to("cuda")
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
tokenizer.pad_token = tokenizer.eos_token # 大多数LLM默认情况下没有填充标记
model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1", device_map="auto", load_in_4bit=True
)
生成的输出太短/太长¶
如果在GenerationConfig文件中没有指定返回 tokens 的最大数量,generate()默认只返回20个 tokens。
建议在你的generate调用中手动设置max_new_tokens以控制它可以返回的新tokens的最大数量。请注意,对于仅解码器架构的LLMs(https://huggingface.co/learn/nlp-course/chapter1/6?fw=pt)会将输入提示作为输出的一部分返回。
model_inputs = tokenizer(["A sequence of numbers: 1, 2"], return_tensors="pt").to("cuda")
# 默认情况下,输出将最多包含20个标记。
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# 设置`max_new_tokens`来控制输出的新tokens的最大长度
generated_ids = model.generate(**model_inputs, max_new_tokens=50)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
错误的生成模式(控制模型选择 Token 的步骤)¶
默认情况下,除非在[GenerationConfig](https://huggingface.co/docs/transformers/main/zh/main_classes/text_generation#transformers.GenerationConfig)文件中指定,否则generate()会在每个迭代中选择最可能的 token(贪婪解码)。
例如,像聊天机器人或写作文章这样的创造性任务,贪婪解码并不是最理想的生成模式;像音频转录或翻译这样的基于输入的任务,贪婪解码则是相对理想的生成模式。你可以在这篇博客文章中了解更多关于这个话题的信息。
在深度学习模型中,尤其是在自然语言处理(NLP)任务中,do_sample 参数通常用于控制生成文本时的采样策略。具体来说,do_sample=True 的作用如下:
- 启用随机采样:
当
do_sample=True时,模型在生成文本时会采用随机采样策略。这意味着模型会根据输出的概率分布随机选择下一个词或标记,而不是始终选择概率最高的词。 这种方法可以增加生成文本的多样性和创造性,使其看起来更自然,减少重复和单调性。 - 对比贪婪解码:
当
do_sample=False时(或默认情况下),模型通常会使用贪婪解码策略,即始终选择概率最高的词作为下一个词。 贪婪解码虽然简单且速度快,但可能导致生成的文本过于保守和重复。 - 温度控制:
在随机采样中,常常会结合一个称为“温度”(
temperature)的参数来调整概率分布的平滑程度。较高的温度会使概率分布更平缓,增加随机性;较低的温度会使分布更尖锐,减少随机性。 当do_sample=True 时,可以通过设置温度参数来进一步控制生成的多样性。
# set_seed 方法在 transformers 库中用于设置随机数生成器的种子。其作用是确保实验的可重复性(reproducibility),即在相同的条件下运行代码时,可以得到相同的结果。
from transformers import set_seed
set_seed(42)
model_inputs = tokenizer(["I am a cat."], return_tensors="pt").to("cuda")
# LLM + 贪婪解码 = 重复、乏味的输出
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# 通过设置采样`do_sample=True`,输出变得更有创意!
generated_ids = model.generate(**model_inputs, do_sample=True)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
错误的填充位置¶
对于仅解码器架构的LLMs,它们会持续迭代你的输入提示。如果你的输入长度不相同,则需要对它们进行填充。
由于某些 LLMs(如GPT系列)在预训练过程中可能没有使用pad tokens,或者没有对 pad tokens 进行特别的处理。如果模型没有经过pad tokens的训练,直接使用右填充可能会导致模型在处理输入时出现错误,因为模型可能会将填充标记误认为是有效信息。
因此在实际应用中,输入序列应采用左填充方式。在生成输出时,必须传递注意力掩码,以确保模型正确地忽略填充标记,只关注有效的输入信息。
# 上面初始化的分词器在默认情况下启用了右填充:
# 第一个序列较短,在序列右侧进行填充,模型将填充标记误认为是有效信息,导致输出的结果不佳。
model_inputs = tokenizer(
["1, 2, 3", "A, B, C, D, E"], padding=True, return_tensors="pt"
).to("cuda")
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# 使用左填充,模型生成了预期的结果!
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1", padding_side="left")
tokenizer.pad_token = tokenizer.eos_token # 大多数LLM默认情况下没有填充标记
model_inputs = tokenizer(
["1, 2, 3", "A, B, C, D, E"], padding=True, return_tensors="pt"
).to("cuda")
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
错误的提示¶
某些模型和任务需要输入特定的提示格式才能正常工作。当未使用标准的输入格式时,模型虽能正常工作,但是性能将会下降,输出效果也不如预期。
有关提示的更多信息,包括哪些模型和任务需要注意输入的提示格式,可在指南中找到。让我们看一个使用了聊天模板的 LLM 示例:
tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-alpha")
model = AutoModelForCausalLM.from_pretrained(
"HuggingFaceH4/zephyr-7b-alpha", device_map="auto", load_in_4bit=True
)
set_seed(0)
prompt = """How many helicopters can a human eat in one sitting? Reply as a thug."""
model_inputs = tokenizer([prompt], return_tensors="pt").to("cuda")
input_length = model_inputs.input_ids.shape[1]
generated_ids = model.generate(**model_inputs, max_new_tokens=20)
print(tokenizer.batch_decode(generated_ids[:, input_length:], skip_special_tokens=True)[0])
# 哦,它没有按照我们的指示以暴徒的风格进行回复!
# 让我们看看当我们写一个更好的提示并为这个模型使用正确的模板时会发生什么(通过`tokenizer.apply_chat_template`)
set_seed(0)
messages = [
{
"role": "system",
"content": "You are a friendly chatbot who always responds in the style of a thug",
},
{"role": "user", "content": "How many helicopters can a human eat in one sitting?"},
]
model_inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to("cuda")
input_length = model_inputs.shape[1]
generated_ids = model.generate(model_inputs, do_sample=True, max_new_tokens=20)
print(tokenizer.batch_decode(generated_ids[:, input_length:], skip_special_tokens=True)[0])
# 正如我们所看到的,它的回复遵循了适当的暴徒风格 😎
更多资源¶
虽然自回归生成过程相对简单,但要充分利用 LLM 的能力可能是一个具有挑战性的任务,因为很多组件之间都有着复杂且密切的关联。以下的资源可以帮助你深入了解和使用LLM:
高级生成用法¶
- 介绍如何控制不同的生成方法、如何设置生成配置文件以及如何进行输出流式传输。
- 介绍聊天LLMs的提示模板。
- 介绍如何充分利用提示设计。
- API参考文档,包括 GenerationConfig 、generate()和与生成相关的类。
LLM排行榜¶
- Open LLM Leaderboard:侧重于比较开源模型的质量。
- Open LLM-Perf Leaderboard:侧重于比较 LLM 的吞吐量。
延迟、吞吐量和内存利用率¶
- 如何优化LLMs以提高速度和内存利用。
- 关于quantization,如 bitsandbytes 和 autogptq 的指南,教你如何大幅降低内存需求。
相关库¶
- text-generation-inference:一个面向生产的LLM服务器。
- optimum:一个🤗 Transformers的扩展,优化特定硬件设备的性能。