#
⚖️ 创建一个合法偏好数据集

作者: David Berenstein 和 Sara Han Díaz

在本教程中，你将学习如何在 HF 推理端点上使用 Notus 模型，基于欧洲人工智能法案中的 RAG 指令创建一个合法的偏好数据集。这是一个完整的端到端示例，展示了如何使用 distilabel 来利用大型语言模型（LLMs）！

distilabel是一个人工智能反馈（AIF）框架，它可以使用 LLMs 生成和标注数据集，并且可以用于许多不同的用例。它以稳健性、效率和可扩展性为目标实现，允许任何人构建可用于多种场景的合成数据集。

为了生成指令数据集，我们将使用与 distilabel 集成的HF 推理端点。这些推理端点由 Hugging Face 提供，允许在专用和自动扩展的基础设施上轻松部署和运行 transformers、diffusers 或 Hub 中的任何可用模型。你可以在这里找到更多关于如何创建你的第一个端点的信息。

我们将为此微调的 LLM 模型是 Notus 7B，这是 Zephyr 7B 的一个微调版本，它使用直接偏好优化（DPO）和 AIF 技术，在多个基准测试中超越了其基础模型，并且完全开源。

本教程包括以下步骤：

- 为 `distilabel` 流水线定义一个自定义生成任务。
- 使用 Haystack 为欧盟人工智能法案创建一个 RAG 流水线。
- 使用 `SelfInstructTask` 生成一个指令数据集。
- 使用 `UltraFeedback` 文本质量任务生成一个偏好数据集。

##
简介

让我们从安装运行 distilabel 以及教程中使用的其他包所需的依赖项开始；尤其是 Haystack。为了更好地可视化和管理结果，也请安装 Argilla。

    !pip install -q -U distilabel "farm-haystack[preprocessing]"
    !pip install -q -U "distilabel[hf-inference-endpoints, argilla]"

###
导入依赖项

本教程的主要依赖项是 distilabel，用于创建合成数据集，以及 Argilla，用于可视化和管理这些数据集，同时也用于微调我们的模型。包 Haystack 用于从我们想要创建数据集的原始 PDF 文档中创建批次。

    import os
    from typing import Dict

    from distilabel.llm import InferenceEndpointsLLM
    from distilabel.pipeline import Pipeline, pipeline
    from distilabel.tasks import TextGenerationTask, SelfInstructTask, Prompt

    from datasets import Dataset
    from haystack.nodes import PDFToTextConverter, PreProcessor

###
环境变量

我们需要提供我们的 HuggingFace 访问 token，可以从设置中获取。此外，为了通过 UltraFeedback 文本质量任务生成偏好数据集，我们还需要 OpenAI 的 api 密钥。你可以在这里找到它。请注意，根据使用的模型不同，将收取不同的费用，因此请确保你检查了 OpenAI 的定价页面。

为了稍后实例化一个 `InferenceEndpointsLLM` 对象，我们还需要作为参数传递 HF 推理端点名称和 HF 命名空间。通过环境变量也是一种非常方便的方式。

    os.environ["HF_TOKEN"] = ""
    os.environ["HF_INFERENCE_ENDPOINT_NAME"] = "aws-notus-7b-v1-3184"
    os.environ["HF_NAMESPACE"] = "argilla"
    os.environ["OPENAI_API_KEY"] = ""

##
使用 Notus 设置推理端点

推理端点是 Hugging Face 管理的一种解决方案，可以轻松部署任何类似 Transformer 的模型。它们是基于 Hugging Face Hub 上的模型构建的。推理端点对于在 LLMs 上进行推理非常方便，无需尝试在本地运行模型。在本教程中，我们将使用推理端点作为 `distilabel` 工作流程的一部分，使用我们的 Notus 模型生成文本。所选端点运行着一个Notus 7B 实例。

###
为 distilabel 流水线定义一个自定义生成任务

为了开始本教程，让我们看看如何为我们的 Notus 模型设置一个端点。这不是我们稍后将看到的端到端示例的一部分，但是如何连接到 Hugging Face 端点以及测试 `distilabel` 流水线的一个示例。

