生成文本
我们刚刚微调了一个基于编码器的模型进行文本分类。下面深入研究训练一个用于文本生成的模型。与文本分类不同,生成式模型通常不需要标注数据。例如,如果我们的目标是生成代码,我们可以收集一个许可代码数据集(例如The Stack),并从头开始训练一个模型。虽然这很有趣,但要获得不错的结果需要大量计算资源(需要数周甚至几个月的训练时间!)。
注:Kocetkov, Denis等的论文《The Stack: 3 TB许可授权的源代码》
注:虽然我们可以在没有标注数据的情况下构建强大的生成式模型,但一种称为RLHF(基于人类反馈的强化学习)的新方法允许我们在训练中引入人类标注数据,以使模型输出对齐某些偏好输出。例如,希望避免模型生成有害内容。我们将在后面学习更多关于这种方法的内容。
与其从头开始训练一个开放式文本生成模型,不如微调一个现有模型,以生成特定风格的文本。这种方法允许我们利用模型现有的语言知识,大大减少对大量数据和算力的需求。例如,可以使用几百条推文来生成具有你独特写作风格的新推文。在这个例子中,我们将使用AG News数据集来训练模型生成商业新闻。
首先过滤出所有标记为商业的样本(标签为2
),并删除不必要的标签列。
ini
filtered_datasets = raw_datasets.filter(lambda example: example["label"] == 2)
filtered_datasets = filtered_datasets.remove_columns("label")
第二个问题是选择哪个基础模型。在微调过程中,选择合适的基础模型是一个至关重要的决定。很多因素会影响这个决定,下面探讨其中的一些因素:
- 模型大小:在个人电脑上部署一个600亿参数的模型是不现实的。模型大小的选择取决于预期的推理需求、硬件容量和部署要求。在本章的后面,我们将深入研究一些技术,这些技术使得使用相同的计算资源来运行具有更多参数的模型成为可能。
- 训练数据:微调模型的性能与基础模型的训练数据与推理数据的匹配度相关。例如,微调一个模型生成符合自有代码库风格的代码时,最好从一个专门针对代码预训练的模型开始。考虑数据源的专业度,特别是并非所有模型都会披露其训练数据。同样,我们不会希望用一个主要基于英语的模型来生成韩语文本。并非所有模型都会披露其数据来源,这使得了解这一点变得具有挑战性。
- 上下文长度:不同的模型有不同的上下文长度限制。上下文长度是模型在进行预测时可以使用的最大标记数。例如,如果上下文长度为1024,则模型可以使用最后的1024个标记来进行预测。要生成长篇文本,需要一个具有较大上下文长度的模型。
- 许可证:选择基础模型时,许可证也至关重要。考虑模型是否符合你的使用要求。模型可能具有商业或非商业许可证,并且开源许可证与开放访问许可证之间存在区别。了解这些许可证对于确保遵守法律和使用限制至关重要。例如,虽然有些模型允许商业使用,但它们可能规定了允许的使用案例和不应使用模型的场景。
评估生成式模型仍是一个挑战,有各种基准来评估具体方面。诸如ARC用于科学问题,HellaSwag用于常识推理以及其它用作不同能力的代理的基准。Hugging Face Open LLM Leaderboard收集了数千个模型的基准结果,并可根据模型大小和类型进行筛选。然而,需要注意的是,这些基准只是系统比较的工具,最终的模型选择应始终基于其在现实任务中的表现。举一个具体的例子,Open LLM Leaderboard中使用的基准并不专注于对话,因此不应作为选择对话模型的主要标准。
下表显示了几个著名的开源预训练LLM(大型语言模型)。需要考虑的因素很多。这个表格并不详尽;还有许多其他开放的LLM,例如Mosaic MPT、Stability StableLM和Microsoft Phi,读者在读到本文时还会有更多。同样,这个表格不包括代码模型。对于这类模型,可能需要查看Big Code Models Leaderboard,在那里可以找到诸如CodeLlama(Meta的知名模型)和BigCode的模型(一个使用宽松许可证代码训练的模型)等。
Model | Creator | Size | Training Data | Open LLM Performance | Context length | Vocab size | License |
---|---|---|---|---|---|---|---|
GPT-2 | OpenAI | 117M 345M 762M 1.5B | Unreleased. Up to 40GB of text from a web scrape | 30.06 33.64 34.08 36.66 | 1024 | 50257 | MIT |
GPT-Neo | EleutherAI | 125M 1.3B 2.7B 20B | The Pile 300B tokens 380B tokens 420B tokens | 31.19 36.04 38.96 43.95 | 2048 | 50257 | MIT |
Falcon | TII UAE | 7B 40B 180B | Partially released RefinedWeb built on top of CommonCrawl 1.5T tokens 1T tokens 3.5T tokens | 47.01 61.48 67.85 | 2048 | 65024 | Apache 2.0 (7B and 40B) Custom (180B) |
Llama 2 | Meta | 7B 13B 70B | Unreleased. 2T tokens | 54.32 58.66 67.35 | 4096 | 32000 | Custom |
Mistral | Mistral | 7B | Unreleased | 60.45 | 8000 | 32000 | Apache 2.0 |
此外,值得注意的是,这个表格偏向于主要基于英语数据训练的模型。然而,强大的中文模型如InternLM、ChatGLM、Qwen和Baichuan也是预训练语言模型领域的重要贡献者。这些信息是为了在选择实验模型时提供参考,而不是列出所有开源模型的详尽列表。
鉴于我们希望在没有强大GPU的环境中进行数据量非常小的快速训练,我们将微调GPT-2的最小变体。鼓励读者尝试使用更大的模型和不同的数据集。在本章后面,我们将探讨一些用于推理和训练大型模型的技术。
和之前一样,我们从加载模型和分词器开始。GPT-2的一个特别之处是它没有指定填充标记,但在进行分词时我们需要这样的标记,以确保所有样本具有相同的长度。我们可以将填充标记设置为与文本结束标记相同。
ini
from transformers import AutoModelForCausalLM
model_id = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = (
tokenizer.eos_token
) # Needed as gpt2 does not specify padding token.
model = AutoModelForCausalLM.from_pretrained(model_id).to(device)
我们对数据集进行分词(但使用GPT2的分词器)。
ini
def tokenize_function(batch):
return tokenizer(batch["text"], truncation=True)
tokenized_datasets = filtered_datasets.map(
tokenize_function,
batched=True,
remove_columns=["text"], # We only need the input_ids and attention_mask
)
tokenized_datasets
css
DatasetDict({
train: Dataset({
features: ['input_ids', 'attention_mask'],
num_rows: 30000
})
test: Dataset({
features: ['input_ids', 'attention_mask'],
num_rows: 1900
})
})
在分类示例中,我们对所有样本进行了填充和截断,以确保它们具有相同的长度。除了在分词阶段进行这些操作外,我们还可以使用数据整合器(collator)来完成这项工作。数据整合器是将样本组装成一个批次的工具。transformers库提供了一些开箱即用的整合器,用于不同任务(例如语言建模)。整合器会动态地将批次中的样本填充到最大长度。除了填充外,语言建模整合器还会为语言建模任务结构化输入,这比之前稍微复杂一些。在语言建模中,我们将输入向右移动一个元素,并将其作为标签。例如,如果输入是I love Hugging Face
,,那么标签就是love Hugging Face
。模型的目标是根据前一个词预测下一个词。实际上,数据整合器会创建一个标签列,其中包含输入的副本。稍后,模型将负责移动输入和标签。
ini
from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
来看如何对三个样本进行操作。如下所示,每个样本的长度不同(37、55和51)。
python
samples = [tokenized_datasets["train"][i] for i in range(3)]
for sample in samples:
print(f"input_ids shape: {len(sample['input_ids'])}")
yaml
input_ids shape: 37
input_ids shape: 55
input_ids shape: 51
凭借整合器,样本填充为批次中的最大长度(55)并且添加了label
列。
python
out = data_collator(samples)
for key in out:
print(f"{key} shape: {out[key].shape}")
css
input_ids shape: torch.Size([3, 55])
attention_mask shape: torch.Size([3, 55])
labels shape: torch.Size([3, 55])
最后,我们需要定义训练参数。本例中,我们修改了几个参数,以展示TrainingArguments
提供的一些控制和灵活性。让我们仔细看看几个关键参数,展示它们对模型训练的重大影响:
- 权重衰减 :权重衰减是一种正则化技术,通过向损失函数添加惩罚项来防止模型过拟合。它防止学习算法分配过大的权重。在
TrainingArguments
中调整权重衰减参数可以微调这种正则化效果,从而影响模型的泛化能力。 - 学习率 :学习率是决定优化步长的关键超参数。在
TrainingArguments
中,可以指定学习率,从而影响训练过程的收敛速度和稳定性。仔细调整学习率可以显著影响模型的性能。 - 学习率调度器类型 :学习率调度器决定了训练期间学习率的变化方式。不同的任务和模型架构可能受益于特定的调度策略。
TrainingArguments
提供了定义学习率调度器类型的选项,使我们可以尝试各种调度,如恒定学习率、余弦衰减等。
ini
training_args = TrainingArguments(
"sft_cml",
push_to_hub=True,
per_device_train_batch_size=8,
weight_decay=0.1,
lr_scheduler_type="cosine",
learning_rate=5e-4,
num_train_epochs=2,
evaluation_strategy="steps",
eval_steps=200,
logging_steps=200,
)
完成所有这些设置后,就像在分类示例中一样,最后一步是使用所有组件创建一个Trainer
实例。主要的区别是这次我们使用了数据整理器,并使用的是5000个样本。
ini
trainer = Trainer(
model=model,
tokenizer=tokenizer,
args=training_args,
data_collator=data_collator,
train_dataset=tokenized_datasets["train"].select(range(5000)),
eval_dataset=tokenized_datasets["test"],
)
scss
trainer.train()
Metric | Epoch 0.32 Value | Epoch 0.64 Value | Epoch 0.96 Value | Epoch 1.28 Value | Epoch 1.6 Value | Epoch 1.92 Value |
---|---|---|---|---|---|---|
loss | 3.7271 | 3.346 | 3.0685 | 2.1435 | 1.9834 | 1.8937 |
learning_rate | 0.0004690767 | 0.0003839567 | 0.0002656976 | 0.0001435552 | 4.774575e-05 | 1.971325e-06 |
eval_loss | 3.6065 | 3.4732 | 3.3985 | 3.4433 | 3.4203 | 3.3980 |
eval_runtime | 4.7941 | 4.9057 | 4.7142 | 4.7107 | 4.9247 | 4.7953 |
eval_samples_per_second | 396.317 | 387.303 | 403.041 | 403.333 | 385.812 | 396.218 |
eval_steps_per_second | 49.644 | 48.515 | 50.486 | 50.523 | 48.328 | 49.632 |
scss
trainer.push_to_hub()
和此前一样,我们可以使用pipeline
指定任务(text-generation
)加载模型及运行推理。
scss
from transformers import pipeline
pipe = pipeline("text-generation", model="AlanHou/sft_cml", device=device)
pipe.tokenizer.pad_token_id = 50256 # pad_token_id for gpt2
print(pipe("Q1", pad_token_id=tokenizer.eos_token_id)[0]["generated_text"])
print(pipe("Wall", pad_token_id=tokenizer.eos_token_id)[0]["generated_text"])
print(pipe("Google", pad_token_id=tokenizer.eos_token_id)[0]["generated_text"])
ini
Q1 profit rises, says Santander Santander still ahead, but sees no gain ATLANTA (Reuters) - SBC, Europe #39;s seventh-largest bank, is claiming annual profits of up to $2.35 billion
Wall St. Looks Ahead; Wall Street Awaits Data (Reuters) Reuters - Stocks were set for a slightly\higher open on today as a fall in crude oil prices and reassuring U.S.\economy data about jobs kept Wall
Google posts surge in revenue Shareholders bought up to 30 percent in an unusual auction that began Wednesday in Philadelphia and ended last night in New York with $2 billion in initial public offerings and 1.5 million shares of debt. The company posted a
可以看到,生成的文本结构与AG新闻的商业部分相似。但生成的内容有时可能是不连贯的,这很正常,因为我们使用了一个较小的基础模型,其质量不高且训练数据很少。使用Mistral 7B或Llama 2的70B变体之类的大模型,肯定会生成更连贯的文本,同时保持相同的格式。
在评估生成文本的质量时,常用的指标是困惑度(perplexity)。困惑度衡量了语言模型对给定数据集的预测能力。较低的困惑度值表示更好的性能,表明模型可以更准确地预测下一个词。虽然困惑度提供了定量指标,但定性评估,包括人工判断,对于评估生成文本的整体连贯性和与预期任务的相关性也至关重要。平衡定量和定性评估可以确保对文本生成模型进行更全面的评估。
指令
在本章的第一部分,我们讨论了微调基于编码器的模型以完成特定的文本分类任务,如话题分类。但这种方法需要为每个任务训练一个新模型。如果我们遇到一个未见过的任务,比如识别文本是否为垃圾邮件,会没有现成的预训练模型可用,需要为其微调一个模型。这促使我们探索其他技术,简要讨论不同方法的优点、局限和用途:
-
微调多个模型:我们可以为每个任务选择并微调一个基础模型,以构建一个专用模型。在微调过程中,所有的模型权重都会更新,这意味着如果我们要解决五个不同的任务,我们最终会有五个微调过的模型。
-
适配器(Adapter) :我们可以冻结基础模型并训练一个称为适配器的小型辅助模型,而不必修改所有的模型权重。每个新任务仍然需要不同的适配器,但它们的体积非常小,因此我们可以轻松地拥有多个适配器而不会增加负担。接下来的部分会讲解适配器。
-
提示词 :正如在第一章中学到的,我们可以利用强大的预训练模型的零样本和少样本能力来解决不同的任务。使用零样本方法,我们编写一个详细说明任务的提示词。使用少样本方法,添加一些解决任务的示例来提高模型的性能。这些能力的表现取决于基础模型的能力。一个非常强大的模型,如GPT-4,可能会产生非常棒的零样本结果,这对于处理各种任务非常有用,如撰写长邮件或总结书籍章节。
-
指令微调:指令微调是一种改进LLM零样本性能的可选且简单的方法。指令微调将任务形式化为指令,如"这篇文章的主题是商业还是体育?"或"将'how are you'翻译成西班牙语"。这种方法主要涉及构建包含多任务指令的数据集,然后使用这些指令数据集的融合对预训练语言模型进行微调。创建指令微调的数据集相对简单;例如,可以利用AG新闻,将输入和标签结构化为指令,通过构建这样的提示词:
bashTo which of the "World", "Sports, "Business" or "Sci/Tech" categories does the text correspond to? Answer with a single word: Text: Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\band of ultra-cynics, are seeing green again.
通过构建足够大的多样化指令数据集,我们可以得到一个通用的指令微调模型,它可以解决许多任务,甚至是未出现过的任务,这是由于跨任务的泛化能力。这一理念是Flan模型的核心,Flan可以直接解决62个任务。这一概念在Flan T5模型中得到了进一步扩展,它是一组开源的指令微调T5模型,能够解决超过1000个任务。需要注意的是,这种模型是用输入(指令)和输出(回答)文本进行训练的;与GPT-2微调示例不同,这是一种监督训练技术。指令微调在T5或BART等编码器-解码器架构中非常流行,因为数据集的输入-输出结构非常适合这种方法。
什么时候应该使用微调、指令微调或提示词工程呢?这取决于任务、可用资源、期望的实验速度等。通常,特定任务或领域的微调模型表现更好。另一方面,它不能直接处理未出现过的任务。指令微调更具灵活性,但定义数据集和结构需要额外的处理。提示词工程是快速实验的最灵活方法,因为它不需要你直接训练模型,但需要一个更强大的基础模型,并且对生成的控制有限。
指令微调研究综述
我们不会构建一个端到端的指令微调示例,因为这主要是一个数据集任务,而不是建模任务,但我们来讨论一些优秀的论文,以便读者可以深入研究这个主题。
- Finetuned Language Models Are Zero-Shot Learners (February 2022)训练了一个名为Flan的模型,使用指令微调,超过了基础模型的零样本性能和其他模型的少样本性能。
- 在Flan之后,出现了一波新的数据集论文。Cross-Task Generalization via Natural Language Crowdsourcing Instructions (March 2022)引入了自然语言指令,这是一个包含61个任务的数据集,有人工指令和193,000对输入-输出对,通过将现有的NLP数据集映射到统一的架构来生成。这样做的前提是,人类可以通过从其他任务的实例中学习(以监督的方式)来遵循指令解决未出现过的问题。作者对编码器-解码器模型BART进行指令微调,相对不使用指令,跨任务泛化能力提升了19%。模型看过的任务越多,表现越好。
- Multitask Prompted Training Enables Zero-Shot Task Generalization (March 2022)遵循了类似不同任务统一数据架构的概念。作者微调T5构建了T0,它是一种在多任务混合数据集上训练的编码器-解码器模型,能够泛化到更多任务。一个令人兴奋的亮点是,数据中表示的任务越多,模型达到的中位性能越高,同时不减少多样性。
- 这一概念后来扩展为Super-NaturalInstructions: Generalization via Declarative Instructions on 1600+ NLP Tasks (October 2022),这是一个包含超过1600个任务和500万个示例的新数据集。这些项目的区别在于数据集的生成方式。T0是基于已有的任务实例构建指令,而自然语言指令则是由NLP研究人员制定指令,众包工作者构建数据集实例。
另一种方法是使用大型语言模型生成输出:Unnatural Instructions (December 2022)是一个基于种子示例自动生成的示例数据集,并要求生成第四个示例。通过让模型对每条指令重新措辞来扩充数据集。Self-Instruct (May 2023)引导语言模型自身生成。其想法是让模型生成指令,然后根据指令生成输入,最后生成输出。(实际中要更微妙。作者提供了8条随机采样指令,要求模型生成更多的任务指令,还删除了相同和近似指令。)合成生成的数据集往往包含更多噪声,可能导致模型比量更少但更精心整理的人类生成数据训练的模型健壮性要差。LIMA (May 2023)是一个更小的英语指令数据集,虽然只有一千个实例,但作者能够微调一个稳健的LLaMA模型。这得益于一个强大的预训练模型和非常仔细的训练数据策划。
这些只是指令微调模型大爆发的冰山一角。Flan-T5是使用FLAN数据集微调的T5模型。Alpaca是一个在InstructGPT生成的指令数据集上微调的LLaMA。WizardLM是一个在Evol-Instruct数据集上微调的LLaMA指令模型。ChatGLM2是一个在英语和中文指令上训练的双语模型。我们不断看到将强大的基础模型与多样化的指令数据集(可以是人类或模型生成的)相结合的模式。
Learning to Generate Task-Specific Adapters from Task Description (June 2021)是另一种提高泛化能力的方法。作者生成任务特定的参数,称为适配器。虽然适配器已经存在多年,但它们的采用最近在自然语言和图像生成中变得广泛。对于有数十亿参数的语言模型,许多人希望微调其领域或任务。下一节将讨论适配器。
回顾本节,指令微调的两个主要组件是强大的基础模型和高质量的指令数据集。指令数据集的质量对于模型至关重要。这些数据集可以是合成生成的(例如,使用自指令),手动生成的,或者两者的结合。研究一再表明,训练数据中表示的任务越多,模型越好。最后,指令模板可能会极大地影响最终性能。现有的数据集在任务的数量和多样性之间进行权衡。
适配器简介
下面深入探讨第四种方法:适配器。到目前为止,我们已经探讨了用于文本分类的DistilBERT微调和生成特定风格文本的GPT-2。这两者在微调过程中修改了模型的所有权重。微调比预训练更有效,因为我们不需要太多的数据或计算能力。然而,随着更大模型的趋势不断增长,在消费者硬件上进行传统微调变得不可行。此外,如果我们想为不同任务微调一个编码器模型,最终会有多个模型。
欢迎使用PEFT!参数高效微调(PEFT)是一组技术,使得在不微调所有模型参数的情况下调整预训练模型成为可能。通常,我们添加少量额外参数,称为适配器,然后微调它们,同时冻结原始预训练模型。这样有什么效果?
- 更快的训练和更低的硬件需求:在进行传统微调时,我们更新许多参数。使用PEFT,只更新适配器,与基础模型相比,其参数数量很少。因此,训练完成得更快,并且可以使用较小的GPU。
- 更低的存储成本:在微调模型后,我们只需要存储适配器,而不是每次微调都存储整个模型。当某些模型需要超过100GB的存储时,如果每个下游模型都需要再次保存所有参数,将无法很好地扩展。适配器可能是原始模型大小的1%。如果我们有100个微调的100GB模型,传统微调需要10,000GB的存储,而PEFT只需要200GB(原始模型和100个1GB的适配器)。
- 相当的性能:PEFT模型的性能通常与完全微调的模型旗鼓相当。
- 无延迟:很快就会看到,训练后,适配器可以合并到预训练模型中,这意味着最终的大小和推理延迟将是相同的。
听起来好得令人难以置信。如何实现的呢?有多种PEFT方法。最流行的包括前缀调优、提示词调优和低秩适配(LoRA),在本章中重点介绍LoRA。LoRA使用低秩分解将权重更新表示为两个较小的矩阵,称为更新矩阵。虽然这可以应用于transformer模型的所有块,我们通常只将它们应用于注意力块。
PEFT是一个使用transformers 和diffusers 实现这些技术的简单库。首先,来看如何为前一节的GPT-2模型构建一个适配器。第一步是创建一个PEFT方法的配置。对于LoRA,我们可以控制秩r
,它控制更新矩阵的大小。
ini
from peft import LoraConfig, get_peft_model
peft_config = LoraConfig(
r=8,
lora_alpha=32,
lora_dropout=0.05,
task_type="CAUSAL_LM",
fan_in_fan_out=True,
)
model = AutoModelForCausalLM.from_pretrained("gpt2")
peft_model = get_peft_model(model, peft_config)
peft_model.print_trainable_parameters()
csharp
trainable params: 294,912 || all params: 124,734,720 || trainable%: 0.2364
初始模型有接近1.25亿个参数,但其中只有约29.5万个会被训练。仅占原始模型大小的0.24%。PEFT的理念是我们可以引入这个模型,并获得与原模型相当的性能,而它的大小只有原模型的1/400。
底层原理是什么呢?在微调一个基础模型时,我们是在更新各层。计算更新矩阵可能会占用大量内存,因此LoRA尝试用更小的矩阵来更新矩阵达到近似效果。例如,假设有一个包含10,000行和20,000列的更新矩阵。这意味着更新矩阵有2亿个值。使用LoRA,我们用两个较小的秩为r
的矩阵来表示更新矩阵。假设秩为8,第一个矩阵A有10,000行和8列,而矩阵B有8行和20,000列(确保相同的输入和输出大小)。A有80,000个值,B有160,000个值。我们从2亿个值减少到240,000个值。这小了800倍!LoRA假设这些矩阵可以让权重更新矩阵近似度很高。(本例灵感来自Sebastian的优秀文章,)
我们谈到了r
参数。如前所述,它控制LoRA矩阵的维度,这会产生能力与过拟合之间的权衡。秩太高会导致适配器过于复杂而产生过拟合。秩太低则会导致性能不足。第二个关键参数是alpha
,它控制适配器对原始模型的影响程度。高alpha
会赋予适配器更大的权重。选择r
和alpha
的值取决于问题和模型。对于大语言模型,可以先让秩为8,且alpha始终是秩的两倍。
微调后,我们可以将LoRA权重合并回原始模型。这意味着运行推理所需的计算量有或没有LoRA是完全相同的。
<math xmlns="http://www.w3.org/1998/Math/MathML"> s c a l i n g = α r scaling = {\alpha \over {r}} </math>scaling=rα
<math xmlns="http://www.w3.org/1998/Math/MathML"> w e i g h t = w e i g h t + s c a l i n g × ( B × A ) weight = weight + scaling \times (B \times A ) </math>weight=weight+scaling×(B×A)
图5-1. LoRA图
LoRA(低秩适配器)如此小巧的好处在于它们变得非常便携且适合于生产环境。想象一下一个使用场景,用户期望聊天机器人或图像生成器能够生成10种不同风格,而这些风格对初始模型是未知的。我们可以根据需要加载和卸载适配器,而不是微调初始模型十次并临时加载模型。最新的一些技术,例如LoRAX,可以在单GPU上服务于超过一百个微调的适配器。
还有一些使用场景可能需要合并多个适配器。就像更新单个适配器一样,我们可以持续更新多个适配器。
<math xmlns="http://www.w3.org/1998/Math/MathML"> w e i g h t + = s c a l i n g 1 × ( B 1 × A 1 ) weight += scaling_1 \times (B_1 \times A_1 ) </math>weight+=scaling1×(B1×A1)
<math xmlns="http://www.w3.org/1998/Math/MathML"> w e i g h t + = s c a l i n g 2 × ( B 2 × A 2 ) weight += scaling_2 \times (B_2 \times A_2 ) </math>weight+=scaling2×(B2×A2)
<math xmlns="http://www.w3.org/1998/Math/MathML"> w e i g h t + = s c a l i n g 3 × ( B 3 × A 3 ) weight += scaling_3 \times (B_3 \times A_3 ) </math>weight+=scaling3×(B3×A3)
最后一个问题是应该更新LoRA的哪些参数。在更多模块中使用LoRA通常会带来稍好的性能,但也需要更多的内存,这可能是值得的事情,可以通过target_modules
参数来完成。我们可以在注意力模块中使用LoRA进行快速实验,这通常是PEFT库的默认设置(默认目标模型取决于模型架构)。也可以使用target_modules=all-linear
选择所有线性模块,排除输出模块。
虽然在本章中我们主要关注文本生成的微调,但PEFT在其他领域也被广泛使用,例如图像生成(我们将在第7章中探讨)、图像分割等。
量化简介
PEFT能以更少的计算和磁盘空间来微调模型。然而,推理期间模型的大小并没有减少。如果在推理一个有300亿参数的模型,仍然需要一个强大的GPU来运行它。例如,一个176B参数的模型如Bloom需要8个A100 GPU,这些GPU非常强大且昂贵(每个成本超过1.5万美元)。在本节中,我们将讨论一些技术,这些技术可以让我们在不降低性能的情况下使用更小的GPU来运行模型。
假设有一个70亿参数的模型。每个参数都有一个数据类型或精度。例如,float32
(FP32
,也称为全精度)类型以32位存储一个浮点数。70亿参数是2240亿位(70亿 * 32位),相当于28GB(2240亿 bit = 280亿字节 = 26吉字节)。FP32
允许以高精度表示广泛的数字,这对于预训练模型非常重要。
然而,在许多情况下不需要这样大的范围。此时,我们可以使用float16
(或FP16
,也称为半精度)。FP16
的精度和数字范围较低(最大可能的数字是64,000),这带来了新的风险:模型可能会溢出(如数字不在可表示的范围内)。
第三种数据类型是脑浮点(Brain Floating-Point),或bfloat16
。BF16
和FP16
一样使用16位,但以不同的方式分配这些位,以便为较小的数字(如神经网络权重中通常看到的那些)获得更高的精度,同时仍覆盖与FP32
相同的总区间。
使用全精度进行训练和推理通常会带来最佳结果,但速度显著较慢。对于训练,人们已经找到了进行混合精度训练的方法,这提供了显著的加速。在混合精度训练中,权重以全精度作为参考,但以半精度进行运算。使用半精度来更新全精度权重。精度对推理没有显著影响,因此我们可以使用半精度加载模型。PyTorch默认情况下以全精度加载所有模型,因此如果我们想使用float16
或bfloat16
,需要在加载模型时指定类型,传递bfloat16
参数。
ini
model = AutoModelForCausalLM.from_pretrained("gpt2", torch_dtype=torch.float16)
加载7B模型时,如果每个参数使用16位而不是32位,则需要14GB的GPU内存,这对于某些消费级GPU完全没问题。像Llama、Mistral和Zephyr这样的7B模型已经成为消费级GPU的流行解决方案,但还有带更多参数的优秀模型。例如,如果我们想在半精度下使用一个34B的模型,则需要68GB的GPU,这远远超出了任何消费级GPU的范围。我们有什么办法可以使用这些模型吗?
直观上,我们可以认为直接减少数字的范围或精度以达到四分之一精度(每个参数使用一个字节,即8位)。不幸的是,这样做会导致显著的性能下降。我们可以通过8位量化实现四分之一精度。8位量化技术的核心思想是将一种类型的值(如fp16
)映射到int8
,这样可以表示[-127, 127]或[0, 256]范围内的值。
有不同的8位量化技术。我们来探讨一下绝对值最大量化(absmax quantization)。给定一个向量,我们首先计算其最大绝对值。然后将127(最大可能值)除以这个最大值。这会产生一个量化因子------在我们用向量乘以这个因子时,可以保证最大的值为127。可以对数组进行反量化以恢复原始数值,但精度会丢失。通过运行代码可以更好地理解这一点:
python
import numpy as np
def scaling_factor(vector):
m = np.max(np.abs(vector))
return 127 / m
array = [1.2, -0.5, -4.3, 1.2, -3.1, 0.8, 2.4, 5.4]
alpha = scaling_factor(array)
quantized_array = np.round(alpha * np.array(array)).astype(np.int8)
dequantized_array = quantized_array / alpha
print(f"Scaling factor: {alpha}")
print(f"Quantized array: {quantized_array}")
print(f"Dequantized array: {dequantized_array}")
print(f"Difference: {array - dequantized_array}")
yaml
Scaling factor: 23.518518518518515
Quantized array: [ 28 -12 -101 28 -73 19 56 127]
Dequantized array: [ 1.19055118 -0.51023622 -4.29448819 1.19055118 -3.10393701 0.80787402
2.38110236 5.4 ]
Difference: [ 0.00944882 0.01023622 -0.00551181 0.00944882 0.00393701 -0.00787402
0.01889764 0. ]
这些差异会导致性能退步。因此,传统的量化技术在处理数十亿参数的模型时未能成功。LLM.int8()是一种允许我们进行8位量化而不退化的技术。该技术的核心思想是提取异常值,即超出某些范围的值,并在FP16
中计算这些异常值的矩阵乘法,而其余部分使用int8
。这种混合精度结构允许我们在8位下处理99.9%的值,并在全精度或半精度下处理1%的值,从而不导致性能下降。那么有什么问题呢?LLM.int8()
的主要目标是减少运行模型推理所需的大型GPU资源。由于额外的转换开销,推理的速度会比使用fp16
慢(慢15-30%)。需要注意的是,虽然近年来的所有GPU都提供了int8
的张量核支持,但一些旧的GPU可能对此支持不佳。
低精度推理的边界正在被新的4位和2位量化技术推动。甚至还有使用小于1位量化的探索。实现无降级的量化是非常令人兴奋的研究领域,特别是模型越来越大。
在本节的开始,我们需要28GB的GPU来加载一个7B参数的模型。现在,我们可以在没有退化的情况下,用7GB的GPU加载相同的模型,代价是推理速度会稍有下降(但并不是太多)。以8位加载模型只需添加load_in_8bit
即可。
ini
from transformers import AutoModelForCausalLM
from transformers import BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(load_in_8bit=True)
model = AutoModelForCausalLM.from_pretrained("gpt2", quantization_config=quantization_config)
除了量化之外,我们还可以采取其他措施来处理非常大的模型。一种流行的推理技术叫做offloading。如果一个模型太大无法装入GPU,可以将其分成多个检查点分片,这些分片由transformers自动处理。这样做有什么好处?如果模型太大,我们可以只加载适当的层或分片,并将其他操作卸载到CPU RAM中,但这样速度要慢得多。这使我们能够处理任何大小的模型,但推理速度的降低使得其对许多大型模型不太可行。
小结
我们来回顾一下PEFT和量化。
- PEFT使我们通过添加适配器和冻结基础模型权重来使用更少的计算资源进行微调。这加速了训练,因为只有少量的权重是可更新的。
- 量化让我们使用比存储时更少的位来加载模型。这减少了加载和运行推理所需的GPU资源。
为什么不同时使用这二者呢?设想一下用8位训练一个模型。不幸的是,如前所述,在预训练或微调大模型时,高精度值是很重要的。另一方面,PEFT冻结了基础模型,只使用一个小的适配器,因此我们可以在这里使用较低的精度,同时达到相同的性能。
QLoRA允许我们用较小的GPU微调大模型。这种技术与LoRA非常相似,但结合了量化。首先,基础模型被量化为4位并冻结。然后,添加LoRA适配器(两个矩阵)并保持在bfloat16
。当进行微调时,QLoRA使用4位存储的基础模型和16位半精度模型进行计算。
再次加载4位模型只需要传递load_in_4bit
参数和device_map="auto"
。我们用Mistral 7B来试试这个方法。
注 :我们在本节使用的是默认值,但transformers允许通过使用
BitsAndBytesConfig
对量化技术进行更细粒度的控制,这使得改变计算类型、嵌套量化等成为可能。
ini
model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1",
load_in_4bit=True,
device_map="auto",
)
QLoRA只是我们工具箱中的一个工具,但不是银弹。它显著减少了GPU需求,同时保持了相同的性能,但也增加了训练模型所需的时间。PEFT部分的所有优点在这里同样适用,使得QLoRA在社区中成为快速微调7B模型的一种流行技术。
我们来做一次快速的QLoRA微调,让生成式模型以特定风格生成文本。以下是各个组件的介绍:
- 模型 :我们将使用Mistral模型。Mistral是一个非常高质量的7B模型。我们使用
load_in_4bit
和device_map="auto"
来加载模型并进行4位量化。 - 数据集:我们将使用Guanaco数据集,该数据集包含人类与OpenAssistant模型之间的对话并经过人工打分。
- PEFT配置 :我们将指定一个具有较好初始默认值的
LoraConfig
:秩 (r
)为8,alpha
为其两倍。 - 训练参数:和之前一样,我们可以配置训练参数(如评估频率和训练轮次)以及模型的超参数(学习率、权重衰减或训练轮次)。
最后一个组件是Trainer
。在前面的部分中,我们使用了transformers的Trainer
,这是一个通用工具,可以封装PyTorch训练代码并添加方便的实用工具来共享模型。当微调一个用于自回归技术的大型语言模型(LLM)时,trl库的SFTTrainer
类是一个不错的工具。它是Trainer
的一个封装,优化了文本生成。它包含有用的LoRA和量化技术以及简单的数据集加载和处理工具。和之前一样,我们可以传递模型(现在已量化)和数据集(我们将使用300个样本进行快速训练)。SFTTrainer
已经带有默认的整合器和数据集工具,因此不需要对数据进行分词和预处理。唯一需要指定的是哪个字段包含训练文本(使用dataset_text_field
)。最后,我们传递一个PEFT配置。
ini
from trl import SFTTrainer
from datasets import load_dataset
from peft import LoraConfig
from transformers import TrainingArguments
dataset = load_dataset("timdettmers/openassistant-guanaco", split="train")
peft_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.05,
task_type="CAUSAL_LM",
)
training_args = TrainingArguments(
"sft_cml5",
push_to_hub=True,
per_device_train_batch_size=8,
weight_decay=0.1,
lr_scheduler_type="cosine",
learning_rate=5e-4,
num_train_epochs=2,
evaluation_strategy="steps",
eval_steps=200,
logging_steps=200,
gradient_checkpointing=True,
)
trainer = SFTTrainer(
model,
args=training_args,
train_dataset=dataset.select(range(300)),
dataset_text_field="text",
peft_config=peft_config,
max_seq_length=512,
)
trainer.train()
scss
trainer.push_to_hub()
以上代码可能需要一个小时或更长时间来运行。别忘了,QLoRA还会导致训练速度变慢。当推送模型时,我们只推送了适配器。来看看如何使用模型和适配器进行推理。
ini
from peft import PeftModel
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1",
torch_dtype=torch.float16,
device_map="auto",
)
model = PeftModel.from_pretrained(
model,
"AlanHou/sft_cml5",
torch_dtype=torch.float16,
)
model = model.merge_and_unload() # This is the main difference
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
pipe("### Human: Hello!###Assistant:", max_new_tokens=100)
以上代码会输出类似下面的内容:
shell
### Human: Hello
###Assistant: Hello!
### Human: How are you?
n###Assistant: I'm doing well, how are you?
### Human: I'm good, thanks!
###Assistant: That's great to hear!
### Human: What's the weather like today?
###Assistant: It's currently 72 degrees and sunny in San Francisco.
真棒!我们刚刚在无需大规模GPU的情况下将一个7B模型微调为对话模型。
项目实操:检索增强生成 (RAG)
大型语言模型(LLMs)只包含用于训练数据或传递给它们的上下文数据。如果想问LLM关于特定主题的信息,只有在其数据中包含该答案时才知道。RAG是一种技术,模型可以访问存储的文档,因此LLM的回答会结合用户输入和这些文档。可惜文档数量可能有数百万,因此不能将它们全部传递给模型。为解决这个问题,我们使用嵌入模型(例如第1章的句子变换器)将文档编码为向量,并将这些向量存储起来(通常称为向量数据库)。
然后我们使用最近邻搜索找到与用户输入最相似的文档。最后,我们将用户输入和检索到的文档传递给LLM。这种方法非常强大,因为它允许模型访问大量信息,并且比重新训练模型更容易更新信息。
我们的目标是构建一个管道,其中:
- 用户输入一个问题
- 管道检索与问题最相似的文档
- 管道将问题和检索到的文档传递给LLM
- 管道生成回答
我们无需为此任务训练任何模型。对于检索,建议使用sentence_transformers预训练模型。对于生成,可以使用你喜欢的模型,例如我们在前一节中训练的模型。
总结
本章探讨了微调大型语言模型的不同技术。我们首先通过传统微调对编码器模型进行文本分类。不过,同样的方法也可用于其他任务,例如从给定文本中回答问题及识别文本中的实体。然后我们探讨了如何微调解码器模型以生成文本。我们还讨论了微调与零样本或少样本生成的优缺点。并且还了解了指令微调如何使生成式模型开箱即用地处理众多任务。
尽管这些技术非常强大,但扩展到越来越大的最新模型是具有挑战性的。因此,我们探讨了如何使用量化技术在较小的GPU上运行大模型的推理。我们还讨论了PEFT技术,以减少计算和磁盘空间需求来微调模型。随后,结合这两种技术微调了一个7B模型,使其成为对话模型。
现在,读者已经掌握了为自有任务微调大型模型的基础知识。新的量化和PEFT技术不断被开发,但基础和目标保持不变。鼓励读者尝试不同的模型和数据集,看看它们的表现。
练习
- 基础模型和微调模型有什么区别?对话模型属于哪种类型?
- 在什么情况下你会选择基础编码器模型进行微调?
- 解释微调、指令微调和QLoRA之间的区别。
- 使用适配器会导致模型体积变大吗?
- 加载一个70B模型在半精度、8-bit量化和4-bit量化下需要多少GPU内存?
- 为什么QLoRA会导致训练速度变慢?
- 在微调过程中,我们在什么情况下会冻结模型权重?
挑战 - 问答任务 。我们解决一个问答任务。目标是接收问题和上下文作为输入,模型输出答案在上下文中的位置。例如,给定问题
What's the weather like today?
和上下文It's currently 72 degrees and sunny in San Francisco.
", 模型应该输出72 degrees and sunny
。SQuAD是一个适合该任务的数据集。如何使用编码器来实现呢?