【大模型开发】开源大模型微调: P-Tuning(Prompt-Tuning)技术

部署运行你感兴趣的模型镜像

以下内容将从原理到代码,再到进一步的优化方向,详尽地阐述基于开源大模型的 P-Tuning(Prompt-Tuning) 技术。为了便于理解,整篇内容会分为以下几个主要部分:

  1. P-Tuning 微调技术原理简介
  2. P-Tuning 与其他微调方法对比
  3. 基于 HuggingFace Transformers 的落地案例代码
  4. 代码详解
  5. 进一步建议和优化方向

1. P-Tuning 微调技术原理简介

1.1 什么是 P-Tuning

P-Tuning 全称为 Prompt-Tuning,它是一种微调大语言模型(Large Language Model, LLM)的方法。与传统的全参数微调(Fine-tuning)不同,P-Tuning 只在模型输入层或中间层插入可学习的“Prompt Embeddings”(也称 Prompt Tokens/Prefix 等),从而极大减少微调参数量。其核心思想可以归纳为:

  • 冻结(freeze)大部分或全部原始模型参数
  • 引入少量可训练的参数(Prompt Embeddings)
  • 通过梯度反向传播仅更新这部分可训练参数

模型在训练过程中会将这些 Prompt Embeddings 拼接到原输入或模型内部隐藏层的输入里,从而让预训练模型更好地针对任务进行表征/生成。因为只训练这部分 Prompt Embeddings,而模型的主体参数并未改变,所以对硬件资源和训练数据需求更小,微调速度也更快。

1.2 背后的动机

预训练大模型(如 BERT、GPT-2/3、T5、Bloom 等)拥有数亿、数百亿、甚至上千亿的参数,如果要对所有参数做微调,不仅需要大量 GPU/TPU 资源,而且可能过度拟合、小数据集难以支撑等问题也会出现。Prompt-Tuning 通过在输入中注入一段可学习的“提示向量”,引导模型对下游任务进行生成或分类,可以极大地减少需要训练的参数量。

1.3 P-Tuning 的思路

以生成式任务为例(如 GPT 类模型),如果我们想让模型执行一个特定任务,那么可以在输入序列前面插入若干个可学习的特殊向量(Prompt Embeddings)。这就好比在原始自然语言输入前面加上了一些“提示词”,只不过这些提示不是固定的离散词,而是可训练的连续向量。
对于分类或序列标注等判别式任务也可以采用类似方法:Prompt Embeddings 可以插在句子前面、后面,或同时插入头尾,然后在微调阶段只更新这部分提示向量的参数,冻结模型其余部分。


2. P-Tuning 与其他微调方法对比

  1. 全参数微调(Fine-tuning)

    • 训练参数:模型全部参数
    • 优点:模型针对任务的拟合程度更好
    • 缺点:需要超大算力与大量数据,且训练开销大
  2. Prompt Engineering(人工提示工程,离散提示)

    • 训练参数:无可学习参数或者极少数可学习参数
    • 依赖人为经验,提示写得好坏会影响效果
  3. Adapter / LoRA

    • 训练参数:仅在模型内部插入 Adapter 层或者低秩矩阵分解部分
    • 和 P-Tuning 类似,都是减少训练参数,但插入点一般在 Transformer Block 内部
  4. P-Tuning

    • 训练参数:仅有一小部分可学习的 Prompt Embeddings
    • 插入点可位于输入词嵌入层或者模型中间层
    • 对推理速度几乎无影响,参数增加量非常小

3. 基于 HuggingFace Transformers 的落地案例代码

下面给出一个相对简化的示例,演示对 GPT-2 进行 P-Tuning。演示思路如下:

  1. 使用预训练的 GPT-2 模型。
  2. 在原始输入序列前面插入一段长度为 prompt_length 的可学习向量(Prompt Embeddings)。
  3. 仅训练这段 Prompt Embeddings 的参数,对 GPT-2 主体参数进行冻结。
  4. 最后在一个小示例上进行微调,验证其可行性。

注意:下面的示例代码基于 transformerspytorch 实现,由于篇幅和可操作环境限制,代码中仅演示关键逻辑与思路,并不一定是最优或在生产环境可直接使用的完整版本。
如果你想要跑通此示例,需安装 transformers>=4.0.0torch>=1.7 等,且需要准备相应数据集。

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from transformers import GPT2LMHeadModel, GPT2Tokenizer

# ====================
# 1. 自定义数据集
# ====================
class SimpleDataset(Dataset):
    def __init__(self, texts, tokenizer, max_length=64):
        self.texts = texts
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self, idx):
        text = self.texts[idx]
        encoding = self.tokenizer(
            text,
            truncation=True,
            padding='max_length',
            max_length=self.max_length,
            return_tensors='pt'
        )
        input_ids = encoding['input_ids'].squeeze(0)
        attention_mask = encoding['attention_mask'].squeeze(0)
        return {
            'input_ids': input_ids,
            'attention_mask': attention_mask
        }

