☰
大模型自改进实战:自洽性投票与数据飞轮闭环指南
2026/10/15 4:12:15 网站建设 项目流程

简介:这份PDF文档《Large Language Models Can Self-Improve》面向自然语言处理研究者、大模型算法工程师及对AI自我进化机制感兴趣的进阶学习者,聚焦一个核心问题:大语言模型能否在不依赖人工标注数据的前提下提升自身推理能力。文档共1个PDF文件,压缩包约532KB,内容为完整英文研究论文,含摘要、引言、方法、消融实验与结果分析等标准学术结构,便于系统研读与引用。目前已有366人学习下载。论文提出利用预训练LLM对未标注问题生成高置信度的思考链理由增强答案,再以这些自生成解作为目标输出进行微调,使540亿参数模型在GSM8K上从74.4%提升至82.1%,DROP从78.2%提升至83.0%,OpenBookQA从90.0%提升至94.4%,ANLI-A3从63.4%提升至67.9%,全程无需真实标签。读者可从中获取自我提升范式的完整实验设计、消融结论及推理微调关键性论证,为无监督模型优化与自主进化研究提供可复现的思路参考。

1. 自改进这件事,为什么值得你花一个周末跑通

大模型自改进(Self-Improve)这个词最近被提得很多,但真正动手跑过一轮的人不多。我第一次看到「Large Language Models Can Self-Improve」这个方向时,直觉是:模型自己生成数据、自己打分、自己再训练自己,这不就是左脚踩右脚上天吗?后来实际跑了一轮小规模实验才明白,它解决的是一个非常具体的工程问题——当你手里只有一批没有标注的题目,请不起标注团队,也调不动更大的教师模型时,能不能让模型自己把答案质量往上抬一档。

答案是能,但有前提。核心思路是:让模型对同一道题生成多个候选答案,用某种「自洽性」信号挑出最可信的那个,把它当作伪标签,再拿这批伪标签做微调或上下文学习。整个过程不需要人工标注,也不需要外部强模型蒸馏。适合谁?适合手上有几百到几千条无标注 prompt、有一张能跑推理的卡、想验证「数据飞轮」在自己场景里到底转不转得动的人。下面我把这条链路拆成能直接抄的步骤。

2. 自改进的闭环到底转在哪几个环节

2.1 从「生成—筛选—回训」三步理解闭环

自改进不是一步到位的魔法,它是一个循环。第一轮:模型对每道题采样 N 个答案(N 通常 5 到 40)。第二轮:对这 N 个答案做筛选,选出「最可能正确」的那个。第三轮:把筛选后的(题目,答案)对当作训练数据,做微调或作为少样本示例。第四轮:用更新后的模型重新生成,回到第一轮。

关键洞察在于:筛选信号的质量决定了整个闭环是上升还是原地打转。如果筛选信号比模型本身的单次生成还差,那这个闭环就是负向的。所以整个方案的重心不在「生成」,而在「怎么筛」。

常见的筛选信号有三类。第一类是自洽性投票(self-consistency):对同一道题采样多条推理链,看最终答案的多数投票结果。这个方法在数学和逻辑题上特别有效,因为正确答案往往收敛,错误答案发散。第二类是模型自评:让模型给自己的答案打分,或者比较两个答案哪个更好。第三类是用一个可验证的信号,比如代码能不能跑通、数学答案能不能代入验证。

我一般会优先选自洽性投票,因为它不需要额外训练一个奖励模型,实现成本最低,而且在有确定答案的任务上效果稳定。自评适合开放式生成任务,但要注意位置偏差——模型倾向于选第一个或最后一个选项,这个坑后面会细说。

2.2 采样策略:温度、top-p 和采样数量的取舍

采样参数直接决定了候选答案的多样性。如果温度太低(比如 0.1),N 个答案几乎一模一样,投票就失去意义。如果温度太高(比如 1.5),答案发散得太厉害,正确答案可能被淹没在噪声里。

我的经验值是:数学和代码任务用 temperature=0.7,top_p=0.95;开放式问答用 temperature=0.9,top_p=0.98。采样数量 N 在 5 到 10 之间通常就能看到投票收益,超过 20 之后边际收益递减明显,但计算成本线性增长。

下面是一段用 Python 做多路采样的最小代码,假设你已经有一个能调用的推理接口:

