大模型参数高效微调技术实战(三)-P-Tuning

随着,ChatGPT 迅速爆火,引发了大模型的时代变革。然而对于普通大众来说,进行大模型的预训练或者全量微调遥不可及。由此,催生了各种参数高效微调技术,让科研人员或者普通开发者有机会尝试微调大模型。

因此,该技术值得我们进行深入分析其背后的机理,之前分享了大模型参数高效微调技术原理综述 的文章。下面给大家分享大模型参数高效微调技术实战 系列文章,该系列共六篇文章,相关代码均放置在GitHub:llm-action

本文为大模型参数高效微调技术实战的第三篇。

P-Tuning 简述

P-Tuning(论文:GPT Understands, Too),该方法将 Prompt 转换为可以学习的 Embedding 层,并用MLP+LSTM的方式来对Prompt Embedding进行一层处理。

相比Prefix Tuning,P-Tuning加入的可微的virtual token,但仅限于输入层,没有在每一层都加;另外,virtual token的位置也不一定是前缀,插入的位置是可选的。这里的出发点实际是把传统人工设计模版中的真实token替换成可微的virtual token。

经过预训练的LM的词嵌入已经变得高度离散,如果随机初始化virtual token,容易优化到局部最优值,而这些virtual token理论是应该有相关关联的。因此,作者通过实验发现用一个提示编码器(即用一个LSTM+MLP去编码这些virtual token以后,再输入到模型)来编码会收敛更快,效果更好。

更加详细的介绍可参考之前的文章:大模型参数高效微调技术原理综述(三)-P-Tuning、P-Tuning v2

P-Tuning 微调实战

为了不影响阅读体验,详细的代码放置在GitHub:llm-action 项目中 peft_p_tuning_clm.ipynb 文件,这里仅列出关键步骤。

第一步,引进必要的库,如:P-Tuning 配置类 PromptEncoderConfig

javascript 复制代码
from peft import (
    get_peft_config,
    get_peft_model,
    get_peft_model_state_dict,
    set_peft_model_state_dict,
    PeftType,
    TaskType,
    PromptEncoderConfig,
)

第二步,创建 P-Tuning 微调方法对应的配置。

ini 复制代码
peft_config = PromptEncoderConfig(task_type=TaskType.CAUSAL_LM, num_virtual_tokens=20, encoder_hidden_size=128)

P-tuning 使用提示编码器(PromptEncoder)来优化提示参数,因此,您需要使用如下几个参数初始化 PromptEncoderConfig:

  • task_type:训练的任务类型,如:序列分类(SEQ_CLS),因果语言建模(CAUSAL_LM)等。
  • num_virtual_tokens:虚拟token的数量,换句话说就是提示(prompt)。
  • encoder_hidden_size:编码器的隐藏大小,用于优化提示参数。
  • encoder_reparameterization_type:指定如何重新参数化提示编码器,可选项有:MLP 或 LSTM,默认值为 MLP。

当使用 LSTM 时, 提示编码器结构如下:

python 复制代码
(prompt_encoder): ModuleDict(
    (default): PromptEncoder(
      (embedding): Embedding(20, 1024)
      (lstm_head): LSTM(1024, 128, num_layers=2, batch_first=True, bidirectional=True)
      (mlp_head): Sequential(
        (0): Linear(in_features=256, out_features=256, bias=True)
        (1): ReLU()
        (2): Linear(in_features=256, out_features=1024, bias=True)
      )
    )
  )

当使用 MLP 时, 提示编码器结构如下:

scss 复制代码
(prompt_encoder): ModuleDict(
    (default): PromptEncoder(
      (embedding): Embedding(20, 1024)
      (mlp_head): Sequential(
        (0): Linear(in_features=1024, out_features=128, bias=True)
        (1): ReLU()
        (2): Linear(in_features=128, out_features=128, bias=True)
        (3): ReLU()
        (4): Linear(in_features=128, out_features=1024, bias=True)
      )
    )
  )

PEFT 中的 P-tuning 的提示编码器是基于英伟达的NeMo库中 prompt_encoder.py 进行的重构,源码如下所示。

