基于大模型的合成数据生成

构建大规模、全面的数据集来测试 LLM 输出可能是一个费力、昂贵且具有挑战性的过程,尤其是从头开始构建时。但是,如果我告诉你,现在只需几分钟就可以生成通常花费数周精心制作的数千个高质量测试用例,你会怎么想?

合成数据生成利用 LLM 创建高质量数据,而无需手动收集、清理和标注大量数据集。借助 GPT-4 等模型,现在可以在更短的时间内合成生成比人工标记数据集更全面、更多样化的数据集,这些数据集可用于借助一些 LLM 评估指标对 LLM(系统)进行基准测试。

在本文中,我将教你有关如何使用 LLM 生成合成数据集(例如,可用于评估 RAG 管道)的所有知识。我们将探索:

  • 合成生成方法(蒸馏和自我改进)
  • 什么是数据演化、各种演化技术及其在合成数据生成中的作用
  • 使用 LLM 从头开始​​创建高质量合成数据的分步教程。
  • 如何使用 DeepEval 在不到 5 行代码中生成合成数据集。

感兴趣吗?让我们深入了解。

1、什么是使用 LLM 的合成数据生成?

使用 LLM 进行合成数据生成涉及使用 LLM 创建人工数据,这些人工数据通常是可用于训练、微调甚至评估 LLM 本身的数据集。生成合成数据集不仅比搜索公共数据集更快、比人工标注更便宜,而且还能提高质量和数据多样性,这对于红队 LLM 应用程序也是必不可少的。

该过程从创建合成查询开始,这些查询是使用知识库中的上下文(通常以文档的形式)作为基本事实生成的。然后,生成的查询会经过多次“演化”,使其复杂化和逼真化,并与生成查询的原始上下文相结合,构成最终的合成数据集。虽然这是可选的,但你也可以选择为每个合成查询-上下文对生成一个目标标签,该标签将作为给定查询的 LLM 系统的预期输出。

数据合成器架构

在生成用于评估的合成数据集时,主要有两种方法:使用模型输出进行自我改进,或从更高级的模型中进行提炼。

  • 自我改进:涉及您的模型从其自身输出迭代生成数据,而无需外部依赖
  • 蒸馏:涉及使用更强大的模型生成合成数据以评估较弱的模型

自我改进方法(例如 Self-Instruct 或 SPIN)受到模型功能的限制,可能会受到偏差和错误放大的影响。相比之下,蒸馏技术仅受可用的最佳模型的限制,可确保最高质量的生成。

2、从知识库生成数据

合成数据生成的第一步是从上下文列表中创建合成查询,这些查询直接来自知识库。本质上,上下文是 LLM 应用程序的理想检索上下文,就像预期输出是 LLM 实际输出的基本事实参考一样。

对于那些想要立即工作的人,以下是使用开源 LLM 评估框架 DeepEval 生成高质量合成数据的方法:

from deepeval.synthesizer import Synthesizer

synthesizer = Synthesizer()
synthesizer.generate_goldens_from_docs(
    document_paths=['example.txt', 'example.docx', 'example.pdf'],
)

2.1 构建上下文

在上下文生成过程中,使用标记拆分器将知识库中的文档(或多个文档)分成块。然后选择随机块,并根据相似性检索其他块并将其与所选块分组。

使用余弦相似度生成上下文

可以使用多种方法计算相似度:

  • 利用相似度或距离算法,例如余弦相似度
  • 使用知识图谱
  • 利用 LLM 本身,虽然不太现实,但准确性最高
  • 应用聚类技术来识别模式
  • 使用机器学习模型根据特征预测分组。

无论如何,总体目标保持不变:有效地聚合相似的信息块。

为了确保这些分组有效并符合 LLM 应用程序的特定需求,最佳做法是在上下文生成过程中镜像应用程序的检索器逻辑。这包括仔细考虑诸如标记拆分方法、块大小和块重叠等方面。

这种对齐可确保合成数据的行为与应用程序的期望一致,从而防止由于检索器复杂性的差异,结果会有所不同。

2.2 从上下文生成合成输入

一旦创建了上下文,就会从中生成合成输入。此方法反转了标准检索操作 - 不是根据输入定位上下文,而是基于预定义的上下文创建输入。这可确保每个合成输入都直接对应于一个上下文,从而提高相关性和准确性。

此外,这些上下文可用于通过将它们与合成生成的输入对齐来选择性地生成预期的输出。

查询生成的非对称方法

这种非对称方法可确保所有组件(输入、输出和上下文)都完全同步。

3、过滤合成数据

在开始改进新生成的数据集之前,必须进行彻底的质量检查,以避免改进本身存在缺陷的输入。此步骤对于确保不会浪费宝贵的资源以及最终数据集仅包含高质量的黄金至关重要。

