LLM工程师手册——监督微调

监督微调(SFT) 是为大型语言模型(LLM)准备实际应用的关键步骤。在初始预训练阶段,LLM 学习如何预测序列中的下一个标记,而通过 SFT 微调则可以利用精心编排的指令和对应的答案对,进一步优化模型的能力。此过程有两个主要目的:一是教会模型理解并遵循特定的对话格式,使其成为有效的对话代理;二是让模型在广泛的知识基础上,专注于特定任务或领域,从而在特定应用中表现更出色。

SFT 的重要性在于,它能够弥合模型通用语言理解与实际应用之间的差距。通过向模型提供理想输入输出模式的示例,SFT 可将 LLM 的行为调整为特定目标,适用于任务完成(例如摘要生成或翻译)或领域专长(如医学或法律知识)。这种定制化方法不仅提升了模型在目标领域的表现,还改善了其执行指令的能力,生成更具相关性和连贯性的响应。

在本章中,我们将涵盖以下主题:

  • 创建高质量的指令数据集
  • SFT 技术
  • 微调的实际实施

读完本章后,您将能够创建自己的指令数据集,并高效地在这些数据集上对 LLM 进行微调。

本章的所有代码示例可在 GitHub 上找到:链接

创建指令数据集

在大多数使用场景中,创建指令数据集是微调过程中最困难的部分。这由多种因素导致。虽然大多数使用场景可以关联到原始文本,但很少能找到自然成对的指令和答案。这些原始文本需要转换为包含指令和答案的格式。此外,数据的质量也至关重要。因此,通常需要投入大量时间手动检查和验证各个样本。通过这种细致的审核,可以确保数据集的准确性和实用性,从而更好地用于模型训练。

本节将介绍一个通用框架,用于创建自己的指令数据集,无论最终的使用场景如何。随后,我们将利用第 3 章抓取的数据,并将其转换为指令数据集。数据生成管道的不同阶段概述在图 5.1 中。

通用框架

指令数据集被定义为指令和答案的配对。指令是模型的输入,在微调过程中用作上下文,答案则是模型的预期输出。在微调时,可以选择使用指令和答案对模型进行训练,或者仅训练答案。指令和答案对遵循一定的模板。某些指令模板(如 Alpaca)引入了额外字段,如"inputs"和"system"。它们都可以视为指令字段的子字段。其中,"inputs"包含模型完成指令所需的数据,"system"是一个元提示,用于引导模型的整体行为。以下是来自 SlimOrca 数据集的一个示例,包含"system"和"instruction":

System 你是一位乐于助人的助手,总是提供解释。假设你在回答一个五岁的小孩。

Instruction 概念:建筑物、商店、小镇

写一个包含这些词的句子。

Output 在我们的小镇上,有一栋大楼里有一家商店,人们去那里买他们最喜欢的玩具和糖果。

表 5.1 -- Open-Orca/SlimOrca 数据集的示例

该示例展示了"system"字段如何用于定义模型的特定行为,例如乐于助人、提供解释,并将回答调整为适合五岁小孩的语言。"instruction"字段提供了必要的数据(概念)和任务(构建句子)。输出字段则展示了预期的答案,尽管这不是唯一可能的答案,但它代表了高质量的回应。

为了构建一个指令数据集,我们希望收集能代表模型实际使用场景的数据。一旦我们收集了足够的样本,目标就是筛选出高质量的数据。在这个背景下,高质量数据可通过以下三个主要维度描述:

  • 准确性:指样本的事实正确性和相关性。在指令数据集的上下文中,这意味着确保回答不仅在事实层面上是正确的,还要与其相应的指令相关。高准确性对于训练能够提供可靠且值得信赖的信息的模型至关重要。
  • 多样性:一个高质量数据集应涵盖广泛的使用场景,涵盖模型可能遇到的潜在查询和任务。这种多样性应包括主题、上下文、文本长度和写作风格的多样性。通过代表性地采样数据,我们使模型能够发展出更强的指令执行能力。
  • 复杂性:简单或过于浅显的样本对提高 LLM 的能力帮助不大。数据集应包含复杂的多步骤推理问题和具有挑战性的任务,以推动模型预期处理的能力边界。这种复杂性有助于开发能够应对复杂现实问题的模型。

在接下来的部分中,我们将学习根据这些维度过滤和评估指令样本的技术。

数据量

Hugging Face Hub 上有许多指令数据集,既有通用数据集,也有针对特定任务或领域的数据集。在处理新的用例时,寻找相关的开源数据集用于微调会很有帮助。如果你的样本数量较少(例如少于 1,000 个),尤其需要通过高质量数据进行补充以增加数据量。

确定理想的样本数量是一项复杂的任务,因为数据质量和模型大小都会产生显著影响。对于大型模型(例如,约 700 亿参数的模型),所需的样本数可以少至 1,000 个高质量样本(参见参考文献中的 LIMA 论文)。而对于较小的模型(例如,约 70 亿参数的模型),它们需要更多样本来学习正确的对话模板。因此,数据质量始终是关键因素,而样本数量越多越好。

要进一步说明,我们可以参考公司和开源社区开发的微调模型。微调模型大致可以分为两种类型:通用模型,旨在再现像 GPT 这样的模型能力;以及任务或领域特定模型,旨在优化特定应用的表现。

通用模型涵盖更多主题,因此需要更多样本。公司之间在样本数量上差异较大。例如,01-ai 的 Yi 模型使用不到 10,000 个样本。而在另一端,Meta 报告称,在整个微调过程中(包括偏好对齐),Llama 3 使用了 1,000 万个样本。在开源社区中,像 OpenHermes 和 Dolphin 这样的模型使用约 100 万个样本。基于这些微调模型的质量,建议至少使用 100 万个样本来创建一个好的通用指令模型。

另一方面,特定用途的模型则需要较少的样本。在这里,我们区分任务特定模型和领域特定模型。

任务特定模型和领域特定模型代表了微调 LLM 的两种不同方法。任务特定模型专注于特定功能,如翻译、摘要生成或情感分析。这些模型通过专注于单一任务的训练,能够在较小模型(通常少于 80 亿参数)上实现高效性能。任务特定微调的数据需求通常较少,范围在 100 到 100,000 个样本之间。这使得任务特定微调成为资源有限应用的理想选择。

领域特定模型则旨在通过特定领域的知识和专业词汇来调整 LLM。这些模型在医学、法律、金融、电子商务、工程和酒店管理等领域中尤为有用。领域特定微调的数据需求因领域的复杂性和广度而异。一些领域(如医学或法律)可能需要接近通用微调的数据量,因为这些领域的技术语料库非常庞大。其他领域(如电子商务或酒店管理)可能只需较少样本,数据需求更接近任务特定微调。

决定领域特定模型数据需求的关键因素是领域的"规模"(即其专业知识和词汇的广度)以及该领域在模型预训练数据中的表示程度。若领域在原始训练数据中已有较好的表示,可能需要较少微调;而对于那些更专业或在训练数据中表现不足的领域,则可能需要更大规模的数据集。即使是开源的 LLM,许多预训练数据集也是闭源的,因此需要通过合理推测来判断其组成(例如,30% 代码或 20% 数学)。

数据整理

在为微调获取数据时,任务特定模型和领域特定模型的数据整理方法有所不同。对于任务特定模型,数据整理通常涉及从现有数据集中收集所需任务的示例,或创建新的数据集。例如,摘要生成模型可能需要收集原文和摘要的配对,而翻译模型则需要收集不同语言的句子。

领域特定的数据整理则更具挑战性。它通常需要与领域专家合作,收集和验证相关的文本、研究论文、技术文档以及其他领域特定内容。有时还需要与拥有大量专业信息资源的组织或机构合作。数据的质量和相关性至关重要,因为它直接影响模型在目标领域内理解和生成内容的能力。

值得注意的是,少样本提示(Few-Shot Prompting)已经成为微调的替代策略,尤其适用于任务特定应用。这种方法通过在输入提示中提供一些示例,利用大型强大模型的能力来执行特定任务。虽然在所有场景下(例如学习新领域)它不能完全替代微调,但少样本提示可以成为一种高效的方法,将模型适应到新任务中,而无需进行大量额外训练。

在实际应用中,任务特定模型和领域特定模型之间的界限有时会变得模糊。例如,一个专门用于医疗诊断的模型既可以被视为任务特定(专注于诊断),也可以视为领域特定(专注于医疗知识)。关键在于了解微调过程的主要目标,并据此调整方法。

在这一阶段,我们应当已收集到适合用例的数据集。下一步是通过基于规则的筛选、数据去重、数据净化以及数据质量评估来提升样本的质量。

