LLM工程师手册——基于偏好对齐的微调

799 阅读40分钟

监督微调(SFT)在使 LLM 执行特定任务方面起到了重要作用。然而,SFT 在捕捉人类偏好的细微差别和模型可能遇到的长尾交互方面存在局限性。为了克服这一限制,开发了更先进的技术来使 AI 系统与人类偏好对齐,统称为偏好对齐。

偏好对齐通过在训练过程中引入直接的人类或 AI 反馈来弥补 SFT 的不足。这种方法使模型能够更细致地理解人类偏好,特别是在简单的监督学习无法胜任的复杂场景中。尽管偏好对齐有多种技术可供选择,本章将主要聚焦于直接偏好优化(DPO),因为它具有简单高效的特点。

在本章中,我们将讨论偏好对齐算法(如 DPO)所需的数据类型。我们将构建自己的数据集来调整模型的写作风格,使其更自然、更真实。我们会介绍 DPO 算法并实现它,以对齐第 5 章训练的模型。

本章涵盖的主题包括:

  • 理解偏好数据集
  • 如何创建我们自己的偏好数据集
  • 直接偏好优化(DPO)
  • 实践中实现 DPO 以对齐模型

在本章结束时,您将能够创建自己的偏好数据集,并使用多种技术对齐模型。

本章的所有代码示例可在 GitHub 上找到:github.com/PacktPublis…

理解偏好数据集

创建高质量偏好数据集的原则与第 5 章中讨论的指令数据集相同。我们希望最大化样本的准确性、多样性和复杂性。为此,我们遵循相同的阶段,如图 6.1 所示:数据整理、去重、去污染、质量评估、探索、生成和增强。

image.png

为了避免重复,本节将重点介绍指令数据集和偏好数据集之间的主要差异。我们将介绍偏好样本的结构以及偏好数据集的理想大小。然后,我们将专注于与指令数据集创建过程最不同的两个阶段:数据生成和评估。

偏好数据

由于不同训练算法对数据的需求不同,偏好数据集缺乏指令数据集的标准化。偏好数据由对给定指令的一系列回答组成,并由人类或语言模型进行排序。本章聚焦于直接偏好优化(DPO),因此我们将研究该算法所需的特定数据格式。

如表 6.1 所示,DPO 数据集的结构简单明了:每个指令与一个偏好的回答和一个被拒的回答配对。训练目标是让模型生成偏好的回答而非被拒的回答。

指令选择的回答被拒的回答
给我讲一个关于章鱼的笑话。为什么章鱼不在赌场玩牌?因为它们数不过八。逗章鱼笑需要多少下?十下。

表 6.1 – 来自 mlabonne/orpo-dpo-mix-40k 数据集的样本示例

在偏好数据集中,被拒的回答与选择的回答一样重要。没有被拒的回答,数据集就只是一个简单的指令集。被拒的回答代表我们希望从模型中消除的行为。这种结构提供了很大的灵活性,使偏好数据集能够在许多场景中使用。以下是一些比单纯使用 SFT 更适合使用偏好数据集的示例:

  • 聊天机器人:在对话式 AI 中,响应质量通常取决于自然性、互动性和上下文适当性等主观因素。偏好数据集可以通过比较较优和较差的回答,使模型学习这些微妙之处。简单的 SFT 可能无法捕捉到在特定上下文中某一回答比另一回答更可取的细微差别。
  • 内容审核:判断内容是否合规或违规通常涉及细致的判断。偏好数据集可以帮助模型通过比较合规和不合规的内容实例,学习区分边界情况。这比通过 SFT 进行二元分类更有效,因为它帮助模型理解审核决策背后的理由。
  • 摘要生成:摘要的质量通常取决于简洁性、相关性和连贯性等因素。通过使用偏好数据集,模型可以学习生成更有用、信息量更丰富的人类偏好摘要。简单的 SFT 可能会生成技术上正确但不符合人类阅读偏好的摘要。
  • 代码生成:在编程任务中,往往有多种正确的解决方案,但有些代码更高效、可读性更强,或者符合更好的实践。偏好数据集可以帮助模型学习这些代码质量的定性方面,而这些方面可能无法通过基于正确性的简单 SFT 捕捉到。
  • 创意写作:对于故事生成或诗歌创作等任务,输出质量高度主观且多方面。偏好数据集比指令数据集更能捕捉人类对风格、创意和情感影响的判断,而指令数据集可能更关注技术上的正确性或对提示的符合程度。
  • 翻译:传统的 BLEU 分数等指标可以衡量翻译的准确性,但它们并不总能捕捉翻译的流畅性或自然性。偏好数据集可以帮助模型生成母语者更偏好的翻译,即使多种翻译在技术上都是正确的。

在所有这些场景中,偏好数据集使训练方式更加精细化,捕捉了超越简单正确性或指令符合性的主观质量评估和人类偏好。这种方法能产生不仅技术上准确,而且更符合人类判断和偏好的模型输出,尤其适用于复杂、开放性任务。

与指令数据集不同,偏好数据集没有 Alpaca 或 ShareGPT 之类的标准存储格式。大多数偏好数据集遵循类似表 6.1 所示的结构,包含指令、偏好回答和被拒回答的列。在偏好对齐中,多轮对话并不常见。截至本文撰写时,主要微调库不支持多轮对话,通常只提取对话中的第一条或最后一条消息。

数据量

