Langchain-Chatchat项目:4.1-P-Tuning v2实现过程
常见参数高效微调方法(Parameter-Efficient Fine-Tuning,PEFT)有哪些呢?主要是Prompt系列和LoRA系列。本文主要介绍P-Tuning v2微调方法。如下所示:
Prompt系列比如,Prefix Tuning(2021.01-Stanford)、Prompt Tuning(2021.09-Google)、P-Tuning(2021.03-Tsinghua)、P-Tuning v2(2022.03-Tsinghua); LoRA系列比如,LoRA(2021.11-Microsoft)、AdaLoRA(2023.03-Microsoft)、QLoRA(2023.05-Washington)。 还有不知道如何分类的比如,BitFit、Adapter Tuning及其变体、MAM Adapter、UniPELT等。

一.P-Tuning v2工作原理
1.Hard/Soft Prompt-Tuning如何设计
提示工程发展经过了从人工或半自动离散空间的hard prompt设计,到采用连续可微空间soft prompt设计的过程,这样的好处是可通过端到端优化学习不同任务对应的prompt参数。
2.P-Tuning工作原理和不足
主要是将continuous prompt应用于预训练模型的输入层,预训练模型后面的每一层都没有合并continuous prompt。

3.P-Tuning v2如何解决P-Tuning不足
P-Tuning v2把continuous prompt应用于预训练模型的每一层,而不仅仅是输入层。

二.P-Tuning v2实现过程
1.整体项目结构
源码参考文献[4],源码结构如下所示:

参数解释如下所示:
(1)--model_name_or_path L:/20230713_HuggingFaceModel/20231004_BERT/bert-base-chinese:BERT模型路径
(2)--task_name qa:任务名字
(3)--dataset_name squad:数据集名字
(4)--do_train:训练过程
(5)--do_eval:验证过程
(6)--max_seq_length 128:最大序列长度
(7)--per_device_train_batch_size 2:每个设备训练批次大小
(8)--learning_rate 5e-3:学习率
(9)--num_train_epochs 10:训练epoch数量
(10)--pre_seq_len 128:前缀序列长度
(11)--output_dir checkpoints/SQuAD-bert:检查点输出目录
(12)--overwrite_output_dir:覆盖输出目录
(13)--hidden_dropout_prob 0.1:隐藏dropout概率
(14)--seed 11:种子
(15)--save_strategy no:保存策略
(16)--evaluation_strategy epoch:评估策略
(17)--prefix:P-Tuning v2方法
执行代码如下所示:
python3 run.py --model_name_or_path L:/20230713_HuggingFaceModel/20231004_BERT/bert-base-chinese --task_name qa --dataset_name squad --do_train --do_eval --max_seq_length 128 --per_device_train_batch_size 2 --learning_rate 5e-3 --num_train_epochs 10 --pre_seq_len 128 --output_dir checkpoints/SQuAD-bert --overwrite_output_dir --hidden_dropout_prob 0.1 --seed 11 --save_strategy no --evaluation_strategy epoch --prefix
2.代码执行流程
(1)P-tuning-v2/run.py
根据task_name=="qa"选择tasks.qa.get_trainer 根据get_trainer得到trainer,然后训练、评估和预测
(2)P-tuning-v2/tasks/qa/get_trainer.py
得到config、tokenizer、model、squad数据集、QuestionAnsweringTrainer对象trainer 重点关注model是如何得到的
# fix_bert表示不更新bert参数,model数据类型为BertPrefixForQuestionAnswering
model = get_model(model_args, TaskType.QUESTION_ANSWERING, config, fix_bert=True)
重点关注QuestionAnsweringTrainer具体实现
trainer = QuestionAnsweringTrainer( # 读取trainer
model=model, # 模型
args=training_args, # 训练参数
train_dataset=dataset.train_dataset if training_args.do_train else None, # 训练集
eval_dataset=dataset.eval_dataset if training_args.do_eval else None, # 验证集
eval_examples=dataset.eval_examples if training_args.do_eval else None, # 验证集
tokenizer=tokenizer, # tokenizer
data_collator=dataset.data_collator, # 用于将数据转换为batch
post_process_function=dataset.post_processing_function, # 用于将预测结果转换为最终结果
compute_metrics=dataset.compute_metrics, # 用于计算评价指标
)
(3)P-tuning-v2/model/utils.py
选择P-tuning-v2微调方法,返回BertPrefixForQuestionAnswering模型,如下所示:
def get_model(model_args, task_type: TaskType, config: AutoConfig, fix_bert: bool = False):
if model_args.prefix: # 训练方式1:P-Tuning V2(prefix=True)
config.hidden_dropout_prob = model_args.hidden_dropout_prob # 0.1
config.pre_seq_len = model_args.pre_seq_len # 128
config.prefix_projection = model_args.prefix_projection # False
config.prefix_hidden_size = model_args.prefix_hidden_size # 512
# task_type是TaskType.QUESTION_ANSWERING,config.model_type是bert,model_class是BertPrefixForQuestionAnswering
model_class = PREFIX_MODELS[config.model_type][task_type]
# model_args.model_name_or_path是bert-base-chinese,config是BertConfig,revision是main
model = model_class.from_pretrained(model_args.model_name_or_path, config=config, revision=model_args.model_revision,)

