简介:这份PDF技术综述面向NLP研究者、算法工程师与大模型学习者,系统解读Prompt-Tuning这一面向AIGC时代的微调新范式。正文先梳理预训练语言模型的三阶段演进,指出传统Fine-tuning存在预训练与下游目标差距过大、小样本易过拟合等痛点;随后依次展开离散与连续Prompt构建、In-Context Learning、Instruction-tuning、Chain-of-Thought、黑盒模型提示方法及超大模型的参数有效性训练,并讨论Prompt的设计选择、效果评估与后续研究方向。压缩包内为1个PDF文件,约17.21MB,章节编排完整,便于按知识点检索通读,已有1557人学习下载。读者可据此建立从「Prompt是任务指令」到「Prompt本质是参数有效性训练」的分层认知,掌握小样本与零样本场景下的策略取舍,并获得面向具体NLP任务的模板设计参考与进一步研究线索。
1. Prompt-Tuning:为什么“不改模型只改输入”反而比全参微调更稳
很多人第一次听到 Prompt-Tuning,会把它理解成“把提示词写得更聪明”——这是把它当成了应用层的技巧。真正的 Prompt-Tuning 不优化那句提示文本,而是把下游任务重新改写成预训练阶段那个掩码填空(MLM)的形式,让 LLM 用自己最熟悉的老本行去答题。它要解决的并不是“怎么问”,而是“为什么微调之后模型在小样本上反而变笨”:新挂上去的分类头从随机初始化开始学,样本一少就过拟合,预训练阶段辛苦学到的语义分布在微调里被冲淡。在 AIGC 语境下这套范式被反复提起,是因为当模型参数涨到百亿量级、全参微调的成本和风险都难以承受时,prompt 把可训练参数压到了几乎为零。想清楚它,等于想清楚了“复用预训练目标”这条贯穿大模型微调的主线。
2. 预训练与微调的目标鸿沟:head 过拟合到底从哪儿来
预训练和微调被当成一条流水线上的两道工序,实际却是两套完全不同的目标函数。搞清楚这条鸿沟有多宽,才能理解 Prompt-Tuning 为什么非要把下游任务“翻译”回预训练任务的语言。
2.1 MLM 与 NSP:预训练阶段到底在优化什么目标
以掩码语言模型为代表的预训练目标,本质是一个自监督的词表分类任务。给定一段完整文本,随机抽取 15% 的位置做处理:其中 80% 换成[MASK],10% 换成一个随机词,10% 原样保留。模型看到被打断的序列,要在[MASK]对应的位置输出整个词表上的概率分布,再用交叉熵去拟合被挖掉的那个真实 token。注意两个容易被忽略的细节:一是预测分布定义在完整词表上,二是被 mask 的位置通常不止一个。
提示:10% 随机替换、10% 保持原样这两个看似奇怪的设计,是为了让编码器不要过度依赖
[MASK]这个符号本身——否则模型会学会“只有看到[MASK]才需要推理”,而下游微调时根本没有[MASK]。
这套目标后来演化出两个重要变体。一个是整词掩码:分词器会把一个词切成若干子词(word piece),如果只 mask 其中一片,剩下的碎片仍然泄露语义,于是改成必须 mask 掉一个完整单词。另一个是实体级掩码:在引入外部知识库的场景下,把文本中整个实体提及一次性 mask 掉,逼迫模型依赖上下文而非实体内部字面来推断。这两条改进都指向同一个方向——让被挖掉的部分语义尽可能完整,这恰恰是后面设计 prompt 模板时反复要面对的问题。
另一条并行的预训练目标是下一句预测(NSP):给两个句子,判断它们是相邻的(蕴含)、来自同一篇但不相邻(中性),还是来自不同文章(矛盾)。构造方式是自监督的——同一篇文章里取相邻句做正例,跨文章取句做负例。不过后续研究发现这个任务对下游效果的正向收益有限,很多模型直接把它删了,预训练目标收敛到以 MLM 为绝对主力。这一点很关键:Prompt-Tuning 后来的大部分设计,切入点都是 MLM,而不是 NSP。
2.2 五类下游任务与新增 head 的参数代价
预训练结束后,面对具体任务需要微调。按输入输出形态,NLP 下游任务大致能归成五类,而每一类在微调时几乎都要在编码器顶部挂一个全新初始化的模块:
| 任务类型 | 典型任务 | 新增 head 结构 | 训练目标 |
|---|---|---|---|
| 单句分类 | 文本分类、意图识别、情感分析 | [CLS]隐状态接入 MLP 分类器 | 类别上的交叉熵 |
| 句子对分类 | 语义推理、文本匹配、检索 | 两段文本拼接后取[CLS]接 MLP | 类别上的交叉熵 |
| 区间预测 | 抽取式阅读理解、实体抽取 | 两个位置分类器预测起止下标 | 起止位置的交叉熵 |
| 单 token 分类 | 序列标注、完形填空、拼写纠错 | 每个 token 隐状态接 MLP | 逐 token 交叉熵 |
| 文本生成 | 生成式摘要、翻译、问答 | 自回归解码器 | 逐 token 的似然 |
这张表里真正的信息量在最后一列和第三列的对比:预训练阶段的目标是“在词表上做分类”,微调阶段的目标是“在任务标签空间上做分类”,而连接这两者的那个 head 是完全随机初始化的。样本充足时这不是问题,模型有足够梯度把这层头训出来;样本只有几百条时,这层头就成了整个系统里最脆弱的一环。全参微调还有个更隐蔽的代价:所有任务各存一份完整模型副本,任务数一多,部署成本线性膨胀。
2.3 两个痛点的量化:语义鸿沟与 head 过拟合
把上面的观察压缩成两句话,就是 Prompt-Tuning 的两条设计动机。
第一条叫语义鸿沟。预训练任务在优化“给定上下文填空”,下游任务在优化“给定句子判类别”,两个目标在函数形式上都不同。为了适配新任务,微调阶段必须引入新参数并大幅更新主干,更新幅度越大,预训练阶段积累的语义就越容易被覆盖,尤其在小样本上,模型会迅速退化成一个“只认得这批标注数据”的窄模型。
第二条叫 head 过拟合。分类头从零学起,它的梯度会反向灌进整个编码器。样本少、任务简单时,模型有强烈的动机去记住训练集里的表面特征而不是学语义——毕竟记住比理解容易得多,这就是血泪经验里最常见的“训练集 0.98、验证集 0.6”的来源。传统的缓解手段是冻结主干只训 head,但 head 容量太小又学不动复杂任务,左右为难。
Prompt-Tuning 给出的答案是:别换目标,把任务伪装成预训练任务。既然预训练在学填空,那就把“这句话是正面还是负面”改写成“这句话后面那句It was [MASK].该填什么”。这样一来,用的还是那个 MLM 头,参数没新增,语义鸿沟被填平,head 过拟合的问题也自然消失——因为根本没有新 head。这也是那句“Prompt 的第三层理解是参数有效性训练”的由来,前两层“任务指令”和“复用预训练目标”都只是表象。
3. PVP 三元组:一次前向传播里任务是怎么被改写成完形填空的
把动机翻译成机制,需要回答三个问题:模板长什么样、标签怎么映射回类别、训练时到底在算哪个概率。这三件事在 2020 年底被一套叫 PVP 的组件统一定义下来,之后几乎所有变体都是在这套框架上做加法。
3.1 Pattern 与 Verbalizer:PVP 的形式化定义
给定一个输入句子 x 和它的类别标签 y,Prompt-Tuning 引入两个组件。
Pattern(也叫 Template)是那段带[MASK]的附加文本。以情感二分类为例,原始输入是[CLS] I like this sci-fi film very much. [SEP],人工设计一个模板It was [MASK].,拼接后得到真正喂进模型的序列:[CLS] I like this sci-fi film very much. [SEP] It was [MASK]. [SEP]。整条序列里有且只有一个[MASK],它的位置就是模型要做决策的位置。
Verbalizer 是“标签词到类别的映射”。[MASK]位置预测出来的是词表上的分布,但情感分析只关心两类,于是约定:如果[MASK]填的是great/nice/wonderful,判为正面;填的是terrible/awful/boring,判为负面。这个映射关系把连续的词表空间收缩到任务标签空间。
两者合在一起称为 Pattern-Verbalizer-Pair。训练目标可以写成一句话:最大化“在给定模板序列下,[MASK]位置填出该类对应标签词”的概率。这个说法的价值在于,它把不同任务统一到了同一个输出接口上——分类、推理、甚至部分抽取任务,都能改写成“在某个位置填某个词”的形式,模型侧的代码几乎不用动,动的只是模板和映射表。
注意:Verbalizer 的构建强依赖 Pattern。模板是
It was [MASK].时,great通顺;模板换成The reviewer felt [MASK].时,答案域就变成happy/angry这类情绪词。模板和标签词必须成对设计,不能拆开单独调。
3.2 从[CLS]分类头切到 MLM 头:一次前向的完整路径
传统微调取的是[CLS]的隐状态,Prompt-Tuning 取的是[MASK]位置的隐状态,再复用预训练时那个已经训好的 MLM 头映射到词表。这个替换动作看起来只是换了个下标,实际上省掉了一整个随机初始化的分类器。
# 假设已有一个带 MLM 头的预训练编码器,词表与 tokenizer 对齐 def build_prompt(sent_ids, pattern_ids, mask_id, cls_id, sep_id): # 拼接:[CLS] 原句 [SEP] 模板 [SEP],模板中已含一个 mask 占位 input_ids = [cls_id] + sent_ids + [sep_id] + pattern_ids + [sep_id] mask_pos = input_ids.index(mask_id) # 定位唯一的 [MASK] 下标 return input_ids, mask_pos def forward(model, input_ids, mask_pos): logits = model(input_ids=input_ids).logits # (seq_len, vocab_size) return logits[mask_pos] # 只保留 [MASK] 位置的词表分布 # verbalizer:类别 -> 标签词集合,词必须能在词表里找到且为单 token verbalizer = { "positive": ["great", "nice", "wonderful"], "negative": ["terrible", "awful", "boring"], } def class_prob(mask_logits, verbalizer, tokenizer): # 先在全词表上做 softmax,得到每个词的归一化概率 probs = torch.softmax(mask_logits, dim=-1) scores = {} for label, words in verbalizer.items(): ids = [tokenizer.convert_tokens_to_ids(w) for w in words] # 类别得分 = 该类下所有标签词的概率质量之和 scores[label] = probs[ids].sum() # 在 verbalizer 覆盖的标签词全集上重新归一化,得到类别分布 total = sum(scores.values()) return {k: v / total for k, v in scores.items()}build_prompt里最关键的是mask_pos:它必须是模板里[MASK]的真实下标,而不是固定值,因为原句长度在变。forward只取一个位置,意味着序列其余部分相当于超大号的上下文条件,这也解释了为什么 prompt 的效果对模板措辞极其敏感——模板改变了上下文,上下文决定了[MASK]位置的分布。
class_prob里的两段归一化容易写错。第一段softmax是在完整词表上做的,这与预训练时 MLM 的训练方式一致;第二段在标签词全集上重新归一化,是为了把“词表空间的概率”压回“类别空间的概率”。如果跳过第二段直接比大小,那么标签词数量多的类天然占优,模型会系统性地偏向某一类。
参数上还有一处细节值得单独说:tokenizer.convert_tokens_to_ids(w)要求w在分词后是单个 token。如果wonderful被分词器切成了两片,这个 id 对应的就只是一个词缀,概率解释立刻失真。所以构建 verbalizer 之前必须先跑一遍 tokenizer 长度检查,这是后面避坑章节里会展开的一条。
3.3 标签词概率的三种聚合方式与损失函数
上面代码用的是“概率求和”,实际实现里聚合方式有三种,行为差异不小。
求和是最常见的选择,它把一类下所有标签词的概率质量加总,等价于“只要填出该类任意一个词都算对”。平均则是在求和基础上除以该类标签词数量,好处是不同类的标签词个数不一致时不会产生系统性偏置,坏处是会削弱“多个近义词同时高分”带来的置信度信号。第三种是加权,给每个标签词一个先验权重,比如更能代表类别的词权重更高,但权重本身又成了要调的参数。
训练损失还是交叉熵,只是目标不再是“类别下标”,而是“该类别在 verbalizer 下的归一化概率”。这个损失函数有一个特性值得注意:它完全建立在预训练已经训好的 MLM 头上,没有新参数参与梯度,所以小样本下的方差显著小于全参微调——这正是第 2 章里那两个痛点的直接兑现。代价是表达能力受限于词表,模型只能通过“选哪个词”来表达决策,无法表达词表之外的连续边界。
集成的思路也是从这个概率接口自然长出来的。同一个句子可以配多个 Pattern,每个 Pattern 有多组 Verbalizer,推理时把所有 PVP 输出的类别概率做加权或投票,模型不需要任何改造,只是前向多跑几遍。后面第 4 章会发现,这套“多跑几遍再投票”的做法在半监督场景下演化成了完整的自训练流程。
4. 模板与标签词的搜索空间:离散模板拼规则,连续模板学向量
PVP 框架搭好之后,剩下的全部问题就是一个:模板和标签词从哪儿来。人工写能跑通,但实验里换一个模板掉十几个点是家常便饭,这条方差让“简单快捷”的优势大打折扣。围绕这个问题,学界分出了两条完全不同的技术路线。
4.1 离散模板的三条路线:人工、启发式与生成式
离散模板的共同点是显式拼出真实字符,且这些字符的词向量在训练中保持冻结。它们不引入任何新参数,代价是模板无法随数据自适应。
人工构建最直观,靠任务先验写一句话,情感分析常用的就是It was [MASK].。问题在实验里暴露得很清楚:固定 Pattern 只换标签词,或者固定标签词只换 Pattern,结果波动都很明显。有人把这种敏感性总结成三条缺陷——构造成本高且依赖领域知识;不能保证最优、方差大;与预训练阶段 MLM 的真实训练分布仍有差异。第三条最容易被忽略:预训练时输入的是长文本、被 mask 的位置往往有多个、预测分布定义在整个词表上,而人工模板是短句、单 mask、只关心少数几个词,两者在语义和分布上并没有完全对齐。
启发式法试图用规则把人工设计规模化。思路是把一个复杂任务拆成若干子模板,每个子模板负责一个语义槽位,再按规则组合成最终 Pattern。以关系抽取为例,任务输入是一句话加两个实体,可以拆成“主语是什么类型”“宾语是什么类型”“两者之间是什么关系”三个子问题,每个子问题配一个子模板,最终把子模板的输出拼起来判定关系。这类做法的好处是模板不再是黑盒里的一句咒语,而是可解释、可复用的规则积木;坏处是规则本身又需要人来写,只是在更大的任务集合上摊薄了成本。
生成式法则把模板当作一个可学习的目标,用序列到序列模型在少量标注样本上生成候选模板,再按验证集表现挑选。它的优势是能跳出人的措辞习惯,找出反直觉但有效的表达;风险是生成的模板可能语义不通顺,在下游任务分布稍有偏移时就失效。
4.2 连续模板:把模板变成可训练的伪 token 向量
离散模板的根本约束是词向量冻结——模板的字符一旦选定,它在整个训练过程中都是同一个向量,模型没法根据上下文微调它。连续模板(也叫软模板 / 软提示)直接打破这个约束:不再拼真实字符,而是在嵌入层插入若干个可训练的向量,让它们和真实 token 的嵌入一起进入 Transformer,随梯度更新。
class SoftPromptWrapper(nn.Module): def __init__(self, plm, n_tokens, hidden): super().__init__() self.plm = plm # 伪 token:没有对应的字符,纯粹是可学习的嵌入向量 self.prompt_emb = nn.Parameter(torch.randn(n_tokens, hidden) * 0.02) # 冻结主干,只训练模板向量,参数量 = n_tokens * hidden for p in self.plm.parameters(): p.requires_grad = False def forward(self, input_ids, attention_mask): tok_emb = self.plm.get_input_embeddings()(input_ids) # (B, L, H) # 伪 token 扩展到 batch 维后拼在序列最左侧 prefix = self.prompt_emb.unsqueeze(0).expand(tok_emb.size(0), -1, -1) inputs_embeds = torch.cat([prefix, tok_emb], dim=1) # attention_mask 同步左移,给伪 token 位置补 1 mask = torch.cat([torch.ones_like(attention_mask[:, :self.prompt_emb.size(0)]), attention_mask], dim=1) return self.plm(inputs_embeds=inputs_embeds, attention_mask=mask)这段代码里有三个决策点。n_tokens是模板长度,通常取十几个到上百个,太短学不到足够信息,太长会挤占真实输入的注意力预算;hidden是编码器隐层维度,所以总参数量是两者相乘,相比整个模型仍然是千分之一量级,这就是“参数有效性训练”的字面含义。初始化用0.02量级的小方差,是为了让伪 token 在训练初期接近“无意义占位符”,不要一上来就把预训练好的注意力分布冲乱。
第二个决策是拼接位置。伪 token 放在序列最左侧而不是插在中间,好处是真实 token 的相对位置全部后移一个固定偏移,位置编码处理简单,推理时也更容易复用缓存;坏处是伪 token 无法和特定词形成局部交互,只能通过注意力全局影响。
第三个决策是主干冻结的范围。全冻结最省显存也最不容易过拟合,但小模型上可能欠拟合;部分解冻(比如只放开最后几层)是常见的折中。这条路线的一个副作用是丢掉了可解释性——你没法再读出模板到底“写了什么”,它变成了一组只对模型自己有意义的向量,排障时只能靠消融实验。
4.3 集成与自训练:Pattern、Verbalizer 与 PVP 三层投票
单个 PVP 的方差问题,在框架层面有两种解法:横向集成降方差,纵向自训练扩数据。
横向集成有三个层次。Pattern 集成是同一句话配多个模板,原本一条样本变成多条,变相做了数据增强,推理时对各模板的类别概率投票或加权。Verbalizer 集成是在同一个模板下为每个类准备多个近义标签词,模型填出任一个都算命中,推理时把同类标签词的概率质量合并。PVP 集成则是两者叠加,一条样本展开成模板数乘标签词组数的组合,是所有方案里方差最低、成本也最高的做法。
纵向自训练对应的是半监督场景下的迭代式流程。先在有标注的小样本上,用多个不同的 PVP 分别训练出多个模型;然后让每个模型去无标注数据上推理打标,按置信度筛掉不确定的样本;把筛出来的伪标注数据和原始标注数据混合,重新训练下一轮;重复若干轮之后,测试时把所有轮次的模型集成投票。它之所以能 work,是因为不同 PVP 犯错的地方往往不相关,投票能把随机误差抵掉;而置信度筛选相当于给伪标签加了一道闸门,避免错误标签在迭代中自我强化。代价是训练成本翻了几倍,且轮数一多容易在伪标签分布上过拟合,需要盯着验证集早停。
5. 避坑:Prompt-Tuning 从实验到上线最容易翻车的五处
这套框架的原理不复杂,但工程实现里的坑非常密集,而且大多不会报错,只会让指标莫名其妙地低一截。下面五条是实际做下来最耗时间的。
5.1 模板与标签词层面的坑
坑一:模板里塞了不止一个[MASK]。现象是 loss 不收敛或者取mask_pos时拿到的位置和预期不符。原因在于 PVP 的整个训练逻辑建立在“单 mask 位置做分类”这个假设上,input_ids.index(mask_id)只会返回第一个下标,后面的 mask 部分被当成普通上下文,标签对齐直接错位。解决方式是构建序列时强制校验 mask 数量为 1,如果确实需要多位置预测,就应该换成分段打分的思路,而不是硬套单点分类。
坑二:标签词被分词器切成了多个 token,或者根本不在词表里。现象是convert_tokens_to_ids返回的 id 对应一个词缀,类别概率算出来看着正常但预测全歪。原因是分词器对低频词或变形词会做子词切分,一个词变成两片,取第一个 id 得到的概率只是“这个前缀出现的概率”。解决办法是在构建 verbalizer 之前,对每个候选词跑一遍分词并断言长度为 1,不满足就换词;这一步花十分钟,能省掉几天的排查。
坑三:各类标签词数量不一致,导致模型系统性偏向某一类。现象是预测结果几乎全落在标签词更多的那一类上。原因是概率求和时标签词多的类天然占据更大的概率质量,等价于给这个类加了一个先验。解决办法有三个:让每类标签词数量严格相等;或者求和后按该类词数取平均;或者在归一化前对每类做长度惩罚。选哪个取决于样本量,样本极少时等数量最稳。
5.2 训练、推理与评测链路上的坑
坑四:训练走 MLM 头,推理却接回了[CLS]分类头。现象是离线评测指标很好,上线后掉一大截,日志里看不出任何异常。原因通常是训练脚本和推理服务是两拨人写的,推理侧沿用了旧的分类模型骨架,[CLS]那个位置压根没被训练过。解决方式是把 verbalizer 聚合逻辑封装进唯一的predict函数,训练和推理共用同一个入口,再加一条数值对齐测试:同一条输入,两条链路输出的类别概率差必须小于阈值。
坑五:换了一个模板就直接和上次的最好结果比。现象是同一份代码,改个模板掉十个点,团队开始怀疑实现有 bug。真实原因是 PVP 本身就高方差,单次实验的最好值没有统计意义。一个可复现的做法是固定一组(比如五组)模板与标签词组合,每组跑三次,报均值和标准差,只有均值差异超过两倍标准差才认定改动有效。这条听起来像玄学,但在小样本场景下,它能挡掉大部分“我以为我在优化,其实我在抽奖”的无效迭代。
注意:连续模板还有一条专属的坑——学习率。可训练参数只有主干的千分之一量级,沿用全参微调的学习率会让伪 token 几乎不动,指标长期卡在随机线附近;反过来调得过大,伪 token 又会把注意力分布推飞。实践中连续模板的学习率通常要比全参微调大一到两个数量级,并且需要配 warmup,前几百步让真实 token 的梯度先稳定下来。
6. 黑盒模型下的取舍:ICL、CoT 与参数有效性训练各自的天花板
前面五章讨论的都是在权重可见的前提下改输入、改嵌入、改训练目标。但真实业务里经常遇到另一种局面:模型只能通过接口调用,拿不到参数,也改不了嵌入层——连续模板这条路直接被掐断。这时唯一能操作的只剩输入文本本身,也就是把示例样本拼进提示里,让模型在上下文中“照葫芦画瓢”地推断任务模式。这条路线不改任何参数,样本量受限于上下文窗口,效果高度依赖模型规模:小模型上示例往往带不来增益,甚至干扰判断;参数量上去之后,同样的示例才开始显著生效。再往前一步,是让模型在给出答案前先输出中间推理步骤,把“直接填答案”变成“先拆解再回答”,在多步推理类任务上收益明显,代价是延迟和 token 成本翻倍。
三种范式的取舍可以放在一张表里看:
| 维度 | 全参微调 | 软模板(连续 prompt) | 离散 prompt / 上下文学习 |
|---|---|---|---|
| 可训练参数 | 全量 | 千分之一量级 | 零 |
| 标注数据需求 | 大 | 小样本可用 | 零样本或极少样本 |
| 可解释性 | 中 | 差(伪 token 无法阅读) | 好(模板是自然语言) |
| 任务切换成本 | 每个任务一份权重 | 每个任务一组向量 | 改输入即可 |
| 效果上限 | 最高 | 接近全参微调 | 受模型规模与上下文长度约束 |
| 适用前提 | 权重可见、算力充足 | 权重可见 | 仅需接口 |
我一般会这么理解这张表:它不是三条互斥的技术路线,而是一条成本递增、可控性递减的滑梯。数据充足、权重可见,全参微调仍然是最省心的选择;数据只有几百条、又要快速试多个任务,软模板的性价比最高;模型根本碰不到,那就只能在输入上做文章,并且要接受效果上限由别人决定这个事实。真正容易翻车的是把它们当成万能药——用上下文学习去啃需要精确边界判断的序列标注任务,或者用软模板去追全参微调的效果上限,最后都会得出“这方法不行”的错误结论。
从那以后,我每次上手一个新任务,都先固定一组 PVP 跑三轮拿方差,确认这条基线稳了,再决定要不要往连续模板或者全参微调上走——跳过这一步直接冲效果,翻车概率高得离谱。这份综述把从预训练目标、PVP 定义到模板搜索、集成与自训练的整条链路都串了起来,经典方法的来龙去脉和形式化定义都在里面,对着自己手上的任务逐章验证,比零散地看几篇论文快得多。希望帮到你。
本文还有配套的精品资源,点击获取