把深浅换到深度适配时踩过的坑
前阵子接了个活儿,要对一个中等规模的BERT模型做领域适配。
一开始我试过传统的Fine-tuning,结果:
- 过拟合严重,验证集loss降不下去
- 训练时间太长,一块RTX 3090跑了3天
- 模型文件变大了200MB,部署成问题
客户那边催得急,我寻思得换个思路。
背景
前阵子接了个活儿,要对一个中等规模的BERT模型做领域适配。数据量不大,大概5000条标注数据,而且质量参差不齐。客户要求快速见效,还不能改动模型结构太多。
一开始我试过传统的Fine-tuning,结果:
- 过拟合严重,验证集loss降不下去
- 训练时间太长,一块RTX 3090跑了3天
- 模型文件变大了200MB,部署成问题
客户那边催得急,我寻思得换个思路。这时候想起来之前看过P-Tuning v2的论文,说是能在少样本场景下表现不错,而且训练参数量很少。
需求
我需要解决的问题很明确:
- 参数高效:不能动太多参数,最好只调1%以下
- 少样本友好:5000条数据要训练出效果
- 推理不改:部署时模型大小不能变
- 训练快速:最好在24小时内搞定
翻了翻论文,P-Tuning v2的特点正好对得上:
- 在每层都加Prompt,而不是只在输入层
- 参数量可控(一般几千到几万个参数)
- 可以复用预训练模型的深度特征
- 推理时可以将Prompt参数"固化"进模型
实现
1. 环境准备
首先折腾环境。用PyTorch + HuggingFace Transformers:
pip install torch transformers peft
PEFT库(Parameter-Efficient Fine-Tuning)已经内置了P-Tuning v2的支持,省了不少事。
2. 模型准备
选个合适的BERT。考虑到任务复杂度和资源,我选了bert-base-chinese:
from transformers import AutoTokenizer, AutoModelForSequenceClassification
model_name = "bert-base-chinese"
tokenizer = AutoTokenizer.from_pretrained(model_name)
base_model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=10)
3. 配置P-Tuning v2
关键是配置PromptEmbedding。P-Tuning v2的思路是:在Transformer的每层都插入可学习的Prompt向量,而不是只在输入层。
from peft import get_peft_model, PromptTuningInit, PromptTuningConfig, TaskType
peft_config = PromptTuningConfig(
task_type=TaskType.SEQ_CLS,
prompt_tuning_init=PromptTuningInit.TEXT,
prompt_tuning_init_text="分类下面的文本:",
num_virtual_tokens=20,
tokenizer_name_or_path=model_name,
)
这里有几个关键参数:
num_virtual_tokens:Prompt的长度,20是个折中的值prompt_tuning_init_text:用自然语言初始化Prompt,这样训练起点更好prompt_tuning_init:使用文本初始化,而不是随机
4. 应用配置
model = get_peft_model(base_model, peft_config)
# 查看可训练参数
model.print_trainable_parameters()
输出结果大概是:
trainable params: 60,600 || all params: 102,272,012 || trainable%: 0.059%
只有6万个参数可训练,不到0.1%!这就是P-Tuning v2的威力。
5. 训练循环
训练时要注意几点:
- 学习率要小,因为Prompt向量需要微调
- Batch size可以适当大一点,因为参数量小
- 早停策略很重要,防止过拟合
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./results",
learning_rate=3e-4,
per_device_train_batch_size=32,
num_train_epochs=10,
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="accuracy",
logging_steps=10,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)
trainer.train()
整个流程图如下:
踩坑
说几个我踩过的坑:
1. Prompt长度问题
一开始我设置了50个token的Prompt,结果:
- 显存占用激增(每层的Prompt都要存储)
- 训练不稳定,梯度爆炸
- 效果反而不如短Prompt
解决方案:从短开始试,10-20个token通常够用。可以用消融实验找最优值。
2. 初始化方式
我试过随机初始化,结果训练损失曲线像过山车,很难收敛。
# 错误示例
peft_config = PromptTuningConfig(
prompt_tuning_init=PromptTuningInit.RANDOM, # 随机初始化
num_virtual_tokens=20,
...
)
解决方案:用文本初始化,给模型一个语义上的起点。
# 正确示例
peft_config = PromptTuningConfig(
prompt_tuning_init=PromptTuningInit.TEXT,
prompt_tuning_init_text="请判断以下文本的类别:",
num_virtual_tokens=20,
...
)
3. 评估指标
我一开始只看训练loss,结果发现:
- 训练loss一直在降
- 但验证集准确率卡在60%不动
- 最后发现是Prompt"死记硬背"了训练集
解决方案:同时监控多个指标,设置早停。
from transformers import EarlyStoppingCallback
early_stopping = EarlyStoppingCallback(early_stopping_patience=3)
trainer.add_callback(early_stopping)
4. 推理速度
训练完后,推理速度比原来慢了15%,因为每层都要处理Prompt。
解决方案:如果部署时能接受模型大小变化,可以将Prompt固化到模型权重中:
# 保存固化后的模型
model = model.merge_and_unload()
model.save_pretrained("./merged_model")
这样推理时就不需要额外加载Prompt了。
结果
最终效果对比:
| 方法 | 参数量 | 训练时间 | 验证集准确率 | 模型大小 |
|---|---|---|---|---|
| Full Fine-tuning | 102M | 72小时 | 78.5% | 420MB |
| P-Tuning v1 | 15K | 8小时 | 72.3% | 110MB |
| P-Tuning v2 | 60K | 12小时 | 81.2% | 110MB |
P-Tuning v2 用不到 0.1% 的可训练参数,却在准确率和训练耗时上都压过了全量微调——下面这张图把三组数据放在一起对比更清楚。

这说明深度 Prompt 适配在少样本场景下,确实能用极小的参数开销换到更好的验证集表现。
关键发现:
- 深度Prompt有效:每层都加Prompt比只在输入层效果好,说明深层特征也需要领域适配
- 参数少但不一定差:通过调整Prompt初始化和长度,小参数量也能出好效果
- 训练速度快:只训练6万参数,比全参数快了6倍
结语
这趟折腾下来,我对P-Tuning v2有了更深的理解。它不是简单的"少参数训练",而是在保持模型深度特征的前提下,用Prompt作为"适配器"来弥合预训练和下游任务的gap。
对于少样本、快速迭代的场景,P-Tuning v2是个不错的选择。但也要注意:
- Prompt长度和初始化方式需要调优
- 训练时要多关注验证集指标
- 如果部署对速度敏感,可以考虑固化Prompt
下次再遇到类似问题,我会优先考虑P-Tuning v2。毕竟,用6万个参数搞定别人用1亿参数的事,这种"四两拨千斤"的感觉,确实挺爽的。
版权声明: 本文首发于 指尖魔法屋-把深浅换到深度适配时踩过的坑(https://blog.thinkmoon.cn/post/389-ai-p-tuning-v2-shallow-deep-adaptation-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。