P-Tuning v2:深层提示微调的进阶实践
P-Tuning v2:深层提示微调的进阶实践
1. 什么是 P-Tuning v2
P-Tuning v2 是由清华大学提出的一种 深层提示微调 方法,在各类自然语言理解(NLU)任务上达到了与全参数微调相当甚至更优的性能,同时仅需训练极少的参数(0.1%~3%)。它解决了第一代提示学习方法在小模型和困难任务上表现不佳的问题,成为大模型高效微调领域的重要基准。
本教程将带你从零理解 P-Tuning v2 的原理,并手把手完成一个可运行的实践案例。无论你是 NLP 初学者还是希望深入优化模型部署的工程师,都能从中获益。
2. 背景:从全参数微调到提示微调
2.1 全参数微调的困境
大语言模型(LLM)参数规模已达数十亿甚至数千亿,对整个模型进行微调需要巨大的显存和算力,且每个下游任务都需要保存一份完整副本,不利于实际部署。
2.2 Prompt Tuning 的思想
提示微调(Prompt Tuning)将任务描述转化为连续可学习的向量(Soft Prompt),冻结原有模型参数,只在输入层拼接少量可训练的虚拟 Token。训练参数量可降至原来的万分之一。
2.3 第一代方法的局限
P-Tuning、Prefix-Tuning 等早期方法虽然在简单分类、生成任务上表现尚可,但在 小模型(如 BERT-base)和 困难序列标注任务(如阅读理解、命名实体识别)上效果明显下降,难以作为通用微调方案。
3. P-Tuning 回顾(v1)
为了更好理解 v2,我们先快速浏览 P-Tuning v1 的核心做法。
- 可训练的连续提示:在输入文本前后插入若干可学习的 Embedding 向量,这些向量不来自词表,而是随机初始化。
- 重参数化处理:为了稳定训练,将提示 Embedding 通过一个小的 LSTM 或 MLP 网络映射后再送入模型,推理时只保留映射后的向量。
- 仅在输入层添加:提示只作用于模型的第一层,后续层看到的是通过自注意力传播的上下文表示。
P-Tuning v1 在许多任务上接近全微调,但在复杂推理任务中仍有差距。
4. P-Tuning v2 的动机与核心改进
4.1 为什么要做深层提示
研究表明,仅在输入层添加提示限制了可训练参数对模型内部表达的调节能力。对于序列标注、抽取式问答等需要深层语义理解的任务,浅层提示无法充分影响高层特征的提取。
P-Tuning v2 受 Prefix-Tuning 启发,在每一层 Transformer 的输入序列前都拼接上可训练的 Key-Value 向量,形成 深层连续提示。这些向量直接参与每一层的注意力计算,相当于为模型提供了一种深度的任务特定偏置。
4.2 与 Prefix-Tuning 的区别
- Prefix-Tuning 主要针对生成任务,Encoder 和 Decoder 都加前缀;P-Tuning v2 专注于 NLU,仅在 Encoder(或双向模型)的各层加前缀。
- v2 的提示向量数量更多(通常 128~256 个),并通过重参数化技巧加速收敛,同时引入多任务学习与共享提示等优化。
4.3 关键技术点
-
逐层提示(Deep Prompt)
在每一 Transformer 层的 Key 和 Value 矩阵前面拼接可训练向量,形状为 [batch, num_tokens, hidden_dim]。数量、初始化方式均可调节。 -
重参数化(Reparameterization)
训练时使用一个轻量 MLP 对提示向量进行映射再喂入各层,推理时可直接使用训练好的向量,去掉 MLP,保持高效推理。 -
多任务学习与共享提示
P-Tuning v2 支持用一个共享的提示模块联合训练多个任务,缓解过拟合并提升少样本表现。 -
分类头与任务适配
保留一个极小的线性分类头(或直接用 Mask Token 投影),不引入其他大参数层,保持高效。
5. P-Tuning v2 的数学直觉
假设原始模型在第 $l$ 层的自注意力计算为: $$ \text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V $$ 加入深层提示后,将可训练的 Key 提示 $P^l_K$ 和 Value 提示 $P^l_V$ 拼接到序列的 Key 和 Value 最前端,变成: $$ K' = [P^l_K ; K], \quad V' = [P^l_V ; V] $$ 注意力输出便包含了提示向量带来的信息偏置。通过反向传播,这些提示将学习到对当前任务最有利的上下文线索。
6. 参数效率对比
以 BERT-base(110M 参数)为例:
| 方法 | 可训练参数量 | 参数量占比 |
|---|---|---|
| 全参数微调 | 110M | 100% |
| Adapter | 0.9M ~ 2M | 0.8%~1.8% |
| P-Tuning v1 | ~0.1M | 0.1% |
| P-Tuning v2 | 0.5M ~ 3.3M | 0.5%~3% |
虽然 P-Tuning v2 参数量比 v1 略多,但仍在极低量级,且性能大幅提升,逼近甚至超越全微调。
7. 实践:使用 Transformers + PEFT 实现 P-Tuning v2
本节用 HuggingFace 的 PEFT 库快速实现一个情感分类任务。我们将冻结 RoBERTa-base,只在深层插入可训练的提示向量。
7.1 环境准备
pip install transformers peft datasets evaluate accelerate
7.2 加载模型与数据集
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import load_dataset
import torch
model_name = "roberta-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
dataset = load_dataset("glue", "sst2")
7.3 配置 P-Tuning v2
PEFT 将 P-Tuning v2 封装为 PromptEncoderConfig,关键参数如下:
num_virtual_tokens:每层添加的虚拟 Token 数量(推荐 128~256)。encoder_hidden_size:重参数化 MLP 的隐藏维度,通常取与提示 Token 数量相同的值或更小。encoder_reparameterization_type:建议使用"MLP"。layers_to_transform:指定插入提示的层索引,设为None表示所有层。
from peft import get_peft_model, PromptEncoderConfig, TaskType
peft_config = PromptEncoderConfig(
task_type=TaskType.SEQ_CLS, # 序列分类任务
num_virtual_tokens=128, # 每层 128 个虚拟 token
encoder_hidden_size=128, # MLP 隐藏层大小
encoder_reparameterization_type="MLP",# 重参数化方式
token_dim=768, # 与模型 hidden_size 一致
num_transformer_submodules=1, # (Q、K、V 整体)只用一次拼接
num_attention_heads=12, # 注意力头数
attention_dropout=0.0,
layers_to_transform=None, # 所有层
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 输出示例:trainable params: 1,212,480 || all params: 124,647,168 || trainable%: 0.9733
7.4 数据预处理
def preprocess(examples):
return tokenizer(examples['sentence'], truncation=True, padding='max_length', max_length=128)
encoded_dataset = dataset.map(preprocess, batched=True)
encoded_dataset = encoded_dataset.rename_column("label", "labels")
encoded_dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
7.5 训练
training_args = TrainingArguments(
output_dir="./ptuning_v2_sst2",
per_device_train_batch_size=32,
per_device_eval_batch_size=32,
num_train_epochs=10,
evaluation_strategy="epoch",
save_strategy="epoch",
logging_steps=50,
learning_rate=5e-5,
weight_decay=0.01,
load_best_model_at_end=True,
metric_for_best_model="accuracy",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=encoded_dataset["train"],
eval_dataset=encoded_dataset["validation"],
compute_metrics=lambda p: {"accuracy": (p.predictions.argmax(-1) == p.label_ids).mean()},
)
trainer.train()
7.6 推理与保存
# 保存提示权重
model.save_pretrained("ptuning_v2_sst2_checkpoint")
# 加载并使用
from peft import PeftModel
peft_model = PeftModel.from_pretrained(model, "ptuning_v2_sst2_checkpoint")
inputs = tokenizer("This movie is fantastic!", return_tensors="pt")
with torch.no_grad():
outputs = peft_model(**inputs)
print(torch.softmax(outputs.logits, dim=-1))
只需保存约 1MB 的提示向量文件,即可在不同任务间快速切换。
8. 应用场景与任务适配
P-Tuning v2 已经在以下任务上验证了其有效性:
- 文本分类:情感分析、主题分类、意图识别
- 自然语言推理:MNLI、QNLI
- 序列标注:命名实体识别(NER)、词性标注
- 抽取式问答:SQuAD 等阅读理解
- 多任务联合学习:用一个共享提示骨干同时解决多个任务
对于长文本任务,建议适当增大 max_length 并微调虚拟 Token 数量。
9. 调优技巧与常见问题
- 虚拟 Token 数量:简单任务用 64,复杂任务用 256~512,显存允许下越大效果越好。
- 学习率:提示向量通常用比全微调稍高的学习率(如 5e-5 ~ 1e-4)。
- MLP 重参数化:训练初期有助于稳定性,推理时移除几乎不影响精度,加快推理。
- 层选择:默认所有层,若显存紧张可仅在高层(如最后 6 层)添加提示。
- 与全微调差距:在极大数据集下可能仍略低于全微调,但针对大多数实际场景足够。
10. 总结
P-Tuning v2 通过 逐层插入可训练的连续提示 和简洁的 重参数化技巧,将参数高效微调方法推向了实用化的新高度。它既保持了极低的存储和训练开销,又在各类 NLU 任务中取得了极具竞争力的效果。
现在你可以尝试在自己的数据集上复现上述代码,体验用不到 1% 的参数完成一个大模型的定制化微调。随着 PEFT 生态的逐渐丰富,P-Tuning v2 已成为大模型迅速落地的重要工具,值得每一位 NLP 开发者掌握。