过滤发生在合成数据生成的两个关键阶段:首先是在上下文生成期间,然后是从这些上下文生成合成输入期间。

3.1 上下文过滤

在上下文生成期间,你可能会随机选择一个低质量的块。通常,你的知识库可能包含复杂的结构或多余的空白,分解后会变得难以理解。聘请 LLM 作为评判员是识别和消除这些低质量上下文的可靠方法。

上下文过滤示例

你可以自定义评估和过滤这些上下文的标准,但以下是一些需要考虑的基本准则:

  • 清晰度:评估信息的清晰度和可理解性。
  • 深度:评估详细分析的水平和原创见解的存在。
  • 结构:审查内容的组织和逻辑进展。
  • 相关性:确定内容与主要主题的相关性。
  • 精度:衡量准确性和对细节的关注。
  • 新颖性:评估内容的独特性和原创性。
  • 简洁性:评估沟通的简洁性和效率。
  • 影响:判断内容对受众的潜在影响。

过滤掉低质量的块后,你还需要确保剩余的块足够相似。这涉及二次过滤过程,在该过程中,你会剔除未通过相似度阈值的块。

这种结构化的过滤方法可确保只有高质量、相关且有用的上下文才能进入合成数据生成的下一阶段。

3.2 输入过滤

第二阶段的重点是从这些上下文生成的合成输入。此步骤至关重要,因为即使是精心策划的上下文有时也会导致生成可能不符合要求标准的输入。

输入过滤示例

以下是你可能希望判断合成输入的几个标准:

  • 自包含:确保输入完整并且可以在没有外部引用的情况下独立运行。
  • 清晰度:检查输入是否清楚地传达了其预期的信息或问题,以避免误解。
  • 一致性:确保输入在主题和事实上与提供的上下文或背景信息相符。
  • 相关性:验证输入是否与预期任务或查询直接相关,确保其有目的且切题。
  • 完整性:确认输入包含有效交互或查询解决所需的所有必要细节。

使用这些标准有助于确保合成输入不仅质量高,而且完全适合其预期应用。

4、合成数据样式

最后,你可能希望针对特定主题定制查询,并自定义其输入和输出格式以适合您的独特用例。

例如,如果你的应用程序涉及将文本转换为 SQL,则输出应准确反映 SQL 语句。在涉及评估 LLM 的场景中,使用带有“分数”和“原因”等键的 JSON 格式可能更合适。
您应该计划在初始生成期间、任何演变变化期间以及生成最终输出后应用特定样式。在初始生成之后重新审视样式至关重要,因为合成查询的演变可能会改变初始样式意图。

第一轮之后的样式调整程度将取决于你对最终产品的控制程度以及相关的成本影响。

5、数据适者生存

让我们澄清什么是数据演变以及为什么它对使用 LLM 进行合成数据生成如此重要。数据演变最早在 Microsoft 的 Evol-Instruct 中引入,它涉及迭代增强现有查询集,以通过快速工程生成更复杂和多样化的查询。此步骤对于确保质量至关重要

数据集的真实性、全面性、复杂性和多样性。这就是合成数据优于公共或人工注释的数据集的原因。

事实上,原作者仅从 175 个人工创建的查询中就成功生成了 250,000 条指令。数据演化有 3 种类型:

  • 深度演化:将简单指令扩展为更详细和更复杂的版本。
  • 广度演化:生成新的、多样化的指令以丰富数据集。
  • 消除演化:删除效率较低或失败的指令。
“1+1”查询的深度(蓝色)和广度(红色)演化

有几种方法可以执行深度演化,例如使输入复杂化、增加推理需求或添加多个步骤来完成任务。每种方法都有助于提高生成的数据的复杂程度。

深度演化确保创建细致入微的高质量查询,而广度演化则增强了多样性和全面性。通过多次演化每个查询或指令,我们增加了其复杂性,从而产生了丰富而多方面的数据集。不过,我说得够多了,让我们向您展示如何将一切付诸行动。

以这个查询为例:

1+1 是什么?

我们可以将其深度演化为类似这样的内容:

在什么情况下 1+1 不等于 2?

我希望我们都同意这比一般的 1+1 更复杂、更现实。在下一节中,我们将展示如何在生成合成数据集时实际使用这些演化方法。

6、分步指南:使用 LLM 生成合成数据

在开始之前,让我们回顾一下我们将要构建的数据合成器架构:

数据合成器架构

你会注意到有五个主要步骤:

  • 文档分块
  • 上下文生成
  • 查询生成
  • 数据演化
  • 标签/预期输出生成(可选)

对于那些想要立即开始工作的人,我已经在 DeepEval 中开源了这个过程,你可以立即准备好生成合成数据集,并支持过滤和样式(我将在最后一节中向你展示)。

from deepeval.synthesizer import Synthesizer