让我们快速了解一下如何使用推理端点。我们已经准备了一个简单的 `TextGenerationTask` 来向模型提问，这种方式与我们使用聊天机器人与 LLMs 交流非常相似。首先，我们定义一个用于问答任务的类，其中包含的函数向 `distilabel` 展示了模型应该如何生成提示、解析输入和输出等。

    class QuestionAnsweringTask(TextGenerationTask):
        def generate_prompt(self, question: str) -> str:
            return Prompt(
                system_prompt=self.system_prompt,
                formatted_prompt=question,
            ).format_as(
                "llama2"
            )  # type: ignore

        def parse_output(self, output: str) -> Dict[str, str]:
            return {"answer": output.strip()}

        @property
        def input_args_names(self) -> list[str]:
            return ["question"]

        @property
        def output_args_names(self) -> list[str]:
            return ["answer"]

`llm` 是 `InferenceEndpointsLLM` 类的一个对象，通过使用它，我们可以开始使用 `llm.generate()` 方法来生成问题的答案。

    llm = InferenceEndpointsLLM(
        endpoint_name_or_model_id=os.getenv("HF_INFERENCE_ENDPOINT_NAME"),  # type: ignore
        endpoint_namespace=os.getenv("HF_NAMESPACE"),  # type: ignore
        token=os.getenv("HF_TOKEN") or None,
        task=QuestionAnsweringTask(),
    )

使用定义了端点和任务信息的 `InferenceEndpointsLLM` 对象，我们可以开始生成文本。让我们问这个 LLM，例如，丹麦第二大城市人口最多的是哪个城市。答案应该是 Aarhus。

    generation = llm.generate([{"question": "What's the second most populated city in Denmark?"}])
    generation[0][0]["parsed_output"]["answer"]

端点工作正常！我们已经成功地为 `distilabel` 流水线设置了一个自定义生成任务。

##
使用 Haystack 为欧洲人工智能法案创建 RAG 流水线

在这个端到端的示例中，我们希望创建一个能够回答问题并填写关于欧盟推广的新人工智能法案信息的专家模型，这是关于人工智能的第一项法规。作为其数字战略的一部分，欧盟希望规范人工智能，以确保更好地发展和使用这项创新技术。这个法案是人工智能的监管框架，不同的风险级别意味着更多的或更少的监管。它们是世界上关于人工智能的第一套规则。

我们想要创建的这个 RAG 的流水线会下载 PDF 文件，将其转换为纯文本并进行预处理，创建我们可以提供给 `distilabel` 的批次，以便开始从中创建指令。让我们看看流水线的第一部分并获取输入数据。需要注意的是，这个流水线的 RAG 部分并不是基于活跃的查询或语义属性的流水线，而是一种更直接的方法，我们下载PDF并预处理其内容。

###
下载人工智能法案 PDF

首先，我们需要下载 PDF 文档本身。如果它不在工作目录中，我们将把它放在那里。

    %%bash

    if [ ! -f "The-AI-Act.pdf" ]; then
        wget -q https://artificialintelligenceact.eu/wp-content/uploads/2021/08/The-AI-Act.pdf
    fi

一旦我们将文件放入工作目录，我们可以使用 Haystack 的转换器和流水线功能来提取文本数据，清洗数据并将其分成不同的批次。之后，这些批次将被用来开始创建合成指令。

    # The converter turns the PDF into text we can process easily
    converter = PDFToTextConverter(remove_numeric_tables=True, valid_languages=["en"])

    # Preprocessing pipelines can have several steps.
    # Ours clean empty lines, header, footers and whitespaces
    # and split the text into 150-char long batches, respecting
    # where the sentences naturally end and begin.
    preprocessor = PreProcessor(
        clean_empty_lines=True,
        clean_whitespace=True,
        clean_header_footer=True,
        split_by="word",
        split_length=150,
        split_respect_sentence_boundary=True,
    )

    doc = converter.convert(file_path="The-AI-Act.pdf", meta=None)[0]
    docs = preprocessor.process([doc])
    print(f"Documents: 1\nBatches: {len(docs)}")

让我们快速查看一下我们刚刚生成的批次。

    inputs = [doc.content for doc in docs]
    inputs[0][0:500]

文件已经被正确地分批处理，从一个大文档变成了最多 150 个字符长的 355 个字符串。现在这个字符串列表可以作为输入，使用 `distilabel` 生成指令数据集。

##
使用 SelfInstructTask 生成指令