DPO 数据集通常比指令数据集所需样本量更少,但对模型行为的影响显著。与指令数据集类似,所需的样本数量取决于模型的大小和任务的复杂性。较大的模型对样本的效率更高,因此所需数据量较少;而复杂任务则需要更多示例以捕捉所需行为。数据质量依然至关重要,通常较多的偏好对配对是有益的。

大型语言模型(LLM)提供商使用通用对齐来提高微调模型的整体性能,这需要包含数百万样本的偏好数据集。包括 Nvidia 和 Meta 在内的主要 AI 企业正在趋同于类似的后训练流程,涉及多轮偏好对齐,并广泛使用合成数据。这种共识表明,这些方法在扩展语言模型能力方面最为有效。

在小规模应用中,开源社区使用的数据集从 10,000 到 100,000 个样本不等,以提升模型性能。这种方法不仅在提升基准分数方面有效,还在模型合并、剪枝及其他修改后帮助网络恢复效果。总体而言,DPO 对最终模型的影响较温和,破坏性小于 SFT。

另一方面,前面描述的特定任务通常需要更少的偏好对。例如,任务特定的对齐旨在改善模型在某一特定功能上的表现,如修改写作风格或拒绝特定指令。这类对齐通常可以通过规模较小的数据集完成,样本数量根据任务复杂性不同在 100 到 10,000 对偏好配对之间。

一个样本较少的应用示例是让模型声明自己不是由 OpenAI、Meta 或其他 LLM 提供商训练的。这可以通过偏好数据集来实现,其中被拒的回答为那些声称其他来源的回答,而选择的回答则是模型正确声明由用户训练的回答。对于此任务,200 到 500 对的偏好配对数据集就足够了。

数据生成与评估

在创建偏好数据集时,数据生成和评估密切相关。我们首先生成回答,然后对其进行评分,以构建最终的数据集。接下来,我们将这两个步骤作为一个整体过程介绍,而不是分为两个独立的步骤。

生成偏好数据

在制作新的偏好数据之前,先查看相关的开源数据集是个好主意。虽然相比指令数据集偏好数据集较少,但在 Hugging Face Hub 上可以找到一些高质量的偏好数据集。这些数据集可以用于特定任务,也可以添加到自己的数据集中。知名的偏好数据集包括 Anthropic 的 HH-RLHF 数据集(包含人类对 AI 回答有用性和无害性的偏好)以及 OpenAI 的“从人类反馈中生成摘要”数据集(专注于文章摘要)。

DPO 数据集可以通过多种方法创建,每种方法在质量、成本和可扩展性之间都有不同的权衡。根据具体应用,这些方法需要不同程度的人类反馈。我们将它们分为四大类:

  • 人工生成、人工评估的数据集:该方法需要雇佣人类来生成提示的回答并评估其质量。这种方法可以捕捉人类偏好的细微差别,适合复杂任务,但资源消耗极大且难以扩展,因此主要用于资源充足的大型 AI 公司。
  • 人工生成、LLM 评估的数据集:如果有大量现成人工生成的内容,这种方法可能有用。然而,由于效率低下,这种方法在实践中很少使用,因为它在生成回答方面仍需大量人力投入,并且在 LLM 评估阶段可能忽略一些细微的偏好。
  • LLM 生成、人工评估的数据集:此方法在质量和效率之间取得了良好平衡。LLM 生成多个回答,人类对其进行排名。由于人类在判断回答优劣方面通常优于从零编写,这种方法受到青睐。它允许快速生成多样化的回答,同时有效地捕捉人类偏好。然而,该方法可能缺乏人类可能产生的创造性或意想不到的回答。
  • LLM 生成、LLM 评估的数据集:完全合成的数据集(由 LLM 生成和评估)因其可扩展性和成本效益而越来越普遍。这种方法可以快速生成大量数据集,并随着 LLM 能力的提升而改善。然而,需要精心设计提示来确保质量和多样性,且可能会延续生成 LLM 的偏见或局限性。

在实践中,人工生成的数据集昂贵且难以扩展,质量未必最高。而人类评估则非常有价值,但难以规模化,因此大数据集使用 LLM 评估具有优势。除了这些高层次的考量,数据获取方式和使用计划也需要考虑。例如,拥有大量用户的应用可以嵌入反馈机制来提供偏好数据。这种机制可以简单到“点赞/不喜欢”评分,也可以更深入地提供文字反馈。

需要注意的是,评估并非总是必需,偏好可以在生成过程中自然显现。例如,可以使用高质量模型生成优选的输出,而用低质量或故意有缺陷的模型生成较差的回答,从而在偏好数据集中形成明确的区分,从而更有效地训练 AI 系统识别并模仿高质量输出。Hugging Face Hub 上的 Intel/orca_dpo_pairs 数据集就是通过这种方式创建的。

另一种方法是将模型生成的输出与人类编写的回答进行比较,这可以提供模型与实际人类偏好对齐程度的洞察,并突出模型可能存在的不足。这可以用于模仿特定的风格,使模型呈现更真实的语气。

数据生成技巧

指令数据集和偏好数据集的生成过程是一致的。设计提示词时应鼓励模型生成多样化且复杂的回答。通过精心设计提示词,明确请求不同的方式或风格,可以确保输出的多样性,反映出人类偏好的丰富性。

例如,生成摘要时,可以请求生成简明摘要、详细摘要和侧重关键点的摘要。这种方法不仅能生成多样的数据集,还可以帮助我们了解不同风格和方法如何与人类偏好对齐。