synthesizer = Synthesizer()
synthesizer.generate_goldens_from_docs(
    document_paths=['example.txt', 'example.docx', 'example.pdf'],
)

如果你来这里是为了了解它的工作原理,请继续阅读。

6.1 文档分块

第一步是分块你的文档。顾名思义,文档分块意味着将其分成更小、更有意义的“块”。这样,你可以将较大的文档分解为可管理的子文档,同时保留其上下文。分块还允许在超出嵌入模型的标记限制的文档中生成嵌入。

此步骤至关重要,因为它有助于识别语义相似的块并根据共享上下文生成查询或任务。

有几种分块策略,如固定大小分块和上下文感知分块。你还可以调整超参数,例如字符大小和块重叠。在下面的示例中,我们将使用基于 token 的分块,字符大小为 1024,没有重叠。以下是分块文档的方法:

pip install langchain langchain_openai
# Step 1. Chunk Documents
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import TokenTextSplitter

text_splitter = TokenTextSplitter(chunk_size=
1024
, chunk_overlap=
0
)
loader = PyPDFLoader("chatbot_information.pdf")
raw_chunks = loader.load_and_split(text_splitter)

获得分块后,将每个分块转换为嵌入。这些嵌入捕获每个分块的语义含义,并与分块的内容组合以形成一个 Chunk 对象列表。

from langchain_openai import OpenAIEmbeddings
...

embedding_model = OpenAIEmbeddings(api_key="...")
content = [rc.page_content for rc in raw_chunks]
embeddings = embedding_model.embed_documents(content)

6.2 上下文生成

要生成上下文,首先随机选择一块数据作为查找相关信息的焦点锚点。

# Step 2: Generate context by selecting chunks
import random
...

reference_index = random.randint(
0
, len(embeddings) - 
1
)
reference_embedding = embeddings[reference_index]
contexts = [content[reference_index]]

接下来,设置相似度阈值并使用余弦相似度来识别相关块以构建上下文:

...

similarity_threshold = 
0.8

similar_indices = []
for i, embedding in enumerate(embeddings):
    product = np.dot(reference_embedding, embedding)
    norm = np.linalg.norm(reference_embedding) * np.linalg.norm(embedding)
    similarity = product / norm
    if similarity >= similarity_threshold:
        similar_indices.append(i)

for i in similar_indices:
    contexts.append(content[i])

此步骤至关重要,因为它允许你通过围绕同一主题多样化信息源来增强查询的稳健性。通过包含具有相似主题的多个数据块,你还可以为模型提供有关该主题的更丰富、更细致入微的信息。

这可确保你的查询全面涵盖该主题,从而产生更全面和准确的响应。

6.3 查询生成

现在是 LLM 的有趣部分。使用 GPT 模型为使用结构化提示创建的上下文生成一系列任务或查询。

提供一个提示,要求模型充当文案撰写者,生成包含输入键(即查询)的 JSON 对象。每个输入都应该是使用提供的上下文可以回答的问题或语句。

# Step 3. Generate a series of queries for similar chunks
from langchain_openai import ChatOpenAI
...

prompt = 
f
"""I want you act as a copywriter. Based on the given context, 
which is list of strings, please generate a list of JSON objects 
with a `input` key. The `input` can either be a question or a 
statement that can be addressed by the given context.

contexts:
{contexts}"""

query = ChatOpenAI(openai_api_key="...").invoke(prompt)

此步骤构成查询的基础,查询将不断发展并包含在最终数据集中。

6.4 查询演化

最后,我们将使用多个演化模板演化步骤 3 中的查询。你可以根据需要定义任意数量的模板,但我们将重点关注三个:多上下文理解、多步骤推理和假设场景。

# Evolution prompt templates as strings
multi_context_template = 
f
"""
I want you to rewrite the given `input` so that it requires readers to use information from all elements in `Context`.

1. `Input` should require information from all `Context` elements. 
2. `Rewritten Input` must be concise and fully answerable from `Context`. 
3. Do not use phrases like 'based on the provided context.'
4. `Rewritten Input` should not exceed 15 words.

Context: {context}
Input: {original_input}
Rewritten Input:
"""

reasoning_template = 
f
"""
I want you to rewrite the given `input` so that it explicitly requests multi-step reasoning.

1. `Rewritten Input` should require multiple logical connections or inferences.
2. `Rewritten Input` should be concise and understandable.
3. Do not use phrases like 'based on the provided context.'
4. `Rewritten Input` must be fully answerable from `Context`.
5. `Rewritten Input` should not exceed 15 words.

Context: {context}
Input: {original_input}
Rewritten Input:
"""

