ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型训练数据收集与标注全流程工程拆解

大模型训练数据收集与标注全流程工程拆解

简介:这份文档围绕ChatGPT技术的训练数据收集与标注方法展开,面向自然语言处理入门者、AI产品从业者及对大模型训练流程感兴趣的学习者,帮助读者理解高质量对话数据从何而来、如何标注与迭代优化。资源包共1个docx文件,约37KB,内容以文字讲解为主,结构清晰,便于快速通读与查阅。文档系统梳理了数据收集、数据标注与迭代调优三大环节:数据收集部分介绍Web爬虫获取对话文本及数据过滤思路;数据标注部分涵盖人工标注、标注指导与标注质量控制,涉及分类、实体识别、意图识别等具体任务;迭代调优部分说明如何通过持续对话评估提升模型鲁棒性与性能。文末附有知识点归纳,方便读者把握重点。目前已有81人学习,适合希望系统了解ChatGPT训练数据工程全流程的读者参考。

1. 大模型训练数据从哪来:一份可复现的收集与标注工程拆解

很多人以为训练一个对话模型,核心是算法和算力,数据不过是“喂进去的语料”。真做过一轮就知道,数据管线才是那个吃掉七成时间、还最容易翻车的地方。标题里说的“训练数据收集与标注方法”,落到工程上其实是一整条流水线:从哪里采、怎么洗、谁来标、标完怎么验、验完怎么存成训练能直接吃的格式。它解决的不是“有没有数据”,而是“数据能不能支撑模型学会指令跟随、多轮对话和安全边界”。这套方法适合两类人:一是想自己微调一个领域对话模型的开发者,二是负责搭建数据标注流程的工程同学。下面按我实际搭过的一条链路讲,能抄的部分直接给代码和参数。

2. 数据收集:先定来源边界,再谈抓取量

2.1 四类数据源的分工与取舍

对话模型的训练数据,常见做法是拆成四类,各自解决不同问题,混在一起采会很难评估质量。

第一类是通用指令数据,形态是“指令 + 输入 + 输出”,用来教模型听懂任务。第二类是多轮对话数据,重点是上下文连贯和角色一致。第三类是领域知识数据,比如某行业的问答对,用来补专业能力。第四类是偏好与安全数据,形态是同一问题的多个回答加排序,用来做对齐阶段。

选型上有个反直觉的点:不是越多越好。我一般会先定一个目标配比,比如通用指令占六成、多轮占两成、领域占一成半、安全占半成,然后按配比反推每类要采多少。这样做的原因是,某一类过量会直接带偏模型的行为分布,后面用再多清洗都救不回来。

采集渠道上,公开数据集、自有业务日志、人工构造是三条主路。自有日志价值最高但涉及隐私,必须先做脱敏;人工构造可控但成本高,适合补长尾场景。这里不展开具体平台,重点是把“来源—授权—脱敏”三件事在采集前就写进流程,否则后面全是后悔药。

2.2 用脚本做原始数据的批量拉取与去重

采集阶段最容易忽略的是去重,重复样本会让模型对某些句式过拟合。下面是一个最小可用的去重脚本,基于文本哈希加 SimHash 近似去重。

import hashlib from datasketch import MinHash, MinHashLSH def exact_dedup(records): seen = set() result = [] for r in records: h = hashlib.md5(r["text"].strip().encode("utf-8")).hexdigest() if h not in seen: seen.add(h) result.append(r) return result def near_dedup(records, threshold=0.8, num_perm=128): lsh = MinHashLSH(threshold=threshold, num_perm=num_perm) minhashes = {} for i, r in enumerate(records): m = MinHash(num_perm=num_perm) for token in r["text"][:500]: # 只取前500字符做签名,控制内存 m.update(token.encode("utf-8")) minhashes[i] = m lsh.insert(i, m) keep = set() for i, m in minhashes.items(): dup = lsh.query(m) if len(dup) == 1: keep.add(i) return [records[i] for i in sorted(keep)]

逻辑说明:exact_dedup用 MD5 做完全重复过滤,成本低先跑一遍。near_dedup用 MinHash 生成文本签名,再用 LSH 分桶找近似重复,threshold=0.8表示相似度超过 0.8 视为重复。num_perm=128是签名长度,越大越准但越慢,一般 64 到 256 之间够用。只取前 500 字符是为了控制内存,长文本场景可以改成按段落切分后再去重。

参数上要注意:阈值设太低会误删正常样本,设太高去重不干净。我的经验是先用 0.85 跑一遍看保留率,如果保留率低于七成,说明阈值偏严,往上调。

2.3 脱敏与合规过滤的落地检查点

脱敏不是简单替换手机号。要过一遍的至少包括:身份证号、手机号、邮箱、银行卡号、详细地址、以及可能指向具体个人的组合信息。做法是用正则先粗筛,再用规则加人工抽检。