引入输出的多样性也是生成合成偏好数据集的关键。可以通过调整温度设置或采用其他采样方法实现。较高的温度设置倾向于生成更具创意和多样性的回答,而较低的设置则生成更专注且确定的输出。这种设置需要在多样性和连贯性之间做权衡,取决于生成数据的类型。例如,生成代码需要低温度(低创意),而写文章则可设置高温度。

使用多个 LLM 生成样本通常比仅使用单个模型效果更佳。某些 LLM 在特定任务上表现更优,此方法也增加了样本的多样性。开源数据集如 argilla/Capybara-Preferences 就采用了这种方法,结合了 GPT-4 和开源权重模型。评估过程则选择最佳回答和被拒回答。

偏好评估

数据评估可以通过人类评分员完成,也可以使用 LLM 自动完成。LLM 评估涉及制定详细的评分标准,创建提示词以清晰传达这些指南,并使用模型选择偏好的和被拒的回答。LLM 评估的可扩展性优于人工评分,且可一致应用标准,但其质量直接依赖于模型的性能和提供的指南,可能会忽略细微的人类偏好或文化差异。然而,随着 LLM 的进步,其细致判断能力也在提高,可能会逐步提高数据集质量。

偏好数据集的 LLM 评估可通过绝对评分或成对排名实现。在绝对评分中,LLM 根据预定义的标准为每个回答分配一个数值或分类评分。这种方法简单,但在不同提示词或评估场次之间可能缺乏一致性。而成对排名则要求 LLM 比较两个回答并选择更优的一个或对其排名。这种方法更接近人类评估格式,结果通常更一致。

对于绝对评分,可以创建一个提示词,列出评估标准并要求 LLM 按特定的分数(例如 1-5 或 差/一般/好/优秀)对回答进行评分。提示词示例如下:“根据相关性、连贯性和有用性为以下回答评分,评分范围为 1-5:[插入回答]。”对于成对排名,提示词可以是:“比较以下两个回答。就相关性、连贯性和有用性而言,哪个更好?回答 A:[插入回答 A] 回答 B:[插入回答 B]。”

偏好数据集的对比性使成对排名成为理想的评估方法。这种方法通常更准确,与人类判断的相关性更高。成对排名模仿了人类比较选项的自然方式,使人类评分员和 LLM 更容易提供一致且有意义的评估。

我们可以通过提供一个真实答案并使用思维链推理进一步提高成对排名的准确性。这种方法鼓励评估 LLM 考虑回答的多个方面,并清晰阐述决策过程,从而实现更全面和有依据的评估。当没有真实答案时,我们可以提示 LLM 创建一个评分备注,即期望答案的描述。在 LLM 对某一主题了解不多的情况下,这种方法尤为有效,因为它促使模型在评估回答前确立清晰的评估标准。

以下是一个 LLM 作为裁判的成对排名提示词的具体实现示例:

指令
您是一名回答裁判,目标是比较回答 A 和回答 B。我想知道哪个回答在相关性、准确性、完整性、清晰性、结构和简洁性方面更好。指令:{instruction} 回答 A:{answer_a} 回答 B:{answer_b} 解释您的推理步骤,并使用以下结构输出最佳回答的字母:推理:(比较两个回答) 最佳回答:(A 或 B)

表 6.2 – 成对排名的 LLM 作为裁判的提示示例,包含一个指令和两个回答

然而需要注意的是,基于 LLM 的评估可能会受多种偏见的影响:

  • 位置偏差:在相对评分中,LLM 裁判往往更偏向首先呈现的回答。这种偏见可能导致结果偏差,从而影响偏好准确性。
  • 长度偏差:与人类类似,LLM 裁判通常偏爱较长的回答,可能会忽略较短且简洁的高质量回答。
  • 家族偏差:LLM 裁判可能偏向由自身或相同家族的模型生成的回答,可能是由于语言模式或知识库的相似性。

为减轻这些偏差并提升偏好数据集的质量,可以采用一些解决方案。一个关键方法是随机化每次比较中回答 A 和回答 B 的顺序,以防止展示顺序对评估产生持续影响。另一个有效策略是提供一些少量示例,展示均衡的评分分布。这些示例可校准裁判 LLM 的内部评分机制,并有效减少长度偏差和家族偏差。此外,使用多模型组成评审团,而不是依赖单一 LLM 裁判,能够显著增强评估过程的稳健性。此多模型方法有助于平衡各模型中可能存在的偏差,最终实现更全面和准确的回答评估。

在下一节中,我们将创建自己的偏好数据集,依靠数据生成过程自然生成选定(人工生成)和被拒(LLM 生成)的回答。

创建我们自己的偏好数据集

目前,我们的模型可以撰写有关机器学习主题的段落,但它的写作风格与原作者不尽相同。这是偏好对齐的典型用例,我们希望改变模型的“语气”,使其更贴近源数据的风格。需要注意的是,实验表明,DPO 往往会使模型更冗长,并倾向于使用非常正式的语言。因此,在训练时需要谨慎使用 DPO,避免这一陷阱,并尝试采用这些博客文章中较为非正式的风格。

在本节中,我们将创建一个偏好数据集,其中选择的回答是从文本中提取的片段,而被拒的回答是由模型生成的。为实现这一点,我们将修改第 5 章创建的代码,该代码用于生成指令数据集。