基于规则的筛选

基于规则的筛选 是一种系统的数据质量控制方法,依赖明确的预定义规则来评估和筛选数据样本。通常,这些规则用于处理常见的质量问题,既可以是简单的检查,也可以是复杂的逻辑操作。基于规则的筛选的主要目标是通过移除不符合特定标准的样本,保持数据质量的高标准。

  • 长度筛选:这是一个简单但有效的基于规则的筛选方法,通过设定响应的长度阈值来控制数据样本的长度。过短的响应往往缺乏足够的信息,而过长的响应可能包含无关或冗余内容。应注意,不同任务和领域对长度的阈值要求可能差异很大。例如,生成简洁摘要的数据集可能需要较低的最大阈值,而详细解释的数据集则可以容许较长的内容。
  • 关键词排除:关键词排除是一种强大的内容筛选方法,专注于样本的内容而非结构。它通过设定关键词或短语列表来排除低质量或不适当的内容,包括低俗语言、垃圾信息词汇或与主题无关的词语。例如,在为专业写作助手准备的数据集中,可能会排除包含俚语或非正式表达的样本,因为这些不符合预期的语气和风格。
  • 格式检查:对于包含结构化数据或有特定格式要求的数据集,格式检查是推荐的筛选方法。这一方法确保所有样本遵循预期格式,保证数据的一致性并便于后续处理。对于包含代码样本、JSON 结构或其他格式化文本的数据集,格式检查尤为重要。例如,在编程指令和解决方案的数据集中,可以通过规则来验证代码样本的语法正确性和样式一致性。

基于规则的筛选在准备指令数据集时具有显著优势。它的速度和效率允许快速处理大量数据,具备高度的可扩展性。规则应用的一致性保证了数据的统一处理,减少了人为错误和偏差。此外,明确的筛选标准定义了透明度和可解释性,便于理解、审计和调整。自动化的基于规则筛选减少了手动干预的需求,并实现了数据质量的持续监控。

然而,基于规则的筛选也有一些需要注意的局限性。预定义的规则可能缺乏捕捉语言和上下文复杂性的精确度,导致有效但不寻常的样本被删除。规则通常是二元的(通过/未通过),这并不总能与语言和指令质量的微妙性完全吻合。此外,随着数据模式和质量标准的发展,规则需要定期更新以保持有效性。不当设计的规则还可能无意中引入或加剧数据集中的偏差。

数据去重

数据集的多样性对于训练能很好泛化到新数据的模型至关重要。当数据集中包含重复或近似重复的数据时,会导致以下问题:

  • 过拟合:模型可能会记住特定示例,而不是学习通用模式。
  • 性能偏差:过多出现的数据点可能会导致模型偏向某些输入类型。
  • 训练效率低下:冗余数据增加了训练时间,但并未提供额外有价值的信息。
  • 评估指标膨胀:测试集中重复的数据可能导致性能估计过于乐观。

去重可分为精确去重模糊去重。精确去重通过数据规范化、哈希生成和去重等步骤移除相同样本。数据规范化可以标准化条目格式,如将文本转换为小写。接着使用 MD5 或 SHA-256 等算法生成每个条目的唯一哈希,比较哈希以找到匹配项,并去重,仅保留每个条目的一份。虽然精确去重适用于完全相同的条目,但它无法检测近似重复或语义相似内容,因此需要更先进的技术来处理这些情况。

模糊去重最常用的方法是MinHash 去重。与其他模糊去重技术相比,MinHash 在保持高精度的同时显著降低了计算复杂性。MinHash 通过生成数据项的紧凑表示或签名来简化数据维度,将其转换为易于比较的指纹。这些签名可以用 Jaccard 相似度等度量来有效识别近似重复项。

除了精确和模糊去重,语义相似性去重则专注于文本的含义。该方法将单词或样本转换为向量表示,常用的词嵌入模型包括 Word2Vec、GloVe 和 FastText。对于上下文更敏感的表示,可以使用 BERT、句子转换器或交叉编码器生成句子或文档的嵌入。获取向量表示后,通过余弦相似度或欧几里得距离等方法比较相似性,高于阈值的样本可视为重复。对于大数据集,可以应用聚类技术(如 K-means、DBSCAN 或层次聚类)将相似向量分组,每个聚类中保留一个代表样本,其他标记为重复。

数据净化

数据净化旨在确保训练数据集中不包含与评估或测试集相同或高度相似的样本。这一步对于确保模型评估质量、防止过拟合或记忆测试数据至关重要。

数据净化可以使用去重技术。首先,可以使用精确匹配移除训练集中与评估集完全相同的样本,通过哈希函数或字符串比较完成。接下来,还可以使用近似重复检测方法(如 MinHash 或基于 n-gram 或嵌入的相似度计算)识别和移除与评估样本非常相似的训练样本。

一种简单的净化方法是在数据去重阶段将评估集加入指令数据集中。这样可以确保仅从指令数据集中移除重复样本,具体实现方式包括仅过滤掉第一个重复样本或记录评估样本的索引等。理想情况下,可以在数据去重阶段自动添加评估集,从而完全自动化该过程,对于多版本基准测试尤为高效。

数据净化的另一方面是排除与评估数据来源相同的样本,可以通过检查重叠的短语、相似的句子结构或公共元数据来实现。实践者还可以使用来源跟踪(追踪数据来源),以识别并排除已知用于评估的数据来源。

数据质量评估

数据质量评估是机器学习的重要组成部分,尤其对于 LLM 来说。该过程涉及评估数据集的各种特性,包括准确性、多样性和复杂性。对于数学准确性等方面可以使用 Python 解释器等工具轻松验证,但评估主观或开放式内容则具有挑战性。

传统的数据质量评估方法包括人工标注,尽管准确性较高,但资源消耗较大。为解决可扩展性问题,机器学习技术也被开发用于自动化评估过程,这包括使用 LLM 作为评估工具、奖励模型以及用于质量预测的分类器。

LLM 作为评估工具的策略是通过提示 LLM 来评估每个样本的质量。这种方法因其灵活性和易用性而流行,但也存在一些挑战。不同的 LLM 在不同任务上的表现不同,它们的评估往往更接近非专家的观点。对于领域特定数据集,建议使用领域专用模型而非通用模型。相对评价方法(例如"答案 A 是否优于答案 B?")通常比绝对评分方法(例如"给答案 A 评分 1 至 4")效果更好。建议在代表性子集上反复调整提示,手动验证响应质量。

示例提示

指令

你是数据质量评估员。你的任务是评估一条指令及其相应的答案,并判断答案对给定任务的有效性。

在评估中,你需要提供答案的优缺点反馈,随后在 1 到 4 的范围内给出评分。

  • 1 分:答案很差,与指令无关。
  • 2 分:答案无帮助,遗漏了指令中的重要部分。
  • 3 分:答案有帮助,但在相关性、准确性和深度方面可以改进。
  • 4 分:答案非常好,完全满足任务要求。

反馈 :相关优缺点
评分:1 至 4 之间的数字

表 5.2 -- LLM 作为评估工具的提示示例

LLM 作为评估工具已知存在一些偏差。首先,在相对评分中存在位置偏差,LLM 倾向于优先选择第一个答案,可以通过随机化答案顺序来缓解。此外,LLM 与人类一样,倾向于长答案。可以通过长度规范化技术来减轻此问题。最后,LLM 对同一模型家族的回答有偏好,可以通过使用多个模型来缓解此问题。

奖励模型是另一种将 LLM 用于数据质量评估的方法。奖励模型源于人类反馈强化学习(RLHF),其输出为一对指令和答案的评分。通常,通过在解码器架构(如 Gemma 或 Llama)之上添加线性层来创建奖励模型。奖励模型通过强化学习或传统微调进行训练。例如,ArmoRM-Llama3-8B-v0.1 模型在 Llama 3 8B 模型之上添加了回归和门控层,输出针对特定维度的评分,如帮助性、正确性、一致性、复杂性和冗长性,从而实现更精细的数据质量评估。

Allen Institute for AI 在 Hugging Face 上托管的 RewardBench 排行榜(allenai/reward-bench)是比较不同奖励模型的良好资源。它结合了各种类型的奖励模型(生成模型、分类器、DPO 等),并基于每条指令的精选答案和被拒答案进行评估。尽管该任务并非直接用于指令数据质量评估,但这是寻找能够区分优劣答案的模型的良好资源。

