ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

LLM无标签自我提升:CoT多路采样与自一致性投票微调流水线

LLM无标签自我提升:CoT多路采样与自一致性投票微调流水线

简介:这份PDF收录了Google与伊利诺伊大学香槟分校团队关于“大型语言模型能否自我提升”的研究论文,适合自然语言处理、大模型推理与自监督学习方向的研究者、算法工程师及高年级学生阅读,用于理解无标注数据条件下提升模型推理能力的思路与实验设计。包内为1个PDF文件,整体约532KB,便于在电脑或平板上直接查阅原文、图表与参考文献。目前已有367人学习下载。论文提出用Chain-of-Thought提示与自一致性生成高置信度推理增强答案,再以这些自生成解答微调预训练LLM,在GSM8K、DROP、OpenBookQA、ANLI-A3等任务上给出540B模型的具体提升数据;同时通过消融研究说明“基于推理的微调”对自我提升的关键作用。读者可据此了解自我学习、迭代优化与无监督数据利用的实验框架,为后续复现、选题或构建自主进化系统提供参考。

1. 零标签涨 7.7 个点:这份 PDF 讲的是模型怎么用自己的答案喂自己

先说一个反直觉的结论:不额外标注一条数据,只把模型自己多次推理、投票选出来的「高置信度答案」回收做微调,GSM8K 上的准确率就能从 74.4% 提到 82.1%。这篇 PDF 讲的正是这件事,标题直译过来就是「大型语言模型能够自我提升」。对日常用 ChatGPT 这类工具做数学题、多跳问答、逻辑判断的从业者来说,它的价值不在于结论多漂亮,而在于把「无标签自我提升」拆成了一条能照着搭的流水线:Chain-of-Thought 多路采样、自一致性多数投票筛样本、混合格式回灌微调。这份材料适合两类人:一类是想在自有数据上继续压推理准确率的 AI 工程师,另一类是想搞明白 ChatGPT 使用里那些「让它多想几遍再回答」的技巧到底有没有工程依据的实践者。

2. 方法骨架拆解:CoT 多路采样、答案投票、混格式回灌三步走

整套方法没有引入任何新网络结构,全部收益来自「怎么造训练数据」。给定一个只有问题、没有答案的数据集 D,和一个预训练好的模型 M,流程是:先用少样本 CoT 示例提示 M,对每道题以大于 0 的温度采样 m 条推理路径;再用多数投票找出最一致的答案;最后把所有通向这个答案的推理路径留下来,按混合格式拼成训练样本,回灌给 M 做微调。得到的模型在贪心解码和多路解码两种评测下都有提升。

这件事的工程含义比学术含义更值得琢磨。传统自训练是给无标签数据打伪标签,然后分类头继续学;这里打标签的不是一个分类器,而是一整个「采样 + 投票」的推理过程,标签也不只是一个答案,而是连同推理链一起带回来的。换句话说,模型交出去的是答案,收回来的是「解题过程」,微调时学的是过程而不只是结论。这也是论文反复强调的一点:对推理链本身做微调,才是自我提升成立的关键,只学答案收益会小很多。

2.1 先固定 CoT 示例模板,再谈采样

动手之前必须先定死提示模板,因为模板决定了后面解析器怎么写。论文用的是少样本 CoT:每道题前面挂几条「问题 + 分步推导 + The answer is X」的示例,让模型模仿这个格式输出。我一般会把示例控制在 4 到 8 条,覆盖不同题型,示例里的推理步骤宁可朴素也不要跳跃,因为模型会连推导风格一起模仿。如果示例里出现「显然」「易得」这类跳步写法,采样出来的路径大概率也是跳步的,答案抽取脚本就得跟着遭殃。

模板定死之后不要再改。采样阶段改了模板,等于换了一个分布,投票结果和后面微调的数据就不同源了。这一点在复现时最容易翻车:为了「提升一点效果」中途调了示例,最后发现提升全部来自模板变化,和自我提升没关系。

2.2 多路解码与答案抽取:解析器写不好后面全白搭

采样阶段的关键参数是温度 T 和路径数 m。T 太小,m 条路径几乎一模一样,投票没有意义;T 太大,路径质量崩掉,投票选出来的「共识」也是错的。论文的做法是 T 取一个中等偏上的值,m 取几十条量级,让同一个问题能覆盖到多条不同的解法。工程上我一般先用 m=16、T=0.7 跑一小批看投票分布,再往上加。

答案抽取是最枯燥也最要命的一环。模型输出的路径是自由文本,必须用正则把最终答案抠出来,抠不出来或者抠错的样本要整条丢掉,不能猜。下面这段是我常用的抽取加投票骨架:

