ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Prompt-Tuning小样本微调:模板、标签词与过拟合控制

Prompt-Tuning小样本微调:模板、标签词与过拟合控制

简介:《Prompt-Tuning:深度解读一种全新的微调范式》是一份面向NLP研究者、算法工程师与大模型学习者的综述型PDF,围绕少样本、零样本场景下如何用提示替代传统Fine-tuning展开,重点回应预训练与下游任务目标差异大、微调易过拟合等问题。压缩包仅含1个PDF文件,约17.21MB,单篇文档自成体系,便于离线通读与按章节检索,目前已有1557人学习下载。正文从预训练语言模型的三个发展阶段讲起,梳理单向与双向语言模型、Transformer结构及MLM、NSP等预训练任务,再延伸至GPT-3之后的离散Prompt与连续Prompt构建、In-Context Learning、Instruction-tuning、Chain-of-Thought,以及黑盒模型下的提示设计与ChatGPT中的相关技术;末尾讨论Prompt的选择策略、有效性评估和未来研究方向,适合希望建立系统认知、梳理技术脉络的读者反复查阅。

1. Prompt-Tuning:为什么冻结全部参数反而更抗过拟合

同一份预训练模型、同一批 32 条标注样本,把分类头换掉做常规微调,准确率常在 70 出头;而冻住主体参数,只在句子后面接一句It was [MASK].,再挑两个词做映射,准确率能到 85 以上。真正起决定作用的,是任务被表达成了什么形状;模型能力本身并不是瓶颈。这正是 Prompt-Tuning 最反直觉的地方:看起来什么都没训,效果却更好。

从 2020 年底 GPT-3 提出 in-context learning 算起,这条线一路走到 PET、P-tuning、指令微调,成了 LLM 在小样本场景下最常被提起的微调范式。它要解决的问题只有一个:把下游任务重新表达成预训练阶段模型已经见过的形状。理解了这一点,AIGC 时代关于 prompt 设计、指令对齐、思维链的讨论才有共同的落点。

后面几章拆三件事:预训练和微调的目标到底错位在哪,PVP 这套组件怎么把分类变成完形填空,以及模板从离散字符搬到连续向量空间之后,付出的代价是什么。

2. 目标错位:预训练与微调之间那道语义鸿沟

在 MLM 任务上,一个一亿参数级别的编码器能在完形填空里填出相当合理的词;换成情感分类,只给 32 条标注样本、加一个随机初始化的分类头去微调,结果往往还不如一个逻辑回归基线。问题并不在于模型不够强,而在于两个阶段问的问题形状不一样。要看清这个错位,得先回到预训练目标本身的设计。

2.1 双向注意力获得了全局视野,也锁定了任务形状

Transformer 结构的核心是把每个 token 与其余所有 token 显式连起来。自注意力里每个位置算出 Query、Key、Value,用 Query 与全部 Key 做点积得到权重,再对 Value 加权求和。任意两个 token 之间都有直连路径,整段序列的推理可以完全写成矩阵乘法,因此可以并行。这解决了 LSTM 时代两个老大难:串行编码带来的速度瓶颈,以及长距离依赖在梯度反传中衰减的问题。位置编码再补上顺序信息,模型就同时拿到了「全局」和「有序」。

代价藏在注意力掩码里。双向编码意味着位置 i 的表示同时看过左边和右边的上下文,这种结构天然擅长「给定上下文,预测中间被挖掉的那个词」;而单向自回归结构擅长「给定前文,续写下一个词」。这个结构差异直接决定了 prompt 的形态:双向模型用 cloze 式模板(句中挖空),单向模型用 prefix 式模板(末尾续写)。很多 prompt 方案在小模型上表现不稳,根源就是在用不适合该模型结构的模板形状。

还有一层容易被忽略:双向编码在预训练时见过的是完整文本,MLM 只是把其中 15% 的位置遮住。所以模型学到的表示,是把「整句语义」压进每个位置的隐状态里,而不是「前文如何决定后文」。后面的模板设计必须顺着这个特性走,逆着来就会额外要求学生成本。