分类器 或仅编码器模型也可以用于数据质量评估。例如,HuggingFaceFW/fineweb-edu-classifier 是一个用于评估网页教育价值的分类器。该模型最初用于预训练数据的质量过滤,但相似的方法也可以应用于大规模指令样本的评估。实际上,fineweb-edu-classifier 将分类头添加到一个嵌入模型(Snowflake/snowflake-arctic-embed-m)上,并在由 Llama 3 70B Instruct 标注的 45 万个样本上训练 20 个周期。

这种方法依赖于仅编码器模型,它们体积较小且更适合分类任务。由于参数数量较少,这些模型运行速度较快,可以扩展到数百万样本。然而,它们在准确性上不如更大的模型,尤其是在复杂的推理任务中,缺乏捕捉细微之处的能力。在小规模应用中,仅编码器模型在过滤异常值或作为自动化数据管道的一部分时仍然非常有用,能够提高处理速度。

数据探索

数据探索是一个持续的过程,需要从业者熟悉训练数据。这包括手动检查和自动分析,每一部分都在理解数据集的特征、优点和潜在缺陷中发挥关键作用。

尽管耗时,手动数据集探索是一个重要步骤。它揭示了自动化流程可能遗漏的错误和不一致之处,包括格式问题、数据录入错误、不连贯的推理和事实错误。该过程为数据集的内容和风格提供了定性见解。为了提高效率,研究人员可以使用分层抽样(选择具有多样性的样本)、系统审查(使用标准检查表)和协作审查(多位审阅者参与)等技术。

图 5.4 显示了使用 Argilla 的示例,Argilla 是一个用于手动数据质量评估和探索的协作平台。

统计分析是一种补充技术,可以揭示词汇多样性、潜在偏差和概念表示情况。该过程使用如 NLTK 或 spaCy 等自然语言处理库进行分词和大规模文本分析。可视化工具(如 Matplotlib 或 Seaborn)可以生成直方图和词云,从而帮助直观地识别模式。这些技术可以提供关于数据集组成、语言广度以及可能的文化或上下文偏好的见解,这些因素会影响模型的输出。

主题聚类则通过自动将相似的文档或文本片段分组,揭示数据中潜在的主题和模式。这一过程对于理解大型文本语料库的内容、识别趋势以及有意义地组织信息尤为重要。主题聚类通常与数据可视化结合,生成显示相似样本聚类的图表。

例如,假设需要构建一个关于各种编程语言的指令数据集。你已经从在线论坛、文档和教程中收集了大量与编程相关的文本。首先,主题聚类可以帮助识别数据集中存在的不同编程语言(如 Python、JavaScript 等)。其次,在每个语言聚类中,还可以进一步识别子主题,如错误处理、数据结构和 Web 框架。这确保在语料库中平衡地呈现每种语言及其子主题。

这样可以确保每个主题在每种编程语言中都得到了正确的覆盖。

用于主题聚类的工具有多种,每种工具都有其优势和不同的实现方法。例如,Hugging Face 的文本聚类工具提供了一个简单的管道,结合句子转换器将文本嵌入到向量空间,UMAP 用于降维,DBSCAN 用于聚类。该工具还可以使用 LLM 自动为聚类标记标签,并生成可视化输出。此外,Nomic Atlas(见图 5.5)、BunkaTopics 和 Lilac 也提供了类似的方法,且附带了其他功能。

数据生成

当现有指令数据集不足时,就需要创建自定义数据。这在公开数据较为稀缺的专用应用中尤为重要。数据生成也是扩充数据集中代表性不足领域的有效方法,比如在之前的例子中补充 JavaScript 错误处理技术的示例。尽管数据可以通过个人手动创建或众包完成,这些方法通常需要高昂的成本和大量时间投入。通过 LLM 进行合成数据生成提供了一种更高效、可扩展的替代方案。结合设计良好的提示工程,该方法可以大规模生成高质量数据,有效地弥补手动数据创建的局限。

合成数据生成的过程通常从准备一组精心设计的提示开始(有时称为分类体系)。这些提示为生成新的多样化示例奠定基础。表 5.3 列出了一些原始 Alpaca 数据集中的种子提示。生成数据的质量在很大程度上取决于生成过程中的提示和技术。精心设计的提示能够引导语言模型生成多样化、相关且高质量的指令-响应对。这些提示通常包含特定的指令、示例和限制条件,以确保生成的数据符合所需的格式和内容。

种子指令示例

  • 有没有不含鸡蛋、却含有蛋白质且约 700-1000 卡路里的早餐食物?
  • 给定对之间的关系是什么?输入:Night : Day :: Right : Left
  • 为以下每个人生成一句话描述。输入:-巴拉克·奥巴马\n- 埃隆·马斯克\n- 泰勒·斯威夫特
  • 描述一种这种刻板印象会伤害你的情况。输入:所有亚洲人都聪明!
  • 为以下电子邮件生成合适的主观标题:输入:"嗨,[人名],\n\n我写这封邮件是想问你是否愿意作为我们在 CVPR 上举办的多模态研讨会的讲师。研讨会将于 2023 年 6 月 20 日举行。\n\n致敬,\n[我的名字]"

表 5.3 -- 原始 Alpaca 数据集中使用的种子提示示例

许多合成数据生成管道包含多个步骤以确保数据质量。这可能包括生成一组初始问题或指令,然后生成相应的答案或响应。一些系统还实现了验证步骤,由另一个模型或一组规则检查生成的指令-响应对是否符合准确性、相关性和指定的标准。

合成数据生成的一个重要方面是控制生成数据的各种属性。这包括指令的复杂性、响应的长度、语言的语气或风格以及涉及的特定主题或领域。通过微调这些参数,可以创建符合特定训练目标的数据集,或针对性地补充现有数据集。使用 Outlines 等库进行结构化生成也有助于确保数据符合特定格式。

此外,合成数据生成对于解决现有数据集中的偏差和不足尤其有用。通过精心设计的生成过程,可以创建更平衡、包容的数据集,代表更广泛的视角、主题和语言风格,从而有助于训练更公平、适合多元用户群体的 LLM。

然而,合成数据生成也带来了一些挑战。一个主要问题是生成的数据可能会继承用于生成的底层语言模型中的偏差或错误。为减少这种情况,许多方法结合了人为监督、多样化提示和附加的过滤机制,以确保生成数据的质量和适当性。

另一点考虑是生成数据的多样性和挑战性。如果合成数据过于简单或重复,可能无法为训练强大的 LLM 提供所需的复杂性。高级合成数据生成技术通常侧重于创建多样化和细致的指令-响应对,以推动模型学习能力的边界。

数据增强

在本节中,数据增强指的是增加数据样本的数量和质量的过程。与数据生成不同,数据增强使用现有的指令样本作为输入。尽管可以对指令和答案对进行重采样,数据增强的主要目的是提升现有样本的质量,尤其关注多样性和复杂性两个方面。

Evol-Instruct 是该领域的开创性方法之一,利用 LLM 将简单指令演化为更高质量的指令。演化后的指令可以使用强大的 LLM 生成答案。该方法包括两种主要策略:深度演化广度演化

  • 深度演化:专注于提升现有指令的复杂性,采用以下几种技术:

    • 增加约束:引入额外的要求或限制,使指令更具挑战性。
    • 深化问题:替换表面问题为更深入的问题,需要更详尽的回答。
    • 具体化:将通用概念替换为更具体的概念,增加指令的细节和精确度。
    • 增加推理步骤:修改指令,显式要求多步骤推理,促进更复杂的问题解决。
    • 复杂化输入:在指令中添加更复杂的数据格式或结构,如 XML、JSON 或代码片段。
  • 广度演化:旨在扩展指令数据集的多样性,生成受现有指令启发的全新指令,重点是创建更多稀有或长尾示例。

以下是一个具体的实现示例,来自 AutoEvol 论文的深度演化提示。只需提供待演化的指令,GPT-4o 等强大模型即可返回原指令的复杂版本。

less 复制代码
你是一位指令重写器,将给定的 #Instruction# 重写成更复杂的版本。请按以下步骤将 "#Instruction#" 重写为更复杂的版本。

第 1 步:仔细阅读 "#Instruction#",列出所有可能的方法以使此指令更加复杂(使其对于如 ChatGPT 和 GPT-4 等知名 AI 助手更具挑战性)。请勿提供改变指令语言的方法!

第 2 步:根据第 1 步生成的 #Methods List# 创建一个综合计划以使 #Instruction# 更复杂。计划应包括来自 #Methods List# 的多种方法。

第 3 步:逐步执行计划并提供 #Rewritten Instruction#。#Rewritten Instruction# 仅允许在 "#Instruction#" 中增加 10 到 20 个字。

第 4 步:仔细审查 #Rewritten Instruction#,识别任何不合理之处。确保 #Rewritten Instruction# 只是 #Instruction# 的更复杂版本。仅提供 #Finally Rewritten Instruction#,无需解释。