import re from collections import defaultdict # 只认 "The answer is X" 这种收尾,避免把中间步骤的数字误当答案 NUM_RE = re.compile(r"answer is\s*\$?\s*(-?[\d,\.]+)", re.I) def extract_answer(path_text: str): """从一条 CoT 路径中抽取最终答案,抽不到返回 None""" m = NUM_RE.search(path_text) if not m: return None return m.group(1).replace(",", "").rstrip(".") def vote_and_collect(qid, paths, min_votes=2): """按答案分桶,返回票数最高的答案和通向它的所有路径""" buckets = defaultdict(list) for p in paths: ans = extract_answer(p) if ans is not None: # 解析失败直接丢弃,不做兜底 buckets[ans].append(p) if not buckets: return None ans, keep = max(buckets.items(), key=lambda kv: len(kv[1])) if len(keep) < min_votes: # 票数不够,视为低置信度,整题丢弃 return None return {"qid": qid, "answer": ans, "paths": keep}

extract_answer里的正则只匹配「answer is」后面的数字,这是有意的:一旦放开去匹配任意数字,模型在中间步骤里算出的中间值会被误当成最终答案,投票分布立刻失真。min_votes这个阈值是置信度门槛,低于它的题不进训练集——宁可少几百条样本,也不要把噪声喂进去。实践里min_votes取 2 到 3 就够,再高会把长尾难题全筛掉,模型反而学不到难样本。

2.3 多数投票筛「高置信度」样本,混格式回灌

投票完成后,通向多数答案的所有路径都会被保留,而不是只留一条。这一点值得单独强调:只留一条最「标准」的路径,训练数据会变得极其同质,模型学不到推理的多样性;保留多条,等于告诉模型「这个问题有几种写法都能得到同一个答案」,泛化会好一些。

接下来是混合格式。论文里的做法是把同一批推理路径用不同的排列方式拼成训练样本——有的是「问题 → 推理 → 答案」,有的是「问题 → 答案 → 推理」。前者训练的是从条件推结论,后者训练的是先给结论再补依据。两种顺序混在一起,目的是让模型不要死记某一种输出模式。这一步在复现时经常被简化掉,但论文的消融显示格式选择对最终效果有实际影响,建议不要省。

3. 动手复现:把三步压成一条能跑的采样-投票-微调流水线

这一章按顺序把可执行的部分落下来。整套流程不需要额外的标注成本,唯一要准备的是一个只有问题的数据集,和一份能跑推理的模型权重。硬件上,采样阶段的显存开销和推理时一样,微调阶段的显存取决于你要不要全参微调——如果只是验证流程跑不跑得通,先拿小模型 + LoRA 走一遍,再换大底座。

3.1 数据侧:只留 question,把标签列直接删掉

第一步是把数据集处理成「只有问题」的形态。这件事听着简单,实操里最常见的错误是删标签删得不彻底——评测集的标签还留在训练文件的某个字段里,导致模型的「自我提升」实际上是偷看了答案。我一般会在预处理阶段显式只保留两个字段:唯一 id 和问题文本,其它列一律 drop。

import json def build_question_only(src_path, dst_path): kept = 0 with open(src_path, encoding="utf-8") as fin, \ open(dst_path, "w", encoding="utf-8") as fout: for line in fin: row = json.loads(line) # 白名单式保留,杜绝标签顺着别的字段漏进来 item = {"qid": row["id"], "question": row["question"].strip()} fout.write(json.dumps(item, ensure_ascii=False) + "\n") kept += 1 return kept

用白名单而不是黑名单,是为了防止数据集里那些名字奇怪的辅助字段(比如solution、hint、rationale)被漏掉。这一步多花十分钟,后面能省掉一整轮「为什么效果虚高」的排查。

采样前还要把 CoT 示例拼进提示。示例本身建议单独存成文件,和问题文本在运行时拼接,而不是预处理时烤进数据里——这样调示例不用重跑预处理,也方便做「换模板」的对照实验。

3.2 采样—投票—构造训练集三合一脚本

下面这段是整条流水线的主干,把采样、抽取、投票、格式化四步串起来。为了可读性,模型调用部分抽象成一个generate函数,实际接自家推理服务或本地权重都行。