如上一节所述,偏好数据集和指令数据集依赖相同的原则。在偏好数据集中,我们需要三元组(指令、回答 1、回答 2),而不是指令和回答的成对数据。这种设置的有趣之处在于,我们在文本片段中拥有真实答案,因此不需要像 LLM 裁判那样复杂的评估过程。为了确保这些提取的片段是高质量的,我们将基于长度和标点符号实现两个额外的质量过滤器。图 6.2 总结了端到端的流程:

image.png

我们现在可以开始实现偏好数据生成流水线:

首先导入必要的库。

import concurrent.futures
import json
import re
from typing import List, Tuple
from datasets import Dataset
from openai import OpenAI
from tqdm.auto import tqdm

我们不再使用 InstructionAnswerSet 类,而是引入了 PreferenceSet 类。该类用于处理指令、生成的回答(被拒的回答)和提取的回答(选择的回答)的三元组。

class PreferenceSet:
    def __init__(self, triples: List[Tuple[str, str, str]]):
        self.triples = triples

    @classmethod
    def from_json(cls, json_str: str) -> 'PreferenceSet':
        data = json.loads(json_str)
        triples = [(triple['instruction'], triple['generated_answer'], triple['extracted_answer'])
                   for triple in data['preference_triples']]
        return cls(triples)

    def __iter__(self):
        return iter(self.triples)

load_articles_from_jsonclean_textextract_substrings 函数保持不变。首先来看 load_articles_from_json,它将包含文章的 JSON 文件(cleaned_documents.json)作为输入,并返回带有文本和元数据的 Hugging Face 数据集(ID、平台、作者 ID、作者全名、链接)。

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"]],
        }
    )

clean_text 函数删除除撇号、句号、逗号、感叹号和问号以外的非字母数字字符,同时将多余的空格替换为单个空格以确保格式正确。

def clean_text(text: str) -> str:
    text = re.sub(r"[^\w\s.,!?']", " ", text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()

extract_substrings 函数将文章分成长度在 1,000 到 2,000 个字符之间的片段。为避免分割打断句子并改变其含义,我们使用正则表达式仅在句子结束后进行分割。

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

generate_preference_triples 函数替换了原来的 generate_instruction_answer_pairs 函数。提示词从指令版本调整为生成三元组而非成对数据,同时提供了我们感兴趣的指令类型、如何从文章中提取答案及其风格的一般指导。

def generate_preference_triples(extract: str, client: OpenAI) -> List[Tuple[str, str, str]]:
    prompt = f"""Based on the following extract, generate five instruction-answer triples. Each triple should consist of:
1. An instruction asking about a specific topic in the context.
2. A generated answer that attempts to answer the instruction based on the context.
3. An extracted answer that is a relevant excerpt directly from the given context.
Instructions must be self-contained and general, without explicitly mentioning a context, system, course, or extract.
Important:
- Ensure that the extracted answer is a verbatim copy from the context, including all punctuation and apostrophes.
- Do not add any ellipsis (...) or [...]  to indicate skipped text in the extracted answer.
- If the relevant text is not continuous, use two separate sentences from the context instead of skipping text.
Provide your response in JSON format with the following structure:
{{
    "preference_triples": [        {{            "instruction": "...",            "generated_answer": "...",            "extracted_answer": "..."        }},        ...    ]
}}
    Extract:
    {extract}
"""

在同一函数中,我们使用 gpt-4o-mini 模型通过 JSON 模式生成回答。系统提示词指定我们需要三元组而非成对数据。JSON 回答会被 PreferenceSet 类直接解析,返回所需的三元组列表。

    completion = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a helpful assistant who generates instruction-answer triples based on the given context. Each triple should include an instruction, a generated answer, and an extracted answer from the context. Provide your response in JSON format.",
            },
            {"role": "user", "content": prompt},
        ],
        response_format={"type": "json_object"},
        max_tokens=2000,
        temperature=0.7,
    )
    result = PreferenceSet.from_json(completion.choices[0].message.content)
    return result.triples

偏好数据生成流水线引入了两个新的过滤函数:filter_short_answersfilter_answer_format。这些函数用于过滤掉过短的回答,并确保回答以大写字母开头并以正确的标点符号结尾。我们使用这些函数作为启发式方法来过滤质量较差的样本。

def filter_short_answers(dataset: Dataset, min_length: int = 100) -> Dataset:
    def is_long_enough(example):
        return len(example['chosen']) >= min_length
    return dataset.filter(is_long_enough)

def filter_answer_format(dataset: Dataset) -> Dataset:
    def is_valid_format(example):
        chosen = example['chosen']
        return (len(chosen) > 0 and
                chosen[0].isupper() and
                chosen[-1] in ('.', '!', '?'))
    return dataset.filter(is_valid_format)

create_preference_dataset 函数替代了原先的 create_instruction_dataset 函数。该函数现在处理三元组而非成对数据,并使用不同的列名生成数据集。

def create_preference_dataset(dataset: Dataset, client: OpenAI, num_workers: int = 4) -> Dataset:
    extracts = extract_substrings(dataset)
    preference_triples = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
        futures = [
            executor.submit(generate_preference_triples, extract, client)
            for extract in extracts
        ]
        for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
            preference_triples.extend(future.result())
    instructions, generated_answers, extracted_answers = zip(*preference_triples)
    return Dataset.from_dict(
        {
            "prompt": list(instructions),
            "rejected": list(generated_answers),
            "chosen": list(extracted_answers)
        }
    )