import re PATTERNS = { "phone": r"1[3-9]\d{9}", "id_card": r"\d{17}[\dXx]", "email": r"[\w.\-]+@[\w\-]+\.[\w.\-]+", "bank_card": r"\d{16,19}", } def mask_text(text): for name, pat in PATTERNS.items(): text = re.sub(pat, f"[{name.upper()}]", text) return text

逻辑说明:每个正则对应一类敏感信息,命中后替换成占位符而不是删除,保留句式结构对训练更友好。参数上,bank_card的 16 到 19 位容易误伤长数字串,实际用的时候建议加前后边界判断,比如要求前后不是数字。跑完正则后必须做人工抽检,抽检比例不低于百分之一,重点看有没有漏网的组合信息。

注意:脱敏和去重要在采集后立刻做,不要等标注完再补。标注过的数据再脱敏,等于让标注员白看了一遍敏感信息。

3. 标注体系设计:把“好回答”拆成可打分的维度

3.1 标注维度的定义与打分粒度

标注最怕的是“凭感觉标”。同一批数据两个人标出来不一致,训练时就是噪声。解决办法是把质量拆成可独立判断的维度,每个维度给明确的分档。

我常用的是四个维度:指令遵循度、事实准确性、表达流畅度、安全性。每个维度分三档,比如指令遵循度分“完全满足 / 部分满足 / 未满足”。分档描述要写成一句话能判断的标准,不能写“较好”“一般”这种模糊词。

打分粒度上,三档比五档更稳。五档看着精细,实际标注员在中间档位上分歧最大,一致性反而差。三档加一个“无法判断”兜底,是性价比最高的方案。

3.2 标注规范文档的写法与示例

规范文档不是写给管理者看的,是写给标注员看的。每一条都要有正例和反例。下面是一个维度定义的表格示例。

维度档位判断标准正例要点反例要点
指令遵循度完全满足回答覆盖指令全部要求问三点答三点漏答其中一点
指令遵循度部分满足覆盖主要要求但有遗漏答了核心问题遗漏次要约束
事实准确性准确关键事实可核实且无误数据、名称正确编造数据
安全性安全无有害、歧视、诱导内容中性客观含攻击性表述

写规范时有个血泪经验:不要用“等等”“类似”这种开放词,标注员会各自理解。每个档位配两到三个真实样例,样例要从采集数据里挑,不要自己编。

3.3 标注任务的分发与一致性校验

分发上,同一条数据至少两人独立标注,第三人仲裁分歧。一致性用 Cohen's Kappa 衡量,低于 0.6 说明规范有问题,要回去改规范而不是怪标注员。

from sklearn.metrics import cohen_kappa_score def check_agreement(labels_a, labels_b): kappa = cohen_kappa_score(labels_a, labels_b) if kappa < 0.6: return f"一致性不足: {kappa:.2f},需修订规范" return f"一致性可接受: {kappa:.2f}"

逻辑说明:labels_a和labels_b是两个标注员对同一批样本的档位序列。Kappa 扣除了随机一致的概率,比单纯算一致率可靠。参数上,0.6 是常用门槛,安全类维度建议提到 0.7,因为安全判断分歧代价更高。跑完如果某维度特别低,单独把该维度的样本拉出来复盘,通常是档位描述有歧义。

4. 从标注结果到训练格式:转换脚本与字段校验

4.1 对话样本的 JSONL 结构设计

训练框架普遍吃 JSONL,每行一个样本。对话样本的常见结构是消息列表,每条消息带角色和内容。

{"messages": [{"role": "user", "content": "帮我写一个排序函数"}, {"role": "assistant", "content": "def sort_list(nums):\n return sorted(nums)"}], "meta": {"source": "instruction", "quality": 3}}

字段设计上,messages是训练直接读的,meta是给自己做数据分析和采样用的。quality存标注的综合档位,方便后面按质量分层采样。不要把所有标注维度都塞进 meta,选综合分加一两个关键维度就够,字段太多会让文件膨胀。

4.2 标注结果转训练格式的脚本

标注结果通常是表格,要转成上面的 JSONL。下面是一个转换脚本,带字段校验。

import json REQUIRED = ["instruction", "response", "quality"] def convert_row(row): for k in REQUIRED: if k not in row or row[k] in (None, ""): raise ValueError(f"缺少字段: {k}") if row["quality"] not in (1, 2, 3): raise ValueError(f"质量档位非法: {row['quality']}") sample = { "messages": [ {"role": "user", "content": row["instruction"].strip()}, {"role": "assistant", "content": row["response"].strip()}, ], "meta": {"source": row.get("source", "unknown"), "quality": row["quality"]}, } return sample def build_jsonl(rows, out_path): with open(out_path, "w", encoding="utf-8") as f: for r in rows: try: f.write(json.dumps(convert_row(r), ensure_ascii=False) + "\n") except ValueError as e: print(f"跳过异常行: {e}")