from concurrent.futures import ThreadPoolExecutor def build_self_training_set(items, generate, cot_examples, m=16, temperature=0.7, min_votes=2, workers=32): """items: [{qid, question}]; generate(prompt, m, temperature) -> [str]""" def work(item): prompt = cot_examples + "\nQ: " + item["question"] + "\nA:" # 一次请求采 m 条,省掉 m 次往返的调度开销 paths = generate(prompt, m=m, temperature=temperature) return vote_and_collect(item["qid"], paths, min_votes) samples = [] with ThreadPoolExecutor(max_workers=workers) as pool: for res in pool.map(work, items): if res is None: # 解析失败或票数不足,跳过 continue samples.append(res) return samples def to_training_records(sample, question, cot_first=True): """把一条高置信度样本展开成若干条训练记录""" out = [] for p in sample["paths"]: body = p.split("The answer is")[0].strip() if cot_first: out.append(f"Q: {question}\nA: {p.strip()}") else: # 答案前置:先给结论,再补推理,逼模型学「结论->依据」的顺序 out.append(f"Q: {question}\nA: The answer is {sample['answer']}.\n{body}") return out

几个参数值得说明。m=16是起点不是终点,m 越大投票越稳,但采样成本线性上涨,实践中先在小验证集上试 8、16、32 三档,看准确率什么时候饱和。temperature=0.7负责制造多样性,太低投票没意义,太高答案质量下降。min_votes=2是置信度下限,配合后面的过滤能显著降低训练集噪声比例。workers只是并发度,和效果无关,按你的推理服务 QPS 调。

跑完之后建议统计三个数:原始题量、进入训练集的题量、平均每题保留的路径数。如果保留率低于三成,说明温度或min_votes设置有问题,先别急着训练。

3.3 微调侧:样本格式、长度控制与评测对齐

训练样本拼好之后,微调本身反而是最不需要创造力的部分。格式上按 2.3 说的混两种顺序,比例我一般取 1:1,让模型同时见到「推理在前」和「结论在前」。长度上要注意截断:CoT 路径普遍偏长,如果训练时把 max_length 设得太小,被截掉的往往是结论句,模型学到的就只剩半截推理。

评测侧必须和训练侧对齐。论文里同时报了贪心解码和多路解码两种结果,你自己验证时也要两种都跑:只看贪心解码,容易低估多路投票带来的收益;只看多路解码,又容易高估模型本身的能力。另外评测集的答案要单独存,绝不能和训练数据放在同一个目录下,避免任何形式的意外泄漏。

一个容易被忽略的细节是:微调后的模型在域外任务上也会变好(论文里报的 AQUA、StrategyQA、MNLI 都属于这类),所以验证集不要只放同分布数据。如果只有同域验证集,你会看到一个很好看但没什么参考价值的曲线。

4. 超参与消融:温度、路径数 m、混合格式该怎么定

论文的消融部分是这份 PDF 里最实用的章节,因为它回答的是「哪些设计是必需的,哪些是可选的」。结论可以概括成一句话:对推理链做微调是必需的,格式和采样参数是放大器。下面按三个维度拆。

4.1 采样温度与路径数 m:先保多样性,再谈收敛

温度和路径数是一对耦合参数,不能分开调。温度低、m 大,采出来的路径高度同质,投票几乎必然一致,看起来「置信度很高」,实际上模型并没有探索到别的解法;温度高、m 小,投票结果随机性太大,同一道题跑两次可能得到不同答案,训练集不稳定。

我的调法是从一个小规模网格开始:T 取 0.5、0.7、1.0,m 取 8、16、32,在几百道题的验证子集上看「投票一致率」和「一致样本的正确率」两个指标。理想区域是一致率在六到八成之间——太低了样本不够,太高了说明模型没有真正被激发出多样性。找到这个区域之后,再固定 T、加大 m 提升稳定性。

4.2 混合格式混的是什么,什么时候该关掉

混合格式的收益来自打破输出顺序的固定模式。但它有个副作用:答案前置的样本会让模型更倾向于先给结论,这在需要严格展示推理过程的场景里是减分项。如果你的下游应用要求「必须先推导再给答案」,答案前置那部分样本就要么去掉,要么降比例。

另一个边界是任务类型。数学题这类有唯一数值答案的任务,格式混合收益明显;开放式生成任务里,「答案前置」很容易变成模型编造结论再硬凑理由,这时候建议只保留推理前置的格式。

4.3 把消融结论翻成调参决策表

下面这张表是我自己复现时的决策依据,左列是要调的旋钮,右列是对应的判断标准和常见取值。具体数值以你手上的验证集为准,不要照抄。

参数作用起始值调整信号
采样温度 T控制路径多样性0.7投票一致率过高就上调,答案质量崩就下调
路径数 m投票稳定性16一致率随 m 提升明显就继续加,饱和就停
最少票数 min_votes训练集置信度门槛2保留率低于三成就降到 1,噪声大就提到 3
格式混合比例控制输出顺序偏好1:1下游要求先推理就降答案前置占比
保留路径策略学多样性还是学单一路径全部保留数据量太少时可只留票数最高的 2 到 3 条