主函数包含新的过滤步骤,并使用偏好数据集创建函数:

def main(dataset_id: str) -> Dataset:
    client = OpenAI()
    # 1. 加载原始数据
    raw_dataset = load_articles_from_json("cleaned_documents.json")
    print("Raw dataset:")
    print(raw_dataset.to_pandas())
    # 2. 创建偏好数据集
    dataset = create_preference_dataset(raw_dataset, client)
    print("Preference dataset:")
    print(dataset.to_pandas())
    # 3. 过滤掉回答较短的样本
    dataset = filter_short_answers(dataset)
    # 4. 基于格式过滤回答
    dataset = filter_answer_format(dataset)
    # 5. 导出
    dataset.push_to_hub(dataset_id)
    return dataset

create_preference_dataset() 函数生成了 2,970 个样本。然后对数据集进行严格过滤,保留了 1,467 个样本,去除了过短或格式不正确的回答(例如回答以大写字母开头或以句号、感叹号或问号结尾的样本)。

最终数据集已发布在 Hugging Face Hub 上,地址为:huggingface.co/datasets/ml…。在图 6.3 中可以看到一个示例,其中捕捉到了写作风格上的微妙差异。两个回答都正确,但选择的(提取的)回答听起来稍微更随意一些。

image.png

为了生成这个数据集,我们多次迭代提示词以生成数据。这需要一些手动评估和实验,直到达到满意的结果。在此过程中,提示词的质量至关重要,因此建议使用类似的流程来生成自己的偏好数据集。

在下一节中,我们将介绍与基于人类反馈的强化学习(RLHF)和直接偏好优化(DPO)相关的概念,这将涉及到一些新参数和在本章最后一节中实现的思想。

偏好对齐

偏好对齐包括了在偏好数据上微调模型的技术。本节概述了这一领域,并重点介绍我们将要实现的技术:直接偏好优化(DPO)。

基于人类反馈的强化学习

基于人类反馈的强化学习(RLHF)结合了强化学习(RL)和人类输入,以使模型符合人类的偏好和价值观。RLHF 的出现是为了解决传统 RL 方法中的一些挑战,特别是为复杂任务指定奖励函数的难度,以及工程化奖励与预期目标不一致的潜在问题。

RLHF 的起源可以追溯到偏好基强化学习(PbRL)领域,该方法由 Akrour 等人和 Cheng 等人于 2011 年独立提出。PbRL 旨在从定性反馈(如行为之间的成对偏好)中推断目标,而不依赖于量化奖励信号。这种方法解决了传统 RL 的一些局限性,因为定义合适的奖励函数可能困难且容易出现奖励利用或意外行为。

“RLHF”一词是在 2021-2022 年左右出现的,随着该方法在训练 LLM 方面的应用而变得突出。然而,其核心思想在此之前的数年间已经逐步发展。Christiano 等人在 2017 年发表的一篇开创性论文展示了从人类偏好中学习奖励模型并用于训练 RL 代理的效果。该研究表明,RLHF 可以与基于人工工程奖励训练的代理相媲美或更优,但需要的人力显著减少。

RLHF 的核心是迭代地改进奖励模型和策略:

  1. 奖励模型学习:RLHF 不使用预定义的奖励函数,而是从人类反馈中学习奖励模型。通常通过向人类展示不同的回答并让他们选择更喜欢的回答来实现。这些偏好用于训练奖励模型,通常采用 Bradley-Terry 模型或类似方法将偏好映射到潜在的效用函数。
  2. 策略优化:在学习的奖励模型基础上,可以使用标准的 RL 算法优化策略。该策略生成新行为,旨在最大化从奖励模型中预测的奖励。
  3. 迭代改进:随着策略的改进,它会生成新的行为供人类评估,从而对奖励模型进行细化。该循环不断进行,理想情况下最终生成一个符合人类偏好的策略。

RLHF 的一个关键创新在于其应对人类反馈高成本的方式。RLHF 允许异步和稀疏的反馈,而不需要持续的人工监督。学习的奖励模型充当人类偏好的代理,使 RL 算法可以在无需每次行为都得到直接人类反馈的情况下持续训练。

例如,图 6.4 展示了 Proximal Policy Optimization (PPO) 算法的高层视图,这是最流行的 RLHF 算法之一。在该算法中,奖励模型用于对训练模型生成的文本进行评分。此奖励由一个额外的 Kullback-Leibler (KL) 散度因子进行正则化,以确保生成的词元分布与训练前的模型(冻结模型)保持相似。

image.png

虽然 RLHF 在使 AI 系统与人类偏好对齐方面效果显著,但由于其迭代性质和对单独奖励模型的依赖,RLHF 面临计算开销高且可能不稳定的挑战。尽管理论上更优,但 RLHF 算法在实验上往往不如一些更简单的方法,其中之一就是 DPO。

直接偏好优化(DPO)

DPO 是由 Rafailov 等人在 2023 年的论文《Direct Preference Optimization: Your Language Model is Secretly a Reward Model》中提出的,它为传统 RLHF 方法提供了一个更简化的替代方案。

DPO 的核心创新在于对偏好学习问题的重新表述。与 RLHF 通常需要训练单独的奖励模型并使用像 PPO 这样的强化学习算法来微调语言模型不同,DPO 采取了更直接的方法。