2.2 MLM 的替换策略,决定了 prompt 里必须有一个空

Masked Language Modeling 的做法是:从大规模无监督语料里随机抽取 15% 的 token 位置作为预测目标,被选中的位置里 80% 替换成[MASK],10% 替换成词表里的随机词,剩下 10% 保持原样。训练目标是让模型在这些位置输出整个词表上的概率分布,损失用交叉熵。

这个 80/10/10 的比例不是随手定的。如果 100% 都用[MASK],模型会迅速学到「只有[MASK]位置才需要预测」,而下游任务里根本没有这个符号,预训练和微调的输入分布就断裂了;10% 随机替换是噪声注入,逼模型不能盲信输入词;10% 保持不变则保证模型对非 mask 位置也维持预测能力。下面是这条样本构造逻辑的最小实现:

import random def build_mlm_sample(tokens, vocab, mask_prob=0.15, mask_token="[MASK]"): """按 BERT 的 80/10/10 策略构造一条 MLM 自监督样本""" masked_tokens = list(tokens) labels = [-100] * len(tokens) # -100 表示该位置不计入损失 n_mask = max(1, int(len(tokens) * mask_prob)) for pos in random.sample(range(len(tokens)), n_mask): labels[pos] = vocab[tokens[pos]] # 监督信号是「原来的那个词」 r = random.random() if r < 0.8: masked_tokens[pos] = mask_token elif r < 0.9: masked_tokens[pos] = random.choice(list(vocab)) # 随机词,逼迫模型不盲信输入 # 剩下 10% 保持原样 return masked_tokens, labels

labels里只有被选中的位置有值,其余填-100被损失函数自动忽略,这就是「稀疏监督」的实现方式。mask_prob控制每句被挖掉的比例,vocab是词表到 id 的映射。注意返回的masked_tokens长度必须和labels严格对齐,这是这类实现里最常见的越界来源。

针对 MLM 有两个经典改进。其一是整词遮盖(Whole Word Masking):分词器会把一个词切成若干 word piece,如果只遮住其中一片,剩下那半截仍然暴露语义,所以要把整个词的所有 piece 一起遮。其二是实体级遮盖:在知识增强的预训练场景里,给定实体库,把文本中出现的整个实体跨度一次性遮住,逼模型依赖上下文而非局部字符去推断。两者都在做同一件事——提高预测难度,让模型学到的上下文表示更有信息量。

BERT 原版还挂了第二个任务 NSP:给定两个句子,判断二者是相邻(entailment)、同文但不邻(neutral)还是跨文(contradiction),取[CLS]位置的隐状态接一个三层 MLP 做分类。后续工作发现这个任务对下游效果没有稳定正向贡献,于是逐渐被删掉,换成整句复原、句子乱序等目标。

这整段历史给 prompt 留下一个关键遗产:MLM 的输出接口是「词表上的一个分布」,而不是「一个类别 id」。所有分类任务的输出,理论上都能编码成「某些词」。这是后面一切设计的接口基础。

2.3 五类下游任务,五种新长出来的头

拿到预训练模型后,传统做法是按任务性质在模型顶部加不同的头。常见的五类任务及其头部形态大致如下:

任务类型代表任务头部形式新增参数量级
单句分类文本分类、意图识别、情感分析[CLS]隐状态 → MLPhidden × 类别数
句对分类语义推理、文本匹配、检索拼接后[CLS]→ MLPhidden × 类别数
区间预测抽取式阅读理解、实体抽取两个位置分类器2 × hidden
单 token 分类序列标注、完形填空、拼写检测每个位置 → MLPhidden × 标签集
文本生成摘要、翻译、生成式问答自回归解码器整个 decoder

单句分类把整句压进[CLS]再映射到类别;句对分类把两句用[SEP]拼起来走同一套流程;区间预测要在 passage 上预测起止两个位置,所以挂两个分类器;单 token 分类对每个位置的隐状态各自分类;文本生成则换用自回归结构,逐词解码。