请严格按照以下格式回复:
第 1 步 #Methods List#:
第 2 步 #Plan#:
第 3 步 #Rewritten Instruction#:
第 4 步 #Finally Rewritten Instruction#:

#Instruction#:
{Instruction}

表 5.4 -- 来自 Zeng 等人 2024 年论文《自动指令演化用于大型语言模型》的 Evol LLM 提示

UltraFeedback 方法是另一种创新方法,重点关注答案质量而非指令质量。它利用 AI 反馈来提高模型响应的质量和多样性。与 Evol-Instruct 演化指令不同,UltraFeedback 使用大量多样化的指令和模型生成广泛的响应,然后借助先进的语言模型(如 GPT-4)对这些响应进行详细的评估和打分,涵盖指令遵循、真实性、诚实性和帮助性等多个维度。

基于这些理念,可以创建自己的增强技术,以生成更具挑战性和多样化的指令数据集。通过改进和演化现有的指令和答案,生成的数据集能更好地训练模型,提升其在复杂、多步骤任务上的表现,并扩展其在不同应用场景下的性能。

创建自定义指令数据集

在本节中,我们将基于第 3 章抓取的数据创建自己的指令数据集。为了生成高质量的指令数据集,我们需要解决两个主要问题:数据的非结构化特性以及可抓取文章数量的限制。

数据的非结构化特性源于我们处理的是原始文本(文章),而不是指令和答案的配对。为了解决这个问题,我们将使用 LLM 进行转换,具体采用回译重述的组合。回译是指通过提供预期答案作为输出,生成相应的指令。然而,将一个段落作为答案并不总是合适,因此我们会对原始文本进行重述,以确保输出格式正确、质量高。此外,我们还可以要求模型遵循作者的写作风格,使其尽可能接近原始段落的风格。尽管这一过程涉及大量的提示工程,但可以自动化并大规模使用,我们将在接下来的实现中看到。

第二个问题是样本数量的限制,这是实际应用中常见的难题。我们能够获取的文章数量有限,限制了指令数据集的规模。在此示例中,样本数量越多,模型就越擅长模仿原作者的风格。为了解决这个问题,我们会将文章分成若干段落,并为每段生成三对指令-答案。这样可以在保持数据集多样性的前提下,增加样本数量。为简单起见,我们将使用 OpenAI 的 GPT-4o-mini 模型,但你也可以使用开源模型。

然而,LLM 在生成结构化输出方面并不总是可靠。即便提供了特定的模板或指令,模型也不一定始终遵循它们。这种不一致性通常需要额外的字符串解析,以确保输出符合预期格式。

为了简化这个过程并确保输出结果结构化,我们可以采用结构化生成技术。结构化生成是一种有效的方法,能够强制 LLM 遵循预定义的模板,例如 JSON、pydantic 类或正则表达式。在下面的实现中,我们将使用 OpenAI 的 JSON 模式功能,通过这种方式更稳健地返回有效的 JSON 对象,从而减少大量的后处理需求。

基于上述描述,下图总结了我们希望构建的合成数据管道的每个步骤。

现在让我们在 Python 中实现这个过程。可以将其作为 LLMOps 管道的一部分,也可以单独作为脚本实现。

首先,确保安装以下库。OpenAI 库用于与模型交互生成指令数据,datasets 库用于将数据格式化为 Hugging Face 兼容格式,tqdm 用于显示数据生成过程中的进度。

ini 复制代码
openai==1.37.1
datasets==2.20.0
tqdm==4.66.4

导入所需的库

python 复制代码
import concurrent.futures
import json
import random
import re
from concurrent.futures import ThreadPoolExecutor
from typing import List, Tuple
from datasets import Dataset
from openai import OpenAI
from pydantic import BaseModel, Field
from tqdm.auto import tqdm

加载文章数据

原始数据为 JSON 文件。我们从 JSON 文件中提取特定字段(如 id、content、platform 等),并将其创建为 Hugging Face 数据集。

kotlin 复制代码
def load_articles_from_json(file_path: str) -> Dataset:
    with open(file_path, "r") as file:
        data = json.load(file)
    return Dataset.from_dict(
        {
            "id": [item["id"] for item in data["artifact_data"]],
            "content": [item["content"] for item in data["artifact_data"]],
            "platform": [item["platform"] for item in data["artifact_data"]],
            "author_id": [item["author_id"] for item in data["artifact_data"]],
            "author_full_name": [item["author_full_name"] for item in data["artifact_data"]],
            "link": [item["link"] for item in data["artifact_data"]],
        }
    )	

清理文本

使用正则表达式清理文章中的特殊字符和多余的空格。

python 复制代码
def clean_text(text):
    text = re.sub(r"[^\w\s.,!?']", " ", text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()

将文章分块

由于数据格式不一致,我们无法提取所有文章的段落或标题,因此使用正则表达式按句子划分文章,并生成长度在 1,000 至 2,000 字符之间的块。

python 复制代码
def extract_substrings(dataset: Dataset, min_length: int = 1000, max_length: int = 2000) -> List[str]:
    extracts = []
    sentence_pattern = r"(?<!\w.\w.)(?<![A-Z][a-z].)(?<=.|?|!)\s"
    for article in dataset["content"]:
        cleaned_article = clean_text(article)
        sentences = re.split(sentence_pattern, cleaned_article)
        current_chunk = ""
        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue
            if len(current_chunk) + len(sentence) <= max_length:
                current_chunk += sentence + " "
            else:
                if len(current_chunk) >= min_length:
                    extracts.append(current_chunk.strip())
                current_chunk = sentence + " "
        if len(current_chunk) >= min_length:
            extracts.append(current_chunk.strip())
    return extracts

创建指令-答案对

定义 InstructionAnswerSet 类来管理生成的指令-答案对。

python 复制代码
class InstructionAnswerSet:
    def __init__(self, pairs: List[Tuple[str, str]]):
        self.pairs = pairs
    @classmethod
    def from_json(cls, json_str: str) -> 'InstructionAnswerSet':
        data = json.loads(json_str)
        pairs = [(pair['instruction'], pair['answer'])
                 for pair in data['instruction_answer_pairs']]
        return cls(pairs)
    def __iter__(self):
        return iter(self.pairs)

生成指令-答案对

使用 LLM 生成指令和答案对。我们使用 GPT-4o mini 模型和 JSON 模式,并设置温度为 0.7 以获得多样化的响应。

ini 复制代码
def generate_instruction_answer_pairs(
    extract: str, client: OpenAI
) -> List[Tuple[str, str]]:
    prompt = f"""Based on the following extract, generate five instruction-answer pairs. Each instruction \
must ask to write about a specific topic contained in the context. each answer \
must provide a relevant paragraph based on the information found in the \
context. Only use concepts from the context to generate the instructions. \
Instructions must never explicitly mention a context, a system, or an extract. \
Instructions must be self-contained and general. \
Answers must imitate the writing style of the context. \
Example instruction: Explain the concept of an LLM Twin. \
Example answer: An LLM Twin is essentially an AI character that mimics your writing style, personality, and voice. \
It's designed to write just like you by incorporating these elements into a language model. \
Provide your response in JSON format with the following structure:
{{
    "instruction_answer_pairs": [
        {{"instruction": "...", "answer": "..."}},
        ...
    ]
}}
Extract:
{extract}
"""

    completion = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system", "content": "You are a helpful assistant who \
            generates instruction-answer pairs based on the given context. \
            Provide your response in JSON format.",
            },
            {"role": "user", "content": prompt},
        ],
        response_format={"type": "json_object"},
        max_tokens=1200,
        temperature=0.7,
    )
    result = InstructionAnswerSet.from_json(completion.choices[0].message.content)
    return result.pairs

创建主函数

使用多线程并发生成指令-答案对,并将结果上传至 Hugging Face Hub。

css 复制代码
def create_instruction_dataset(
    dataset: Dataset, client: OpenAI, num_workers: int = 4
) -> Dataset:
    extracts = extract_substrings(dataset)
    instruction_answer_pairs = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
        futures = [executor.submit(generate_instruction_answer_pairs, extract, client)
            for extract in extracts
        ]
        for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
            instruction_answer_pairs.extend(future.result())
    instructions, answers = zip(*instruction_answer_pairs)
    return Dataset.from_dict(
        {"instruction": list(instructions), "output": list(answers)}
    )

最终运行

调用主函数完成整个流程。