(4)P-tuning-v2/model/question_answering.py(重点)
主要是BertPrefixForQuestionAnswering(BertPreTrainedModel)模型结构,包括构造函数、前向传播和获取前缀信息。
(5)P-tuning-v2/model/prefix_encoder.py(重点)
在BertPrefixForQuestionAnswering(BertPreTrainedModel)构造函数中涉及到前缀编码器PrefixEncoder(config)。
(6)P-tuning-v2/training/trainer_qa.py
继承关系为QuestionAnsweringTrainer(ExponentialTrainer)->ExponentialTrainer(BaseTrainer)->BaseTrainer(Trainer)->Trainer,最核心训练方法如下所示:

3.P-tuning-v2/model/prefix_encoder.py实现
该类作用主要是根据前缀prefix信息对其进行编码,假如不考虑batch-size,那么编码后的shape为(prefix-length, 2*layers*hidden)。假如prefix-length=128,layers=12,hidden=768,那么编码后的shape为(128,2*12*768)。
class PrefixEncoder(torch.nn.Module):
def __init__(self, config):
super().__init__()
self.prefix_projection = config.prefix_projection # 是否使用MLP对prefix进行投影
if self.prefix_projection: # 使用两层MLP对prefix进行投影
self.embedding = torch.nn.Embedding(config.pre_seq_len, config.hidden_size)
self.trans = torch.nn.Sequential(
torch.nn.Linear(config.hidden_size, config.prefix_hidden_size),
torch.nn.Tanh(),
torch.nn.Linear(config.prefix_hidden_size, config.num_hidden_layers * 2 * config.hidden_size)
)
else: # 直接使用Embedding进行编码
self.embedding = torch.nn.Embedding(config.pre_seq_len, config.num_hidden_layers * 2 * config.hidden_size)
def forward(self, prefix: torch.Tensor):
if self.prefix_projection: # 使用MLP对prefix进行投影
prefix_tokens = self.embedding(prefix)
past_key_values = self.trans(prefix_tokens)
else: # 不使用MLP对prefix进行投影
past_key_values = self.embedding(prefix)
return past_key_values
这里面可能会有疑问,为啥还要乘以2呢?因为past_key_values前半部分要和key_layer拼接,后半部分要和value_layer拼接,如下所示:
key_layer = torch.cat([past_key_value[0], key_layer], dim=2)
value_layer = torch.cat([past_key_value[1], value_layer], dim=2)
说明:代码路径为transformers/models/bert/modeling_bert.py->class BertSelfAttention(nn.Module)的forward()函数中。
4.P-tuning-v2/model/question_answering.py
简单理解,BertPrefixForQuestionAnswering就是在BERT上添加了PrefixEncoder,get_prompt功能主要是生成past_key_values,即前缀信息的编码表示,用于与主要文本序列一起输入BERT模型,以帮助模型更好地理解问题和提供答案。因为选择的SQuAD属于抽取式QA数据集,即根据question从context中找到answer的开始和结束位置即可。
class BertPrefixForQuestionAnswering(BertPreTrainedModel):
def __init__(self, config):
self.bert = BertModel(config, add_pooling_layer=False) # bert模型
self.qa_outputs = torch.nn.Linear(config.hidden_size, config.num_labels) # 线性层
self.prefix_encoder = PrefixEncoder(config) # 前缀编码器
def get_prompt(self, batch_size): # 根据前缀token生成前缀的编码,即key和value值
past_key_values = self.prefix_encoder(prefix_tokens)
past_key_values = past_key_values.view(
bsz, # batch_size
seqlen, # pre_seq_len
self.n_layer * 2, # n_layer表示BERT模型的层数
self.n_head, # n_head表示注意力头的数量
self.n_embd # n_embd表示每个头的维度
)
return past_key_values
def forward(self, ..., return_dict=None):
past_key_values = self.get_prompt(batch_size=batch_size) # 获取前缀信息
attention_mask = torch.cat((prefix_attention_mask, attention_mask), dim=1)
outputs = self.bert(
......
past_key_values=past_key_values,
)
return QuestionAnsweringModelOutput( # 返回模型输出,包括loss,开始位置的logits,结束位置的logits,hidden states和attentions
loss=total_loss,
start_logits=start_logits,
end_logits=end_logits,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
重点是outputs = self.bert(past_key_values=past_key_values),将past_key_values传入BERT模型中,起作用的主要是transformers/models/bert/modeling_bert.py->class BertSelfAttention(nn.Module)的forward()函数中。接下来看下past_key_values数据结构,如下所示:

5.BertSelfAttention实现
BERT网络结构参考附件1,past_key_values主要和BertSelfAttention部分中的key和value进行拼接,如下所示:
(self): BertSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
具体past_key_values和key、value拼接实现参考代码,如下所示:

经过BertSelfAttention部分后,输出outputs的shape和原始输入的shape是一样的,即都不包含前缀信息。
附件1:BERT网络结构
打印出来BERT模型结构,如下所示:
BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(21128, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(token_type_embeddings): Embedding(2, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) #embeddings层做了LayerNorm
(dropout): Dropout(p=0.1, inplace=False) #embeddings层做了Dropout
)
(encoder): BertEncoder(
(layer): ModuleList(
(0-11): 12 x BertLayer( #BertLayer包括BertAttention、BertIntermediate和BertOutput
(attention): BertAttention( #BertAttention包括BertSelfAttention和BertSelfOutput
(self): BertSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(output): BertSelfOutput(
(dense): Linear(in_features=768, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): BertIntermediate(
(dense): Linear(in_features=768, out_features=3072, bias=True)
(intermediate_act_fn): GELUActivation()
)
(output): BertOutput(
(dense): Linear(in_features=3072, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
)
(pooler): BertPooler(
(dense): Linear(in_features=768, out_features=768, bias=True)
(activation): Tanh()
)
)
BERT模型相关类结构在文件D:\Python310\Lib\site-packages\transformers\models\bert\modeling_bert.py中,如下所示:

附件2:SQuAD数据集
SQuAD是斯坦福大学推出的机器阅读理解问答数据集,其中每个问题的答案来自于对应阅读段落的一段文本,即(问题,原文,答案)。一共有107,785问题,以及配套的536篇文章。除了SQuAD 1.1之外,还推出了难度更大的新版本SQuAD 2.0(《Know What You Don't Know: Unanswerable Questions for SQuAD》_ACL2018)。
(1)训练集数据

(2)验证集数据

(3)加载SQuAD数据集
"""
执行脚本:python3 dataset_test.py --model_name_or_path L:/20230713_HuggingFaceModel/20231004_BERT/bert-base-chinese --task_name qa --dataset_name squad --do_train --do_eval --max_seq_length 128 --per_device_train_batch_size 2 --learning_rate 5e-3 --num_train_epochs 10 --pre_seq_len 128 --output_dir checkpoints/SQuAD-bert --overwrite_output_dir --hidden_dropout_prob 0.1 --seed 11 --save_strategy no --evaluation_strategy epoch --prefix
"""
from transformers import AutoTokenizer, HfArgumentParser, TrainingArguments
from arguments import get_args, ModelArguments, DataTrainingArguments, QuestionAnwseringArguments
from tasks.qa.dataset import SQuAD
if __name__ == '__main__':
args = get_args() # 从命令行获取参数
model_args, data_args, training_args, qa_args = args # model_args是模型相关参数,data_args是数据相关的参数,training_args是训练相关的参数
tokenizer = AutoTokenizer.from_pretrained( # 读取tokenizer
model_args.model_name_or_path, # 模型名称
revision=model_args.model_revision, # 模型版本
use_fast=True, # 是否使用fast tokenizer
)
dataset = SQuAD(tokenizer, data_args, training_args, qa_args)
print(dataset)
打个断点看下dataset数据结构如下所示:

input_ids:经过tokenizer分词后的subword对应的下标列表 attention_mask:在self-attention过程中,这一块mask用于标记subword所处句子和padding的区别,将padding部分填充为0 token_type_ids:标记subword当前所处句子(第一句/第二句/ padding) position_ids:标记当前词所在句子的位置下标 head_mask:用于将某些层的某些注意力计算无效化 inputs_embeds:如果提供了,那就不需要input_ids,跨过embedding lookup过程直接作为Embedding进入Encoder计算 encoder_hidden_states:这一部分在BertModel配置为decoder时起作用,将执行cross-attention而不是self-attention encoder_attention_mask:同上,在cross-attention中用于标记encoder端输入的padding past_key_values:在P-Tuning V2中会用到,主要是把前缀编码和预训练模型每层的key、value进行拼接。 use_cache:将保存上一个参数并传回,加速decoding output_attentions:是否返回中间每层的attention输出 output_hidden_states:是否返回中间每层的输出 return_dict:是否按键值对的形式返回输出,默认为真。
觉得P-Tuning v2里面还有很多知识点没有讲解清楚,只能后续逐个讲解。仅仅一个P-Tuning v2仓库代码涉及的知识点非常之多,首要就是把Transformer和BERT标准网络结构非常熟悉,还有对各种任务及其数据集要熟悉,对BERT变体网络结构要熟悉,对于PyTorch和Transformer库的深度学习模型训练、验证和测试流程要熟悉,对于Prompt系列微调方法要熟悉。总之,对于各种魔改Transformer和BERT要了如指掌。
参考文献:
[1]P-Tuning论文地址:https://arxiv.org/pdf/2103.10385.pdf
[2]P-Tuning代码地址:https://github.com/THUDM/P-tuning
[3]P-Tuning v2论文地址:https://arxiv.org/pdf/2110.07602.pdf
[4]P-Tuning v2代码地址:https://github.com/THUDM/P-tuning-v2
[5]BertLayer及Self-Attention详解:https://zhuanlan.zhihu.com/p/552062991
[6]https://rajpurkar.github.io/SQuAD-explorer/
[7]https://huggingface.co/datasets/squad
Langchain-Chatchat项目:4.1-P-Tuning v2实现过程的更多相关文章
- AngularJS进阶(三十九)基于项目实战解析ng启动加载过程
基于项目实战解析ng启动加载过程 前言 在AngularJS项目开发过程中,自己将遇到的问题进行了整理.回过头来总结一下angular的启动过程. 下面以实际项目为例进行简要讲解. 1.载入ng库 2 ...
- 项目由Windows2003 迁移到Windows 2008 过程,报 JS错误
这两天在做服务器迁移,遇到了一些小的问题,现在做个粗略的记录 原服务器环境:Windows 2003 现服务器环境:Windows 2008 其中SSB项目在迁移部署后发现,报 JS的错误. 我在想除 ...
- 个人从源码理解angular项目在JIT模式下的启动过程
通常一个angular项目会有一个个模块(Module)来管理各自的业务,并且必须有一个根模块(AppModule)作为应用的入口模块,整个应用都围绕AppModule展开.可以这么说,AppModu ...
- 项目中一个Jenkins权限配置的过程
需求:需要不同账号登录,只看到自己需要看到的job,比如: test01账号登录看到tes01t_job test02账号登录,只看到test02_job 分析:目的是不同项目,不希望看到其他项目或者 ...
- Scrum 项目6.0-展示Sprint回顾的过程及成果。
6.0----------------------------------------------------- sprint演示 1.坚持所有的sprint都结束于演示. 团队的成果得到认可,会感觉 ...
- 记一次vue2项目部署nginx静态文件404解决过程
github上下的一个vue2的项目,运行可以的,webpack打包后,nginx请求报错: 发现路径很奇怪啊,所以果断来到build.js文件中看看是不是哪里不对. 已经一番引用查找: 发现在这里配 ...
- nodeJS学习(8)--- WS/...开发 NodeJS 项目-节3 <使用 mongodb 完整实例过程>
使用 mongodb 的小系统 参考:https://my.oschina.net/chenhao901007/blog/312367 1. Robomongo 创建项目的数据库和数据表 参考:htt ...
- react-native-cli运行项目及打包apk失败的解决过程
刚开始学习react native,第一步自然是搭建好开发环境,node及jdk本身就有,Python2.Android studio以及Android sdk的安装倒是没什么大问题,按照官网的教程做 ...
- 项目中的process.bpmn的读-过程
1.这次项目中遇到了process.bpmn类的封装好的类.怎么读呢?不知道,一周过去了,总算明白点. 2.首先也是从Controller开始,走进service层,比如mybatis,调用的就不是m ...
- 已使用.netframework,version=v4.6.1 而不是目标框架netcoreapp,version=v2.1 还原包,此包可能与项目不完全兼容
已使用.netframework,version=v4.6.1 而不是目标框架netcoreapp,version=v2.1 还原包,此包可能与项目不完全兼容 NU1202: 包 System.Run ...
随机推荐
- 【环境搭建】docker+nginx部署PHP
目的 使用docker容器完成nginx的安装以及部署PHP网页 步骤 一. 安装nginx 1. 拉取Nginx镜像 docker pull nginx //拉取镜像 docker images / ...
- rsync 命令
linux上的rsync命令详解 15个rsync命令实施 -z: --compress 使用压缩机制 -v: --verbose 打印详细信息 -r: --recursive 以递归模式同步子目录 ...
- Win10 下 tensorflow-gpu 2.5 环境搭建
Win10 下 tensorflow-gpu 2.5 环境搭建 简介 机器学习环境搭建,tensorflow_gpu-2.5.0 + CUDA 11.2 + CUDNN 8.1 :环境必须是这个,具体 ...
- JFrame一些基础小知识
JFrame.setLocationRelativeTo方法 JFrame.setLocationRelativeTo()是一个Java Swing中的方法,它用于将窗口居中显示在屏幕上. 当你调用该 ...
- 管于pyinstaller 打包完成后不能运行的问题
方案一: 进入项目路径,在cmd窗口输入python 文件名.之后查看结果,看是否有模块未安装,或者是未导入模块.因为pyinstaller打包时,是按照被打包文件上的导入的库名进行打包的,所以需要将 ...
- 若依前后端分离版:增加新的登录接口和新的用户表,用于小程序或者APP获取token,并使用若依的验证方法
相关原创链接直接放这: 基于若依框架springsecurity添加多种用户登录解决方案(springsecurity多用户登录:前端用户.后端用户)_若依多用户表登录_云优的博客-CSDN博客 若依 ...
- 好用的css3特性-过渡和2D变换
css3中有很多非常好用的特性,今天来总结一下与动画相关,包括过渡.2D变换. 首先来介绍一下过渡,过渡是在进行变化的时候进行的一个缓冲,如果没有过渡,当变更了元素的位置.大小的数据时,会一瞬间完成变 ...
- VMware虚拟机的安装与配置
一.VMware简介 VMware Workstation Pro 是业界标准的桌面 Hypervisor,用于在 Linux 或 Windows PC 上运行虚拟机. Workstation 16 ...
- frp内网穿透环境搭建--服务端ubuntu 客户端win10
前提条件:1个公网ip服务器,例如我的是腾讯云服务器ubuntu20 下载frp软件,下的是0.33.0版本,该版本直接把软件封装成服务,能用ubuntu直接定义开机自启等 github:https: ...
- CodeForces 1187E Tree Painting
题意:给定一棵\(n\)个点的树 初始全是白点 要求你做\(n\)步操作,每一次选定一个与一个黑点相隔一条边的白点,将它染成黑点,然后获得该白点被染色前所在的白色联通块大小的权值. 第一次操作可以任意 ...