问题出在两个地方。第一是语义鸿沟:预训练问的是「这个空最可能填什么词」,微调问的是「这句话属于哪一类」,两个目标在参数空间里没有共享的优化路径,新头等于从零开始学一个映射。第二是头部过拟合:hidden 取 768、二分类时头部参数约 1500 个,绝对值不大,但它是随机初始化的,要拟合的是「高维语义空间到决策边界」的映射,几十条样本的情况下方差完全主导。更麻烦的是,训练时梯度会从这个随机头反传回整个 backbone,把预训练学到的表示一起带偏。

这两个毛病在小样本场景下会被放大到不可用。而工业界真实场景里,标注数据往往就是几十到几百条。Prompt-Tuning 的全部动机,就长在这个缺口上。

3. PVP:一次前向传播里完成分类的底层逻辑

PET 这项工作的贡献在于把「模板 + 标签词」固化成一对可复用的组件 Pattern-Verbalizer-Pair,让 prompt 从零散技巧变成一套有形式化定义的范式。它的核心动作只有一句话:不改模型输出端的结构,改输入端的形状,让分类问题在词表空间里被回答。

3.1 Template 与 Verbalizer 把标签空间折叠回词表

拿二分类情感分析举例。原始输入是一句I like the sci-fi films very much.,传统微调取[CLS]表征接 MLP 输出 positive / negative。Prompt 的做法是拼一个带空位的模板上去:

[CLS] I like the sci-fi films very much. [SEP] It was [MASK]. [SEP]

整段喂进编码器,只取[MASK]位置的输出,得到一个覆盖整个词表的分布。然后定义一个 Verbalizer,把标签映射到具体的词:positive → great,negative → terrible。取这两个词对应的 logit,在它们构成的子空间上做 softmax,就得到类别概率。

形式化地写,给定输入 x、模板 T、标签词映射 v,类别 y 的概率是:

p(y|x) = exp(s(v(y), T(x))) / Σ_{y'∈Y} exp(s(v(y'), T(x)))

其中s(v, T(x))是模型在[MASK]位置对词 v 输出的 logit。这一步的关键在于「先取子集再归一化」,而不是直接在五万维词表上取 argmax:

import torch import torch.nn.functional as F def pvp_forward(model, tokenizer, text, template, verbalizer): """ text: 原始句子 template: 含 <MASK> 占位符的模板,如 "It was <MASK>." verbalizer: {标签: 标签词},如 {"positive": "great", "negative": "terrible"} """ prompt = template.replace("<MASK>", tokenizer.mask_token) inputs = tokenizer(text, prompt, return_tensors="pt") # 两段拼成一条序列 mask_pos = (inputs["input_ids"] == tokenizer.mask_token_id).nonzero()[0, 1] logits = model(**inputs).logits[0, mask_pos] # 只关心 mask 位置的词表分布 labels = list(verbalizer.keys()) word_ids = [tokenizer.convert_tokens_to_ids(verbalizer[l]) for l in labels] label_logits = logits[word_ids] # 投影到标签词子空间 return dict(zip(labels, F.softmax(label_logits, dim=-1).tolist()))

mask_pos的取法依赖分词器是否在句首插入特殊符号,取错一位就会拿到[SEP]的隐状态,输出全是噪声。word_ids要求每个标签词恰好对应一个 token id,多 token 的标签词(比如very good)在这里会被静默截断成第一个 token,这是后面排查章节要专门讲的一个坑。子空间归一化的动机很实际:原分布的概率质量分散在几万个词上,两个标签词的 logit 直接比较不构成概率,必须重新归一化才能算交叉熵。

换个角度看,这套操作本质是一次数据增强:模板往输入里注入了一段先验(「这句话在描述一种情绪」),标签词把输出约束在预训练已经理解的语义附近。样本没变多,但每个样本携带的监督信息变多了。

