一、前言
目前大模型的微调方法有很多,且大多可以在消费级显卡上进行,开发者完全可以在本地环境中微调自己的大模型。然而在实际操作中,我们常面临一个核心问题:数据集的构建。网络上虽然有许多开源数据集,但往往难以直接满足特定业务场景的需求。我们更希望使用某本书、某个作者的作品、内部聊天记录或特定角色的对话来微调模型。
用于微调的高质量数据通常是成千上万的问答对(Instruction-Input-Output)。如果完全依靠人工搜集和标注,将耗费大量时间成本。本文将介绍一种高效的方式:利用大语言模型自动生成高质量数据集,并使用该数据集对大模型进行微调。
二、构造数据集
2.1 目的与格式
微调数据集通常采用问答对形式,例如 Alpaca 数据集的标准结构如下:
{
"instruction": "保持健康的三个提示。",
"input": "",
"output": "以下是保持健康的三个提示:\n\n1. 保持身体活动。每天做适当的身体运动..."
}
但在实际场景中,我们拥有的原始数据往往是一大段非结构化的文本,例如书籍内容或文档片段:
小时候,那时我还只有6岁,看到一本描写原始森林壮观景象的书,名叫真实的故事。书里有一幅很精彩的插画,画的是一条大蟒蛇正在吞食一只动物...
我们的目标是将这种大段文本转换为标准的 Alpaca 格式。过去这只能依赖人工,现在我们可以借助大模型的能力,通过 Prompt 工程让模型根据上下文提取对话和问答内容。
2.2 Prompt 设计
在系统提示词中,我们需要明确指示模型根据上下文提取问答对。示例如下:
QA_PAIRS_SYSTEM_PROMPT = """
<Context></Context> 标记中是一段文本,请学习和分析它,并整理学习成果:
- 提出问题并给出每个问题的答案。
- 答案需详细完整,尽可能保留原文描述。
- 答案可以包含普通文字、链接、代码、表格等 Markdown 元素。
- 最多提出 30 个问题。
"""
为了便于程序解析,我们需要规定严格的输出格式(JSON 数组):
QA_PAIRS_HUMAN_PROMPT = """
请按以下格式整理学习成果:
<Context>
{text}
</Context>
[
{{"question": "问题 1", "answer": "答案 1"}},
{{"question": "问题 2", "answer": "答案 2"}}
]
------
我们开始吧!
"""
2.3 处理文档
首先导入必要的模块,包括文件加载、文本分割及 LangChain 组件:
import json
from typing import List
from tqdm import tqdm
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from langchain_openai import AzureChatOpenAI
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
我们需要将长文档切分为适合模型处理的片段:
def split_document(filepath):
loader = UnstructuredFileLoader(filepath)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=2048,
chunk_overlap=128
)
documents = loader.load_and_split(text_splitter)
return documents
2.4 构建 Chain
构建生成数据集的 Chain,包含 Prompt、LLM 和 Output Parser 三部分。
2.4.1 Prompt 整合
prompt = ChatPromptTemplate.from_messages([
("system", QA_PAIRS_SYSTEM_PROMPT),
("human", QA_PAIRS_HUMAN_PROMPT)
])
2.4.2 LLM 配置
建议选择长上下文能力强于待微调模型的基座。此处以 Azure OpenAI 为例:
llm = AzureChatOpenAI(
azure_endpoint=endpoint,
deployment_name=deployment_name,
openai_api_key=api_key,
openai_api_version="2024-02-01",
)
2.4.3 结果解析
定义 Pydantic 模型以确保输出符合 JSON Schema:
from langchain_core.pydantic_v1 import BaseModel, Field
class QaPair(BaseModel):
question: str = Field(description='问题内容')
answer: str = Field(description='问题的回答')
class QaPairs(BaseModel):
qas: List[QaPair] = Field(description='问答对列表')
parser = JsonOutputParser(pydantic_object=QaPairs)
连接 Chain 并封装为函数:
def create_chain():
prompt = ChatPromptTemplate.from_messages([
("system", QA_PAIRS_SYSTEM_PROMPT),
("human", QA_PAIRS_HUMAN_PROMPT)
])
llm = AzureChatOpenAI(
azure_endpoint=endpoint,
deployment_name=deployment_name,
openai_api_key=api_key,
openai_api_version="2024-02-01",
)
parser = JsonOutputParser(pydantic_object=QaPairs)
chain = prompt | llm | parser
return chain
主流程执行逻辑:
def main():
chain = create_chain()
documents = split_document('The.Little.Prince.txt')
datas = []
bar = tqdm(total=len(documents))
for idx, doc in enumerate(documents):
bar.update(1)
try:
out = chain.invoke({'text': doc.page_content})
if isinstance(out, dict) and 'qas' in out:
datas.extend(out['qas'])
except Exception as e:
print(f"Error processing doc {idx}: {e}")
with open('dataset.json', 'w', encoding='utf-8') as f:
json.dump(datas, f, ensure_ascii=False, indent=2)
print(f"Total pairs generated: {len(datas)}")
if __name__ == '__main__':
main()
三、微调模型
准备好数据集后,可以使用 PEFT 库进行高效微调。这里以 Hugging Face Transformers 和 LoRA 技术为例。
3.1 加载模型与配置 LoRA
LoRA(Low-Rank Adaptation)通过在旁支网络注入可训练参数,大幅降低显存需求。
from peft import LoraConfig, TaskType, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer
# 配置 LoRA 参数
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=8, # 低秩矩阵维度
lora_alpha=32, # 缩放系数
lora_dropout=0.1
)
# 加载基座模型
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Phi-3-mini-4k-instruct",
trust_remote_code=True,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")
# 应用 LoRA
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
3.2 数据处理与 Tokenization
需要将自定义数据集转换为模型可接受的输入格式。
from datasets import load_dataset
def tokenize_function(example):
encoded = tokenizer(
example['question'],
truncation=True,
padding='max_length',
max_length=512
)
# 标签设为答案部分
labels = tokenizer(
example["answer"],
truncation=True,
padding="max_length",
max_length=512
)["input_ids"]
encoded["labels"] = [-100] * len(encoded["input_ids"]) # 默认忽略
# 仅将答案部分的 token 设为标签
input_len = len(encoded["input_ids"])
label_start = input_len - len(labels)
encoded["labels"][label_start:] = labels
return encoded
# 加载本地数据集
dataset = load_dataset('json', data_files={'train': 'dataset.json'})
tokenized_dataset = dataset.map(tokenize_function, batched=True)
3.3 训练配置与执行
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="outputs",
learning_rate=2e-4,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
num_train_epochs=3,
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
fp16=True,
logging_steps=10
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"],
tokenizer=tokenizer,
)
trainer.train()
model.save_pretrained("outputs")
四、推理部署
微调完成后,需要加载适配器进行推理。LoRA 允许我们在不修改基座权重的情况下切换不同任务。
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Phi-3-mini-4k-instruct",
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")
model = model.to("cuda")
model.load_adapter('outputs', adapter_name='lora01')
model.set_adapter("lora01")
model.eval()
inputs = tokenizer("作者小时候看了一本关于什么的书?", return_tensors="pt")
outputs = model.generate(
input_ids=inputs["input_ids"].to("cuda"),
max_new_tokens=100,
temperature=0.7
)
print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True)[0])
五、常见问题与优化建议
5.1 数据质量控制
在使用大模型生成数据集时,可能会遇到幻觉或格式错误。建议在生成后进行简单的过滤:
- 长度过滤:去除过短或过长的问答对。
- 重复检测:计算相似度,去除重复样本。
- 人工抽检:随机抽取 5%-10% 的数据进行人工审核。
5.2 训练稳定性
- 学习率调整:若 Loss 震荡剧烈,可降低学习率;若收敛过慢,可适当提高。
- 早停机制:监控验证集 Loss,当连续 N 个 epoch 未下降时停止训练,防止过拟合。
- 混合精度训练:开启
fp16或bf16可显著减少显存占用并加速训练。
5.3 推理性能优化
- 量化:对于资源受限环境,可使用 INT4 或 INT8 量化模型。
- 批处理:在推理端支持批量请求,提高吞吐量。
- 缓存 KV:启用 Key-Value Cache 可减少重复计算。
六、总结
本文详细介绍了如何利用大语言模型自动化构建垂直领域数据集,并结合 PEFT 技术对 Phi-3 等开源模型进行低成本微调。通过这一流程,开发者可以快速构建专属知识库,无需依赖昂贵的 API 服务。未来可进一步探索多模态数据融合及分布式训练方案,以适应更大规模的应用场景。