import collections from typing import List def sample_candidates(prompt: str, n: int = 8, temperature: float = 0.7) -> List[str]: """ 对同一道题采样 n 个候选答案。 temperature 控制多样性,数学题建议 0.7,开放题建议 0.9。 """ candidates = [] for _ in range(n): # 这里的 call_model 是你自己的推理封装,返回纯文本答案 ans = call_model(prompt, temperature=temperature, top_p=0.95, max_tokens=512) candidates.append(ans.strip()) return candidates def majority_vote(candidates: List[str]) -> str: """ 对候选答案做多数投票。 注意:这里假设答案是短文本或可归一化的形式, 如果是长推理链,需要先抽取最终答案再投票。 """ # 简单归一化:去空格、转小写 normalized = [c.lower().replace(" ", "") for c in candidates] counter = collections.Counter(normalized) top_answer, count = counter.most_common(1)[0] # 返回原始形式中第一个匹配的答案 for c in candidates: if c.lower().replace(" ", "") == top_answer: return c return candidates[0]

这段代码的逻辑很直白:先采样,再对归一化后的答案做计数投票。参数说明:n是采样数量,建议从 8 开始试;temperature控制随机性,太低会导致候选重复,太高会让投票失效。majority_vote里的归一化步骤很关键——如果答案是「42」和「42.0」,不归一化就会被当成两个不同答案,投票结果就偏了。

2.3 把筛选结果变成训练数据:格式与配比

筛选完之后,你得到了一批(prompt, chosen_answer)对。接下来要决定怎么用它。两条路:一是做上下文学习,把 chosen_answer 当作少样本示例塞进新 prompt;二是做微调,用这批数据更新模型参数。

上下文学习的好处是零训练成本,缺点是示例占上下文窗口,而且效果受示例选择影响大。微调的好处是效果更稳定,缺点是需要训练资源和时间。我一般会先跑上下文学习验证信号质量,如果投票后的答案确实比单次生成好,再上微调。

微调数据的格式通常是 JSONL,每行一个样本:

{"prompt": "计算 17 乘以 23 的结果", "response": "17 * 23 = 391"} {"prompt": "解释什么是梯度下降", "response": "梯度下降是一种优化算法,通过沿损失函数梯度的反方向更新参数来最小化损失。"}

配比上,自改进数据不要全部替换原始训练数据。我的做法是自改进数据占 30% 到 50%,剩余用原始数据或通用指令数据填充。全部用自改进数据容易导致模型在筛选信号覆盖不到的任务上退化,这个现象在只做数学题自改进时特别明显——模型数学变好了,但闲聊能力下降了。

3. 用自洽性投票跑通第一轮自改进

3.1 准备一批无标注 prompt 和推理接口

第一步是准备数据。你不需要标注答案,只需要一批 prompt。来源可以是业务日志里脱敏后的问题、公开数据集的题目部分、或者自己构造的指令。数量上,500 到 2000 条就能看到趋势,太少噪声大,太多第一轮跑不完。

推理接口方面,如果你用本地模型,可以用 transformers 或 vLLM 起一个服务。如果调 API,注意并发限制和成本。下面是一个用本地模型做批量采样的骨架:

from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_name = "your-local-model-path" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16, device_map="auto") def call_model(prompt: str, temperature: float = 0.7, top_p: float = 0.95, max_tokens: int = 512) -> str: inputs = tokenizer(prompt, return_tensors="pt").to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, do_sample=True, temperature=temperature, top_p=top_p, max_new_tokens=max_tokens, pad_token_id=tokenizer.eos_token_id ) # 去掉 prompt 部分,只保留生成内容 generated = outputs[0][inputs["input_ids"].shape[1]:] return tokenizer.decode(generated, skip_special_tokens=True)

参数说明:do_sample=True是必须的,否则每次生成结果一样;temperature和top_p按上一节的经验值设;max_new_tokens根据任务长度调整,数学题 256 够用,长推理链可能要 1024。device_map="auto"让 transformers 自动分配显存,单卡多卡都能跑。

3.2 投票筛选与置信度过滤

采样完之后,不是所有题都适合做自改进。如果 8 个候选答案里有 7 个不一样,说明模型在这道题上本身就不确定,投票结果不可信。这时候需要置信度过滤:只保留投票一致性高的样本。

一个简单的过滤规则是:最高票数占总票数的比例超过阈值(比如 0.6)才保留。下面是对应的代码:

def filter_by_confidence(candidates: List[str], threshold: float = 0.6): """ 返回 (chosen_answer, confidence) 或 None。 confidence 是最高票占比。 """ normalized = [c.lower().replace(" ", "") for c in candidates] counter = collections.Counter(normalized) top_answer, count = counter.most_common(1)[0] confidence = count / len(candidates) if confidence < threshold: return None for c in candidates: if c.lower().replace(" ", "") == top_answer: return c, confidence return None

阈值 0.6 是个起点。如果你发现过滤后剩下的样本太少,降到 0.5;如果发现噪声还是大,升到 0.7。这个参数没有理论最优值,取决于你的任务难度和采样数量。N=8 时,0.6 意味着至少 5 个答案一致。