注意:模板和标签词的选择对结果影响极大。同一份数据、同样的训练条件,换个模板或者换个标签词,准确率能差出十几个点。这个方差是 PET 之后所有研究要解决的头号问题。

3.2 复用 MLM head 为什么能压住过拟合

这里有个广泛流传的误解,需要说清楚:PET 在训练时是更新整个模型参数的,并不是「冻结主体只训 head」。它真正省掉的是那个随机初始化的新分类头。

省掉一个随机头,为什么能带来这么明显的收益?原因有三层。

第一层是初始化先验。MLM head 在预训练阶段已经被海量语料训过,它知道great通常出现在什么样的上下文之后,知道terrible的语义邻域在哪里。这个头带着信息,不是噪声。而随机初始化的分类头什么都不知道,它把梯度反传回 backbone 时,等于在用一个错误的方向拽整个表示空间。

第二层是假设空间约束。分类头允许模型输出任意的决策边界,而复用 MLM head 等于把输出限制在「语言模型认为合理的词」这个子集里。约束越强,小样本下的方差越小。用偏差-方差的语言说:偏差略微上升,方差大幅下降,总体误差下降。

第三层是监督位置的稀疏性。整个序列只有[MASK]那一个位置产生损失,梯度从单点反传。这在直觉上像是信息变少了,实际上起到正则作用——模型不必为每个 token 都调整表示,只需让整句语义在掩码位置汇聚出正确结论。

主流方案在这三个维度上的差异可以对照着看:

方案主体是否更新是否新增参数新增参数量
传统 Fine-tuning是是(新分类头)hidden × 类别数
PET / 离散 Prompt是否0
P-tuning / 连续 Prompt否是(软提示向量)token 数 × hidden

第二行和第三行的区别值得注意:离散 prompt 方案通常仍会微调整个模型,只是不新增结构;连续 prompt 方案才真正冻结主体,只优化那几百到几千个提示向量。把两者混为一谈,会导致在错误的地方找问题——如果你的方案是 PET 式全量微调,那调小学习率、限制训练轮次仍然是必要的;如果是 P-tuning 式冻结主体,那 prompt 向量的初始化方式才是关键。

3.3 从 GPT-3 的 In-Context Learning 到 PET 的 iPET

GPT-3 提出的 in-context learning 是 prompt 的前身:不改一个参数,把几个「输入-输出」示例直接拼进上下文,模型就能照猫画虎地完成新样本的预测。配套的 demonstration learning 更进一步,除了示例还给出标签的语义描述,让模型理解「什么样的回答算对」。这套方法在千亿参数级别上表现惊人,但它有个硬门槛——模型规模必须足够大,能力才会涌现;换到一亿参数级别的小模型上,零样本直接崩。

于是研究重心转向:这套思路能不能落到小模型的微调阶段?PET 给出的答案是能,但需要把提示做得更结构化。它在少量标注样本上做 PVP 式训练,把 in-context learning 从「推理时的技巧」变成「训练时的目标函数」。

PET 还留了一个半监督的扩展叫 iPET,流程值得细看,因为它展示了一个很实用的自训练循环:

  1. 从同一个预训练模型出发,复制出若干份,每份配一套不同的 PVP;
  2. 各份模型在少量标注数据上分别训练;
  3. 每份模型对无标注数据打标,按预测置信度筛掉低质量样本,只保留高置信部分;
  4. 把新标注数据和原始标注数据混合,重新训练下一轮;
  5. 重复若干轮后,测试时对所有模型的输出做集成投票。

这个循环的巧妙之处在于多样性来源:不同的模板和标签词让各份模型看到不同的「任务视角」,犯的错不完全相关,因此打标结果的噪声可以被投票抵消一部分。代价是训练成本随模型份数线性增长,实际使用时通常复制 3 到 5 份就够。

4. 模板搜索:从人工试错到离散、连续两条路