读这张表的方式是:先调温度和 m 把保留率压到合理区间,再调 min_votes 控噪声,最后才动格式比例。顺序反了会白折腾,因为前面两个参数直接决定了训练集的规模和分布,后面的调整都建立在这上面。

论文里还报了一组消融,结论是如果不做推理链、只学答案,提升会大幅缩水。这条对做工程的人最有价值:它意味着这套方法不是「伪标签自训练」的换皮,收益确实来自推理过程本身。如果你复现出来发现提升很小,第一个该检查的就是训练样本里到底有没有完整的推理链。

5. 避坑与排查:从采样到微调的五条血泪记录

5.1 采样与解析阶段的三个坑

现象:投票一致率异常高,九成以上的题都是同一个答案。原因:采样温度设得太低,或者推理服务在服务端做了贪心裁剪,m 条路径其实是同一条。解决:先打日志看同一道题的 m 条输出是否逐字符相同,相同就说明多样性没打开;再查服务端的采样参数有没有被默认值覆盖。

现象:准确率虚高,但换一批题就崩。原因:预处理时标签列没删干净,模型在提示里看到了答案。解决:按 3.1 的白名单方式重建数据集,并且把训练文件和评测文件放到不同目录,跑之前用脚本 grep 一遍标签字段名。

现象:大量样本被丢弃,保留率不到一成。原因:答案抽取正则太窄,模型输出里常见的是「答案是 108」「= $108」这类变体,只匹配一种写法会漏掉大半。解决:抽取出错时先人工看 50 条失败样本的收尾写法,把正则扩到 3 到 5 种常见模式,同时保留「抽不到就丢弃」的原则,不要写兜底逻辑去猜。

5.2 训练与评测阶段的两个坑

现象:微调后同分布准确率涨了,域外任务反而掉了。原因:训练集里全是同一类题的高置信度样本,分布过窄,模型被过拟合到了单一题型。解决:训练集要覆盖多个任务、多种题型,混合比例按题量而不是按文件;如果只能拿到单一任务的数据,降低训练轮数,别让模型把这批数据背下来。

现象:训练 loss 正常下降,评测却不如微调前。原因:训练样本被截断,结论句落在 max_length 之外,模型学到的是不完整的推理。解决:统计训练样本的长度分布,把 max_length 设到 95 分位以上;实在压不下来,就在拼样本时把结论句前置,保证它不会被截掉。

6. 进阶路径:让模型自己出题、自己写 CoT 模板

流程跑通之后,最值得往下挖的是怎么进一步减少人工介入。论文给了两个方向,都不是花架子。

6.1 让模型自己写 CoT 示例

少样本 CoT 的示例是人挑的,这本身就是一笔人工成本。论文试过让模型自己生成示例模板,再拿去做零样本推理,GSM8K 上做到 74.2%,对比直接零样本提示的 43.0% 和朴素多路投票的 70.1%,差距非常明显。工程上的做法是:先用手写的少量示例让模型对一批题生成推理,从中挑格式最规整、步骤最完整的几条作为模板,再拿这套模板去跑全量采样。挑样本这一步不需要人看答案对错,只看格式,成本很低。

6.2 让模型自己扩题

第二个方向是从有限的输入问题里生成新的问题,扩充训练集规模。这一步的风险比上一步大:扩出来的题如果偏离原分布太远,投票筛出来的样本会把模型带偏。我的做法是给扩题加两道闸——一是要求新题必须复用原题的实体和数值结构,二是扩出来的题先走一遍投票,保留率明显低于原题的直接整批丢掉。

6.3 验证自提升是否真的发生

判断「自我提升」有没有真的发生,不能只看一个数字。我一般会跑三组对照:原始模型、原始模型 + 多路投票(不微调)、微调后的模型。如果第三组只比第二组高一点点,说明收益主要来自投票而不是微调,这时候要回头查训练样本质量;如果第三组在贪心解码下也明显高于第一组,才说明模型本身的能力被改写了。这个对照实验花不了多少算力,但能省掉很多自欺欺人的结论。

最后说个我自己的教训。第一次复现这套流程时,我看到验证集涨了就急着换大底座,结果换完发现效果全没了。回头查日志才发现,小模型那次「涨点」是因为投票一致率高得离谱,本质上等于把温度调成了 0。从那以后我每次跑自提升,都强制先打三张表:同一题 m 条输出的重复率、训练集保留率、三种解码设置下的对照结果,三张表都正常了才允许往下走。希望帮到你。

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

RELATED READING

延伸阅读

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