python 复制代码
class PromptEncoder(torch.nn.Module):
    def __init__(self, config):
        super().__init__()
        self.token_dim = config.token_dim
        self.input_size = self.token_dim
        self.output_size = self.token_dim
        self.hidden_size = config.encoder_hidden_size
        self.total_virtual_tokens = config.num_virtual_tokens * config.num_transformer_submodules
        self.encoder_type = config.encoder_reparameterization_type

        # 初始化 embedding 层
        self.embedding = torch.nn.Embedding(self.total_virtual_tokens, self.token_dim)
        if not config.inference_mode:
            # 根据PromptEncoder重参数化类型初始化相应的lstm和mlp
            if self.encoder_type == PromptEncoderReparameterizationType.LSTM:
                lstm_dropout = config.encoder_dropout
                num_layers = config.encoder_num_layers
                # LSTM
                self.lstm_head = torch.nn.LSTM(
                    input_size=self.input_size,
                    hidden_size=self.hidden_size,
                    num_layers=num_layers,
                    dropout=lstm_dropout,
                    bidirectional=True,
                    batch_first=True,
                )

                self.mlp_head = torch.nn.Sequential(
                    torch.nn.Linear(self.hidden_size * 2, self.hidden_size * 2),
                    torch.nn.ReLU(),
                    torch.nn.Linear(self.hidden_size * 2, self.output_size),
                )

            elif self.encoder_type == PromptEncoderReparameterizationType.MLP:
                warnings.warn(
                    f"for {self.encoder_type}, the `encoder_num_layers` is ignored. Exactly 2 MLP layers are used."
                )
                layers = [
                    torch.nn.Linear(self.input_size, self.hidden_size),
                    torch.nn.ReLU(),
                    torch.nn.Linear(self.hidden_size, self.hidden_size),
                    torch.nn.ReLU(),
                    torch.nn.Linear(self.hidden_size, self.output_size),
                ]
                self.mlp_head = torch.nn.Sequential(*layers)

            else:
                raise ValueError("Prompt encoder type not recognized. Please use one of MLP (recommended) or LSTM.")

    def forward(self, indices):
        input_embeds = self.embedding(indices)
        if self.encoder_type == PromptEncoderReparameterizationType.LSTM:
            output_embeds = self.mlp_head(self.lstm_head(input_embeds)[0])
        elif self.encoder_type == PromptEncoderReparameterizationType.MLP:
            output_embeds = self.mlp_head(input_embeds)
        else:
            raise ValueError("Prompt encoder type not recognized. Please use one of MLP (recommended) or LSTM.")

        return output_embeds

第三步,通过调用 get_peft_model 方法包装基础的 Transformer 模型。

python 复制代码
model = AutoModelForCausalLM.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()

通过 print_trainable_parameters 方法可以查看可训练参数的数量(仅为300,288)以及占比(仅为0.05366%)。

csharp 复制代码
trainable params: 300,288 || all params: 559,514,880 || trainable%: 0.05366935013417338

第四步,模型训练的其余部分均无需更改,当模型训练完成之后,保存高效微调部分的模型权重以供模型推理即可。

python 复制代码
peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
model.save_pretrained(peft_model_id)

输出的模型权重文件如下所示:

css 复制代码
/data/nfs/llm/model/bloomz-560m_P_TUNING_CAUSAL_LM
├── [ 451]  adapter_config.json
├── [ 81K]  adapter_model.bin
└── [ 129]  README.md

0 directories, 3 files

注意:这里只会保存经过训练的增量 PEFT 权重。其中,adapter_config.json 为 P-Tuning 配置文件;adapter_model.bin 为 P-Tuning 权重文件。

第五步,加载微调后的权重文件进行推理。

python 复制代码
from peft import PeftModel, PeftConfig

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
config = PeftConfig.from_pretrained(peft_model_id)
# 加载基础模型
model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path)
# 加载PEFT模型
model = PeftModel.from_pretrained(model, peft_model_id)

# 编码
inputs = tokenizer(f'{text_column} : {dataset["test"][i]["Tweet text"]} Label : ', return_tensors="pt")

# 模型推理
outputs = model.generate(
        input_ids=inputs["input_ids"], 
        attention_mask=inputs["attention_mask"], 
        max_new_tokens=10, 
        eos_token_id=3
    )

# 解码
print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True))

至此,我们完成了 P-Tuning 的训练及推理。

结语

本文对 P-Tuning 的基本原理进行了简述;同时,讲解了使用 P-Tuning 微调技术进行模型训练及推理。下文将对 Prefix Tuning / P-Tuning v2 技术进行实战讲解。

如果觉得我的文章能够能够给您带来帮助,期待您的点赞收藏加关注~~

相关推荐
Darling噜啦啦2 小时前
LLM 记忆管理三剑客:截断、总结、检索,彻底搞懂 Agent 的 Memory 系统
langchain·llm·agent
今日无bug3 小时前
大模型的回答是怎么一个字一个字蹦出来的?前端流式输出解析
前端·llm·agent
桃西西呀7 小时前
《牛来》票房涨 1000 倍是真的吗?——2026暑期档 124 亿里的数学
人工智能·数据分析·llm
tachibana28 小时前
如何设计多 Agent 的协作与动态切换机制?
网络·人工智能·ai·大模型·llm·agent
程序员三明治9 小时前
【体验毛坯房】Deep Harness 入门教程
java·人工智能·后端·大模型·llm·deepseek·dsh
桃西西呀10 小时前
AI 为什么一本正经地胡说八道?3 个底层原因 + 2 个防坑法
人工智能·llm·ai编程
阿黎梨梨10 小时前
AI也有记忆?LangChain Memory 管理指南
langchain·node.js·llm
武子康10 小时前
删掉邮箱后,Agent Trace 仍可能泄露什么:一条可重放脱敏流水线
人工智能·llm·agent
MicrosoftReactor10 小时前
技术速递|如何在投入生产环境前评估 LLM
ai·llm·生产评估
武子康11 小时前
RoboLab 解读:机器人策略评测为什么不能只看二元成功率
人工智能·llm·agent