人工设计模板的成本很高,而且不稳定。有人做过对比实验:固定数据集和训练条件,只换模板,效果波动能到十几个点;只换标签词,波动同样明显。这意味着调参的人必须穷举模板和标签词的组合才能找到好结果,prompt「简单快捷」的优势被抵消干净。往深了说,人工模板有三个绕不过去的缺陷:依赖领域先验知识;离散搜索空间无法求导,只能靠试;以及预训练时的 MLM 见过的是长文本、多个掩码、开放词表,而 PVP 给的是短模板、单个掩码、封闭标签集,两者在分布上始终有偏差。

于是研究方向分成两支:一支继续在离散空间里做搜索和生成,另一支干脆把模板搬进连续向量空间。

4.1 启发式规则与生成式模板

启发式法的思路是用规则拼装模板。关系抽取任务里,给定句子和两个实体(主体、客体),可以预先定义若干子模板,比如「主体是人」「客体是人」「主体是客体之父」,再按规则把子模板组合成完整 pattern。这种做法的好处是可解释、可控,坏处是规则要人写,换任务就得重来。

生成式法则让小模型来写模板。LM-BFF 的做法是:拿一个预训练的生成模型(比如 T5),给它几个标注样本作为示例,让它「生成」若干个可能的模板,然后回到标注数据上,用这些候选模板跑一遍 PVP 训练并打分,选分数最高的那个。本质上是用生成模型做离散空间里的启发式搜索:

def generate_and_score_templates(gen_model, gen_tok, task_model, samples, n_candidates=20): """先用生成模型产出候选模板,再用标注数据挑选最优的一个""" demo = "\n".join(f"{s['text']} => {s['template_with_slot']}" for s in samples[:5]) prompt = f"{demo}\n新句子:{samples[0]['text']} =>" inputs = gen_tok(prompt, return_tensors="pt") # 采样多条候选,temperature 控制多样性 outputs = gen_model.generate(**inputs, num_return_sequences=n_candidates, do_sample=True, temperature=0.7, max_new_tokens=16) candidates = [gen_tok.decode(o, skip_special_tokens=True) for o in outputs] best, best_score = None, -1.0 for tpl in set(candidates): if "<MASK>" not in tpl: # 必须含掩码位,否则无法接 MLM head continue score = evaluate_pvp(task_model, tpl, samples) # 在标注集上跑一遍取准确率 if score > best_score: best, best_score = tpl, score return best, best_score

num_candidates控制候选数量,temperature越高生成越随机。evaluate_pvp是一段就地微调加评估,能挑出与当前模型最匹配的模板,而不只是「语义上通顺」的模板——这两者经常不是一回事。代价是每个候选都要训一次,候选数一多成本就上去了,实践中常配合早停和更小的训练轮次。

4.2 Verbalizer 的构造:手工、搜索、连续

标签词的问题比模板更隐蔽。手工映射看起来最直接:正类用great,负类用terrible。但词表里有几万个词,凭什么这两个最优?相关实验给出的结论很反直觉:同一个模板下换标签词,效果波动可以接近换模板。

搜索法的做法是给每个类别先取一个初始标签词,然后在词表里找与它语义相近的词扩充候选集,再用标注数据做剪枝——目标是选出一组标签词,使得各类别之间的预测分布尽量可分。剪枝可以写成优化问题,也可以用简单的贪心:每次加入一个候选词,看验证集准确率是否提升。

还有一类做法直接放弃离散词,把 verbalizer 变成连续表示:对每个类别,取该类所有样本在掩码位置的隐状态平均,得到一个类原型向量,预测时比较当前样本与各原型的距离。这本质上把分类头重新引入,但它引入得有约束——原型由数据决定,不是随机初始化,方差小得多。

4.3 连续提示:伪 token 与向量空间里的模板

离散模板的根本限制在于它不能求导。模板是字符,选定了就固定,模型没法在训练中调整它。连续提示的动机就在这里:既然模板的作用是给输入注入任务信息,那这份信息为什么不能是一组可训练向量?