逻辑说明:convert_row先校验必填字段,再校验质量档位范围,任何一项不合法就抛错。build_jsonl逐行写入,异常行打印后跳过而不是中断,方便批量处理时定位问题。参数上,ensure_ascii=False保证中文不被转义,文件体积更小也更可读。质量档位限定在 1 到 3,和前面三档标注对齐。

4.3 训练前的数据分布检查

转完格式别急着训,先看分布。至少看三个数:总样本量、各质量档位占比、各来源占比。质量档位如果集中在最低档,说明采集或标注有问题,训出来效果不会好。

from collections import Counter def check_distribution(path): quality = Counter() source = Counter() with open(path, encoding="utf-8") as f: for line in f: obj = json.loads(line) quality[obj["meta"]["quality"]] += 1 source[obj["meta"]["source"]] += 1 total = sum(quality.values()) print(f"总量: {total}") for k, v in quality.items(): print(f"质量{k}: {v} ({v/total:.1%})") for k, v in source.items(): print(f"来源{k}: {v} ({v/total:.1%})")

逻辑说明:用 Counter 统计两个关键分布,输出占比。参数上没有需要调的,重点看结果。我的经验是高质量档位占比低于两成时,要么补采要么重新标,硬训会浪费算力。来源分布如果某一类超过七成,要考虑是不是采集脚本只跑通了那一路。

5. 避坑与排查:标注管线里最常见的五个翻车点

5.1 标注员理解偏差导致整批数据报废

现象:某批次数据一致性 Kappa 只有 0.4,两个标注员对同一维度判断差异极大。原因:规范文档里档位描述用了“较好”这类模糊词,标注员各自理解。解决:把该维度描述改成可判断的具体条件,补正反例,重标该批次。规范修订后先小批量试标,Kappa 过 0.6 再全量铺开。

5.2 去重阈值过严误删有效样本

现象:去重后保留率只有五成,明显偏低。原因:SimHash 阈值设成 0.7,短文本之间本来就相似,被大量误判为重复。解决:把阈值调到 0.85,并对短于 50 字符的样本单独处理,短文本只做完全重复过滤,不做近似去重。

5.3 脱敏正则误伤正常内容

现象:训练数据里出现大量[BANK_CARD]占位符,但原文并没有银行卡号。原因:bank_card正则匹配 16 到 19 位数字,把长订单号、时间戳串也命中了。解决:加前后边界判断,要求匹配串前后不是数字,并把订单号这类业务字段加入白名单。

5.4 格式转换时静默丢数据

现象:转换后样本量比标注表少了一截,但脚本没报错。原因:异常行被except捕获后只打印,批量跑的时候日志被刷过去没注意。解决:把异常行单独写到一个错误文件,转换结束后统计错误行数,超过总量百分之一就中断并人工检查。

5.5 质量档位分布失衡拖垮训练

现象:模型训完在简单指令上表现正常,复杂指令一塌糊涂。原因:质量分布里最低档占了七成,模型学到的是“敷衍回答”的模式。解决:按质量档位分层采样,保证高质量档位在训练集里占比不低于三成,不足的部分补采或重新标注。

6. 进阶:用分层采样和主动学习把标注成本压下来

数据管线跑到后面,瓶颈往往不是采不到,而是标不起。全量人工标注成本太高,我的做法是分层采样加主动学习,把标注预算花在最有价值的样本上。

分层采样的逻辑是:先按来源和质量粗分,再从每层里按比例抽。高质量层多抽,低质量层少抽但保留一部分做负样本。这样训练集的质量分布可控,不会因为随机采样被低质数据淹没。

主动学习的做法是:先用一小批已标注数据训一个初版模型,让它对未标注数据打分,挑出模型最不确定的样本优先送标。不确定度的衡量可以用输出概率的熵,熵越高说明模型越没把握。

import numpy as np def entropy_select(probs, top_k=100): # probs: 模型对每个样本各类别的输出概率,shape=(n, num_classes) ent = -np.sum(probs * np.log(probs + 1e-9), axis=1) idx = np.argsort(ent)[::-1][:top_k] return idx

逻辑说明:entropy_select计算每个样本的预测熵,熵越大越不确定,取前top_k个送标。1e-9是防止 log 零的平滑项。参数上,top_k按每轮标注预算定,一般每轮几百到几千条。跑几轮后,模型在难样本上的表现会明显提升,而标注量可能只有全量的两三成。

这套流程我自己跑下来,最大的体会是:数据管线里没有一步是可以跳过的,但每一步都可以用脚本和规范把它变得可复现。规范写细一点,脚本多校验一次,后面就少一次返工。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进