构建一个大规模、全面的数据集来测试LLM的输出可能是一个耗时、昂贵且充满挑战的过程,尤其是从零开始。但如果我告诉你,现在只需几分钟就能生成你花费数周精心打造的数千个高质量测试用例,会怎样呢?
合成数据生成利用LLMs来创建高质量数据,无需手动收集、清理和标注海量数据集。借助如 GPT-4 这样的模型,现在可以在更短时间内合成出比人工标注更全面、更多样化的数据集,这些数据集可用于在LLM评测指标的帮助下对LLM(系统)进行基准测试。
在本文中,我将教你如何利用LLMs生成合成数据集(例如可用于评测 RAG 流程)所需了解的一切。我们将探讨:
- 合成生成方法(蒸馏与自我改进)
- 数据演化的概念、多种演化技术及其在合成数据生成中的作用
- 使用LLMs从零开始创建高质量合成数据的逐步教程
- 如何用 DeepEval 在 5 行代码内生成合成数据集
感兴趣吗?让我们深入了解。
什么是使用LLMs的合成数据生成?
使用LLMs进行合成数据生成涉及利用LLM创建人工数据,这些通常是可用于训练、微调甚至评测LLMs自身的数据集。生成合成数据集不仅比搜寻公共数据集更快、比人工标注更经济,还能产生更高质量和多样性的数据,这对于红队测试LLM应用同样至关重要。
该过程始于合成查询的创建,这些查询基于知识库(通常以文档形式存在)中的上下文作为基础事实生成。生成的查询随后会经过多次“进化”以复杂化和逼真化,当与原始生成上下文结合时,便构成了最终的合成数据集。虽然可选,你还可以选择为每个合成查询-上下文对生成目标标签,这将作为LLM系统针对给定查询的预期输出。
数据合成器架构
在生成用于评测的合成数据集时,主要有两种方法:利用模型输出进行自我改进,或从更先进的模型中进行蒸馏。
- 自我改进:涉及你的模型从自身输出迭代生成数据,无需外部依赖
- 蒸馏:涉及使用更强的模型生成合成数据来评测较弱的模型
自我改进方法,如 Self-Instruct 或 SPIN,受限于模型的能力,并可能遭受偏见和错误的放大。相比之下,蒸馏技术仅受限于现有最佳模型,确保了最高质量的生成。
从你的知识库生成数据
合成数据生成的第一步涉及从你的上下文列表中创建合成查询,这些上下文直接来源于你的知识库。本质上,上下文充当了你LLM应用程序的理想检索上下文,就像预期输出作为你LLM实际输出的真实参考一样。
对于希望立即获得可行方案的用户,以下是使用 DeepEval(开源LLM评测框架,GitHub 地址:https://github.com/confident-ai/deepeval)生成高质量合成数据的方法:
from deepeval.synthesizer import Synthesizer
synthesizer = Synthesizer()
synthesizer.generate_goldens_from_docs(
document_paths=['example.txt', 'example.docx', 'example.pdf'],
)
构建上下文
在上下文生成过程中,知识库中的一个或多个文档会被分词器分割成多个片段。随后随机选取一个片段,并根据相似度检索并组合其他相关片段。
使用余弦相似度的上下文生成
相似性可以通过多种方法计算:
- 利用相似性或距离算法,例如余弦相似度
- 运用知识图谱
- 直接利用LLMs本身,虽不够现实,但提供了最高精确度
- 应用聚类技术识别模式
- 使用机器学习模型基于特征预测分组。
无论如何,首要目标始终如一:有效地聚合相似的信息块。
为确保这些分组有效且符合你的LLM应用程序的特定需求,最佳实践是在上下文生成过程中镜像应用程序的检索逻辑。这包括仔细考虑诸如令牌分割方法、块大小和块重叠等方面。
这种对齐保证了合成数据的行为与应用程序的期望保持一致,防止由于检索器复杂性差异导致结果出现偏差。
从上下文生成合成输入
一旦上下文创建完成,随后便从中生成合成输入。此方法逆转了标准的检索操作——不是根据输入查找上下文,而是基于预定义的上下文创建输入。这确保了每个合成输入都直接对应一个上下文,从而提升相关性和准确性。
此外,这些上下文还可用于通过将其与合成生成的输入对齐,选择性地产生预期输出。
非对称查询生成方法
这种非对称方法确保所有组件——输入、输出和上下文——都能完美同步。
过滤合成数据
在开始对新生成的数据集进行演化之前,必须进行彻底的质量检查,以避免精炼那些本身存在缺陷的输入。这一步骤至关重要,确保不浪费宝贵资源,并保证最终数据集仅包含高质量的金标数据。
过滤发生在合成数据生成的两个关键阶段:最初是在上下文生成期间,随后是在基于这些上下文生成合成输入时。
上下文过滤
在上下文生成过程中,有可能随机选中低质量片段。通常,你的知识库可能包含复杂结构或过多空白,分解后变得难以理解。采用LLMs作为评判标准,是识别并剔除这些低质量上下文的有效方法。
上下文过滤示例
你可以自定义评测和筛选这些上下文的标准,但以下是一些基础指导原则供参考:
- 清晰度:评测信息的清晰易懂程度。
- 深度:考察详细分析的水平及原创见解的存在。
- 结构:审视内容的组织与逻辑推进。
- 相关性:判断内容与主题的关联性。
- 精确度:衡量准确性和对细节的关注。
- 新颖性:评测内容的独特性和原创性。
- 简洁性:判断交流的简短与高效程度。
- 影响:评测内容对受众的潜在影响。
在完成低质量片段过滤后,还需确保剩余片段足够相似。这涉及一个二次筛选过程,剔除未达到相似度阈值的片段。
这种结构化的过滤方法确保只有高质量、相关且有用的上下文能进入合成数据生成的下一阶段。
输入过滤
第二阶段的过滤专注于从这些上下文中生成的合成输入。这一步骤至关重要,因为即便是精心筛选的上下文,有时也可能导致生成的输入不符合既定标准。
输入过滤示例
以下是你可能希望用于评测合成输入的几项标准:
- 自包含性:确保输入内容完整且能独立运作,无需外部引用。
- 清晰度:检查输入是否明确传达了其预期信息或问题,以避免误解。
- 一致性:保证输入在主题和事实上与提供的上下文或背景信息保持一致。
- 相关性:验证输入是否直接关联到目标任务或查询,确保其目的明确且紧扣主题。
- 完整性:确认输入包含有效交互或查询解决所需的所有必要细节。
采用这些标准有助于确保合成输入不仅质量上乘,还能完美契合其目标应用场景。
合成数据的样式设计
最后,你可能希望根据特定主题调整查询,并自定义其输入输出格式以适应独特的用例需求。
例如,若你的应用涉及将文本转换为 SQL,输出应准确反映 SQL 语句。在包含评测LLM的场景中,采用带有'score'和'reason'等键的 JSON 格式可能更为合适。
你应计划在初始生成阶段、任何演化变更期间以及最终输出生成后应用特定的样式设计。在初始生成后重新审视样式至关重要,因为合成查询的演变可能会改变最初的样式意图。
首轮过后风格调整的程度将取决于你对最终产品期望的控制水平及相关的成本影响。
数据适者生存
让我们明确什么是数据进化,以及为何它对使用LLMs进行合成数据生成如此重要。数据进化最初由微软的 Evol-Instruct 提出,涉及通过提示工程迭代增强现有查询集,以生成更复杂多样的查询。这一步骤对于确保数据集的质量、全面性、复杂性和多样性至关重要,正是这一点使得合成数据优于公开或人工标注的数据集。
事实上,原作者仅用 175 条人工创建的查询就生成了 25 万条指令。数据演化共有三种类型:
- 深度演化:将简单指令扩展为更详细复杂的版本。
- 广度演化:生成新颖多样的指令以丰富数据集。
- 淘汰演化:移除效果不佳或失败的指令。
'1+1'查询的深度(蓝色)和广度(红色)演化
实现深度进化有多种方法,例如复杂化输入、增加推理需求或为完成任务添加多个步骤。每种方法都有助于提升生成数据的复杂度和精细度。
深度进化确保创建出细致入微、高质量的查询,而广度进化则增强了多样性和全面性。通过对每个查询或指令进行多次进化,我们提升了其复杂性,从而形成一个丰富且多层面的数据集。不过理论说得够多了,接下来让我们展示如何将这一切付诸实践。
以这个查询为例:
1 加 1 等于多少?
我们可以将其深度演化为类似这样的形式:
在什么情况下 1 加 1 不等于 2?
我希望大家都能认同,这比普通的 1 加 1 更为复杂且贴近现实。在下一部分,我们将展示如何在生成合成数据集时实际运用这些演化方法。
分步指南:使用LLMs生成合成数据
在开始之前,让我们回顾一下将要构建的数据合成器架构:
数据合成器架构
你会注意到主要有五个步骤:
- 文档分块
- 上下文生成
- 查询生成
- 数据演化
- 标签/预期输出生成(可选)
对于那些希望立即上手实践的人,我已将此流程开源至 DeepEval,你可以即刻开始支持筛选与样式定制(我将在最后部分展示)的合成数据集生成工作。
from deepeval.synthesizer import Synthesizer
synthesizer = Synthesizer()
synthesizer.generate_goldens_from_docs(
document_paths=['example.txt', 'example.docx', 'example.pdf'],
)
如果你来此是为了了解其工作原理,请继续阅读。
1. 文档分块
第一步是对文档进行分块处理。顾名思义,文档分块就是将其划分为更小、有意义的“块”。通过这种方式,你可以将大型文档分解为易于管理的子文档,同时保持其上下文。分块还能为超出嵌入模型令牌限制的文档生成嵌入。
这一步至关重要,因为它有助于识别语义相似的块,并根据共享上下文生成查询或任务。
有多种分块策略可供选择,如固定大小分块和上下文感知分块。你还可以调整字符大小和块重叠等超参数。在下面的示例中,我们将使用基于令牌的分块方法,字符大小设为 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)
获得分块后,将每一块转换为嵌入向量。这些嵌入捕捉了每个块的语义含义,并与块内容结合形成一系列块对象列表。
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)
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])
此步骤至关重要,因为它能通过围绕同一主题多样化信息来源来增强查询的鲁棒性。纳入多个共享相似主题的数据块,还能为模型提供更丰富、更具细微差别的主题信息。
这确保了你的查询能全面覆盖主题,从而得到更全面且准确的回答。
3. 查询生成
现在进入与LLMs相关的有趣环节。利用 GPT 模型,通过结构化提示为已创建的上下文生成一系列任务或查询。
提供一个提示,要求模型扮演文案撰写员的角色,生成包含 input 键(即查询)的 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)
此步骤构成了查询的基础,这些查询将被演进并包含在最终数据集中。
4. 查询演化
最后,我们将利用多种演进模板对第三步的查询进行演进。你可以定义任意数量的模板,但我们将重点关注三个:多上下文理解、多步推理和假设场景。
# 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)
至此,我们完成了最终进化版查询! 重复该流程即可生成更多查询语句,并持续优化数据集。为便于评测,需将生成的输入查询及其关联上下文规范化为适配目标场景的测试框架。
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(系统)了!
使用 DeepEval 生成合成数据集
在这最后一部分,我想向你展示一个经过实战检验的数据合成器,我已将其开源在 DeepEval 中。这包括从合成数据生成到将其格式化为测试用例,准备用于LLM评测和测试,你只需两行代码即可使用。最棒的是,你可以利用任何你选择的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 会接收你的文档,为你完成所有分块和上下文生成工作,然后生成合成的“goldens”,这些基本上是最终构成你合成数据集的数据行
结论
使用LLMs生成合成数据集非常棒,因为这是快速且低成本获取大量数据的途径。然而,生成的数据可能显得极其重复,且往往无法充分代表底层数据分布,以至于难以被视为有用。本文中,我们探讨了如何通过先从文档中筛选相关上下文,再利用其生成可用于测试和评测LLM系统的查询来解决这一问题。
我们还深入探讨了数据进化(Data Evolution)技术,该技术可显著提升合成查询请求的真实性。若你计划从零构建数据合成器,本文堪称绝佳指南。但若追求更高鲁棒性且可直接投入生产环境的解决方案,建议采用DeepEval工具链。其具备开源特性与极简操作界面(绝非虚言),并集成完整的评估测试套件,使你能无缝对接生成的合成数据集,对LLM系统进行全维度测试与评估。