具体做法是:在输入 embedding 序列的最前面(或掩码位置)插入 k 个可训练的向量,模型主体全部冻结,只优化这 k 个向量。它们不对应词表里的任何词,因此被称为伪 token(pseudo token)。P-tuning 进一步发展出用一个小的 LSTM 或 MLP 来生成这些向量,目的是建模伪 token 之间的依赖关系——直接把它们当成独立参数,容易收敛到局部最优。

class SoftPrompt(torch.nn.Module): def __init__(self, n_tokens, hidden, model, init_from_vocab=None): super().__init__() self.model = model for p in model.parameters(): # 主体参数全部冻结,只留梯度给提示向量 p.requires_grad = False self.prompt = torch.nn.Parameter(torch.randn(n_tokens, hidden)) if init_from_vocab is not None: # 用词表里已有词的 embedding 初始化,收敛更快 self.prompt.data.copy_(init_from_vocab) def forward(self, input_ids, attention_mask): embeds = self.model.get_input_embeddings()(input_ids) # [B, L, H] prompt = self.prompt.unsqueeze(0).expand(embeds.size(0), -1, -1) inputs = torch.cat([prompt, embeds], dim=1) # 伪 token 拼在最前 mask = torch.cat([attention_mask.new_ones(embeds.size(0), self.prompt.size(0)), attention_mask], dim=1) return self.model(inputs_embeds=inputs, attention_mask=mask)

n_tokens一般取 10 到 100,太少表达不了任务信息,太多会挤压真实输入的注意力预算。hidden必须与模型隐层维度一致,否则拼不上。init_from_vocab这一步影响很大:随机初始化时损失经常在开头几轮原地打转,用词表里若干实词的 embedding 做初始化,收敛速度明显不同。前向里inputs_embeds走的是绕过 embedding 层的入口,因此input_ids对应的位置编码要自己留意——部分实现会对伪 token 单独处理位置编号。

这套方案的收益是参数量极小(几千个浮点数),在超大规模模型上尤其划算;代价是可解释性全部丢失,你没法像看模板那样看出模型在「问什么」。

5. 排查:Prompt-Tuning 效果不如 Fine-tuning 的五个现场

这套方法调起来确实有点玄学的味道,但每次翻车基本都能归到几个固定位置。下面五条是血泪经验里出现频率最高的。

5.1 模板与任务语义不匹配

现象:换了个模板,验证集准确率从 85 掉到 65,训练损失还降得很正常。

原因:模板承担的是「把任务意图翻译成预训练见过的话术」的职责。如果任务是判断两个实体之间是不是父子关系,模板却写成They are [MASK].,模型接收到的是「判断这两个词的关系」,而不是「判断父子关系」,语义粒度对不上。损失能降,是因为模型总能找到某种捷径把训练集拟合掉。

解决:模板里必须出现任务的关键语义槽位。关系抽取要显式写出实体位置和关系类型提示;情感分析要出现情绪相关的主语。更稳的做法是一次准备 5 到 10 个候选模板,用一小部分验证数据做筛选,而不是凭直觉定一个。

5.2 标签词落在词表的低频区

现象:模型预测出来的词全是[MASK]附近的高频词,你定义的两个标签词从来没被选中过。

原因:MLM head 的输出分布受预训练语料词频强烈影响。你选的标签词如果在整个语料里出现次数很少,它的 logit 天然偏低,在子空间 softmax 里几乎拿不到概率质量,梯度信号极弱。

解决:优先选高频、语义明确的单字词或常见词。选定之后做个 sanity check:把标签词喂进一个空白上下文,看模型对它的预测概率量级是否合理。如果必须用低频词,考虑扩充为多个同义标签词并加权合并,或者改用语料统计做先验校准。

5.3 学习率照搬全量微调

现象:训练两三轮之后模型开始输出无意义内容,验证集准确率断崖式下跌。