ini 复制代码
def main(dataset_id: str) -> Dataset:
    client = OpenAI()
    raw_dataset = load_articles_from_json("cleaned_documents.json")
    instruction_dataset = create_instruction_dataset(raw_dataset, client)
    filtered_dataset = instruction_dataset.train_test_split(test_size=0.1)
    filtered_dataset.push_to_hub("mlabonne/llmtwin")
    return filtered_dataset

最终,我们使用此过程生成了 3,335 对指令和答案。

正如上一节所述,我们可以通过增加样本的多样性和复杂性来进一步优化此指令数据集。更高级的提示工程也可以通过提供预期结果的示例来提高生成数据的质量。最后,通过逐条审核样本进行质量评估,可以过滤掉低质量样本。为了保持简洁和易于理解,我们将在本节保持简单的方法,而在第 6 章创建偏好数据集时探讨更高级的方法。

在接下来的章节中,我们将介绍监督微调(SFT)技术及相关概念。

探索 SFT 及其技术

监督微调(SFT) 是在包含指令和答案配对的小规模数据集上重新训练预训练模型的过程。SFT 的目标是将只能执行下一个词预测的基础模型转变为一个有用的助手,能够回答问题并遵循指令。SFT 还可以用于提升基础模型的通用性能(通用微调),注入新的知识(如新语言、领域等),聚焦特定任务,采用特定语气等。

在本节中,我们将讨论何时使用微调,并探讨与存储格式和对话模板相关的概念。最后,我们将介绍三种流行的 SFT 实现方式:完全微调低秩适配(LoRA)量化感知低秩适配(QLoRA)

何时进行微调

在大多数情况下,建议先从提示工程入手,而不是直接微调模型。提示工程可以用于开源或闭源模型。通过使用少样本提示或检索增强生成(RAG)等技术,许多问题在不使用 SFT 的情况下即可高效解决。提示工程还可以帮助建立稳健的评估流程,测量准确性、成本和延迟等指标。如果这些结果不符合要求,则可以考虑创建指令数据集(如上一节所示)。如果有足够的数据,微调便成为一种选择。

除了这些技术考虑之外,SFT 在数据控制("掌握数据")和定制化(微调模型是独一无二的)方面也满足了常见需求。相比围绕聊天机器人构建应用程序,微调让开发者能够创建与 LLM 更丰富的交互方式,例如工具分析、内容审核和增加上下文。需要注意的是,虽然本书重点关注开源模型,但一些 LLM 提供商也提供自动微调服务。虽然这些服务在控制和定制化方面不如自己管理微调管道,但在特定情况下(例如机器学习工程资源有限)可能是一个有趣的权衡。

尽管微调有这些优势,但也存在局限性。通常认为,SFT 利用基础模型中已有的知识,并将参数重新聚焦于特定目标。这带来了几个影响。首先,距离预训练集中已有知识较远的内容(如未知或罕见的语言)可能难以有效学习。此外,一项研究表明,在新知识上微调模型可能导致更频繁的"幻觉"。根据使用的 SFT 技术,还可能出现擦除基础模型中已有知识的风险(称为"灾难性遗忘")。

指令数据集格式

指令数据集采用特定格式存储,以组织指令和答案。通常,数据集中每个样本可以表示为一个 Python 字典,键为提示类型(如 systeminstructionoutput),值对应实际文本。三种最常见的格式为 AlpacaShareGPTOpenAI。以下表格展示了这些数据格式的常见组织方式:

名称 JSONL 格式
Alpaca {"instruction": "...", "input": "...", "output": "..."}
{"instruction": "...", "output": "..."}
ShareGPT {"conversations": [{"from": "...", "value": "..."}, ...]}
OpenAI {"conversations": [{"role": "...", "content": "..."}, ...]}
OASST {"INSTRUCTION": "...", "RESPONSE": "..."}
原始文本 {"text": "..."}

表 5.5 -- 指令数据存储格式示例

注意,对于 Alpaca 格式,input 键是可选的,只有在存在时才会将其内容附加到 instruction 键的内容后。我们还添加了"原始文本"格式,以展示 SFT 本质上并不与预训练不同。如果选择在原始文本上重新训练模型,这种微调通常称为"持续预训练"。

上一节创建的数据集包含两个列(instructionoutput),符合 Alpaca 格式。Alpaca 适用于单轮指令和回答,意味着其限制为一个指令和一个回答。当需要处理对话(多个指令和回答)时,ShareGPT 或 OpenAI 格式更适合。通过将每条消息作为列表中的字典存储,这些格式可以在每个样本中表示任意长度的对话。

单轮和多轮对话的选择直接影响存储类型,并取决于最终的使用场景。

聊天模板

将指令-答案对从数据集格式解析后,我们可以将它们结构化为聊天模板。聊天模板提供了一种统一的方式将指令和答案呈现给模型。

通常,这些模板包括特殊的标记,以标识消息的开始和结束,或标明消息的发送者。由于基础模型并非为遵循指令而设计,因此没有内置的聊天模板。这意味着在微调基础模型时,可以选择任意模板;若要微调一个指令模型(不推荐),则需要使用相同的模板,否则可能会降低性能。

和指令数据集格式类似,聊天模板也有多种选择:如 ChatMLLlama 3Mistral 等。在开源社区中,ChatML(最初由 OpenAI 提供)是一种流行的选择,它通过两个特殊标记(<|im_start|><|im_end|>)来指示发言者。例如,将 Table 5.1 中的指令-答案对应用于 ChatML 模板时,结果如下:

sql 复制代码
<|im_start|>system
You are a helpful assistant, who always provide explanation. Think like you are answering to a five year old.<|im_end|>
<|im_start|>user
Concepts: building, shop, town
Write a sentence that includes all these words.<|im_end|>
<|im_start|>assistant
In our little town, there is a shop inside a big building where people go to buy their favorite toys and candies.<|im_end|>

表 5.6 -- 将 ChatML 模板应用于 Table 5.1 的示例

可以看到,仍然有三部分:systemuserassistant。每部分从 <|im_start|> 标记开始,并以 <|im_end|> 标记结束。当前的发言者通过字符串(如 "system")标识,而非特殊标记。这一字符串在微调时会被分词并作为输入提供给模型。

在推理过程中,由于无法提供预期答案,我们只提供 systemuser 部分(如图 5.6 所示),并通过添加 <|im_start|>assistant\n 提示模型回答。由于模型已通过该模板微调,它能够理解下一步的内容应是与用户指令相关的答案,并符合系统提示的指导。通过这种方式,微调后的模型获得了遵循指令的能力。

使用聊天模板的常见问题是:每一个空格和换行符都极其重要。添加或移除任何字符都会导致分词错误,从而对模型性能产生负面影响。因此,建议使用可靠的模板工具,如 Transformers 库中实现的 Jinja。表 5.7 显示了一些常见的模板示例,包括 Alpaca,它既是指令数据集格式的名称,也是一个聊天模板。