# ====================
# 2. P-Tuning 模块定义
# ====================
class PromptEmbedding(nn.Module):
    """
    定义可学习的 Prompt Embeddings,用于在输入序列前面插入。
    """
    def __init__(self, prompt_length, embedding_dim):
        super().__init__()
        # 这里可以直接用一个简单的可学习参数矩阵
        self.prompt_embeddings = nn.Parameter(
            torch.randn(prompt_length, embedding_dim)
        )

    def forward(self, batch_size):
        """
        Returns:
            prompt_embeddings of shape [batch_size, prompt_length, embedding_dim]
        """
        # 将 [prompt_length, embedding_dim] 扩展到 [batch_size, prompt_length, embedding_dim]
        return self.prompt_embeddings.unsqueeze(0).expand(batch_size, -1, -1)

# ====================
# 3. 整合到 GPT-2 的 forward
# ====================
class GPT2WithPromptTuning(nn.Module):
    def __init__(self, base_model_name='gpt2', prompt_length=5):
        super().__init__()
        self.base_model = GPT2LMHeadModel.from_pretrained(base_model_name)
        self.tokenizer = GPT2Tokenizer.from_pretrained(base_model_name)
        self.tokenizer.pad_token = self.tokenizer.eos_token  # 处理GPT-2没有pad_token的情况

        # 冻结 GPT-2 的参数
        for param in self.base_model.parameters():
            param.requires_grad = False

        # 获取 GPT-2 的 token embedding size
        embedding_dim = self.base_model.transformer.wte.weight.size(1)
        # 定义可学习的 prompt embedding
        self.prompt_length = prompt_length
        self.prompt_encoder = PromptEmbedding(prompt_length, embedding_dim)
    
    def forward(self, input_ids, attention_mask=None, labels=None):
        batch_size = input_ids.size(0)

        # 1. 获取可学习的 prompt embeddings
        prompt_embeds = self.prompt_encoder(batch_size) # [batch_size, prompt_length, embedding_dim]

        # 2. 原始输入的 token embeddings
        #    GPT2LMHeadModel 里的 transformer.wte 是 token embedding 模块
        input_embeds = self.base_model.transformer.wte(input_ids)  # [batch_size, seq_len, embedding_dim]

        # 3. 拼接:先放 prompt 的 embeddings, 再放原来输入的 embeddings
        #    最后输入到 GPT2 中
        concat_embeds = torch.cat([prompt_embeds, input_embeds], dim=1)

        # 4. 同样处理 attention_mask
        if attention_mask is not None:
            # prompt 部分不需要“遮挡” (1 表示有效)
            prompt_mask = torch.ones(batch_size, self.prompt_length, dtype=attention_mask.dtype, device=attention_mask.device)
            attention_mask = torch.cat([prompt_mask, attention_mask], dim=1)

        # 5. GPT-2 forward
        outputs = self.base_model(
            inputs_embeds=concat_embeds,
            attention_mask=attention_mask,
            labels=labels
        )
        return outputs

# ====================
# 4. 训练示例
# ====================
def train_one_epoch(model, dataloader, optimizer, device):
    model.train()
    total_loss = 0.0
    for batch in dataloader:
        optimizer.zero_grad()

        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)

        # GPT-2 的常规做法是 labels = input_ids (语言模型任务)
        outputs = model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        labels=input_ids)

        loss = outputs.loss
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
    return total_loss / len(dataloader)

def main():
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    # 1. 初始化模型
    prompt_length = 5
    model = GPT2WithPromptTuning('gpt2', prompt_length=prompt_length).to(device)

    # 2. 示例数据
    texts = [
        "今天天气非常好,适合出去散步。",
        "人工智能正在深刻改变我们的生活。",
        "P-Tuning 是一种高效的微调大模型的方法。"
    ]
    dataset = SimpleDataset(
        texts, tokenizer=model.tokenizer, max_length=32
    )
    dataloader = DataLoader(dataset, batch_size=2, shuffle=True)

    # 3. 只训练 prompt_encoder 的参数
    optimizer = torch.optim.AdamW(model.prompt_encoder.parameters(), lr=1e-3)

    # 4. 训练
    for epoch in range(5):
        loss = train_one_epoch(model, dataloader, optimizer, device)
        print(f"Epoch: {epoch} | Loss: {loss:.4f}")

    # 5. 测试生成
    model.eval()
    test_text = "P-Tuning 技术可以"
    input_ids = model.tokenizer(test_text, return_tensors='pt')['input_ids'].to(device)
    # 构造输入时,也会先拼接 prompt embeddings
    with torch.no_grad():
        generated = model.base_model.generate(
            input_ids=input_ids,
            max_length=50,
            num_beams=5,
            early_stopping=True
        )
    print("Input:", test_text)
    print("Generated:", model.tokenizer.decode(generated[0], skip_special_tokens=True))

if __name__ == "__main__":
    main()

上面示例的关键点在于:

  • PromptEmbedding 类中定义了 self.prompt_embeddings,即可学习参数。
  • 冻结了 GPT-2 主体的所有参数,只训练 PromptEmbedding 对象。
  • forward 中,通过在 inputs_embeds 里把 prompt embeddings 和原始 token embedding 拼在一起,实现 P-Tuning。