由于我们的推理端点已经启动并运行，我们应该能够使用 distilabel 生成指令。通过我们的端点由 LLM 创建的这些指令，将形成一个指令数据集，其中的指令是由我们刚刚提取的数据创建的。

为了示例的顺利进行，我们使用了上面生成的 50 个批次的一个子集，以减轻性能压力。

    instructions_dataset = Dataset.from_dict({"input": inputs[0:50]})

    instructions_dataset

使用 `SelfInstructTask` 类，我们可以为构建提示生成一个 Self-Instruct 规范，就像在 Self-Instruct 论文中所做的那样。`distilabel` 将从人工制作的输入开始，在这个案例中，就是我们从 AI 法案 PDF 创建的批次，然后基于这些输入生成指令。之后，可以使用 Argilla 来审查这些指令，以保留最好的那些。

我们可以通过传递一个应用描述作为参数来告诉模型它应该做什么；我们希望这个模型能够回答我们关于 AI 法案的任何问题。

    instructions_task = SelfInstructTask(
        application_description="A assistant that can answer questions about the AI Act made by the European Union."
    )

现在，我们来定义一个生成器，传入 `SelfInstructTask` 对象，并创建一个 `Pipeline` 对象。

    instructions_generator = InferenceEndpointsLLM(
        endpoint_name_or_model_id=os.getenv("HF_INFERENCE_ENDPOINT_NAME"),  # type: ignore
        endpoint_namespace=os.getenv("HF_NAMESPACE"),  # type: ignore
        token=os.getenv("HF_TOKEN") or None,
        task=instructions_task,
    )

    instructions_pipeline = Pipeline(generator=instructions_generator)

我们的流水线已经准备好用来生成指令了。下面就开始吧！

    generated_instructions = instructions_pipeline.generate(dataset=instructions_dataset, num_generations=1, batch_size=8)

流水线已经成功生成了指令，基于输入的主题和行为。让我们收集所有这些指令，看看它们是什么样的。

    >>> instructions = []
    >>> for generations in generated_instructions["instructions"]:
    ...     for generation in generations:
    ...         instructions.extend(generation)

    >>> print(f"Number of generated instructions: {len(instructions)}")

    >>> for instruction in instructions[:5]:
    ...     print(instruction)

    Number of generated instructions: 178
    What are the reasons for and objectives of the proposal for a Regulation laying down harmonised rules on artificial intelligence?
    How can artificial intelligence improve prediction, optimise operations and resource allocation, and personalise service delivery?
    What benefits can artificial intelligence bring to the European economy and society as a whole?
    How can the use of artificial intelligence support socially and environmentally beneficial outcomes?
    What are the high-impact sectors that require AI action according to the AI Act by the European Union?

这些初始指令构成了我们的指令数据集。遵循人机协同的方法，我们应该将指令推送到 Argilla 进行可视化，并能够根据质量对它们进行排序。这些注释对于制作高质量的数据至关重要，确保最终模型有更好的性能。然而，这一步是可选的。

###
将指令数据集推送到Argilla以进行可视化和注释。

让我们快速查看一下由 `SelfInstructTask` 生成的指令。

    generated_instructions[0]

对于每个输入，即 AI 法案 PDF 文件的每个批次，我们都有一个生成器提示，其中包含了关于如何行动的通用指南，以及应用程序描述参数。每个输入已经生成了 4 条指令。

现在正好是将指令数据集上传到 Argilla，审查并手动注释它的最佳时机。

    instructions_rg_dataset = generated_instructions.to_argilla()
    instructions_rg_dataset[0]

    instructions_rg_dataset.push_to_argilla(name=f"notus_AI_instructions")

在 Argilla 的用户界面中，每个输入-指令元组都会单独显示，并且可以单独进行注释。

##
使用 Ultrafeedback 文本质量任务生成偏好数据集

一旦我们有了指令数据集，我们将会通过 UltraFeedback 文本质量任务创建一个偏好数据集。这是一种在自然语言处理中用于评估生成文本质量的任务类型；我们的目标是提供关于生成文本质量的详细反馈，而不仅仅是二元的标签。 我们的 `pipeline()` 方法允许我们为给定的任务创建一个带有提供的 LLMs 的 `Pipeline` 实例，这在你想要为给定任务使用预定义或自定义的 `Pipeline` 时非常有用。我们将指定我们的任务和子任务，我们想要使用的生成器（在这个案例中，是基于文本生成任务的生成器）以及我们的 OpenAI API 密钥。

