使用大型语言模型生成文本
大型语言模型 (LLM) 是文本生成的背后关键组件。简而言之,它们由经过大型预训练的 Transformer 模型组成,这些模型被训练以根据一些输入文本预测下一个词(或更确切地说,是 token)。由于它们一次预测一个 token,因此您需要做一些更复杂的事情来生成新的句子,而不仅仅是调用模型——您需要进行自回归生成。
自回归生成是在推理时间迭代调用模型,并使用其生成的输出作为输入,前提是有一些初始输入。在 🤗 Transformers 中,这由 generate() 方法处理,该方法可用于所有具有生成功能的模型。
本教程将向您展示如何
- 使用 LLM 生成文本
- 避免常见陷阱
- 帮助您充分利用 LLM 的后续步骤
在开始之前,请确保已安装所有必要的库
pip install transformers bitsandbytes>=0.39.0 -q
生成文本
经过 因果语言建模 训练的语言模型将文本 token 序列作为输入,并返回下一个 token 的概率分布。
使用 LLM 进行自回归生成的一个关键方面是如何从该概率分布中选择下一个 token。只要您最终得到下一个迭代的 token,此步骤中可以做任何事情。这意味着它可以像从概率分布中选择最可能的 token 那样简单,也可以像在从最终分布中采样之前应用十几个转换那样复杂。
上面描述的过程会重复进行,直到达到某个停止条件。理想情况下,停止条件由模型决定,模型应该学习何时输出序列结束 (EOS
) token。如果情况并非如此,则生成将在达到某个预定义的最大长度时停止。
正确设置 token 选择步骤和停止条件对于使您的模型按照预期在您的任务中表现至关重要。这就是为什么我们为每个模型都配备了一个 GenerationConfig 文件,该文件包含良好的默认生成参数化,并与您的模型一起加载。
让我们来谈谈代码吧!
如果您对基本的 LLM 使用感兴趣,我们高级的 Pipeline
接口是一个很好的起点。但是,LLM 通常需要高级功能,例如量化和对 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
确保模型被移动到您的 GPU(s)load_in_4bit
应用 4 位动态量化 以大幅降低资源需求
还有其他方法可以初始化模型,但这是一个从 LLM 开始的好基线。
接下来,您需要使用 分词器 预处理您的文本输入。
>>> 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
变量保存分词后的文本输入以及注意力掩码。虽然 generate() 会尽力在未传递注意力掩码时推断它,但我们建议您在可能的情况下传递它,以获得最佳效果。
分词输入后,您可以调用 generate() 方法来返回生成的 token。然后应该将生成的 token 转换为文本,然后再打印。
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A list of colors: red, blue, green, yellow, orange, purple, pink,'
最后,您不需要一次只做一条序列!您可以对输入进行批处理,这将在较小的延迟和内存成本下极大地提高吞吐量。您只需确保正确地填充您的输入(稍后会详细介绍)。
>>> tokenizer.pad_token = tokenizer.eos_token # Most LLMs don't have a pad token by default
>>> 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)
['A list of colors: red, blue, green, yellow, orange, purple, pink,',
'Portugal is a country in southwestern Europe, on the Iber']
就这样!只需几行代码,您就可以利用 LLM 的强大功能。
常见陷阱
存在许多 生成策略,有时默认值可能不适合您的用例。如果您的输出与您的预期不符,我们创建了一个最常见陷阱及其如何避免的列表。
>>> from transformers import AutoModelForCausalLM, AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
>>> tokenizer.pad_token = tokenizer.eos_token # Most LLMs don't have a pad token by default
>>> model = AutoModelForCausalLM.from_pretrained(
... "mistralai/Mistral-7B-v0.1", device_map="auto", load_in_4bit=True
... )
生成的输出太短/太长
如果在 GenerationConfig 文件中未指定,generate
默认情况下最多返回 20 个 token。我们强烈建议您在 generate
调用中手动设置 max_new_tokens
以控制它可以返回的新的 token 的最大数量。请记住,LLM(更确切地说,是 解码器专用模型)还会将输入提示作为输出的一部分返回。
>>> model_inputs = tokenizer(["A sequence of numbers: 1, 2"], return_tensors="pt").to("cuda")
>>> # By default, the output will contain up to 20 tokens
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A sequence of numbers: 1, 2, 3, 4, 5'
>>> # Setting `max_new_tokens` allows you to control the maximum length
>>> generated_ids = model.generate(**model_inputs, max_new_tokens=50)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A sequence of numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,'
生成模式错误
默认情况下,除非在 GenerationConfig 文件中指定,generate
会在每次迭代中选择最可能的 token(贪婪解码)。根据您的任务,这可能是不希望的;创意任务(如聊天机器人或撰写文章)受益于采样。另一方面,以输入为基础的任务(如音频转录或翻译)受益于贪婪解码。使用 do_sample=True
启用采样,您可以在此 博客文章 中了解更多有关此主题的信息。
>>> # Set seed or reproducibility -- you don't need this unless you want full reproducibility
>>> from transformers import set_seed
>>> set_seed(42)
>>> model_inputs = tokenizer(["I am a cat."], return_tensors="pt").to("cuda")
>>> # LLM + greedy decoding = repetitive, boring output
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'I am a cat. I am a cat. I am a cat. I am a cat'
>>> # With sampling, the output becomes more creative!
>>> generated_ids = model.generate(**model_inputs, do_sample=True)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'I am a cat. Specifically, I am an indoor-only cat. I'
填充边错误
LLM 是 解码器专用 架构,这意味着它们会继续迭代您的输入提示。如果您的输入长度不同,则需要对其进行填充。由于 LLM 没有经过训练以从填充 token 继续,因此您的输入需要左填充。请确保您也不要忘记将注意力掩码传递给 generate!
>>> # The tokenizer initialized above has right-padding active by default: the 1st sequence,
>>> # which is shorter, has padding on the right side. Generation fails to capture the logic.
>>> 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]
'1, 2, 33333333333'
>>> # With left-padding, it works as expected!
>>> tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1", padding_side="left")
>>> tokenizer.pad_token = tokenizer.eos_token # Most LLMs don't have a pad token by default
>>> 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]
'1, 2, 3, 4, 5, 6,'
提示错误
某些模型和任务需要特定的输入提示格式才能正常工作。如果没有应用此格式,您将获得静默的性能下降:模型有点工作,但效果不如您按照预期提示那样好。有关提示的更多信息,包括哪些模型和任务需要谨慎使用,请参阅此 指南。让我们看看一个使用 聊天模板 的聊天 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])
"I'm not a thug, but i can tell you that a human cannot eat"
>>> # Oh no, it did not follow our instruction to reply as a thug! Let's see what happens when we write
>>> # a better prompt and use the right template for this model (through `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])
'None, you thug. How bout you try to focus on more useful questions?'
>>> # As we can see, it followed a proper thug style 😎
更多资源
虽然自回归生成过程相对简单,但充分利用您的 LLM 可能是一项具有挑战性的工作,因为有很多可移动的部分。为了帮助您更深入地了解 LLM 的使用和理解,请参考以下步骤:
高级生成使用
- 关于如何控制不同的生成方法,如何设置生成配置文件以及如何流式传输输出的指南;
- 加速文本生成;
- 用于聊天 LLMs 的提示模板;
- 提示设计指南;
- 关于GenerationConfig,generate()和generate 相关类的 API 参考。大多数类(包括 logits 处理器)都有使用示例!
LLM 排行榜
- 开放 LLM 排行榜,专注于开源模型的质量;
- 开放 LLM-Perf 排行榜,专注于 LLM 的吞吐量。
延迟、吞吐量和内存使用
- 关于如何优化 LLM 的速度和内存的指南;
- 关于量化(如 bitsandbytes 和 autogptq)的指南,它向您展示了如何大幅减少内存需求。
相关库
optimum
,这是 🤗 Transformers 的扩展,它针对特定硬件设备进行了优化。outlines
,一个允许您约束文本生成的库(例如,生成 JSON 文件);SynCode
,用于上下文无关语法引导生成的库。(例如 JSON、SQL、Python)text-generation-inference
,一个用于 LLMs 的生产就绪型服务器;text-generation-webui
,一个用于文本生成的 UI;