发布时间:2025-06-25 10:34:57编辑:123阅读(203)
如何构建高质量数据集?
目标是微调后让其在特定场景下具备一定的推理能力,并且在 Web 安全领域具备专家级水准。
注意:在非 Web 安全领域下,模型并不一定能够稳定输出推理过程,因为仅针对 Web 安全领域的数据集进行了训练,这在一定程度上激发了模型的推理能力,想要让模型在更多领域稳定输出推理过程,需要更丰富的数据集。
对于企业内部的代码、需求文档等一般都属于企业内部的机密数据,若直接使用外部模型可能存在隐私泄漏风险,因此此类任务一般需要私有部署的模型承担,而满血版模型的部署成本非常昂贵,所以选择使用对小参数模型进行微调。 希望微调后的模型可用于在企业内部私有部署,回答 Web 安全相关问题,对可能引发安全问题的业务场景以及代码进行预警,并且在 Web 安全领域的能力上可和满血版模型持平甚至超越满血版模型。
数据集在微调任务中起着至关重要的作用。毫不夸张的说,要想得到好的微调效果,数据集的质量要远大于其他参数的设置,如果数据集太小、多样性不足、数据噪声太大、样本偏差严重等问题都会导致微调任务失败。按照经验来讲,在一次微调任务中,大概 80% 的时间应该花在数据集的准备和处理上,因为微调的工具、流程和参数的调整都是有经验可循的,而数据集的构建却需要结合具体业务场景。从数据采集时需覆盖多维度场景,到清洗时剔除噪声与偏差样本,再到标注时确保一致性与准确性,每个环节都影响最终效果。此外,还需合理划分训练/验证/测试集,通过数据增强扩充样本多样性,让模型在微调中真正学习到关键特征。
数据集格式要求
在不同的微调阶段,使用的数据集的格式要求是不一样的。
本次进行的是指令监督微调(SFT),在 LLaMA Factory 中主要支持 Alpaca 格式和 ShareGPT 两种格式:
Alpaca 格式的指令微调数据集
格式要求:
[
{
"instruction": "人类指令(必填)",
"input": "人类输入(选填)",
"output": "模型回答(必填)",
"system": "系统提示词(选填)",
"history": [
["第一轮指令(选填)", "第一轮回答(选填)"],
["第二轮指令(选填)", "第二轮回答(选填)"]
]
}
]
实际案例:
[
{
"instruction": "将这句英文翻译成中文",
"input": "Hello,how are you?",
"output": "你好,你好吗?",
"system":"翻译任务,请确保翻译准确,自然",
"history": [
["将这句英文翻译成中文","你好,你好吗?"],
["将这句中文翻译成英文","How are you?"]
]
}
]
ShareGPT格式的指令微调数据集:
[
{
"conversations": [
{
"from": "human",
"value": "人类指令"
},
{
"from": "function_call",
"value": "工具参数"
},
{
"from": "observation",
"value": "工具结果"
},
{
"from": "gpt",
"value": "模型回答"
}
],
"system": "系统提示词(选填)",
"tools": "工具描述(选填)"
}
]
实际案例:
[
{
"conversations": [
{
"from": "human",
"value": "帮我计算一下2+2"
},
{
"from": "function_call",
"value": "calculator:1|operation=addition|a=2|b=2"
},
{
"from": "observation",
"value": "calculator:1|result=4"
},
{
"from": "gpt",
"value": "2+2的结果是4"
}
],
"system": "使用计算器工具完成数学计算任务",
"tools": "calculator:执行基本数学运算"
}
]
数据集配置文件
看到 LLaMA Factory 提供了两个数据集相关的配置:数据路径和数据集(名称)
数据集处理过程
LLaMA Factory 基于 dataset_info.json 实现了对多种不同的数据集格式兼容,一般情况下大家只需要了解 dataset_info.json 是怎么配置的即可,但是为了加深大家对数据集的理解,下面用一个具体的例子告诉大家一个你自定义的数据集最终会被处理成什么样子。假定我们的原始原始数据结构是这样的(Alpaca 格式):[
{
"instruction": "今天的天气怎么样?",
"input": "",
"output": "今天的天气不错,是晴天。",
"history": [
["今天会下雨吗?", "今天不会下雨,是个好天气。"],
["今天适合出去玩吗?", "非常适合,空气质量很好。"]
]
}
]
在数据加载阶段,通过 dataset_info.json 配置数据集文件的路径:
"my_dataset": {
"file_name": "my_dataset.json",
"formatting": "alpaca"
}
在微调任务开始后, LLaMA Factory 首先会将数据集转换诶标准格式:{
"_prompt": [
{"role": "user", "content": "今天会下雨吗?"},
{"role": "assistant", "content": "今天不会下雨..."},
{"role": "user", "content": "今天适合出去玩吗?"},
{"role": "assistant", "content": "非常适合..."},
{"role": "user", "content": "今天的天气怎么样?"}
],
"_response": [{"role": "assistant", "content": "今天的天气不错..."}]
}
然后根据上面选择的对话模版,将数据集按照模版进行格式化,比如本次选择的模版是 Qwen:
format_user = "<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n"
format_assistant = "{{content}}<|im_end|>\n"
format_system = "<|im_start|>system\n{{content}}<|im_end|>\n"
格式化后数据集就是这样的:
<|im_start|>system
You are Qwen...<|im_end|>
<|im_start|>user
今天会下雨吗?<|im_end|>
<|im_start|>assistant
今天不会下雨...<|im_end|>
<|im_start|>user
今天适合出去玩吗?<|im_end|>
<|im_start|>assistant
非常适合...<|im_end|>
<|im_start|>user
今天的天气怎么样?<|im_end|>
<|im_start|>assistant
今天的天气不错...<|im_end|>
随后, LLaMA Factory 将会对数据进行分词处理,将文本转换为数字序列(token IDs)。这一步骤至关重要,因为模型仅能处理数字而非原始文本:
[151, 8243, 29973, 29901, 29871, 29896, 29889, 29877, 29889, 29889, 152, 151, 8251, 29901, 29871, 29896, 29889, 29877, 29889, 29889, 152]
随后,LLaMA Factory 会对数据集进行标签分配,标签分配是微调过程的核心环节,决定模型在哪些部分计算损失并学习生成内容。在对话模型训练中,模型需学会:理解用户输入(不生成用户输入)、生成助手回复,因此,需明确标记模型应生成的部分(助手回复)和仅需理解的部分(用户输入)。所以这一步就是把不参与损失计算(用户输入)的部分标记出来,LLaMA Factory 使用特殊值IGNORE_INDEX(值为-100)标记不参与损失计算的位置。以示例文本为例:
<|im_start|>user
今天会下雨吗?
<|im_end|>
<|im_start|>assistant
今天不会下雨,是个好天气。
<|im_end|>
标签分配规则为: 用户部分(含特殊标记):全部为-100 助手部分(含回复内容和结束标记):实际 token ID 。
训练时,模型仅因未正确预测助手回复而计算损失,避免因未预测用户输入而受惩罚。
数据完成分词和标签分配后,组织为以下格式传递给训练循环:
{
"input_ids": [所有token IDs],
"attention_mask": [全部为1的序列,表示所有token都需要关注],
"labels": [混合了-100和实际token ID的序列]
}
前向传播:模型接收完整input_ids输入,生成每个位置的预测。
损失计算:仅在labels不为-100的位置计算损失。
反向传播:基于损失更新模型参数
数据集的构造思路
数据集的质量好坏直接决定着微调任务的成败,根据最近我收集的各种微调后效果不好的案例中,大部分也都是数据集质量的问题,我总结了以下几个可能会导致微调任务失败的问题:
数据量太小: 数据量不足时,模型无法学习到足够的特征规律,如果你调的训练轮数很大,非常容易出现过拟合(仅记住少量样本的噪声信息),导致泛化能力差,在新数据上表现不佳。当然不同的微调任务对数据量的要求不一样,如果是领域知识注入类任务,(7B模型)至少要 1000 条数据起步,数据集的数量的扩大也要随着模型参数的变大而增加。另外也别迷信 “数据越多越好”,若数据质量差,海量数据反而会放大噪声影响。优先保证单条数据的有效性,再考虑扩充数量。
噪声数据多:如文本数据中存在大量错别字、格式混乱,图像数据模糊、标注错误等,会误导模型学习错误的映射关系。所以数据集一定要进行清洗,文本数据重点筛除乱码、重复内容、与任务无关的段落(比如微调法律模型时混入娱乐新闻);标注数据需检查标签 - 内容一致性(如情感分类中 “好评” 文本误标为 “差评”)。
样本偏差严重: 数据分布与实际应用场景差异大(如训练数据中 90% 是正面评论,而实际测试数据正负比例均衡),导致模型在真实场景下失效。
任务相关性不够:要模型学会一个领域的知识,不是说这个领域的所有数据都建议用于训练,比如你要微调一个金融问答模型时,就不要混入大量的财经新闻,因为新闻侧重事件描述,而问答侧重问题解答,数据形式需贴近实际应用场景(如 “问题 - 答案” 对)。
数据多样性不足: 样本覆盖的场景、特征维度单一(如文本数据仅包含短句子,缺乏长句或复杂句式;图像数据仅包含晴天场景,没有雨天、夜晚等),模型无法适应真实应用中的多样性需求。此外,若指令类型单一(如仅包含「解释型」指令),缺乏「推理型」「操作型」指令,会导致模型在实际应用中能力受限。
在本地微调任务中,以下是数据集构造思路:为确保模型能学习到专业的 Web 安全知识,选择从 Web 安全领域的专业书籍来提取部分数据集;为确保模型能学习到 Web 安全领域最前沿的知识,精选几篇 Web 安全相关的最新论文来提取部分数据集:
《The Hidden Risks of LLM-Generated Web Application Code: A Security-Centric Evaluation of Code Generation Capabilities in Large Language Models》
《WASP: Benchmarking Web Agent Security Against Prompt Injection Attacks》
《A Human Study of Cognitive Biases in Web Application Security》
《Web安全深度剖析》
《白帽子讲Web安全》
为确保模型能够学习均衡,不遗漏 Web 安全领域的知识,弥补和满血版大模型的差距,选取更为强大的满血版大模型(DeepSeek R1 0528)作为教师模型来蒸馏部分数据集;同时将提取教师模型的推理过程(COT)来作为数据集的一部分,用于训练基础模型的推理能力;为确保数据集的多样性,在数据集构造过程中将使用 GA(Genre-Audience)的思路来对数据集进行增强;在数据集构造完成后,邀请 Web 安全领域专家来对数据集进行 Review(人工审核),确保数据集最终质量。
在本次任务中,将使用如下模型:
Qwen3-235B-A22B:用于建立领域树、从文本块中提取问题(结构化输出更稳定)
DeepSeek-r1-250528:用户从文本块中为指定问题提取答案(推理模型,可获取思维链)
MinerU:用于自定义视觉模型解析 PDF文件(比基础解析效果更好)
MinerU下载地址:https://github.com/opendatalab/MinerU/blob/master/README_zh-CN.md
MinerU本地部署教程:https://py3study.com/Article/details/id/20108.html
数据集构建的整体逻辑:
MinerU将上面的5个pdf文档转换为markdown格式
使用langchain递归字符文本分割(内置很多文本分割方法,选择合适的)
使用Qwen3-235B-A22B从分割的文本块中提取问题
使用DeepSeek-r1-250528从分割的文本块中提取思维链和答案
最后将问题,答案,思维链,生成对应的微调数据集JSON或者JSONL格式
langchain安装
pip install langchain -i https://mirrors.aliyun.com/pypi/simple
pdf文件目录:
完整代码:
from langchain.text_splitter import RecursiveCharacterTextSplitter import os from openai import OpenAI import re import json pdf_path = os.path.join(os.getcwd(), "markdown_files") # 设置pdf路径 api_key = "sk-###################################" # 设置api key global_prompt_words = "语言:中文" # 全局提示词,通过自定义提示词可以提高生成数据集的质量 problem_prompt_words = "生成的问题要和web安全领域相关,禁止生成和论文引用文献、论文作者、图片和表格数据描述相关问题" # 生成问题提示词 answer_prompt_words = "生成的答案要和web安全领域相关,答案不允许出现,根据提供的资料这样的话术,直接给出自然的回复" # 生成答案提示词 openai_api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1" web_data_path = os.path.join(os.getcwd(), "web_dataset.jsonl") def text_split(abs_pdf_file): text_splitter = RecursiveCharacterTextSplitter( chunk_size=1600, # 块长度 chunk_overlap=120, # 重叠字符 length_function=len, separators=["\n\n", "\n", "。", ".", '?', '?'], # 设置分隔符 is_separator_regex=False # 分隔符是正则表达式时设置为True ) with open(abs_pdf_file, mode='r', encoding='utf8') as f: text = f.read() markdown_text = text_splitter.split_text(text) return markdown_text def get_problems(text, model, messages): problems_all_list = [] content = f"""{text}\n根据以上内容生成基于web安全的问题,{global_prompt_words}\n{problem_prompt_words}""".format(text, global_prompt_words,problem_prompt_words) messages[1]['content'] = content ret = get_model_response(model, messages) problems_list = re.findall(r"(?<=\.\s).*?(?=\n|$)", ret) for problem in problems_list: problems_all_list.append(problem.strip()) return problems_all_list def get_answer(text, problem, messages, model): content = f"""{text}\n根据以上文本内容回答下面基于web安全的问题:{problem}\n{global_prompt_words}\n{answer_prompt_words}""".format(text, problem,global_prompt_words,problem_prompt_words) messages[1]['content'] = content ret = get_model_response(model, messages) return ret def get_model_response(model_name, messages): client = OpenAI( api_key=api_key, base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) completion = client.chat.completions.create(model=model_name, messages=messages) if model_name == 'deepseek-r1': content = completion.choices[0].message.reasoning_content + completion.choices[0].message.content return content else: return completion.choices[0].message.content if __name__ == '__main__': web_dataset = { 'messages': [ { 'role':'system', 'content':'你是一个精通web安全领域的专家', }, { 'role':'user', 'content':'', }, { 'role':'assistant', 'content':'', } ] } files = os.listdir(pdf_path) model = 'qwen-plus' messages = [ {"role": "system", "content": "你是一个专业web安全领域生成问题助手"}, {"role": "user", "content": ""}, ] # 获取所有pdf基于chunk_size分割的文本块 all_text_spilt = [] for file in files: abs_pdf_file = os.path.join(pdf_path, file) markdown_text = text_split(abs_pdf_file) for i in markdown_text: all_text_spilt.append(i) # 获取文本块中的问题, 获取文本块中基于问题的答案。 for text in all_text_spilt: if text: text = text.strip().replace('{', '').replace('}', '') problems_all_list = get_problems(text, model, messages) for problem in problems_all_list: model = 'deepseek-r1' answer = get_answer(text, problem, messages, model) # 构建web安全数据集 web_dataset['messages'][1]['content'] = problem web_dataset['messages'][2]['content'] = answer with open(web_data_path, mode='a', encoding='utf-8') as fb: fb.write(json.dumps(web_dataset, ensure_ascii=False)) fb.write('\n')
以下是在数据集 Review 环节总结的一些标准:
答案与文献事实一致:无样本量、漏洞类型、数据来源等关键信息错误.
不超出文献范围:答案仅包含文献明确提及的内容,不添加未提及的数据集应用场景、评估指标等推断信息。
答案直接回应问题:不堆砌无关信息(如问题问“数据集来源”,答案不可包含“模型训练效果”)。
问题聚焦数据集属性:优先保留指向“数据构成、标注方式、样本特征”的问题,剔除与数据集无关的模型方法、政策合规等提问。
无关键信息缺失:时间范围、数据格式字段、标注一致性指标等需明确(如“2023年数据”应补充“2023.1-2023.12”)。
术语统一:相同概念表述一致(如“跨站脚本攻击”与“XSS”需统一为一种说法)。
重复问题合并:同一数据集的同类问题(如“来源是哪里”与“采集渠道”)仅保留一个。
模糊答案修正:“质量较高”“包含多种攻击”等表述需补充具体数据(如“标注Kappa系数0.85”“包含XSS/SQL注入/CSRF”)。
经过人工Review, 最终得到 高质量 Web 安全数据集,用于本次微调任务。
加载和验证数据集
创建数据集配置文件,内容如下:
vim /home/sam_admin/LLaMA-Factory/web_dataset/dataset_info.json
{
"web security data ShareGPT": {
"file_name": "web_dataset.jsonl",
"formatting": "sharegpt",
"columns": {
"messages": "messages"
},
"tags": {
"role_tag": "role",
"content_tag": "content",
"user_tag": "user",
"assistant_tag": "assistant",
"system_tag": "system"
}
}
}
回到LLaMA Factory,在数据路径处粘贴上面配置文件路径
然后点击预览数据集,能够加载到数据,说明数据集配置成功了:
49112
48245
38987
36110
30520
27315
26318
21144
20987
19329
83°
88°
203°
383°
234°
898°
938°
924°
934°
889°