这一节动手:写一份六段对话的数据、存成 JSON、转成 Llama 2 模板、报出六个训练前该知道的数。 中间有一个岔路要选:保多轮还是拆单轮。演示里两种都算了,代价和收益都不一样。
上一节讲了原则,这一节照着做一遍。 JSON 怎么读怎么写,17 课 编程原理 里讲过; 这一节的重点不是语法,是字段名这件事为什么改不得。
一、结构化:先定字段,再填内容
每条样本至少两个字段:id 和 conversations。
id 不参与训练,作用只有一个——出错时能定位到哪一条。
conversations 里每项两个字段:from 和 value。
from 取 human 与 gpt,这是社区约定,很多处理脚本只认这两个词。
也可以存成 JSONL,一行一条。
好处是能流式读,坏处是不能直接肉眼打开——几万条的时候编辑器会卡死,反而不好查。
字段名看着无所谓,其实是接口。 改一个名字,整条数据管道都要跟着改,而改的地方越多,漏改一处就越难发现。
二、两种构造法,各有各的代价
保多轮:一条样本是一整段对话。样本数少,但有上下文,模型能学到「接着上文说」。 拆单轮:把每轮拆成一条「问-答」。样本数翻几倍,训练更省 token,但把代词一起拆散了。 演示里的账:六段对话 584 token,拆成十二条 582 token,总 token 几乎一样。差别不在成本,在信息。
拆开之后,那条「那它要不要单独接地」失去了指代对象。模型学不到「它指什么」,只学到「看到它就答」。 比例怎么定:看业务里代词出现的频率。客服对话里「这个」「那个」满地都是,就得以多轮为主; 如果业务里用户每句都说得完整,拆单轮更划算。
三、转模板:系统提示只写一次
Llama 2 的约定是把系统提示塞进第一个 [INST] 里的 <<SYS>> 段,
后面每轮只写 [INST] 用户话 [/INST] 助手话 </s>。
只写一次不是省 token 的技巧,是模板本身的定义。写两次,模型会以为指令在变,输出风格来回跳。
转完要回头数 token:六段数据从 584 涨到 904,平均 1.55 倍。这个倍数直接乘在显存和时长上。
膨胀主要来自系统提示(每条一次)和 </s> 这类标记。
数据量一大,系统提示那几十个字就要乘以几万。
四、训练前报六个数字
总条数、总 token、最短与最长、平均、超上限的条数、含代词的条数。六个都要报。 报数不是为了好看,是为了横向比较。 同样是「几百条数据」,一条 50 token 和一条 500 token 是两个量级的工作量。 「含代词的条数」这一项最容易被漏,但它决定了你的数据能不能训出多轮能力——全是独立问句的数据, 训完模型依然不会处理「它」。
超上限的条数要报,并且要真的去看那几条被截断之后长什么样。 数字告诉你有没有问题,内容告诉你问题有多严重。
五、动手 Demo:真造一份多轮数据集:写盘、转模板、两种构造法
六段亮化工程现场的对话,二十四轮。 先写成 ShareGPT 形状的 JSON 真落盘,再转成 Llama 2 模板数 token, 最后比较保多轮与拆单轮两种构造法。 指代检测用的是关键词表(它、这个、那面、刚才…),不是语义理解——所以它只会多报,不会漏报, 这个方向的误差是可以接受的。
data = []
for cid, _topic, ms in DIALOGS:
data.append({u'id': cid,
u'conversations': [{u'from': u'human' if r == U else u'gpt',
u'value': t} for r, t in ms]})
put(root, u'train.json', json.dumps(data, ensure_ascii=False, indent=2))
# 转模板:SYS 只在第一个 [INST] 里
for i, (r, t) in enumerate(ms):
if r == U:
if i == 0:
parts.append(u'<s>[INST] <<SYS>>\n%s\n<</SYS>>\n\n%s [/INST] ' % (SYS, t))
else:
parts.append(u'[INST] %s [/INST] ' % t)
else:
parts.append(u'%s </s>' % t)
# 拆单轮:每两轮拆成一条
for i in range(0, len(ms), 2):
single.append((ms[i][1], ms[i + 1][1]))
实跑输出:
--------------------------------------------------------------------
真造一份多轮数据集:写盘、转模板、两种构造法
--------------------------------------------------------------------
① 六段对话
--------------------------------------------------------------------
| 编号 | 话题 | 轮数 | token |
|---|---|---|---|
| d01 | 外立面改造 | 4 | 118 |
| d02 | 预算压缩 | 4 | 108 |
| d03 | 配电位置 | 4 | 97 |
| d04 | 验收准备 | 2 | 58 |
| d05 | 工期与到场 | 6 | 114 |
| d06 | 色温选择 | 4 | 89 |
六段对话合计 24 轮,584 token。平均每轮 24 token——这个数决定了单条样本的长短。
② 写成 ShareGPT 形状的 JSON
--------------------------------------------------------------------
文件 3044 字节,6 条记录。
┌─ 头两条,原样摘录
│ {
│ "id": "d01",
│ "conversations": [
│ {
│ "from": "human",
│ "value": "甲方说第三版图纸的外立面要改"
│ },
│ {
│ "from": "gpt",
└─ …
! 存的时候用 human/gpt 这两个名字,是为了跟社区数据对齐——大多数处理脚本默认认这两个词,
不认 user/assistant。
③ 转成 Llama 2 模板
--------------------------------------------------------------------
| 编号 | 字符 | token | 膨胀 |
|:---:|---:|---:|---:|
| d01 | 172 | 171 | 1.45 |
| d02 | 167 | 162 | 1.50 |
| d03 | 159 | 150 | 1.55 |
| d04 | 110 | 106 | 1.83 |
| d05 | 196 | 173 | 1.52 |
| d06 | 154 | 142 | 1.60 |
膨胀那一列是把系统提示、[INST]、</s> 都算进去之后,比原始对话多出来的倍数。
! 系统提示只在第一个 [INST] 里出现一次,不是每轮一次——这是 Llama 2 的约定,
写错了模型会以为每轮都换了新指令。
④ 两种构造法:保多轮,还是拆单轮
--------------------------------------------------------------------
| 构造法 | 条数 | 总 token | 指代问题 |
|---|---:|---:|---|
| 保多轮 | 6 | 584 | 0(有前文) |
| 拆单轮 | 12 | 582 | 1(拆开后指代悬空) |
拆单轮总 token 反而少一点(系统提示不重复了),但 1 条里的「它」失去了指代对象。
! 这就是多轮数据的核心矛盾:保多轮省 token、有上下文,但样本数少;
拆单轮样本多,但把「指代」这个能力训没了。两种都要有,比例看业务里代词出现的频率。
⑤ 训练前要报的几个数
--------------------------------------------------------------------
| 指标 | 值 |
|---|---|
| 总样本 | 6 段 / 拆开后 12 条 |
| 总 token | 584 |
| 最短 / 最长 | 58 / 118 |
| 平均 | 97 |
| 超 512 的 | 0 |
| 含代词的 | 1 |
! 六段数据、几十个 token 的量级,只能用来验流程。
真机上一份能训的数据至少几百条,但流程是先在小数据上跑通,再放大——放大之后出问题,
你根本分不清是数据还是代码。
①六段对话二十四轮,最短 58 token,最长 118。平均每轮 24 token——这个数偏小,因为样例写得短;
真实对话一轮通常上百 token。
②真落盘,3044 字节。注意摘录里 from 的值是 human 和 gpt,不是 user 和 assistant。
这不是笔误,是跟社区数据对齐——大多数处理脚本默认认这两个词。
③转模板:膨胀 1.45 到 1.83 倍。
四轮的那几段倍数低(系统提示被摊薄),两轮的那段最高(1.83,系统提示占的比例大)。
数据越短,模板开销占比越高。
④是这一节的核心对比。
保多轮六条 584 token,拆单轮十二条 582 token——总 token 几乎一样,条数翻了一倍。
但拆开之后有一条的「它」没有指代对象了。
这是拆单轮的隐性代价:你省了上下文,也把「理解上下文」这个能力从数据里删掉了。
⑤六个数字里,「含代词的 1」是最该盯的一项。
它告诉你这份数据里只有一条能训指代——训出来的模型八成还是不会处理「它」。
最后那句提醒很重要:六段数据只能验流程。
放大到几百条之后出问题,你分不清是数据还是代码,所以流程一定要在小数据上先跑通。
30 秒实验
- 把 d05(六轮)拆成单轮,看总 token 和条数怎么变。 再想想:拆开之后「下雨怎么办」那一轮还能不能看懂?
- 把系统提示从三十字扩到两百字,重跑第③步。膨胀比涨到多少?按五千条数据算,多花多少 token?
- 给每条 JSON 加一个
meta字段(来源、日期、标注人)。 看文件涨多少字节,再想想:这个字段该不该进训练?
常见误区
- 用
user/assistant存数据。 社区脚本大多认human/gpt,改名之后整条工具链都要改,而报错往往出现在很后面的环节。 - 系统提示每轮都写一遍。多花 token 是小事,模型以为指令在变是大事——输出风格会开始漂。
- 写完就训,不校验。
跑完发现一半的
from拼错了、有三分之一是空回答,而训练过程一路正常,损失曲线也好看。
自测三问(点开对答案)
1. 为什么保多轮和拆单轮要混着用? —— 保多轮能训「理解上下文」和指代,但样本少;拆单轮样本多、成本低,但把上下文切断了。 两类都在,模型才既会答独立问题、又能接住「它」。 2. 模板膨胀 1.55 倍,主要花在哪? —— 两处:系统提示每条重复一遍,以及 ``、`[INST]` 这些标记。 数据越短,系统提示占的比例越高,膨胀比越大。 3. `id` 字段不参与训练,为什么还要有? —— 为了定位。几万条数据里有一条格式不对,没有 id 你只能一行行翻; 有 id,报错信息里带一个编号,直接跳到那一行。数据造好了,下一节解决「怎么把它塞进上下文」——这是多轮对话里最费钱的一步。
- ☐ (本篇完,下一篇 4.3 通过多轮对话存储解决信息流失问题)
相关笔记
- 17 课 编程原理:结构化数据与序列化
- 数据集与原则:七条原则怎么落成代码
- 数据集准备:模板与 token 膨胀
- 🦙 Llama 大模型实践 · 索引