> 请注意，不使用 OpenAI 模型来获取此反馈也是可能的。然而，性能将会受到影响，反馈的质量也会较低。

    preference_pipeline = pipeline(
        "preference",
        "instruction-following",
        generator=InferenceEndpointsLLM(
            endpoint_name_or_model_id=os.getenv("HF_INFERENCE_ENDPOINT_NAME"),  # type: ignore
            endpoint_namespace=os.getenv("HF_NAMESPACE", None),
            task=TextGenerationTask(),
            max_new_tokens=256,
            num_threads=2,
            temperature=0.3,
        ),
        max_new_tokens=256,
        num_threads=2,
        api_key=os.getenv("OPENAI_API_KEY", None),
        temperature=0.0,
    )

我们还需要从 Argilla 检索我们的指令数据集，因为它将是这个流水线的输入。

    remote_dataset = rg.FeedbackDataset.from_argilla("notus_AI_instructions", workspace="admin")
    instructions_dataset = remote_dataset.pull(max_records=100)  # get first 100 records

    instructions_dataset = instructions_dataset.format_as("datasets")
    instructions_dataset

    instructions_dataset[0]

在根据我们的指令生成文本之前，我们需要重命名数据集中的某些列。从前面的部分，我们仍然有旧的输入，即来自 PDF 的批次。我们需要将它们改为我们生成的指令。

    instructions_dataset = instructions_dataset.rename_columns({"input": "context", "instruction": "input"})

现在，让我们使用刚刚创建的流水线以及生成我们指令的主题来构建一个数据集。

    preference_dataset = preference_pipeline.generate(
        instructions_dataset,  # type: ignore
        num_generations=2,
        batch_size=8,
        display_progress_bar=True,
    )

让我们来看一下偏好数据集的一个实例：

    preference_dataset[0]

##
使用 Argilla 进行人工反馈

你可以直接使用 distilabel 创建的 AI 反馈，但我们已经看到，通过加入人工反馈可以提升 LLM 的质量。我们提供了一个 `to_argilla` 方法，它为 Argilla 创建了一个数据集，并附带了现成的定制元数据过滤器以及语义搜索，让你能够尽可能快速和有趣地提供人工反馈。你可以查看Argilla 文档来了解如何安装和运行。

如果你正在使用 Docker 快速启动镜像或 Hugging Face Spaces 运行Argilla，你需要使用 URL 和 API_KEY 初始化 Argilla 客户端：

    import argilla as rg

    # Replace api_url with the url to your HF Spaces URL if using Spaces
    # Replace api_key if you configured a custom API key
    rg.init(api_url="http://localhost:6900", api_key="owner.apikey", workspace="admin")

一旦我们成功地制作出了偏好数据集，Argilla 的用户界面就是最适合我们用来查看和标记这些数据的东西。就像我们对指令数据集所做的那样，我们只需要把这个数据集变成 Argilla 能理解的格式，然后上传到 Argilla 上就可以开始工作了。

    # Uploading the Preference Dataset
    preference_rg_dataset = preference_dataset.to_argilla()

    # Adding the context as a metadata property in the new Feedback dataset, as this
    # information will be useful later.
    for record_feedback, record_huggingface in zip(preference_rg_dataset, preference_dataset):
        record_feedback.metadata["context"] = record_huggingface["context"]

    preference_rg_dataset.push_to_argilla(name=f"notus_AI_preference")

在Argilla用户界面中，我们可以看到输入（一个指令），以及 LLM 基于该指令创建的两个生成文本。

##
结论

总结一下，我们已经完成了一个使用 distilabel 的端到端示例。我们建立了一个推理端点，定义了一个从 PDF 提取信息的 distilabel 流水线，并创建和手动审查了从该输入生成的指令和偏好数据集。最终的偏好数据集非常适合进行微调，你可以使用 Argilla 的 ArgillaTrainer 轻松完成这一工作。如果你想深入了解，请查看以下资源：

- 使用 ArgillaTrainer 训练模型
- Ⓜ️ 将 LLM 作为聊天助手进行监督式微调：Mistral 7B
- 🌠 通过优化检索和重排模型来改进 RAG

< > Update on GitHub