原因:传统微调常用的学习率对小样本 prompt 训练来说偏大。参数量少、监督信号稀疏(只有一个掩码位置),大学习率会在几千步内把预训练表示冲掉。原型方案里还有个更隐蔽的问题:提示向量和学习率不匹配,向量更新过猛会直接跳到分布外。

解决:把学习率降到常规值的十分之一量级,同时加长 warmup、限制训练轮次。连续提示方案里,提示向量和模型主体可以用不同学习率分组。判断标准是看验证集是否在前期就出现拐点,出现就立刻降。

5.4 多 token 标签词被截断成单 token

现象:标签词明明写了 "very good",代码跑通了但效果异常差,打印出来发现用的只有 "very"。

原因:分类路径依赖「标签词对应单一 token id」这个假设。分词器会把多词短语切成多个 piece,取 id 时只取到第一个,剩下的被静默丢弃。更糟的是这个错误不报错,只在指标上体现。

解决:构造 verbalizer 时强制校验len(tokenizer.encode(word)) == 1,不满足就换词或者用多 token 打分(对多 token 序列的各位置 logit 求和)。这个校验应该写进构造函数的断言里,不要靠人眼。

5.5 连续提示随机初始化,损失不降

现象:软提示方案跑了几百步,损失曲线几乎是平的,换成离散模板立刻正常。

原因:伪 token 的向量是随机初始化的,它们和真实词的 embedding 不在同一个分布里,模型前几层看到的就是一组噪声,梯度传不回去。这个问题在冻结主体的设定下尤其严重,因为主体没法自适应。

解决:用词表里若干实词的 embedding 做初始化,或者先用离散模板训一个模型,取其模板词的 embedding 作为起点。另外可以加一个小的投影层让伪 token 先过一遍归一化。收敛判据不要只看损失,要看掩码位置的预测分布是否在向标签词集中。

6. 设计取舍:Prompt-Tuning 什么时候真的赢

这套范式的收益高度依赖三个变量:模型规模、标注数据量,以及任务与预训练目标的距离。把这三个变量摆在一起,很多「prompt 到底行不行」的争论其实就消解了。

模型规模足够大(百亿参数以上)时,零样本和少样本的能力会涌现,此时不更新参数的 in-context learning 性价比最高,训练成本为零;模型规模中等、标注数据几十到几百条时,离散 prompt 加全量微调的组合通常最稳,它省掉的是随机头带来的方差;标注数据上万条时,传统全量微调反而经常反超,因为此时方差不再是主要矛盾,模型容量和任务特定的结构调整开始起作用。至于连续提示,它真正的战场是「模型大到无法全量微调、但又必须适配下游任务」的场景——用几千个参数换一个可用的任务适配。

验证一套 prompt 方案是否真的有效,最可靠的办法是画数据量曲线:在 16、32、64、128、256 条标注样本上分别跑 Fine-tuning 和 Prompt-Tuning,看两条曲线在哪个点交叉。单点对比没有意义,因为随机种子带来的波动可能就有几个点。配套要做的是方差控制:准备多套 PVP 做集成投票,固定种子跑三次取均值,报告标准差而不是只报最好成绩。这三件事做下来,结论才站得住。

代价也要说清楚。离散提示的方差大、可解释性好;连续提示方差小、彻底黑箱;集成能压方差,但推理成本翻倍。没有哪种方案在所有维度上占优,选型就是在这几条曲线上找自己最能接受的那个点。

我自己的习惯是:任何 prompt 方案上线前,必须先在同一份数据上跑一遍全量微调作为对照基线,且必须跑三次看方差;如果 prompt 只在某一次跑赢了,那就当它没赢。这个规矩来自一次教训——当时只对比了一次结果就下了结论,换了个随机种子之后优势全没了。从那以后我每次评估 prompt 方案,都强制走一遍「多模板 × 多数据量 × 多随机种子」的矩阵,宁可慢两天,也不吃这个后悔药。希望帮到你。

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

RELATED READING

延伸阅读

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