3.3 用筛选数据做一轮上下文学习验证

在正式微调之前,先用筛选出来的数据做一次上下文学习验证。具体做法:从筛选后的样本里挑 4 到 8 条,作为少样本示例拼进新 prompt,在留出的测试集上看效果。

def build_few_shot_prompt(query: str, examples: List[dict], k: int = 4) -> str: """ examples 是筛选后的 {"prompt": ..., "response": ...} 列表。 随机选 k 条拼进 prompt。 """ import random selected = random.sample(examples, min(k, len(examples))) parts = [] for ex in selected: parts.append(f"问题:{ex['prompt']}\n答案:{ex['response']}") parts.append(f"问题:{query}\n答案:") return "\n\n".join(parts)

这一步的价值在于快速验证。如果上下文学习后测试集准确率没有提升,说明筛选信号有问题,微调也救不回来。如果提升了,再投入微调。我一般会留 100 到 200 条测试样本,不参与任何采样和筛选。

4. 避坑:自改进闭环里最容易翻车的五个地方

4.1 投票投出了错误答案

现象:筛选后的答案在测试集上准确率反而比单次生成低。原因:模型在某个错误答案上形成了「多数一致」,也就是系统性偏差。比如模型倾向于把「9.11」算成大于「9.9」,如果采样时这个错误反复出现,投票就会把它选出来。

解决:引入外部验证信号做二次过滤。数学题可以把答案代回原式验证,代码题可以跑单元测试。没有外部验证时,降低置信度阈值,只保留一致性极高的样本,宁可少要质量。

4.2 自评时的位置偏差

现象:用模型自评比较两个答案时,它总是选第一个。原因:自评 prompt 里选项顺序影响了模型判断,这是已知的位置偏差。解决:把两个答案的顺序交换后再评一次,只有两次都选同一个才采纳。这个操作叫「双向评估」,成本翻倍但能消掉大部分位置偏差。

4.3 微调后通用能力下降

现象:自改进数据微调后,目标任务涨了,但闲聊、写作等通用能力掉了。原因:自改进数据分布窄,模型过拟合到筛选信号的模式上。解决:控制自改进数据比例在 50% 以下,混入通用指令数据。另外学习率要比正常微调低,我一般用 1e-5 到 2e-5,训练 1 到 2 个 epoch 就停。

4.4 采样成本失控

现象:N=40、题目 2000 条,推理成本是单次生成的 40 倍,跑一轮烧掉大量算力。原因:没有做成本预估。解决:先用 100 条题、N=8 跑通流程,确认信号有效后再放大。另外可以用 vLLM 这类推理框架做批处理,吞吐能提升几倍。

4.5 筛选后的数据格式不统一

现象:微调时 loss 不收敛或收敛后效果差。原因:筛选出的答案格式五花八门,有的带「答案是:」前缀,有的带换行,有的中英文混用。解决:在筛选后加一步格式化清洗,统一去掉前缀、统一标点、统一语言。这一步看起来琐碎,但不做的话微调效果会打折扣。

5. 让自改进真正转起来的一个进阶技巧

前面讲的都是单轮自改进。真正让数据飞轮转起来的是多轮迭代:第一轮筛选出的数据微调模型,用新模型重新采样,再筛选,再微调。但多轮迭代有个陷阱——模型会逐渐收敛到筛选信号的偏好上,多样性下降,后面几轮的投票一致性会虚高。

我的做法是每轮保留一部分原始模型的采样结果,和新模型的采样结果混在一起投票。这样既利用了新模型的提升,又保留了原始模型的多样性。具体比例我一般用 7:3,新模型占 7,旧模型占 3。

另一个技巧是动态调整采样温度。第一轮用 0.7,如果发现投票一致性太高(比如平均 confidence 超过 0.9),说明模型太确定了,下一轮把温度升到 0.9 增加多样性。反过来如果一致性太低,降到 0.5 让答案收敛一些。

验证多轮迭代有没有效果,不能只看训练集上的投票一致性,那是个自欺欺人的指标。要留一个固定的测试集,每轮结束后跑一次。如果测试集指标连续两轮不涨,就停下来,说明当前筛选信号已经到顶了,再迭代只是浪费算力。

最后说个我自己的教训:我最早跑自改进时,一心想着把 N 调大、把阈值调高,觉得这样数据质量最好。结果跑了三轮,测试集指标纹丝不动。后来才发现问题出在测试集本身——我用的测试集和训练数据同分布,模型早就记住了,指标虚高。换了一个真正留出的、分布略有差异的测试集之后,才看到自改进的真实收益。所以测试集的独立性比什么参数都重要,这一步偷懒,后面全白跑。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询