名称 Jinja 模板
Alpaca ### Instruction: What is the capital of France? ### Response: The capital of France is Paris.<EOS>
ChatML `<
Llama 3 `<
Phi-3 `<
Gemma <bos><start_of_turn>user What is the capital of France?<end_of_turn> <start_of_turn>model The capital of France is Paris.<end_of_turn>

表 5.7 -- 常见聊天模板示例

Jinja 支持循环和条件语句,允许同一模板用于训练和推理(通过 add_generation_prompt 实现)。

参数高效的微调技术

虽然文献中存在许多微调技术,SFT 已主要集中在三种核心技术上:全量微调(Full Fine-Tuning)低秩适配(LoRA)量化感知低秩适配(QLoRA) 。我们将分别介绍每种技术,并根据不同的使用场景分析它们的优缺点。

全量微调

全量微调(Full Fine-Tuning) 是最直接的 SFT 技术,涉及重新训练基础模型中的每一个参数。像预训练一样,SFT 也使用下一个词预测作为训练目标。数据集结构是持续预训练和全量微调之间的主要区别。

此方法通常提供最佳效果,但需要大量计算资源。内存使用取决于多种因素,包括模型规模、训练技术和优化方法。简单情况下,单 GPU 设置下所需内存可以按以下公式估算:

对于 32 位浮点精度(FP32)的基本配置,可以估算内存需求如下:

  • 参数:神经网络中的可学习权重和偏置,大型语言模型中通常包含注意力机制、前馈层和嵌入层中的权重。成本:4 字节/参数(FP32),或 2 字节/参数(FP16/BF16)。
  • 梯度:损失函数相对于每个模型参数的偏导数,用于更新参数。成本:4 字节/参数。
  • 优化器状态:Adam 或 AdamW 等优化算法会维护过去梯度和平方梯度的平均值。Adam 为每个参数维护两个额外值。成本:8 字节/参数(对于 Adam 优化器)。
  • 激活值:模型前向传播中的中间输出,用于计算反向传播中的梯度。成本因批量大小而异,通常对于小批量较小。

这给出了每个参数约 16 字节的基准,占用约 112 GB 的显存(对于 7B 模型)和 1,120 GB 的显存(对于 70B 模型)。然而,这通常低估了需求,因为未考虑激活值、临时缓冲区和其他开销。

可以使用多种技术来减少 LLM 微调中的内存需求,例如模型并行梯度累积高效内存优化器 (如 8-bit Adam)和激活检查点。结合这些技术,内存使用可显著降低,例如混合精度加模型并行可将每参数成本降至约 14-15 字节。

此外,全量微调直接修改预训练权重,具有破坏性。如果训练不如预期,可能会导致"灾难性遗忘",即擦除已有知识。这也使得持续预训练更具挑战性。因此,由于全量微调的复杂性和高计算要求,参数高效技术通常更受青睐,用于创建特定任务或领域的模型。

LoRA

LoRA(低秩适配) 是一种参数高效的 LLM 微调技术,旨在解决大规模神经网络适配中的计算挑战。LoRA 通过引入可训练的低秩矩阵,以低计算成本调整模型行为而无需改变原始参数,其主要优点包括:

  • 显著减少训练时的内存使用
  • 加快微调过程
  • 保留预训练模型权重(非破坏性)
  • 通过交换 LoRA 权重快速切换任务

LoRA 利用低秩分解技术高效更新模型权重。它通过引入两个小矩阵 AAA 和 BBB,形成对原始权重矩阵 WWW 的低秩更新。这些优势使得 LoRA 尤其适合计算资源有限的研究人员和开发者,使 LLM 微调更加普及。

数学上,这可以表示为:

Weffective=W+BAW_{\text{effective}} = W + BAWeffective​=W+BA

其中,WWW 是原始权重矩阵,BBB 和 AAA 是 LoRA 矩阵,而 WeffectiveW_{\text{effective}}Weffective​ 是推理过程中使用的有效权重矩阵。

矩阵 AAA 和 BBB 的尺寸被选定为其乘积的形状与 WWW 相同,但具有更低的秩。这个秩(通常用 rrr 表示)是 LoRA 的一个重要超参数。在训练过程中,原始权重 WWW 保持不变,只有 BBB 和 AAA 被更新。这种方法大大减少了可训练参数的数量,从而显著节省内存并加快训练速度。

为了有效实现 LoRA,我们需要选择正确的超参数和目标模块。LoRA 具有两个超参数:

  • 秩 rrr:确定 LoRA 矩阵的大小。通常的初始值是 r=8r = 8r=8,但在某些情况下,使用高达 256 的值也能得到良好效果。较大的秩可能会捕获更多任务多样性,但也可能导致过拟合。
  • 缩放因子 α\alphaα:应用于 LoRA 更新的缩放因子。实际训练时,将冻结的权重 WWW 按比例更新。通常的经验值是设定 α=2r\alpha = 2rα=2r,即对 LoRA 更新应用 2 倍的缩放因子。在出现过拟合或欠拟合时,可以尝试不同的比例。

此外,可以添加一个 Dropout 层以防止过拟合。Dropout 率通常设置在 0 到 0.1 之间,作为可选的正则化因素,略微降低训练速度。

LoRA 可以应用于模型架构的多个部分。最初,LoRA 主要集中于修改注意力机制中的查询(Q)和值(V)矩阵。但实验表明,扩展 LoRA 至模型的其他关键组件也有显著的好处。这些额外的目标模块包括:

  • 注意力层中的键(K)矩阵
  • 注意力机制中的输出投影层(通常记作 O)
  • 注意力层之间的前馈层或多层感知机(MLP)块
  • 线性输出层

不过,增加 LoRA 适配模块的数量也会增加可训练参数的数量和内存需求。

使用 LoRA,可以在单个 GPU 上微调一个 7B 参数的模型,所需显存仅为 14-18 GB,具体取决于配置。这相比于全量微调显著降低了硬件需求,全量微调通常需要多块高端 GPU。在可训练参数方面,LoRA 大幅减少了数量。例如,即使使用秩为 16 的 LoRA 适配每个模块,一个 Llama 3 8B 模型中只有 4200 万个 LoRA 可训练参数,占比仅为模型参数的 0.5196%。

在质量方面,LoRA 也可以达到与全量微调相当甚至更好的效果。多个 LoRA 权重集可以组合,用于不同任务或领域,从而实现灵活的部署和任务切换,而无需重新训练。不同的项目支持多 LoRA 权重的服务,例如 LoRAX,此外 Hugging Face 的 Text Generation Inference (TGI) 和 Nvidia 的 Inference Microservices (NIM) 也支持此功能。

QLoRA

QLoRA 是由 Dettmers 等人提出的一种微调大型语言模型(LLM)的方法,旨在解决高计算成本的挑战。通过结合量化技术与 LoRA,QLoRA 使开发者能够在相对小型、普遍可用的 GPU 上微调模型。

QLoRA 的核心方法是将基础模型参数量化为自定义的 4 位 NormalFloat(NF4)数据类型,从而大幅减少内存使用。与 LoRA 类似,QLoRA 不会在微调时更新所有模型参数,而是为模型特定层引入小型的、可训练的低秩矩阵(适配器)。在训练期间,仅这些适配器被更新,而原始模型权重保持不变。为了进一步降低内存使用,QLoRA 采用双重量化方法,对量化常数本身也进行量化。此外,QLoRA 使用分页优化器,通过 Nvidia 的统一内存功能管理训练过程中的内存峰值。

与 LoRA 相比,QLoRA 在内存方面具有显著的节省效果,将 GPU 内存峰值需求减少多达 75%。例如,对于一个 7B 参数的模型,QLoRA 将初始化期间的峰值内存使用从 14 GB 降低到 9.1 GB,减少了 35%。在微调期间,内存节省率提升至 40%,从 LoRA 的 15.6 GB 降低到 QLoRA 的 9.3 GB。不过,这种内存效率以训练时间增加为代价,QLoRA 的训练速度比 LoRA 慢约 30%。在模型性能方面,QLoRA 与 LoRA 仅存在细微差异。

总之,QLoRA 在内存限制是首要考虑因素时尤其有用,比如在处理非常大的模型或硬件 GPU 内存有限的情况下。然而,如果训练速度至关重要且内存充足,LoRA 可能是更合适的选择。

选择 QLoRA 还是 LoRA 应基于项目的具体需求、可用硬件以及在内存使用、训练速度和模型性能之间的平衡需求。

训练参数

在微调大型语言模型(LLMs)时,多个超参数会指导训练过程,并显著影响模型的收敛性、泛化能力和整体效果。

学习率和调度器

学习率是最重要的超参数,控制训练过程中模型参数的更新幅度。它通常取值范围从 1e-6 到 1e-3,通常推荐从约 1e-5 开始。学习率过低时,训练进展缓慢,可能陷入次优解;过高则可能导致训练不稳定或发散,从而影响性能。调节学习率有助于找到适合特定任务和模型的最优值。

学习率调度器在训练过程中调整学习率,通常在初始阶段使用较高的学习率,随后逐渐降低以精细微调模型。最常见的调度器是线性和余弦调度器:线性调度器逐步降低学习率,而余弦调度器在初始阶段下降较慢,到训练后期下降较快。常见方法是在训练前 5% 的步数内从 0 增加到初始值,然后在剩余的 95% 步数内衰减,帮助稳定早期训练,并在收敛时进行更精细的更新。线性和余弦调度器的性能通常相当。

批量大小

批量大小决定每次权重更新前处理的样本数量,常见取值范围为 1 到 32。较大的批量通常提供更稳定的梯度估计并加快训练速度,但也需要更多内存。在显存有限的 GPU 上,可以使用梯度累积技术来克服内存限制:即使用较小的 mini-batch 多次前向和后向传播,积累梯度后统一更新参数。例如,若 GPU 仅能处理 8 个样本的批量,而想实现 32 的有效批量大小,可将梯度累积步数设置为 4。

最大长度和打包

最大序列长度定义了模型能处理的最长输入序列,通常设置在 512 到 4096 个 token 范围内,甚至可达 128,000 个 token。超过此限制的序列会被截断。截断长度直接影响批量大小和内存使用,需要在 GPU 能力和数据特性之间权衡。

打包通过将多个较短样本组合到单个批量中,提升每个批量的利用率。这在含有许多短序列的数据集上尤为有效,但需通过注意力掩码确保模型不会跨样本关注不同的 token。

训练轮次

训练轮次(epoch)是另一关键参数,表示数据集的完整训练次数。LLM 微调常见轮次为 1 到 10,典型情况下为 2 到 5 轮。更多轮次可提高模型性能,但轮次过多可能导致过拟合。可以通过监控验证性能并应用早停机制动态确定最佳轮次。

优化器

优化器用于调整模型参数以最小化损失函数。AdamW 是 LLM 微调的首选,尤其是其 8 位版本,在显存消耗上更为高效。对于显存极度受限的场景,AdaFactor 是一个内存高效的替代选择,但其性能可能不及 AdamW。paged 版本的优化器(如 paged AdamW 8-bit)可以将部分内存卸载至 CPU,以进一步降低显存使用。

权重衰减

权重衰减通过对大权重施加惩罚,帮助模型学习更简单、泛化性更好的特征。权重衰减值通常在 0.01 到 0.1 之间,以 0.01 为常用起点。权重衰减过大会影响模型学习重要特征的能力,过小则可能无法提供足够的正则化效果。最佳值依赖于具体模型和数据集,建议尝试不同的取值。

这些训练参数的优化能有效提升微调性能,且通常需要实验和调整以找到最佳组合。

梯度检查点

梯度检查点 是一种在训练过程中减少内存消耗的技术,通过只存储前向传播中生成的部分中间激活值来实现。在标准的训练过程中,所有的中间激活值都会保存在内存中,以便在反向传播过程中计算梯度。然而,对于像 LLM 这样非常深的网络,这种方法在硬件资源受限时(特别是在显存较少的 GPU 上)会变得难以操作。

梯度检查点通过在网络的特定层保存激活值来应对这一挑战。对于未保存激活值的层,在反向传播过程中会根据需要重新计算这些激活值来完成梯度计算。这种方法在计算时间和内存使用之间进行权衡:尽管显著降低了内存需求,但由于需要重新计算部分激活值,整体计算时间可能会有所增加。

除了上述参数和技术,还有其他一些参数和方法,但其重要性通常不如前面讨论的关键参数。在下一节中,我们将通过一个具体的例子探讨如何选择和调整这些参数。

实践中的微调

现在,让我们在自定义数据集上微调一个开源模型。在本节中,我们将展示一个高效实现 LoRA 和 QLoRA 的示例。您可以根据可用的硬件配置选择最合适的技术。

有许多高效的开源模型可用于特定任务或领域的用例。选择最相关的 LLM 时,需要考虑以下三个主要参数:

  • 许可证:一些模型许可证仅允许非商业用途,这对企业微调需求可能不利。此领域常见自定义许可证,可能会根据公司用户数量等进行限制。
  • 预算:参数较少的模型(<10B)相比大型模型来说更便宜,便于微调和推理部署。因为它们可以运行在成本较低的 GPU 上,并且每秒处理更多的 tokens。
  • 性能:评估基模型在通用基准或与最终用例相关的特定任务/领域基准上的表现非常重要。这有助于确保模型在微调后在目标任务上具备良好的性能。

在本章中,我们将选择 Meta 发布的开源模型 Llama 3.1 8B。它使用了允许商业用途的自定义许可证"Llama 3.1 Community License Agreement"。8B 参数量使其小到可以适用于大多数 GPU,同时性能相比竞争对手达到较高水平。我们可以通过 Open LLM Leaderboard 和模型卡上的其他基准来验证其性能。

以下是一些推荐的用于微调模型的工具和库:

  • TRL:由 Hugging Face 创建和维护,用于使用 SFT 和偏好对齐训练 LLMs 的库。它功能齐全,在算法上保持最新,适用于单 GPU 和多 GPU 场景,支持 FSDP 和 DeepSpeed。
  • Axolotl:由 Wing Lian 创建,使用 YAML 配置文件简化 LLMs 微调流程。基于 TRL,包含许多额外功能,如自动合并存储在不同格式中的数据集。支持单 GPU 和多 GPU 设置,适用于 FSDP 和 DeepSpeed。
  • Unsloth:由 Daniel 和 Michael Han 创建,使用自定义内核加速训练(2-5 倍)并减少内存使用(最多 80%)。基于 TRL,提供许多实用工具,如自动将模型转换为 GGUF 量化格式。目前仅支持单 GPU 设置。

为了最大化效率,我们将使用 Unsloth 库进行微调。以下代码是 LLMOps 管道的一部分,也可作为独立脚本使用。它可以在多个环境中运行,例如 SageMaker、云 GPU(如 Lambda Labs 或 RunPod)、Google Colab 等。我们在不同的 GPU 上测试了代码,如 A40、A100 和 L4。

要安装 Unsloth 库及其依赖项,建议直接从书籍的 GitHub 仓库(github.com/PacktPublis...)或Unsloth 的仓库(github.com/unslothai/u...)安装,因为安装步骤会定期更新以解决可能的依赖冲突。

首先,我们需要访问一个受限模型,并(可选)将微调后的模型上传到 Hugging Face(huggingface.co/)。这需要登录到您的帐... API 密钥(路径为Settings | Access Tokens | Create new token)存储在.env文件中:

ini 复制代码
HF_TOKEN = YOUR_API_KEY

确保您的 Comet ML API 密钥也存储在.env文件中:

ini 复制代码
COMET_API_KEY = YOUR_API_KEY

导入所有必要的包:

javascript 复制代码
import os
import torch
from trl import SFTTrainer
from datasets import load_dataset, concatenate_datasets
from transformers import TrainingArguments, TextStreamer
from unsloth import FastLanguageModel, is_bfloat16_supported

接下来加载要微调的模型及其对应的分词器。我们使用 Unsloth 的 FastLanguageModel 类的 .from_pretrained() 方法。除了指定模型名称外,还需要设置最大序列长度(在本例中为 2048)。最后,load_in_4bit 参数指示是否要使用 QLoRA(量化的预训练权重)或 LoRA。

在本例中我们将使用 LoRA,因为它的训练速度更快且质量更高,但如果不满足 VRAM 要求,也可以轻松切换为 QLoRA。

ini 复制代码
max_seq_length = 2048
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="meta-llama/Meta-Llama-3.1-8B",
    max_seq_length=max_seq_length,
    load_in_4bit=False,
)

现在模型已加载,我们可以定义 LoRA 配置。在这里,我们使用 rank 值 32,足够模仿写作风格并从指令样本中复制知识。如果结果不理想,可以将此值增加到 64 或 128。我们还设置了 alpha 为 32,不使用 dropout 和偏置,以加快训练速度。最后,我们将目标设为每个线性层,以最大化微调过程的质量。

ini 复制代码
model = FastLanguageModel.get_peft_model(
    model,
    r=32,
    lora_alpha=32,
    lora_dropout=0,
    target_modules=["q_proj", "k_proj", "v_proj", "up_proj", "down_proj", "o_proj", "gate_proj"],
)

接下来,需要将数据准备成适合微调的格式。在本例中,我们的 llmtwin 数据集中只有 3000 个样本,这可能导致模型无法正确学习聊天模板。为了解决这一问题,我们将使用一个名为 FineTome 的高质量通用数据集对其进行增样。FineTome 是通过 fineweb-edu-classifier 过滤后的 arcee-ai/The-Tome 版本。我们只使用数据集中的 10,000 个训练样本,而不是完整的 100,000 个样本。我们将这两个数据集合并以创建最终的数据集。

ini 复制代码
dataset1 = load_dataset("mlabonne/llmtwin")
dataset2 = load_dataset("mlabonne/FineTome-Alpaca-100k", split="train[:10000]")
dataset = concatenate_datasets([dataset1, dataset2])

现在,我们需要使用一个聊天模板来格式化数据。为了方便起见,我们使用 Alpaca 模板。这个模板不需要额外的标记,因此错误率较低(但相较于 ChatML,性能可能会略微受影响)。在这里,我们将所有指令和答案映射到 Alpaca 模板中。我们手动在每个消息的末尾添加结束符(EOS)标记,以确保模型学会输出该标记。否则,模型可能会持续生成答案而不会停止。

ini 复制代码
alpaca_template = """Below is an instruction that describes a task. Write a response that appropriately completes the request.
### Instruction:
{}
### Response:
{}"""
EOS_TOKEN = tokenizer.eos_token
dataset = dataset.map(format_samples, batched=True, remove_columns=dataset.column_names)

准备好数据集后,我们可以将其划分为训练集(95%)和测试集(5%)用于训练期间的验证。

ini 复制代码
dataset = dataset.train_test_split(test_size=0.05)

模型现在可以开始训练。SFTTrainer()类存储了所有训练所需的超参数。此外,我们提供了模型、分词器、LoRA 配置和数据集。根据之前章节的推荐,我们设置学习率为 3e-4,采用线性调度器,最大序列长度为 2048。我们将此模型训练三个 epoch,批次大小为 2,梯度累积步数为 8(有效批次大小为 16)。我们还选择了 adamw_8bit 优化器,权重衰减设为 0.01。根据所用 GPU 的情况,激活时自动使用 FP16 或 BF16。最后,我们将训练记录报告给 Comet ML 以进行实验追踪。

ini 复制代码
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    dataset_text_field="text",
    max_seq_length=max_seq_length,
    dataset_num_proc=2,
    packing=True,
    args=TrainingArguments(
        learning_rate=3e-4,
        lr_scheduler_type="linear",
        per_device_train_batch_size=2,
        gradient_accumulation_steps=8,
        num_train_epochs=3,
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        warmup_steps=10,
        output_dir="output",
        report_to="comet_ml",
        seed=0,
    ),
)
trainer.train()

在我们合并后的数据集上训练这个模型可能需要几小时。例如,在 A100 GPU 上训练需要 50 分钟。

训练完成后,我们可以通过一个快速示例来测试模型。目的不是正式评估微调后的模型,而是确保分词器或聊天模板没有明显错误。

为了快速推理,我们可以使用 Unsloth 的 FastLanguageModel.for_inference()。我们将指令直接格式化为 Alpaca 格式。请注意,我们提供了一个空答案来在用户指令末尾附加助手头部(### Response:),这会强制模型回答指令而不是继续生成。我们还使用文本流媒体来实时显示生成结果,而不是等待完整生成。

ini 复制代码
FastLanguageModel.for_inference(model)
message = alpaca_template.format("Write a paragraph to introduce supervised fine-tuning.", "")
inputs = tokenizer([message], return_tensors="pt").to("cuda")
text_streamer = TextStreamer(tokenizer)
_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=256, use_cache=True)

以下是模型生成的答案:

有监督微调是一种通过提供指令及其对应答案的精心策划的数据集来增强语言模型的方法。该过程旨在使模型的响应符合人类预期,从而提高其准确性和相关性。目标是确保模型能够有效应对各种查询,使其成为聊天机器人和虚拟助手等应用的宝贵工具。

这个结果是正确的,并且符合 Alpaca 聊天模板的格式。

现在,模型已经成功微调,我们可以使用以下函数将其本地保存和/或推送到 Hugging Face Hub。

ini 复制代码
model.save_pretrained_merged("model", tokenizer, save_method="merged_16bit")
model.push_to_hub_merged("mlabonne/TwinLlama-3.1-8B", tokenizer, save_method="merged_16bit")

恭喜您从零开始微调了一个基础模型!在训练过程中,您可以通过 Comet ML 监控训练损失、验证损失以及其他许多指标。您需要确保这些指标符合预期。图 5.11 显示了在 Comet ML 中对应于上述代码的训练运行情况。

特别需要监控以下三个指标:

  1. 训练损失:它衡量模型在训练任务上的表现。损失值应该平均持续下降,这表明性能在不断提高。我们期望在训练开始时迅速下降,然后进入一个较长的平台期。如果出现损失值的波动或持续上升,说明训练失败。在这种情况下,您可能需要检查数据质量、分词器的问题,并调整学习率和批量大小等参数。在图 5.11(loss)中,您可以看到三个阶段分别对应我们的三个训练周期。
  2. 验证损失:它使用验证集来衡量损失,而不是训练集。一个良好拟合的模型通常会表现出训练和验证损失的同步下降,最终趋于稳定,两者之间的差距应尽量小,但通常会存在一个微小的差距,因为模型在训练数据上的表现总是略好于验证数据。如果训练损失继续下降而验证损失开始上升,则表明模型可能出现了过拟合。相反,如果两条曲线保持平稳且损失值较高,则表明模型欠拟合。没有普适的"推荐范围",因为损失值依赖于具体问题和损失函数,但应关注两条曲线的收敛性和稳定性。在图 4.11(eval_loss)中,我们看到在步骤 340 处有轻微的增加。这仍然是可接受的,但可能表明模型开始出现过拟合迹象。
  3. 梯度范数:它表示训练过程中梯度向量的大小。大的梯度范数可能表明训练不稳定,例如出现过拟合,尤其是当训练和验证损失出现分歧时。另一方面,稳定或下降的梯度范数通常表示模型正在收敛到局部最优。为减轻梯度范数过大引起的问题,可以使用梯度剪裁技术,即设定梯度范数的最大阈值,从而限制参数更新的大小。

通常可以尝试不同的学习率,并根据最低损失值选择最佳模型。需要注意的是,这只是实际评估的代理,下一章会介绍更深入的评估方法。

总结

本章涵盖了LLM微调的重要方面,包括理论和实践。我们探讨了指令数据管道,以及如何从数据收集到增强的过程中创建高质量的数据集。管道的每个阶段都提供了优化的机会,尤其是在质量评估、数据生成和增强方面。这个灵活的管道可以根据具体用例调整,选择最相关的阶段和技术。

我们将这一框架应用到第3章的实际数据中,利用LLM将原始文本转换为指令-回答对。接着,我们探讨了监督微调(SFT)技术,包括SFT的优势和局限性、使用聊天模板存储和解析指令数据集的方法,以及三种主要的SFT技术:全量微调、LoRA和QLoRA。我们根据这些方法对内存使用、训练效率和输出质量的影响进行了比较。本章以一个实际示例结束,演示了如何在自定义指令数据集上微调Llama 3.1 8B模型,突出了成功微调的关键步骤和实现细节。

在下一章中,我们将使用偏好对齐技术创建新版本的TwinLlama-3.1-8B。我们将生成一个包含被选中和被拒绝答案的新数据集,以帮助校准模型的回答类型。我们还将详细介绍这一框架的多种应用及其实现方法。

参考文献

  • Tahori, Gulrajani, Zhang, Dubois 等. "Alpaca: A Strong, Replicable Instruction-Following Model." crfm.stanford.edu, 2023年3月13日, crfm.stanford.edu/2023/03/13/....
  • Subhabrata Mukherjee 等. "Orca: Progressive Learning from Complex Explanation Traces of GPT-4." arXiv预印本 arXiv:2306.02707, 2023年6月.
  • Wing Lian 等. "Open-Orca/OpenOrca." huggingface.co, 2023年, huggingface.co/datasets/Op....
  • Weihao Zeng 等. "Automatic Instruction Evolving for Large Language Models." arXiv预印本 arXiv:2406.00770, 2024年6月.
  • Chunting Zhou 等. "LIMA: Less Is More for Alignment." arXiv预印本 arXiv:2305.11206, 2023年5月.
  • 01.AI. "Yi: Open Foundation Models by 01.AI." arXiv预印本 arXiv:2403.04652, 2024年3月.
  • Alex Birch. "LLM微调内存需求." blog.scottlogic.com, 2023年11月24日, blog.scottlogic.com/2023/11/24/....
  • Quentin Anthony 等. "Transformer数学基础101." blog.eleuther.ai, 2023年4月18日, blog.eleuther.ai/transformer....
  • Edward J. Hu 等. "LoRA: 大语言模型的低秩适应." arXiv预印本 arXiv:2106.09685, 2021年6月.
  • Tim Dettmers 等. "QLoRA: 量化LLM的高效微调." arXiv预印本 arXiv:2305.14314, 2023年5月.
相关推荐
阿_旭25 分钟前
基于YOLO11/v10/v8/v5深度学习的维修工具检测识别系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·qt·ai
YRr YRr29 分钟前
深度学习:Cross-attention详解
人工智能·深度学习
阿_旭29 分钟前
基于YOLO11/v10/v8/v5深度学习的煤矿传送带异物检测系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·目标检测·yolo11
算家云1 小时前
如何在算家云搭建Aatrox-Bert-VITS2(音频生成)
人工智能·深度学习·aigc·模型搭建·音频生成·算家云
曹申阳1 小时前
2. JVM的架构模型和生命周期
jvm·架构
小言从不摸鱼2 小时前
【NLP自然语言处理】深入解析Encoder与Decoder模块:结构、作用与深度学习应用
人工智能·深度学习·神经网络·机器学习·自然语言处理·transformer·1024程序员节
湫ccc2 小时前
Bert框架详解(上)
人工智能·深度学习·bert
车载诊断技术2 小时前
电子电气架构 --- 整车控制系统
网络·架构·汽车·soa·电子电器架构
一叶飘零_sweeeet2 小时前
Dubbo 构建高效分布式服务架构
分布式·架构·dubbo