它在标准的 RLHF 目标(在 KL 散度约束下最大化预期奖励)下,推导出最优策略的闭式表达。这一数学洞见使 DPO 能够直接用策略来表达偏好学习问题,无需单独的奖励模型或复杂的强化学习算法。

在实际操作中,DPO 可以实现为一个简单的二元交叉熵损失函数,直接作用于语言模型的输出概率。这个损失函数鼓励模型对偏好的回答赋予更高的概率,对非偏好的回答赋予更低的概率,同时保持与参考(冻结)模型的接近程度。参考模型的重要性通过 0 到 1 之间的 beta 参数直接控制。当 beta 等于 0 时,参考模型被忽略,这意味着训练出的模型可能与 SFT 模型非常不同。在实际应用中,0.1 是最常用的值,但可以根据需要进行调整,我们将在下一节详细探讨。

这种方法的简单性允许使用标准的梯度下降技术进行优化,不需要在训练期间从模型中采样或实现复杂的 RL 算法。图 6.5 展示了 DPO 算法的高层视图,与图 6.4 相比,大大简化了训练过程。

image.png

DPO 相较于传统的 RLHF 方法具有多项优势。正如前面提到的,它显著简化了偏好学习流程,降低了 RLHF 方法带来的工程复杂性。通过不再需要单独的奖励模型和 RL 算法,DPO 比传统 RLHF 方法在计算上更加高效。特别是在使用适配器(如 LoRA、QLoRA)进行训练时,冻结的模型和训练的模型无需分开。由于我们只在训练适配器,实际模型未被修改,这样就可以只加载一个模型而不是两个,从而节省额外的显存(VRAM)。

尽管简单,DPO 的表现通常能匹配更复杂的 RLHF 方法。DPO 在训练期间往往更稳定,对超参数的敏感性较低。其简化的方式使 DPO 更易于实现和扩展,尤其适合不具备丰富 RL 知识的小团队。

虽然 RLHF 可以通过多轮训练实现迭代改进并动态适应新偏好,DPO 提供了一条更直接的途径来实现类似的结果。选择 DPO 还是基于 PPO 的 RLHF 方法,通常取决于实现的简易性和潜在的性能上限之间的权衡。对于包含数百万偏好样本的大规模训练,基于 PPO 的方法仍然具有更高的性能上限。然而,对于大多数应用而言,DPO 在计算和工程成本较低的情况下提供了大部分的性能收益。

RLHF 和 DPO 在整合合成数据时都获益良多。随着 LLM 能力的提升,LLM 生成的数据在质量和多样性上已超越了人类创造的内容。这带来了良性循环,更好的模型生成了更好的训练数据,从而进一步提升模型的表现。两种方法的迭代特性使得模型可以多轮精细化,每一轮聚焦于模型性能的不同方面,并逐步增强各领域的能力。

尽管具有诸多优势,DPO 并非没有缺点。和 RLHF 一样,DPO 仍需要成对的偏好数据,这可能会耗费大量成本和时间来收集。此外,DPO 缺乏某些与强化学习方法相关的理论保证。在一些复杂任务或环境中,RLHF 的灵活性可能会带来更大的优势。

尽管如此,DPO 在大多数情况下是理想的选择,包括我们所举的双胞胎 LLM 示例。在下一节中,我们将使用 Unsloth 实现它。

实现 DPO

在本节中,我们将对第 5 章中创建的 TwinLlama-3.1-8B 模型进行 DPO 微调。为了便于使用并最大化性能,我们将再次使用 Unsloth 库实现 DPO。根据显存情况,可以选择 LoRA(质量更高、速度更快、显存占用更高)或 QLoRA(质量和速度较低、显存占用较少)。此技术以及其他偏好对齐算法在 TRL 和 Axolotl 中也有实现。

此示例可视为 DPO 的高级应用。我们模仿写作风格的目标与 DPO 倾向于鼓励正式语言的自然趋势存在冲突。这部分是因为被选择的回答往往比被拒的回答更正式。在实际操作中,这将迫使我们进行轻微微调,使用较低的学习率和训练轮数。为找到最佳超参数,我们训练了超过 20 个模型,并在一组问题(如“写一段介绍监督微调的段落”)上比较其输出。这使我们能够选择最适合该任务的模型和参数。

依赖项与第 5 章中 SFT 的依赖项相同,可在本书的 GitHub 仓库(github.com/PacktPublis…)或 Unsloth 的仓库(github.com/unslothai/u…)中找到。

首先,我们需要访问受限的模型,并(可选)将微调后的模型上传到 Hugging Face(huggingface.co/)。这需要登录帐户。如果还没有帐户,可以创建一个并将 API 密钥(设置 | 访问令牌 | 创建新令牌)存储在 .env 文件中:

HF_TOKEN = YOUR_API_KEY

确保 .env 文件中包含 Comet ML API 密钥,否则代码在训练开始时会崩溃并报错。

COMET_API_KEY = YOUR_API_KEY

在导入必要的包之前,我们需要为 TRL 中的 DPOTrainer 类应用补丁,以修复 DPO 在 notebook 环境中的日志记录。

from unsloth import PatchDPOTrainer
PatchDPOTrainer()

现在可以导入其他库。DPO 和 SFT 之间的主要区别在于引入了 DPOConfigDPOTrainer(特定于 DPO 训练):

