4. 实现前缀微调(Prefix Tuning)
任务说明
本案例为模型实现前缀微调(Prefix Tuning):冻结主干模型参数,仅训练插入到每一层注意力前的可学习前缀向量,使模型在特定提示风格下生成结果发生可控变化。完成后,同一提示词在微调前后的生成文本应呈现明显差异,且训练过程仅更新前缀参数。
环境依赖
pip install torch transformers datasets
完整实现
import torch
from torch import nn
from transformers import GPT2LMHeadModel, GPT2Tokenizer
class PrefixTuningGPT2(nn.Module):
def __init__(self, model_name="", prefix_length=20, num_layers=12, hidden_size=768):
super().__init__()
self.model = GPT2LMHeadModel.from_pretrained(model_name)
self.tokenizer = GPT2Tokenizer.from_pretrained(model_name)
self.tokenizer.pad_ = self.tokenizer.eos_
self.prefix_length = prefix_length
self.num_layers = num_layers
self.hidden_size = hidden_size
# 冻结主干模型
for param in self.model.parameters():
param.requires_grad = False
# 可学习前缀嵌入(每层独立前缀)
self.prefix_embeds = nn.Parameter(
torch.randn(num_layers, 2, prefix_length, hidden_size) * 0.02
)
def forward(self, input_ids, attention_mask=None, labels=None):
batch_size = input_ids.shape[0]
past_key_values = []
for layer_idx in range(self.num_layers):
# 每层前缀拆分为 key/value 状态
prefix_k = self.prefix_embeds[layer_idx, 0].unsqueeze(0).expand(batch_size, -1, -1)
prefix_v = self.prefix_embeds[layer_idx, 1].unsqueeze(0).expand(batch_size, -1, -1)
# 构造与 匹配的 past_key_values 格式
past_key_values.append((prefix_k, prefix_v))
# 将前缀作为 past_key_values 传入,实现前缀参与注意力计算
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
past_key_values=past_key_values,
labels=labels,
use_cache=False
)
return outputs
def generate(self, prompt, max_length=50):
self.eval()
inputs = self.tokenizer(prompt, return_tensors="pt")
input_ids = inputs["input_ids"].to(next(self.parameters()).device)
attention_mask = inputs["attention_mask"].to(next(self.parameters()).device)
with torch.no_grad():
# 生成时同样注入前缀
batch_size = input_ids.shape[0]
past_key_values = []
for layer_idx in range(self.num_layers):
prefix_k = self.prefix_embeds[layer_idx, 0].unsqueeze(0).expand(batch_size, -1, -1)
prefix_v = self.prefix_embeds[layer_idx, 1].unsqueeze(0).expand(batch_size, -1, -1)
past_key_values.append((prefix_k, prefix_v))
outputs = self.model.generate(
input_ids=input_ids,
attention_mask=attention_mask,
past_key_values=past_key_values,
max_length=max_length,
pad__id=self.tokenizer.eos__id,
do_sample=True,
temperature=0.8
)
return self.tokenizer.decode(outputs[0], skip_special_tokens=True)
def train_prefix(model, train_texts, epochs=3, lr=1e-3):
"""在给定文本上训练前缀向量"""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
optimizer = torch.optim.AdamW([model.prefix_embeds], lr=lr)
model.train()
for epoch in range(epochs):
total_loss = 0
for text in train_texts:
inputs = model.tokenizer(text, return_tensors="pt", truncation=True, max_length=64)
input_ids = inputs["input_ids"].to(device)
attention_mask = inputs["attention_mask"].to(device)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids)
loss = outputs.loss
total_loss += loss.item()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(train_texts):.4f}")
if __name__ == "__main__":
# 1. 初始化模型
model = PrefixTuningGPT2(model_name="", prefix_length=20)
# 2. 微调前生成(观察基线效果)
print("=== 微调前生成 ===")
print(model.generate("The weather today is"))
# 3. 构造训练数据:希望模型输出的风格
train_data = [
"The weather today is sunny and warm, perfect for a walk in the park.",
"The weather today is rainy and cold, remember to bring an umbrella.",
"The weather today is cloudy with a chance of snow in the evening.",
"The weather today is windy and dry, ideal for flying kites."
]
# 4. 训练前缀
print("\n=== 开始训练前缀 ===")
train_prefix(model, train_data, epochs=5, lr=1e-3)
# 5. 微调后生成(对比效果)
print("\n=== 微调后生成 ===")
print(model.generate("The weather today is"))
运行与验证
运行方式:直接执行 python prefix_tuning.py。首次运行会自动下载模型(约500MB),需保持网络畅通。
验证方法:
- 观察训练输出:Loss应随epoch逐步下降,说明前缀向量正在学习训练文本的分布特征。
- 对比微调前后的生成结果:微调前模型输出为通用续写;微调后输出应明显偏向训练数据中的天气描述风格(如包含sunny/rainy/cloudy等词汇)。
- 参数冻结检查:训练过程中仅
model.prefix_embeds的requires_grad为True,主干模型参数保持冻结。可通过print(model.model.lm_head.weight.requires_grad)验证输出为False。
通过条件:微调后生成文本中,天气相关词汇出现频率显著高于微调前,且训练Loss较初始值下降超过30%。
常见问题
- 显存不足:可减小
prefix_length至10或使用-small替代默认模型。 - 生成结果无变化:增大训练epoch至10或调高学习率至5e-3,确保前缀向量充分更新。
- past_key_values格式错误:的past_key_values要求每层包含(key, value)二元组,维度为(batch, num_heads, seq_len, head_dim),本实现已按768维hidden_size自动匹配。