这一章是关于提高LLMs在特定场景中可靠性和性能的技术和最佳实践,比如复杂的推理和问题解决任务。调整模型以适应特定任务或确保模型输出符合预期的过程被称为条件化。在本章中,我们将讨论微调和提示作为条件化方法。
微调涉及对预训练的基础模型进行在与所需应用相关的特定任务或数据集上的训练。这一过程使得模型能够适应,变得更加准确并且在预期用例中具有上下文相关性。
另一方面,在推断时通过提供额外的输入或上下文,LLMs可以生成符合特定任务或风格的文本。提示工程在释放LLM推理能力方面非常重要,提示技术构成了研究人员和从业者在与LLMs一起工作时的宝贵工具包。我们将讨论并实施高级提示工程策略,如少样本学习、思维树和自一致性。
在整个章节中,我们将使用微调和提示来处理LLMs。你可以在本书的GitHub存储库中找到相应的代码,网址为github.com/benman1/gen...
本章的主要部分包括:
- LLMs的条件化
- 微调
- 提示工程
让我们从讨论条件化开始,探讨其重要性以及如何实现它。
条件化LLMs
在多样化的数据上对LLM进行预训练,以学习语言模式,得到一个具有广泛理解各种主题的基础模型。虽然像GPT-4这样的基础模型能够在各种主题上生成令人印象深刻的文本,但通过对其进行条件化,可以提升其在任务相关性、特定性和连贯性方面的能力,并引导模型的行为与道德和适当性的考虑保持一致。在本章中,我们将专注于微调和提示技术这两种条件化方法。
条件化指的是一系列用于引导模型生成输出的方法。这不仅包括提示的制作,还包括更系统性的技术,比如在特定数据集上进行微调,以使模型的响应持续适应特定的主题或风格。
条件化技术使LLMs能够理解并执行复杂的指令,提供与我们期望密切匹配的内容。这涵盖了从即兴互动到系统培训的各种情境,这些培训可以引导模型的行为以在专业领域(如法律咨询或技术文档)中实现可靠的性能。此外,条件化的一部分包括实施防范措施,以防止生成恶意或有害内容,例如引入过滤器或训练模型避免产生某些类型的问题输出,从而更好地使其符合期望的伦理标准。
对齐(Alignment)指的是训练和修改LLMs的过程和目标,以使它们的一般行为、决策过程和输出符合更广泛的人类价值观、伦理原则和安全考虑。
这两个术语并不是同义词;虽然条件化可以包括微调,侧重于通过不同层次互动的各种技术影响模型,但对齐关注的是将模型的行为根本和全面地校准到人类伦理和安全标准。
条件化可以在模型生命周期的不同阶段应用。一种策略涉及在代表预期用例的数据上对模型进行微调,以帮助模型专注于该领域。这种方法取决于这些数据的可用性以及将其整合到训练过程中的能力。另一种方法涉及在推断时动态地对模型进行条件化,其中输入提示通过附加上下文进行定制,以塑造期望的输出。这种方法提供了灵活性,但在实时环境中可能增加模型操作的复杂性。
在接下来的部分,我将总结关于条件化的关键方法,如微调和提示工程,讨论其基本原理,并审视它们的相对优缺点。
条件化的方法
随着像GPT-3这样的大型预训练语言模型的出现,人们对调整这些模型以适应下游任务的技术表现出越来越浓厚的兴趣。随着LLMs的不断发展,它们将在更广泛的应用领域中变得更加有效和有用,我们可以期待在微调和提示技术方面的未来进展,以更深入地参与涉及推理和工具使用的复杂任务。
已经提出了几种用于条件化的方法。以下是对不同技术进行总结的表格:
将这些技术结合起来使开发人员能够更好地控制生成式人工智能系统的行为和输出。最终目标是确保在从训练到部署的所有阶段都融入人类价值观,以创建负责任且与期望结果一致的人工智能系统。
在本章中,我们强调微调和提示,因为它们以其在LLMs条件化中的有效性和普及性而脱颖而出。微调涉及通过在专门任务上进行额外训练来调整预训练模型的所有参数。这种方法旨在增强模型在特定目标上的性能,并且已知能够产生稳健的结果。然而,微调可能会消耗大量资源,存在性能与计算效率之间的权衡。为了解决这些局限性,我们探讨了诸如适配器和低秩适应(LoRA)之类的策略,引入了稀疏性的元素或实施参数的部分冻结以减轻负担。
另一方面,基于提示的技术提供了一种在推断时动态调整LLMs的方法。通过精心设计输入提示以及随后的优化和评估,这些方法可以引导LLMs的行为朝着期望的方向发展,而无需进行繁重的重新训练。提示可以经过精心设计,以引发特定行为或概括特定知识领域,为模型条件化提供了一种多才多艺且资源智能的方法。
此外,我们深入探讨了在微调过程中强化学习与人类反馈(RLHF)的变革性作用,其中人类反馈充当模型学习轨迹的关键指南。RLHF已展示出在改进GPT-3等语言模型的能力方面具有潜力,使微调成为更具影响力的技术。通过整合RLHF,我们利用人类评估者对模型行为的微妙理解,进一步完善模型输出,确保其不仅相关而准确,而且与用户意图和期望一致。
所有这些不同的条件化技术有助于开发既高性能又符合各种应用期望的LLMs。让我们首先讨论一下InstructGPT为何具有如此变革性影响,而该模型是通过RLHF进行训练的。
强化学习与人类反馈
在2022年3月的论文中,来自OpenAI的欧阳等人展示了使用RLHF(强化学习与人类反馈)结合近端策略优化(PPO)来使LLMs(例如GPT-3)与人类偏好一致。
RLHF是一种在线方法,使用人类偏好对LM进行微调。它包括三个主要步骤:
- 监督预训练:首先通过标准的监督学习在人类演示上对LM进行训练。
- 奖励模型训练:通过对LM输出的人类评分进行奖励模型训练,以估计奖励。
- 强化学习微调:通过强化学习对LM进行微调,以最大化来自奖励模型的预期奖励,使用像PPO这样的算法。
主要变化RLHF允许将微妙的人类判断纳入语言模型的训练中,通过学得的奖励模型。因此,人类反馈可以引导和提高语言模型的能力,超越标准的监督微调。这个新模型可以用于按照自然语言给出的指令,并以比GPT-3更准确和相关的方式回答问题。尽管参数数量少了100倍,InstructGPT在用户偏好、真实性和减少伤害方面均优于GPT-3。
从2022年3月开始,OpenAI开始发布GPT-3.5系列模型,这是GPT-3的升级版本,其中包括使用RLHF进行微调。
InstructGPT通过引入强化学习与人类反馈方法打破了传统微调方法的界限,为改进语言模型打开了新的途径。尽管RL训练可能不稳定且计算成本高昂,但其成功激发了进一步研究,以改进RLHF技术,减少对齐的数据需求,并为各种应用开发更强大和可访问的模型。
低秩适应
随着LLMs变得越来越庞大,用普通消费者硬件对它们进行训练变得困难,并且为每个特定任务部署它们会变得昂贵。有一些方法可以降低计算、内存和存储成本,同时在低数据和超领域场景中提高性能。
参数高效微调(PEFT)方法使每个任务可以使用小的检查点,从而使模型更具可移植性。这一小组经过训练的权重可以添加到LLM的顶部,允许同一模型用于多个任务而无需替换整个模型。
低秩适应(LoRA)是PEFT的一种类型,其中预训练模型的权重被冻结。它在Transformer架构的每一层引入可训练的秩分解矩阵,以减少可训练参数的数量。与微调相比,LoRA在拥有更少可训练参数和更高训练吞吐量的情况下实现了相当的模型质量。
QLORA方法是LoRA的一种扩展,通过将梯度反向传播到冻结的4位量化模型中,使大型模型能够进行高效微调,从而形成可学习的低秩适配器。这使得你可以在单个GPU上对一个65B参数的模型进行微调。QLORA模型在Vicuna上实现了ChatGPT性能的99%,利用了新的数据类型和优化器等创新。QLORA将微调65B参数模型的内存要求从超过780 GB降低到小于48 GB,而不影响运行时或预测性能。
量化是指减少神经网络(如LLMs)中权重和激活的数值精度的技术。量化的主要目的是减小大型模型的内存占用和计算需求。 关于LLMs的量化的一些关键点包括:
- 它涉及使用比标准的单精度浮点数(FP32)更少的比特来表示权重和激活。例如,权重可以量化为8位整数。
- 这使得您可以将模型大小缩小最多4倍,并提高在专用硬件上的吞吐量。
- 量化通常对模型的准确性影响较小,特别是在重新训练时。
- 常见的量化方法包括标量、向量和乘积量化,它们可以单独或分组地对权重进行量化。
- 通过估计激活的分布并适当地进行分箱,还可以对激活进行量化。
- 量化感知训练在训练过程中调整权重,以最小化量化损失。
- 像BERT和GPT-3这样的LLMs已经被证明通过微调可以很好地适用于4-8位的量化。
在下一部分中,我们将讨论在推理时对LLMs进行条件化的方法,其中包括提示工程。
推理时条件化
一个常用的方法是在推理时进行条件化(输出生成阶段),在这个阶段,特定的输入或条件会动态地被提供,以引导输出生成过程。在某些情境下,LLM的微调可能并不总是可行或有益:
- 有限的微调服务:当模型只能通过缺乏或具有受限微调能力的API访问时。
- 数据不足:在某些情况下,可能缺乏用于微调的数据,无论是用于特定下游任务还是相关应用领域。
- 动态数据:在应用中存在经常变化的数据,例如与新闻相关的平台,经常进行微调变得具有挑战性,可能导致潜在的缺点。
- 上下文敏感应用:对于动态和特定上下文的应用,例如个性化聊天机器人,无法基于个体用户数据进行微调。
对于推理时的条件化,通常我们在文本生成过程的开始提供一个文本提示或指示。这个提示可以是几句话,甚至是一个单词,充当所需输出的明确指示。
一些推理时条件化的常见技术包括:
- 提示调整:为预期行为提供自然语言指导。对提示设计敏感。
- 前缀调整:在LLM层之前添加可训练的向量。
- 限制标记:强制包含/排除某些词汇。
- 元数据:提供高级信息,如流派、目标受众等。
提示可以帮助生成符合特定主题、风格甚至模仿特定作者写作风格的文本。这些技术在推理时提供上下文信息,例如用于上下文学习或检索增强。
提示调整的一个例子是前缀调整,其中类似"编写一个关于...的适合儿童的故事"的指示被添加到提示之前。例如,在聊天机器人应用中,通过使用用户消息对模型进行条件化,有助于生成个性化和与当前对话相关的响应。
其他例子包括在提示之前添加相关文档以协助LLM完成写作任务(例如新闻报道、维基百科页面和公司文件),或在提示LLM之前检索并添加用户特定数据(财务记录、健康数据和电子邮件),以确保生成个性化答案。通过在运行时在LLM输出上施加上下文信息,这些方法可以引导模型,而无需依赖传统的微调过程。
通常,演示是推理任务指令的一部分,其中提供了少量示例来引导期望的行为。强大的LLM,如GPT-3,可以通过提示技术解决任务,而无需进一步的训练。在这种方法中,要解决的问题被呈现给模型作为文本提示,其中包含类似问题及其解决方案的一些文本示例。模型必须通过推理提供提示的完成。零提示涉及没有已解决的示例,而少量提示包括一小部分相似(问题和解决方案)对的示例。
已经证明,通过提示可以轻松控制像GPT-4这样的大型冻结模型,并且可以在不进行大量微调的情况下引导模型行为。
通过提示可以在低开销的情况下将模型条件化为新知识,但需要仔细设计提示以获得最佳结果。这是我们将在本章的一部分中讨论的内容。在前缀调整中,会在推理时训练并提供连续的任务特定向量供模型使用。类似的想法已经被提出用于适配器方法,例如参数高效迁移学习(PELT)或阶梯侧调整(LST)。
在推理时进行条件化还可以在采样过程中发生,例如基于语法的采样,其中输出可以受到某些明确定义的模式的限制,例如编程语言语法。
在下一节中,我们将使用PEFT和量化对一个小型开源LLM(OpenLLaMa)进行微调,用于问答(QA),然后将其部署到Hugging Face。
微调
正如本章第一节讨论的那样,对LLMs进行模型微调的目标是优化模型,使其生成比原始基础模型更与任务和上下文相关的输出。 进行微调的原因在于预训练的LMs被设计用于建模通用语言知识,而非特定的下游任务。它们的能力只有在适应应用程序时才会显现出来。微调允许更新预训练权重以适应目标数据集和目标。这使得可以从通用模型中转移知识,同时为专门的任务进行定制。 总体而言,微调有三个对这些模型的用户立即明显的优势:
- 可操控性:模型遵循指示的能力(指示微调)。
- 可靠的输出格式:这对于API调用/函数调用非常重要。
- 定制的语气:这使得可以根据任务和受众适当地调整输出风格。
- 一致性:模型的输出应与核心价值观相符,例如关于安全性、安全性和隐私考虑的价值观。
对预训练神经网络进行微调的思想起源于2010年代初的计算机视觉研究。Howard和Ruder(2018)展示了在下游任务上微调像ELMo和ULMFit这样的模型的有效性。开创性的BERT模型(Devlin等人,2019)确立了对预训练transformers进行微调作为NLP中的事实标准方法。
在本节中,我们将微调一个用于问答的模型。这个步骤并不特定于LangChain,但我们将指出一些定制,其中LangChain可能适用。您可以在书籍的GitHub存储库的notebooks目录中找到代码。
作为第一步,我们将使用库和环境变量设置微调。
微调设置
微调在各种任务上始终能够取得强大的结果,但需要大量的计算资源。因此,在一个可以访问强大的GPU和内存资源的环境中进行微调是个好主意。我们将在Google Colab上运行此操作,而不是在本地环境中进行,这样我们可以免费运行LLMs的微调(仅有一些限制)。
Google Colab是一个计算环境,提供不同的方式来加速计算任务,如张量处理单元(TPUs)和图形处理单元(GPUs)。这些都在免费和专业版中都可用。对于本节的任务,免费版已经足够了。您可以在以下网址登录Colab环境:colab.research.google.com/
请确保您将Google Colab机器设置在顶部菜单中为TPU或GPU,以确保您有足够的资源来运行以下代码,而且训练时间不会太长。我们将在Google Colab环境中安装所有所需的库 - 我正在添加这些库的版本,以使我们的微调可重复:
peft
: PEFT(版本0.5.0)trl
: Proximal Policy Optimization(0.6.0)bitsandbytes
: k位优化器和矩阵乘法例程,用于量化(0.41.1)accelerate
: 使用多GPU、TPU和混合精度训练和使用PyTorch模型的库(0.22.0)transformers
: Hugging Face transformers库,支持JAX、PyTorch和TensorFlow后端(4.32.0)datasets
: 社区驱动的开源数据集库(2.14.4)sentencepiece
: 用于快速分词的Python包装器(0.1.99)wandb
: 用于在Weights和Biases上监视训练进度的库(0.15.8)langchain
: 用于在训练后将模型加载回LangChain LLM的库(0.0.273)
我们可以在Colab笔记本中使用以下命令安装这些库:
diff
!pip install -U accelerate bitsandbytes datasets transformers peft trl sentencepiece wandb langchain huggingface_hub
要从Hugging Face下载并训练模型,我们需要在平台上进行身份验证。请注意,如果您以后想将模型推送到Hugging Face,您需要生成一个具有在Hugging Face上写入权限的新API令牌:huggingface.co/settings/to...
我们可以在笔记本中通过以下方式进行身份验证:
csharp
from huggingface_hub import notebook_login
notebook_login()
在提示时,粘贴您的Hugging Face访问令牌。
在开始执行代码之前,需要注意一点:在执行代码时,您需要登录不同的服务,因此在运行笔记本时请确保注意!
Weights and Biases(W&B)是一个MLOps平台,可以帮助开发者监视和记录端到端的ML训练工作流程。如前所述,我们将使用W&B来了解训练的效果如何以及模型是否随着时间的推移而改进。对于W&B,我们需要为项目命名;或者,我们可以使用wandb的init()方法:
lua
import os
os.environ["WANDB_PROJECT"] = "finetuning"
要使用W&B进行身份验证,您需要在www.wandb.ai创建一个免费账户。您可以在授权页面wandb.ai/authorize找到API密钥。再次强调,我们需要粘贴我们的API令牌。
如果之前的训练仍然活跃 - 这可能是由于笔记本的先前执行导致的,如果您第二次运行它 - 让我们确保启动一个新的训练!这将确保我们获得新的报告和W&B上的仪表板:
python
import wandb
if wandb.run is not None:
wandb.finish()
接下来,我们需要选择一个数据集,用于对模型进行优化。我们可以在此处使用许多不同的数据集,适用于编码、叙述、工具使用、SQL生成、小学数学问题(GSM8k)或许多其他任务。Hugging Face提供了丰富的数据集,可以在此URL查看:huggingface.co/datasets。这些数据集涵盖了许多不同甚至是最专业的任务。
我们也可以定制自己的数据集。例如,我们可以使用LangChain设置训练数据。有很多可用于过滤的方法,可以帮助减少数据集中的冗余。尽管在本章中展示数据收集作为一个实用的步骤会很有吸引力,但由于其复杂性,这超出了本书的范围。
从Web数据中过滤质量可能更加困难,但有很多可能性。对于代码模型,我们可以应用代码验证技术来将片段评分作为质量过滤器。如果代码来自GitHub,我们可以按存储库所有者的星号或按星号进行过滤。
对于自然语言中的文本,质量过滤并不是一个简单的问题。搜索引擎排名可以作为一个受欢迎程度的过滤器,因为它通常基于用户与内容的互动。此外,知识蒸馏技术可以通过事实密度和准确性进行调整,作为过滤器。
在这个示例中,我们将微调用于问答性能的Squad V2数据集。您可以在Hugging Face上查看有关数据集的详细描述:huggingface.co/spaces/eval...:
ini
from datasets import load_dataset
dataset_name = "squad_v2"
dataset = load_dataset(dataset_name, split="train")
eval_dataset = load_dataset(dataset_name, split="validation")
我们正在使用训练和验证的两个拆分。Squad V2数据集有一个部分用于训练,另一个用于验证,如load_dataset(dataset_name)的输出所示:
css
DatasetDict({
train: Dataset({
features: ['id', 'title', 'context', 'question', 'answers'],
num_rows: 130319
})
validation: Dataset({
features: ['id', 'title', 'context', 'question', 'answers'],
num_rows: 11873
})
})
我们将使用验证拆分进行早停。早停将允许我们在验证错误开始下降时停止训练。
Squad V2数据集由各种特征组成,我们可以在这里看到:
python
{'id': Value(dtype='string', id=None),
'title': Value(dtype='string', id=None),
'context': Value(dtype='string', id=None),
'question': Value(dtype='string', id=None),
'answers': Sequence(feature={'text': Value(dtype='string', id=None),
'answer_start': Value(dtype='int32', id=None)}, length=-1, id=None)}
在训练中的基本思想是用一个问题提示模型,并将答案与数据集进行比较。在下一节中,我们将使用这个设置微调一个开源的LLM。
开源模型
我们希望使用一个在本地能够以不错的标记速率运行的小型模型。LLaMa-2模型需要通过您的电子邮件地址签署许可协议并得到确认(公平地说,这可能非常快),因为它带有商业用途的限制。OpenLLaMa等衍生品表现相当不错,可以在HF排行榜上看到:huggingface.co/spaces/Hugg... OpenLLaMa版本1不能用于编码任务,因为其分词器的原因。因此,让我们使用v2!我们将使用一个3B参数模型,即使在旧硬件上也可以使用:
ini
model_id = "openlm-research/open_llama_3b_v2"
new_model_name = f"openllama-3b-peft-{dataset_name}"
我们也可以使用更小的模型,如EleutherAI/gpt-neo-125m,这也可以在资源使用和性能之间取得特别好的折衷。
让我们加载模型:
ini
import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
)
device_map="auto"
base_model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
base_model.config.use_cache = False
Bits and Bytes配置使我们能够将模型量化为8、4、3甚至2位,具有更快的推理速度和更低的内存占用,而性能损失不大。
我们将在Google Drive上存储模型检查点;您需要确认登录到您的Google账户:
javascript
from google.colab import drive
drive.mount('/content/gdrive')
我们需要通过Google进行身份验证,以使其工作。
我们可以将模型检查点和日志的输出目录设置为我们的Google Drive:
ini
output_dir = "/content/gdrive/My Drive/results"
如果您不想使用Google Drive,只需将其设置为计算机上的一个目录。
对于训练,我们需要设置一个分词器:
ini
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
现在,我们将定义我们的训练配置。我们将设置LORA和其他训练参数:
ini
from transformers import TrainingArguments, EarlyStoppingCallback
from peft import LoraConfig
# More info: https://github.com/huggingface/transformers/pull/24906
base_model.config.pretraining_tp = 1
peft_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.1,
r=64,
bias="none",
task_type="CAUSAL_LM",
)
training_args = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=10,
max_steps=2000,
num_train_epochs=100,
evaluation_strategy="steps",
eval_steps=5,
save_total_limit=5,
push_to_hub=False,
load_best_model_at_end=True,
report_to="wandb"
)
这里需要解释一下一些参数。push_to_hub参数意味着我们可以在训练过程中定期将模型检查点推送到HuggingSpace Hub。为使其工作,您需要设置HuggingSpace身份验证(如前所述,需要写权限)。如果选择这个选项,作为output_dir,我们可以使用new_model_name。这将是模型在Hugging Face上可用的存储库名称:huggingface.co/models。 或者,正如我在这里所做的,您可以将模型保存在本地或云端,例如在Google Drive的一个目录中。我将max_steps和num_train_epochs设置得很高,因为我注意到训练可以在许多步骤后仍然改进。早期停止和较高数量的最大训练步骤应该有助于使模型提供更高的性能。对于早期停止,我们需要将evaluation_strategy设置为"steps",并将load_best_model_at_end设置为True。 eval_steps是两个评估之间的更新步数。save_total_limit=5表示只保存最后五个模型。最后,report_to="wandb"表示我们将发送训练统计信息、一些模型元数据和硬件信息到W&B,我们可以在那里查看每个运行的图形和仪表板。
然后,训练可以使用我们的配置:
ini
from trl import SFTTrainer
trainer = SFTTrainer(
model=base_model,
train_dataset=dataset,
eval_dataset=eval_dataset,
peft_config=peft_config,
dataset_text_field="question", # this depends on the dataset!
max_seq_length=512,
tokenizer=tokenizer,
args=training_args,
callbacks=[EarlyStoppingCallback(early_stopping_patience=200)]
)
trainer.train()
训练可能需要相当长的时间,即使在TPU设备上运行。频繁的评估会大大减慢训练速度。如果禁用早期停止,可以加快速度。
在训练进行时,我们应该看到一些统计数据,但展示性能图表会更好,就像我们在W&B上看到的那样。
训练完成后,我们可以将最终检查点保存在磁盘上以便重新加载:
lua
trainer.model.save_pretrained(
os.path.join(output_dir, "final_checkpoint"),
)
现在,我们可以通过手动推送到Hugging Face,与朋友分享我们所取得的性能。我们可以使用我们的Hugging Face用户名和存储库名称(新模型名称)的组合来加载模型。让我们快速展示如何在LangChain中使用此模型。通常,peft模型存储为适配器,而不是完整的模型;因此,加载方式有点不同:
ini
from peft import PeftModel, PeftConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
from langchain.llms import HuggingFacePipeline
model_id = 'openlm-research/open_llama_3b_v2'
config = PeftConfig.from_pretrained("benji1a/openllama-3b-peft-squad_v2")
model = AutoModelForCausalLM.from_pretrained(model_id)
model = PeftModel.from_pretrained(model, "benji1a/openllama-3b-peft-squad_v2")
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
max_length=256
)
llm = HuggingFacePipeline(pipeline=pipe)
到目前为止,我们在Google Colab上完成了所有操作,但我们同样可以在本地执行;只需注意您需要安装Hugging Face peft库!
商业模型
到目前为止,我们已经展示了如何对开源LLM进行微调和部署。一些商业模型也可以在自定义数据上进行微调。例如,OpenAI的GPT-3.5和Google的PaLM模型都提供了这种能力。这已经与一些Python库集成了起来。
使用Scikit-LLM库,这只需要几行代码。在本节中,我们不会详细介绍整个过程,但请查看Scikit-LLM库或不同云LLM提供商的文档,以获取所有细节。Scikit-LLM库不是我们在第3章"LangChain入门"中讨论的设置的一部分,因此您需要手动安装它。我还没有包含训练数据X_train。您需要自己准备训练数据集。
对于文本分类,可以这样微调PaLM模型:
ini
from skllm.models.palm import PaLMClassifier
clf = PaLMClassifier(n_update_steps=100)
clf.fit(X_train, y_train) # y_train是标签的列表
labels = clf.predict(X_test)
同样,您可以这样微调GPT-3.5模型进行文本分类:
ini
from skllm.models.gpt import GPTClassifier
clf = GPTClassifier(
base_model = "gpt-3.5-turbo-0613",
n_epochs = None, # int或None。当为None时,将由OpenAI自动确定
default_label = "Random", # 可选
)
clf.fit(X_train, y_train) # y_train是标签的列表
labels = clf.predict(X_test)
有趣的是,在OpenAI提供的微调中,所有输入都会经过一个审查系统,以确保输入符合安全标准。
这就结束了微调。LLM可以在没有任何任务特定调整的情况下进行部署和查询。通过提示,我们可以实现少样本学习,甚至是零样本学习,正如我们将在下一节中讨论的那样。
提示工程
提示是我们向语言模型提供的指令和示例,用于引导其行为。它们对于引导LLM的行为至关重要,因为它们允许您在不进行昂贵的重新训练的情况下将模型输出与人类意图对齐。精心设计的提示可以使LLM适用于原始训练之外的各种任务。提示充当指示,向LLM演示所需的输入-输出映射是什么。
提示由三个主要组件组成:
- 描述任务要求、目标和输入/输出格式的说明。它们清楚地向模型解释任务。
- 展示所需输入-输出对的示例。它们提供不同演示,说明不同输入应该如何映射到输出。
- 模型必须对其进行操作以生成输出的输入。
以下图示显示了提示不同语言模型的一些示例(来源:Pre-train, Prompt, and Predict - A Systematic Survey of Prompting Methods in Natural Language Processing by Liu and colleagues, 2021):
提示工程,也被称为上下文学习,是指通过精心设计的提示技术来引导LLM行为,而无需更改模型权重。其目标是使模型输出与给定任务的人类意图一致。另一方面,提示调整提供了对模型行为的直观控制,但对提示的确切措辞和设计敏感,因此需要精心制定的准则以实现期望的结果。那么,好的提示是什么样的呢?
最重要的第一步是从简单开始,并进行迭代。从简明扼要、直截了当的说明开始,根据需要逐渐增加复杂性。将复杂的任务分解为更简单的子任务。这样可以避免一开始就使模型不堪重负。尽可能具体、描述详细关于确切任务和期望的输出格式。提供相关示例在演示所需的推理链或输出样式方面非常有效。
对于复杂的推理任务,引导模型解释其逐步思考过程会提高准确性。像"链式思维提示"这样的技术引导模型进行明确推理。提供少量示例进一步演示所需的推理格式。将复杂问题分解为较小、更易管理的子任务的问题分解提示还通过启用更有结构的推理过程来提高可靠性。对多个候选响应进行采样并选择最一致的响应有助于减少与依赖单一模型输出相比的错误和不一致性。
与其关注不要做什么,不如清楚指定期望的操作和结果。直接、明确的指令效果最佳。避免使用不明确或模糊的提示。从简单开始,具体说明,提供示例,要求解释,分解问题,以及采样多个响应 - 这些都是使用精心设计的提示工程有效引导LLM的一些最佳实践。通过迭代和实验,可以优化提示以提高可靠性,即使对于复杂任务,也可以达到通常可与微调相媲美的性能。
在了解了最佳实践之后,让我们看一些提示技术,从简单到越来越高级的!
提示技术
基本的提示方法包括零射击提示,仅使用输入文本,以及少射击提示,其中包含几个演示示例,显示所需的输入-输出对。研究人员已经确定了像大多数标签偏见和最近偏见这样的偏见,这些偏见导致了少射击性能的变异性。通过示例选择、排序和格式设置的谨慎提示设计可以帮助缓解这些问题。
更高级的提示技术包括指令提示,其中明确描述任务要求,而不仅仅是演示。自一致性采样生成多个输出并选择与示例最符合的输出。思维链(CoT)提示生成明确的推理步骤,导致最终的输出。这对于复杂的推理任务尤其有益。CoT提示可以手动编写,也可以通过增强-修剪-选择等方法自动生成。
一些提示技术结合了外部信息检索,以在生成输出之前向LLM提供缺失的上下文。对于开放域问答,可以通过搜索引擎检索相关段落并合并到提示中。对于封闭式问答,具有证据-问题-答案格式的少量示例比QA格式更有效。
在接下来的几个小节中,我们将介绍前面提到的一些技术。LangChain提供了工具,可以启用高级提示工程策略,如零射击提示、少射击学习、思维链、自一致性和思维树。这里描述的所有这些技术通过提供更清晰的说明、使用有针对性的数据进行微调、采用问题分解策略、结合多样的采样方法、整合验证机制以及采用概率建模框架,增强了LLM在复杂任务上的推理能力的准确性、一致性和可靠性。
您可以在本书的GitHub存储库中的提示目录中找到本节中的所有示例。让我们从最简单的策略开始:我们只是要求一个解决方案。
Zero-shot prompting
零射击提示与少射击提示相反,是直接向LLM提供任务说明而不提供任何演示或示例。此提示测试了预训练模型理解和遵循说明的能力:
python
from langchain import PromptTemplate
from langchain.chat_models import ChatOpenAI
model = ChatOpenAI()
prompt = PromptTemplate(input_variables=["text"], template="Classify the sentiment of this text: {text}")
chain = prompt | model
print(chain.invoke({"text": "I hated that movie, it was terrible!"}))
这输出了带有输入文本的情感分类提示,没有任何示例:
ini
content='The sentiment of this text is negative.' additional_kwargs={} example=False
Few-shot learning
Few-shot learning向LLM展示与任务相关的少量输入-输出示例,而不提供明确的说明。这使得模型能够仅通过演示来推断意图和目标。精心选择、排序和格式化的示例可以改善模型的推理能力。然而,少射击学习可能容易受到偏见和试验间的变异性影响。添加明确的说明可以使模型更透明地理解意图,并提高鲁棒性。总体而言,提示结合了说明和示例的优势,以最大程度地引导LLM执行手头的任务。
FewShotPromptTemplate允许您向模型展示任务的少量演示示例,而无需明确的说明。
让我们扩展前面关于情感分类的示例,使用少射击提示。在此示例中,我们希望LLM将客户反馈分类为积极、消极或中性。我们为其提供了一些示例:
ini
examples = [{
"input": "I absolutely love the new update! Everything works seamlessly.",
"output": "Positive",
},{
"input": "It's okay, but I think it could use more features.",
"output": "Neutral",
}, {
"input": "I'm disappointed with the service, I expected much better performance.",
"output": "Negative"
}]
我们可以使用这些示例创建以下提示:
ini
from langchain.prompts import FewShotPromptTemplate, PromptTemplate
from langchain.chat_models import ChatOpenAI
example_prompt = PromptTemplate(
template="{input} -> {output}",
input_variables=["input", "output"],
)
prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=example_prompt,
suffix="Question: {input}",
input_variables=["input"]
)
print((prompt | ChatOpenAI()).invoke({"input": " This is an excellent book with high quality explanations."}))
我们应该得到以下输出:
ini
content='Positive' additional_kwargs={} example=False
您可以期望LLM使用这些示例来指导其对新句子的分类。少射击方法在不进行大量训练的情况下引导模型,而是依赖于其预训练知识和示例提供的上下文。
为了针对每个输入选择定制的示例,FewShotPromptTemplate可以接受SemanticSimilarityExampleSelector,该选择器基于嵌入而不是硬编码的示例。SemanticSimilarityExampleSelector会自动找到每个输入的最相关示例。
对于许多任务,标准的少射击提示效果很好,但在处理更复杂的推理任务时,还有许多其他技术和扩展。
Chain-of-thought prompting
CoT提示旨在通过让模型提供中间步骤来鼓励推理,从而导致最终答案。这是通过在提示前加入指示来展示其思考过程来完成的。
CoT有两个变体,即零射击和少射击。在零射击CoT中,我们只需将指令"让我们逐步思考!"添加到提示中。
当要求LLM通过一个问题进行推理时,通常更有效的做法是在陈述最终答案之前要求其解释其推理过程。这鼓励LLM首先通过逻辑思考问题,而不是仅仅猜测答案然后试图在之后证明。要求LLM解释其思考过程与其核心能力很好地协调。 例如:
ini
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
reasoning_prompt = "{question}\nLet's think step by step!"
prompt = PromptTemplate(
template=reasoning_prompt,
input_variables=["question"]
)
model = ChatOpenAI()
chain = prompt | model
print(chain.invoke({
"question": "There were 5 apples originally. I ate 2 apples. My friend gave me 3 apples. How many apples do I have now?",
}))
运行此示例后,我们将得到推理过程以及结果:
yaml
content='Step 1: Originally, there were 5 apples.\nStep 2: I ate 2 apples.\nStep 3: So, I had 5 - 2 = 3 apples left.\nStep 4: My friend gave me 3 apples.\nStep 5: Adding the apples my friend gave me, I now have 3 + 3 = 6 apples.' additional_kwargs={} example=False
前述方法也被称为零射击链式思考。 少射击链式思考提示是一种少射击提示,其中推理被解释为示例解决方案的一部分,其目的是鼓励LLM在做出决策之前解释其推理过程。 如果我们回到之前的少射击示例,可以将它们扩展如下:
ini
examples = [{
"input": "I absolutely love the new update! Everything works seamlessly.",
"output": "Love and absolute works seamlessly are examples of positive sentiment. Therefore, the sentiment is positive",
},{
"input": "It's okay, but I think it could use more features.",
"output": "It's okay is not an endorsement. The customer further thinks it should be extended. Therefore, the sentiment is neutral",
}, {
"input": "I'm disappointed with the service, I expected much better performance.",
"output": "The customer is disappointed and expected more. This is negative"
}]
在这些示例中,解释了做出决策的原因。这鼓励LLM以解释其推理的方式给出类似的结果。
已经证明,CoT提示可以导致更准确的结果;然而,发现这种性能提升与模型规模成正比,并且在较小的模型中改进微乎其微,甚至是负面的。
Self-consistency
使用自一致性提示,模型会对一个问题生成多个候选答案。然后,这些答案相互比较,选择最一致或最频繁的答案作为最终输出。自一致性提示在事实验证或信息综合的背景下特别有用,因为准确性至关重要。
在第一步中,我们将为一个问题或问题创建多个解决方案:
ini
from langchain import PromptTemplate, LLMChain
from langchain.chat_models import ChatOpenAI
solutions_template = """
Generate {num_solutions} distinct answers to this question:
{question}
Solutions:
"""
solutions_prompt = PromptTemplate(
template=solutions_template,
input_variables=["question", "num_solutions"]
)
solutions_chain = LLMChain(
llm=ChatOpenAI(),
prompt=solutions_prompt,
output_key="solutions"
)
在第二步中,我们想要统计不同的答案。我们可以再次使用一个LLM:
ini
consistency_template = """
For each answer in {solutions}, count the number of times it occurs. Finally, choose the answer that occurs most.
Most frequent solution:
"""
consistency_prompt = PromptTemplate(
template=consistency_template,
input_variables=["solutions"]
)
consistency_chain = LLMChain(
llm=ChatOpenAI(),
prompt=consistency_prompt,
output_key="best_solution"
)
让我们用SequentialChain将这两个链条结合在一起:
ini
from langchain.chains import SequentialChain
answer_chain = SequentialChain(
chains=[solutions_chain, consistency_chain],
input_variables=["question", "num_solutions"],
output_variables=["best_solution"]
)
让我们提一个简单的问题并检查答案:
ini
print(answer_chain.run(
question="Which year was the Declaration of Independence of the United States signed?",
num_solutions="5"
))
我们应该得到类似这样的响应:
arduino
1776 is the year in which the Declaration of Independence of the United States was signed. It occurs twice in the given answers (3 and 4).
我们应该根据投票得到正确的响应;然而,我们生成的五个答案中有三个是错误的。 这种方法利用了模型推理和利用内部知识的能力,同时通过关注最常见的答案来降低异常值或错误信息的风险,从而提高了LLM给出的响应的整体可靠性。
Tree-of-thought
在Tree-of-Thought(ToT)提示中,我们生成给定提示的多个解决问题步骤或方法,然后使用AI模型对其进行评论。评论将基于模型对解决方案适用于问题的判断。
实际上,现在在LangChain的实验性软件包中有ToT的实现;然而,让我们通过使用LangChain逐步示例来详细说明如何实施ToT。
首先,我们将使用PromptTemplates定义四个链组件。我们需要一个解决方案模板、一个评估模板、一个推理模板和一个排名模板。
首先,让我们生成解决方案:
ini
solutions_template = """
Generate {num_solutions} distinct solutions for {problem}. Consider factors like {factors}.
Solutions:
"""
solutions_prompt = PromptTemplate(
template=solutions_template,
input_variables=["problem", "factors", "num_solutions"]
)
然后,让我们要求LLM评估这些解决方案:
ini
evaluation_template = """
Evaluate each solution in {solutions} by analyzing pros, cons, feasibility, and probability of success.
Evaluations:
"""
evaluation_prompt = PromptTemplate(
template=evaluation_template,
input_variables=["solutions"]
)
在这一步之后,我们想要对它们进行更深入的思考:
ini
reasoning_template = """
For the most promising solutions in {evaluations}, explain scenarios, implementation strategies, partnerships needed, and handling potential obstacles.
Enhanced Reasoning:
"""
reasoning_prompt = PromptTemplate(
template=reasoning_template,
input_variables=["evaluations"]
)
最后,根据我们迄今为止的推理,我们可以对这些解决方案进行排名:
ini
ranking_template = """
Based on the evaluations and reasoning, rank the solutions in {enhanced_reasoning} from most to least promising.
Ranked Solutions:
"""
ranking_prompt = PromptTemplate(
template=ranking_template,
input_variables=["enhanced_reasoning"]
)
接下来,我们从这些模板创建链,然后将所有链连接在一起:
ini
from langchain.chains.llm import LLMChain
from langchain.chat_models import ChatOpenAI
solutions_chain = LLMChain(
llm=ChatOpenAI(),
prompt=solutions_prompt,
output_key="solutions"
)
evalutation_chain = LLMChain(
llm=ChatOpenAI(),
prompt=evaluation_prompt,
output_key="evaluations"
)
reasoning_chain = LLMChain(
llm=ChatOpenAI(),
prompt=reasoning_prompt,
output_key="enhanced_reasoning"
)
ranking_chain = LLMChain(
llm=ChatOpenAI(),
prompt=ranking_prompt,
output_key="ranked_solutions"
)
请注意,每个output_key对应于以下链的prompt中的input_key。最后,我们将这些链连接成一个SequentialChain:
ini
from langchain.chains import SequentialChain
tot_chain = SequentialChain(
chains=[solutions_chain, evalutation_chain, reasoning_chain, ranking_chain],
input_variables=["problem", "factors", "num_solutions"],
output_variables=["ranked_solutions"]
)
让我们运行tot_chain并查看打印输出:
sql
1. Train or fine-tune language models using datasets that are relevant to the reasoning task at hand.
2. Develop or adapt reasoning algorithms and techniques to improve the performance of language models in specific reasoning tasks.
3. Evaluate existing language models and identify their strengths and weaknesses in reasoning.
4. Implement evaluation metrics to measure the reasoning performance of the language models.
5. Iteratively refine and optimize the reasoning capabilities of the language models based on evaluation results.
It is important to note that the ranking of solutions may vary depending on the specific context and requirements of each scenario.
我完全同意这些建议。它们展示了ToT的优势。这其中许多主题都是本章的一部分,而一些将在第9章《生成式AI在生产中》中讨论,届时我们将探讨评估LLMs及其性能。
这使我们能够在推理过程的每个阶段利用LLM。ToT方法通过促进探索来避免了走入死胡同。如果您想看到更多示例,在LangChain Cookbook中,您可以找到一个用于玩数独的ToT。
对于释放LLM推理能力而言,提示设计非常重要,并且它为模型和提示技术的未来进展提供了潜在的可能性。这些原则和技术为与LLMs一起工作的研究人员和实践者提供了宝贵的工具包。
总结
调节允许引导生成式AI以提高性能、安全性和质量。在本章中,焦点是通过微调和提示来进行调节。在微调中,语言模型通过训练许多以自然语言指令构建的任务示例以及适当的响应来进行训练。通常通过强化学习与人类反馈进行此操作;然而,已经开发了其他技术,据证明其在资源占用较低的情况下产生具有竞争力的结果。在本章的第一个示例中,我们实施了一个用于问答的小型开源模型的微调。
有许多提示技术可以提高LLMs在复杂推理任务中的可靠性,包括逐步提示、替代选择、推理提示、问题分解、采样多个响应以及使用独立的验证模型。这些方法已被证明可以增强推理任务中的准确性和一致性。正如我们在示例中所展示的,LangChain提供了解锁高级提示策略的构建块,如few-shot学习、CoT、ToT等。
在第9章《生成式AI在生产中》,我们将讨论生成式AI的生产化以及与之相关的关键问题,例如评估LLM应用程序、将它们部署到服务器并对其进行监控。