import os
import torch
from datasets import load_dataset
from transformers import TrainingArguments, TextStreamer
from unsloth import FastLanguageModel, is_bfloat16_supported
from trl import DPOConfig, DPOTrainer

此步骤加载我们在第 5 章中微调的模型。我们使用相同的配置,将 max_seq_length 设置为 2048。通过设置 load_in_4bitTrue 可以激活 QLoRA。以下代码中,我们将执行 LoRA DPO 微调,以提高速度和质量。

max_seq_length = 2048
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="mlabonne/TwinLlama-3.1-8B",
    max_seq_length=max_seq_length,
    load_in_4bit=False,
)

接下来,我们为 LoRA 配置准备模型。我们将 rank (r)lora_alpha 从 32(第 5 章中设置的值)增加到 64,以允许更具表现力的微调。保持 lora_dropout 为 0 以提高速度,并将目标设为每个线性模块。

model = FastLanguageModel.get_peft_model(
    model,
    r=64,
    lora_alpha=64,
    lora_dropout=0,
    target_modules=["q_proj", "k_proj", "v_proj", "up_proj", "down_proj", "o_proj", "gate_proj"],
)

我们加载 llmtwin-dpo 数据集(训练分割),其中包含提示词、选择的回答和被拒的回答。

dataset = load_dataset("mlabonne/llmtwin-dpo", split="train")

数据准备与第 5 章中的 SFT 示例明显不同。这里的数据是包含提示词、选择的回答和被拒的回答的三元组。在 format_samples 函数中,我们将 Alpaca 的聊天模板应用到每条消息。注意,只有指令需要聊天格式;选择的和被拒的回答只需连接句子结束符(EOS)标记。最后,我们按 95%/5% 比例创建训练/测试分割。

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
def format_samples(example):
    example["prompt"] = alpaca_template.format(example["prompt"])
    example["chosen"] = example['chosen'] + EOS_TOKEN
    example["rejected"] = example['rejected'] + EOS_TOKEN
    return {"prompt": example["prompt"], "chosen": example["chosen"], "rejected": example["rejected"]}

dataset = dataset.map(format_samples)
dataset = dataset.train_test_split(test_size=0.05)

模型和数据准备就绪,可以开始微调。与 SFT 相比,DPO 引入了一些新参数,如 ref_modelbeta。由于我们使用 LoRA(或 QLoRA),我们并未直接训练模型,而是训练适配器。这意味着可以使用原始模型(未加载适配器)作为参考,从而节省大量显存。beta 参数控制参考模型的重要性。标准值 0.1 在大多数情况下效果良好,但基于我们的实验,我们将其增加到 0.5。原因是较低的值导致训练模型语言较为正式,而更接近参考模型有助于修正此问题。

学习率也较低(从 SFT 的 3e-4 降至 2e-6)。我们训练 1 个 epoch(而非 3 个),并将 max_seq_length 参数拆分为两个新参数:max_prompt_length(仅用于提示)和 max_length(用于提示和回答)。注意,我们还用 DPOConfig 类替换了 TrainingArguments 类。

trainer = DPOTrainer(
    model=model,
    ref_model=None,
    tokenizer=tokenizer,
    beta=0.5,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    max_length=max_seq_length//2,
    max_prompt_length=max_seq_length//2,
    args=DPOConfig(
        learning_rate=2e-6,
        lr_scheduler_type="linear",
        per_device_train_batch_size=2,
        per_device_eval_batch_size=2,
        gradient_accumulation_steps=8,
        num_train_epochs=1,
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),
        optim="adamw_8bit",
        weight_decay=0.01,
        warmup_steps=10,
        output_dir="output",
        eval_strategy="steps",
        eval_steps=0.2,
        logging_steps=1,
        report_to="comet_ml",
        seed=0,
    ),
)
trainer.train()

模型训练完成后,我们可以进行快速检查。该步骤与 SFT 示例类似,准备模型用于推理,并生成提示的响应。

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)

训练后的 DPO 模型返回以下回答:

监督微调是一种利用标注数据来提高预训练语言模型性能的方法。这种技术涉及在特定任务上对预训练模型进行细化,比如内容创建或客户服务。通过向模型提供相关数据和指导,它可以学习生成更符合预期结果的输出。这种方法允许创建更专门的模型,能够更准确和高效地处理复杂任务。

我们可以将其与 SFT 模型的回答进行比较:

监督微调是一种通过提供包含指令及其对应答案的精选数据集来增强语言模型的方法。该过程旨在使模型的响应与人类期望对齐,从而提高其准确性和相关性。目标是确保模型能够有效回应广泛的查询,使其在聊天机器人和虚拟助手等应用中成为有价值的工具。

DPO 模型提供的回答更准确,且更符合预期的写作风格。它正确地指出了预训练语言模型是 SFT 的源模型,并提到领域或任务特定的微调,而不是与“人类期望”的对齐,这更接近偏好对齐阶段。回答也较为非正式,更符合博客文章的风格。

最后一步是将训练后的模型本地保存并推送到 Hugging Face Hub。

model.save_pretrained_merged("model", tokenizer, save_method="merged_16bit")

恭喜!我们成功训练并导出了 DPO 模型。该模型现已在 Hugging Face Hub 上发布,链接:huggingface.co/mlabonne/Tw…。与 SFT 相比,DPO 训练过程中需跟踪一些额外的指标。图 6.6 展示了 Comet ML 仪表板上的主要指标,可通过以下链接公开访问:www.comet.com/mlabonne/ll…

