大模型微调原理与代码实战案例(二):P-Tuning

随着ChatGPT的快速崛起,大型模型的时代正在发生革命性变化。但对于很多人而言,进行大型模型的预训练或全面微调似乎是遥不可及的。

不过随着多种高效参数微调技术的涌现,科研人员和普通开发者都有机会尝试微调这些庞大的模型了。

本文我将分享了大模型微调技术的原理及代码案例 ,完整版代码,可在文末获取

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 微调实战

为了不影响阅读体验,完整版代码在公众号:机器学习社区,回复:tuning,即可获取,这里仅列出关键步骤。

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

from peft import (
    get_peft_config,
    get_peft_model,
    get_peft_model_state_dict,
    set_peft_model_state_dict,
    PeftType,
    TaskType,
    PromptEncoderConfig,
)

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

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 时, 提示编码器结构如下:

(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 时, 提示编码器结构如下:

(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 进行的重构,源码如下所示。

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)
        ifnot 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 模型。

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%)。

trainable params: 300,288 || all params: 559,514,880 || trainable%: 0.05366935013417338

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

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
model.save_pretrained(peft_model_id)

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

/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 权重文件。

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

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 技术进行实战讲解。

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

技术交流群

完整版代码在公众号:机器学习社区,回复:tuning,即可获取。

建了实战技术交流群!想要进交流群的同学,可以直接加微信号:mlc2060。加的时候备注一下:研究方向 +学校/公司+知乎,即可。然后就可以拉你进群了。

前沿技术资讯、算法交流、求职内推、算法竞赛、面试交流(校招、社招、实习)等、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企开发者互动交流~

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值