4. 代码详解

下面逐行解释代码中的关键部分:

  1. PromptEmbedding 模块

    self.prompt_embeddings = nn.Parameter(
        torch.randn(prompt_length, embedding_dim)
    )
    
    • 这里我们用正态分布随机初始化一段形状为 [prompt_length, embedding_dim] 的可学习参数。
    • 训练过程中会通过反向传播更新这部分参数。
  2. 在 GPT-2 上冻结参数

    for param in self.base_model.parameters():
        param.requires_grad = False
    
    • 这样所有 GPT-2 内部参数都不会在训练中被更新。
  3. 获取 GPT-2 的 embedding_dim

    embedding_dim = self.base_model.transformer.wte.weight.size(1)
    
    • GPT-2 的词向量大小一般为 768 或者更大,通过 size(1) 获取。
  4. 模型的 forward 流程

    prompt_embeds = self.prompt_encoder(batch_size)
    input_embeds = self.base_model.transformer.wte(input_ids)
    concat_embeds = torch.cat([prompt_embeds, input_embeds], dim=1)
    
    • prompt_embeds 形状为 [batch_size, prompt_length, embedding_dim]
    • input_embeds 形状为 [batch_size, seq_len, embedding_dim]
    • 拼接后是 [batch_size, (prompt_length + seq_len), embedding_dim]
  5. 优化器只包含 model.prompt_encoder 的参数

    optimizer = torch.optim.AdamW(model.prompt_encoder.parameters(), lr=1e-3)
    
    • 这样确保只有 P-Tuning 部分被训练,GPT-2 主体参数被固定。
  6. 训练目标

    loss = outputs.loss
    
    • 这里用 GPT-2 自带的语言模型损失(CrossEntropyLoss),label 是原句本身,这种形式也可轻松扩展到其他下游任务(分类、摘要、问答等)。

5. 进一步建议和优化方向

  1. Prompt Embeddings 的插入位置

    • 例子里只在输入词嵌入层前面插入。如果想要更强的表达能力,可以在 Transformer 的中间层、甚至多层插入 Prompt Embeddings,这被称为 Deep P-Tuning
    • 这需要对模型的每一层输入做一些修改,让 prompt tokens 能够在中间层也被拼接进去。
  2. Prompt Embeddings 的初始化

    • 简单随机初始化有时不足以让 Prompt Embeddings 迅速收敛。
    • 可以尝试从相似任务中学来的先验知识来初始化,或者对 Prompt Embeddings 做一些正则约束。
  3. Prompt Embeddings 的长度

    • prompt_length 的大小非常影响性能:太短,模型难以表达足够信息;太长,会增加训练难度且影响推理效率。通常需要在验证集上进行调参。
  4. 损失函数和训练策略

    • 如果是文本生成场景,以语言模型自回归损失为主。
    • 如果是分类场景,可能需要让模型在拼接了 Prompt Embeddings 后去做分类,对模型输出的 [CLS] 位或者平均池化后加线性层。
    • 学习率batch size 等也要适度调参,以免 prompt embedding 训练过快或过慢。
  5. 与 LoRA / Adapter 等方法的组合

    • 不同参数高效微调方法可以组合使用,例如在中间层插入 Adapter 模块的同时,再在输入端做 P-Tuning,对多种下游任务可能有更强的性能和泛化能力。
  6. 支持多语言或长文本

    • 如果场景是多语言,可以针对每种语言设计特定的 Prompt Embeddings(或共享一部分),增加模型跨语言的效果。
    • 如果是非常长的文本(例如一些大模型支持 4k、8k Token 上下文),要关注拼接后序列长度超标的问题。
  7. 推理部署注意事项

    • 推理时 Prompt Embeddings 和文本 tokens 一起输入模型,不会增加任何显式的计算开销,只是序列变长了 prompt_length 个 Token。
    • 如果需要在推理时切换不同任务,可以将 Prompt Embeddings 存储起来,然后在推理输入时,选择要用的 Prompt Embeddings 与文本拼接。
  8. 安全和合规

    • 在实际业务场景中,对大模型进行 Prompt-Tuning 需要关注合规性,以及下游任务的潜在滥用风险。
    • 需要构建相应的监控或对生成结果进行过滤或审计。

总结

P-Tuning(Prompt-Tuning)为大模型的微调提供了一种非常高效灵活的思路——只需在输入序列或中间层插入少量可学习向量并进行训练,即可让大模型针对特定任务实现较好的效果,而不需要动辄数十亿参数的全量微调。在上面给出的示例中,我们通过 HuggingFace Transformers 展示了一个简化版本的 P-Tuning,对于有更高要求的场景可以在此基础上做许多改进和扩展,如 Deep P-Tuning、多层插入 Prompt Embeddings、结合 LoRA / Adapter、调参等。

希望通过以上内容,能够帮助你理解并实现一个最小化可行版本的 P-Tuning,同时掌握在工业界或学术研究中进一步优化的思路。

哈佛博后带小白玩转机器学习哔哩哔哩_bilibili

总课时超400+,时长75+小时

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值