image.png

我们来回顾这些指标:

  • 训练损失:我们仍然希望损失平均持续下降。值得注意的是,损失可能会迅速降至零,这表明模型不再学习新的内容。这种情况不一定会导致过拟合或不良模型,但需要密切监控。
  • 验证损失:验证损失的情况与训练损失类似。我们期望它与训练损失之间有一个小差距。
  • 梯度范数:我们希望看到小的梯度范数,并且出现尖峰的情况较少。
  • 奖励:我们有两种不同的奖励:选择的回答和被拒的回答。它们对应于训练模型和参考模型输出的对数概率之间的平均差异。随着时间的推移,我们期望模型倾向于选择被选择的回答并拒绝被拒的回答,这意味着两者之间的差距应该增大。该差距通过边际指标直接跟踪,定义为选择的回答和被拒的回答的奖励之差。训练良好的模型的边际值将迅速增加并达到稳定。
  • 准确率:该指标表示模型正确识别选择回答的比例。我们希望在训练过程中准确率逐步提高,但不需要达到 100%。如果准确率达到 100%,尤其是快速达到这一水平,说明偏好数据集对模型来说可能过于简单。尽管 LLM 仍可以从这样的数据集中学习,但增加更具挑战性的示例可能更有帮助。

总体而言,DPO 比 SFT 略难监控和调试,因为其过程更复杂,涉及一个参考模型。然而,它仍然比 PPO 和其他 RLHF 算法容易使用得多。只要拥有高质量的偏好数据集和强大的微调模型,可以尝试不同的 rank、beta 参数、学习率和 epoch 数,来观察哪个实验最好地捕捉到你的偏好。

虽然本章的目标并非自动评估模仿写作风格的模型,但这是可能实现的。一个可能的解决方案是比较不同模型(如 SFT 和 DPO)生成的文本中单词的分布与真实数据集的分布。在本例中,我们预计 SFT 模型会输出许多在 GPT-4o-mini 中过度出现的词(如“delve into”)。而 DPO 模型输出的分布应该更接近选择的回答。

总结

本章探讨了用于改进大型语言模型(LLM)的偏好对齐技术,介绍了偏好数据集的概念,解释了其结构及在捕捉人类偏好细微差别中的重要性。我们通过对比原始文章和 AI 生成文本,构建了一个定制的偏好数据生成流程。该流程可根据具体用例重复使用和定制。

此外,我们概述了 RLHF 的演变,并引入了 DPO 作为一种更简单且更高效的替代方案。最后,我们使用 Unsloth 库对第 5 章的 TwinLlama-3.1-8B 模型进行了 DPO 微调。我们的分步教程提供了模型训练的实际指导,同时强调了与 SFT 的关键区别。最终模型已发布在 Hugging Face Hub 上。

在下一章中,我们将探讨 LLM 评估的重要话题,解决评估 LLM 性能的挑战和当前方法。我们将讨论创建特定领域的评估集,解释评估为何是该领域的一个持续难题,并引入使用更大模型评估较小模型的概念(即 LLM 作为裁判)。该章将以一个全面的评估流程收尾,提供结构化的框架以实现一致且有效的 LLM 评估。

参考文献

  • Rafailov, R. 等人. "Direct Preference Optimization: Your Language Model is Secretly a Reward Model." arXiv 预印本 arXiv:2305.18290, 2023年5月.
  • Kaufmann, T. 等人. "A Survey of Reinforcement Learning from Human Feedback." arXiv 预印本 arXiv:2312.14925, 2023年12月.
  • Anthropic. “GitHub - anthropics/hh-rlhf: 人类偏好数据,用于‘通过人类反馈的强化学习训练一个有帮助且无害的助手’。” github.com, 2022年, github.com/anthropics/….
  • Stiennon, N. 等人. “Learning to summarize from human feedback.” arXiv 预印本 arXiv:2009.01325, 2020年9月.
  • Intel Neural Compressor. “Habana Gaudi2 上的监督微调与直接偏好优化实践。” medium.com, 2024年3月26日, medium.com/intel-analy….
  • Argilla. “GitHub - argilla-io/distilabel.” github.com, 2024年8月23日, github.com/argilla-io/….
  • Databricks. “Enhancing LLM-as-a-Judge with Grading Notes.” databricks.com, 2024年7月22日, www.databricks.com/blog/enhanc….
  • Akrour, R., Schoenauer, M., & Sebag, M. (2011). "Preference-Based Policy Learning." 10.1007/978-3-642-23780-5_11.
  • Cheng, W., Fürnkranz, J., Hüllermeier, E., & Park, S.-H. (2011). "Preference-Based Policy Iteration." 10.1007/978-3-642-23780-5_30.
  • Christiano, P. 等人. “Deep reinforcement learning from human preferences.” arXiv 预印本 arXiv:1706.03741, 2017年6月.
  • Ouyang, L. 等人. “Training language models to follow instructions with human feedback.” arXiv 预印本 arXiv:2203.02155, 2022年3月.
  • Schulman, J. 等人. “Proximal Policy Optimization Algorithms.” arXiv 预印本 arXiv:1707.06347, 2017年7月.
  • unslothai. “GitHub - unslothai/unsloth: Finetune Llama 3.1, Mistral, Phi & Gemma LLMs 快 2-5 倍,内存减少 80%。” github.com, 2024年8月21日, github.com/unslothai/u….