hypothetical_scenario_template = 
f
"""
I want you to rewrite the given `input` to incorporate a hypothetical or speculative scenario.

1. `Rewritten Input` should encourage applying knowledge from `Context` to deduce outcomes.
2. `Rewritten Input` should be concise and understandable.
3. Do not use phrases like 'based on the provided context.'
4. `Rewritten Input` must be fully answerable from `Context`.
5. `Rewritten Input` should not exceed 15 words.

Context: {context}
Input: {original_input}
Rewritten Input:
"""

你可以看到每个模板都对输出施加了特定的约束。您可以根据希望评估查询在最终数据集中显示的方式随意调整它们。我们将使用这些模板多次演化原始查询,每次随机选择模板。

# Step 4. Evolve Queries
...

example_generated_query = "How do chatbots use natural language understanding?"
context = contexts 
original_input = example_generated_query 
evolution_templates = [multi_context_template, reasoning_template, hypothetical_scenario_template]

# Number of evolution steps to apply
num_evolution_steps = 
3


# Function to perform random evolution steps
def evolve_query(original_input, context, steps):
    current_input = original_input
    for _ in range(steps):
        # Choose a random (or using custom logic) template from the list
        chosen_template = random.choice(evolution_templates)
        # Replace the placeholders with the current context and input
        evolved_prompt = chosen_template.replace("{context}", str(context)).replace("{original_input}", current_input)
        # Update the current input with the "Rewritten Input" section
        current_input = ChatOpenAI(openai_api_key="...").invoke(evolved_prompt)
    return current_input

# Evolve the input by randomly selecting the evolution type
evolved_query = evolve_query(original_input, context, num_evolution_steps)

这就是我们最终的进化查询!重复此过程以生成更多查询并进一步优化您的数据集。出于评估目的,你需要正确格式化这些输入

将查询和上下文整合到合适的测试框架中。

6.5 预期输出生成

虽然此步骤是可选的,但我强烈建议为每个演化查询生成预期输出。这是因为对于人类评估者来说,纠正和注释预期输出比从头开始创建它们更容易。

# Step 5. Generate Expected Output
...

# Define prompt template
expected_output_template = 
f
"""
I want you to generate an answer for the given `input`. This answer has to be factually aligned to the provided context.

Context: {context}
Input: {evolved_query}
Answer:
"""

# Fill in the values
prompt = expected_output_template.replace("{context}", str(context)).replace("{evolved_query}", evolved_query)

# Generate expected output
expected_output = ChatOpenAI(openai_api_key="...").invoke(prompt)

作为最后一步,将演化查询、上下文和预期输出合并为合成数据集中的数据行。

from pydantic import BaseModel
from typing import Optional, List
...

class SyntheticData(BaseModel):
	query: str
	expected_output: Optional[str]
	context: List[str]

synthetic_data = SyntheticData(
	query=evolved_query, 
	expected_output=expected_output, 
	context=context
)

# Simple implementation of synthetic dataset
synthetic_dataset = []
synthetic_dataset.append(synthetic_data)

现在你需要做的就是重复步骤 1-5,直到拥有一个合理大小的合成数据集,你以后可以使用它来评估和测试你的 LLM(系统)!

7、使用 DeepEval 生成合成数据集

在最后一节中,我想向您展示一个久经考验的数据合成器,我在 DeepEval 中将其开源。这包括从合成数据生成到将其格式化为可用于 LLM 评估和测试的测试用例,你只需 2 行代码即可使用。最好的部分是,你可以利用你选择的任何 LLM。以下是使用 DeepEval 生成合成数据集的方法:

pip install deepeval
from deepeval.synthesizer import Synthesizer

synthesizer = Synthesizer()
synthesizer.generate_goldens_from_docs(
    document_paths=['example.txt', 'example.docx', 'example.pdf'],
)

你可以在 DeepEval 的文档中阅读有关如何使用 DeepEval 的合成器生成合成数据集的更多信息,但总而言之,DeepEval 会接收您的文档,为你完成所有分块和上下文生成,然后生成合成的“黄金”,这些黄金基本上是最终形成合成数据集的数据行。够简单吗?

8、结束语

使用 LLM 生成合成数据集非常棒,因为它是一种快速且廉价的获取大量数据的方法。但是,生成的数据看起来非常重复,而且往往不能很好地代表底层数据分布,因此不被视为有用。在本文中,我们讨论了如何通过首先从文档中选择相关上下文来解决此问题,然后使用它来生成可用于测试和评估 LLM 系统的查询。

我们还探讨了数据演变,我们用它来使合成查询更加逼真。如果你想从头开始构建数据合成器,本文将是一个很好的教程。但是,如果你正在寻找更强大且可用于生产的东西,可以使用 DeepEval。它是开源的,非常易于使用(真的),并且拥有一整套评估和测试套件,你可以使用生成的合成数据集无缝地测试和评估您的 LLM 系统。


原文链接:Using LLMs for Synthetic Data Generation: The Definitive Guide

汇智网翻译整理,转载请标明出处