目的
为避免一学就会、一用就废,这里做下笔记
说明
- 本文内容紧承前文-模型微调1-基础理论,欲渐进,请循序
- 前面学完了微调基础理论和BitFit微调,这里选择另一种微调方法Prompt Tuning进行学习和实战

Prompt Tuning原理
是什么
- 它属于Additive类型,不改变预训练模型参数,而是原模型基础上增加一层,仅训练增加层的参数
- 增加的位置:原始输入嵌入之后,嵌入矩阵进入Encoder/Decoder之前 。以Transformer架构为例,示意如下:

增加的这一层做了什么?
推理时,它将嵌入后的矩阵 (矩阵每一行是一个输入词向量)和自身针对特定任务训练好的矩阵 (矩阵每一行是一个Prompt词向量)拼接成新的矩阵,然后输出给下一层的Encoder/Decoder。

它和提示词工程的Prompt是什么关系?
相同点:
- 都是在数据进入推理之前,对原始数据拼接上额外的信息,以提升生成效果
- 基于上面的原因,字面名称上都叫prompt
不同点:
| 维度 | 提示词工程 | Prompt Tuning |
|---|---|---|
| prompt来源 | 人为设定 | 训练得到 |
| prompt形式 | 自然语言(离散的) | 张量(连续的,由多个高维向量组成的矩阵) |
| prompt可解释性 | 强 | 弱 |
| 优化效果上限 | 中 | 高 |
它是否能根据不同的任务类型,匹配不同的提示词?
是的,这是Prompt Tuning的优秀之处!
从结果上看,不同类型的任务对应不同的Prompt,但只需要启动一个基础模型实例,这在训练垂直领域多任务大模型时,显得很优雅。

它如何做到这一点?
Prompt Tuning实现多任务匹配的核心在于:建立"任务-Prompt"映射系统。 以下是具体实现方式:
输入文本
任务匹配机制
基于标识符匹配
基于分类器路由
基于内容检索
显式任务标识
如"task=sentiment"
小型分类器
自动识别任务类型
向量相似度检索
最相似任务Prompt
任务Prompt库
HashMap结构
加载对应Prompt Embedding
拼接: Prompt + 输入文本
冻结的基础模型
任务输出
训练阶段
为每个任务训练独立Prompt
实战经验
1. 训练提效方法:初始化参数
优先使用具体提示词向量作为初始化参数,训练会快些。且效果大概率也更好,工程验证数据如下:

2. 提示词的数量N设置多少比较好?
建议20-100,工程验证数据如下:

3. 优化效果和模型本身的大小有关
对于参数较多的模型,优化效果较好,接近全量训练调参,工程验证数据如下:

实战代码(Jupyter)
Step1 导入相关模块
python
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer
python
import datasets
datasets.__version__
'4.5.0'
python
import transformers
transformers.__version__
'4.56.2'
Step2 加载数据集
python
ds = load_dataset("json", data_dir="./alpaca_data_zh/")
ds = ds['train']
ds
Dataset({
features: ['instruction', 'input', 'output'],
num_rows: 48818
})
python
ds[:3]
{'instruction': ['保持健康的三个提示。', '三原色是什么?', '描述原子的结构。'],
'input': ['', '', ''],
'output': ['以下是保持健康的三个提示:\n\n1. 保持身体活动。每天做适当的身体运动,如散步、跑步或游泳,能促进心血管健康,增强肌肉力量,并有助于减少体重。\n\n2. 均衡饮食。每天食用新鲜的蔬菜、水果、全谷物和脂肪含量低的蛋白质食物,避免高糖、高脂肪和加工食品,以保持健康的饮食习惯。\n\n3. 睡眠充足。睡眠对人体健康至关重要,成年人每天应保证 7-8 小时的睡眠。良好的睡眠有助于减轻压力,促进身体恢复,并提高注意力和记忆力。',
'三原色通常指的是红色、绿色和蓝色(RGB)。它们是通过加色混合原理创建色彩的三种基础颜色。在以发光为基础的显示设备中(如电视、计算机显示器、智能手机和平板电脑显示屏), 三原色可混合产生大量色彩。其中红色和绿色可以混合生成黄色,红色和蓝色可以混合生成品红色,蓝色和绿色可以混合生成青色。当红色、绿色和蓝色按相等比例混合时,可以产生白色或灰色。\n\n此外,在印刷和绘画中,三原色指的是以颜料为基础的红、黄和蓝颜色(RYB)。这三种颜色用以通过减色混合原理来创建色彩。不过,三原色的具体定义并不唯一,不同的颜色系统可能会采用不同的三原色。',
'原子是物质的基本单位,它由三种基本粒子组成:质子、中子和电子。质子和中子形成原子核,位于原子中心,核外的电子围绕着原子核运动。\n\n原子结构具有层次性。原子核中,质子带正电,中子不带电(中性)。原子核非常小且致密,占据了原子总质量的绝大部分。电子带负电,通常围绕核运动,形成若干层次,称为壳层或电子层。电子数量与质子数量相等,使原子呈电中性。\n\n电子在每个壳层中都呈规律分布,并且不同壳层所能容纳的电子数也不同。在最里面的壳层一般只能容纳2个电子,其次一层最多可容纳8个电子,再往外的壳层可容纳的电子数逐层递增。\n\n原子核主要受到两种相互作用力的影响:强力和电磁力。强力的作用范围非常小,主要限制在原子核内,具有极强的吸引作用,使核子(质子和中子)紧密结合在一起。电磁力的作用范围较大,主要通过核外的电子与原子核相互作用,发挥作用。\n\n这就是原子的基本结构。原子内部结构复杂多样,不同元素的原子核中质子、中子数量不同,核外电子排布分布也不同,形成了丰富多彩的化学世界。']}
Step3 数据集预处理
python
tokenizer = AutoTokenizer.from_pretrained("Langboat/bloom-1b4-zh")
tokenizer
BloomTokenizerFast(name_or_path='Langboat/bloom-1b4-zh', vocab_size=46145, model_max_length=1000000000000000019884624838656, is_fast=True, padding_side='left', truncation_side='right', special_tokens={'bos_token': '<s>', 'eos_token': '</s>', 'unk_token': '<unk>', 'pad_token': '<pad>'}, clean_up_tokenization_spaces=False, added_tokens_decoder={
0: AddedToken("<unk>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
1: AddedToken("<s>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
2: AddedToken("</s>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
3: AddedToken("<pad>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
}
)
python
def process_func(example):
MAX_LENGTH = 256
input_ids, attention_mask, labels = [], [], []
instruction = tokenizer("\n".join(["Human: "+ example["instruction"], example["input"]]).strip() + "\n\nAssistant: ")
response = tokenizer(example["output"] + tokenizer.eos_token)
input_ids = instruction["input_ids"] + response["input_ids"]
attention_mask = instruction["attention_mask"] + response["attention_mask"]
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"]
if len(input_ids) > MAX_LENGTH:
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
python
tokenized_ds = ds.map(process_func, remove_columns=ds.column_names)
tokenized_ds
Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 48818
})
python
# 解码看看预处理结果
tokenizer.decode(tokenized_ds[2]["input_ids"])
'Human: 描述原子的结构。\n\nAssistant: 原子是物质的基本单位,它由三种基本粒子组成:质子、中子和电子。质子和中子形成原子核,位于原子中心,核外的电子围绕着原子核运动。\n\n原子结构具有层次性。原子核中,质子带正电,中子不带电(中性)。原子核非常小且致密,占据了原子总质量的绝大部分。电子带负电,通常围绕核运动,形成若干层次,称为壳层或电子层。电子数量与质子数量相等,使原子呈电中性。\n\n电子在每个壳层中都呈规律分布,并且不同壳层所能容纳的电子数也不同。在最里面的壳层一般只能容纳2个电子,其次一层最多可容纳8个电子,再往外的壳层可容纳的电子数逐层递增。\n\n原子核主要受到两种相互作用力的影响:强力和电磁力。强力的作用范围非常小,主要限制在原子核内,具有极强的吸引作用,使核子(质子和中子)紧密结合在一起。电磁力的作用范围较大,主要通过核外的电子与原子核相互作用,发挥作用。\n\n这就是原子的'
python
# 解码看看预处理结果
tokenizer.decode(list(filter(lambda x: x!=-100, tokenized_ds[2]["labels"])))
'原子是物质的基本单位,它由三种基本粒子组成:质子、中子和电子。质子和中子形成原子核,位于原子中心,核外的电子围绕着原子核运动。\n\n原子结构具有层次性。原子核中,质子带正电,中子不带电(中性)。原子核非常小且致密,占据了原子总质量的绝大部分。电子带负电,通常围绕核运动,形成若干层次,称为壳层或电子层。电子数量与质子数量相等,使原子呈电中性。\n\n电子在每个壳层中都呈规律分布,并且不同壳层所能容纳的电子数也不同。在最里面的壳层一般只能容纳2个电子,其次一层最多可容纳8个电子,再往外的壳层可容纳的电子数逐层递增。\n\n原子核主要受到两种相互作用力的影响:强力和电磁力。强力的作用范围非常小,主要限制在原子核内,具有极强的吸引作用,使核子(质子和中子)紧密结合在一起。电磁力的作用范围较大,主要通过核外的电子与原子核相互作用,发挥作用。\n\n这就是原子的'
python
# 看看预处理结果的维度
len(tokenized_ds[2]["input_ids"])
256
python
# 看看预处理结果的维度
len(tokenized_ds[2]["labels"])
256
Step4 模型创建
python
# 引入基础模型
base_model = AutoModelForCausalLM.from_pretrained("Langboat/bloom-1b4-zh", low_cpu_mem_usage=True)
python
# 基础模型将跑在哪个设备上(GPU、CPU)
base_model.device
device(type='cpu')
python
# 基础模型的结构信息
base_model
BloomForCausalLM(
(transformer): BloomModel(
(word_embeddings): Embedding(46145, 2048)
(word_embeddings_layernorm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(h): ModuleList(
(0-23): 24 x BloomBlock(
(input_layernorm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(self_attention): BloomAttention(
(query_key_value): Linear(in_features=2048, out_features=6144, bias=True)
(dense): Linear(in_features=2048, out_features=2048, bias=True)
(attention_dropout): Dropout(p=0.0, inplace=False)
)
(post_attention_layernorm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(mlp): BloomMLP(
(dense_h_to_4h): Linear(in_features=2048, out_features=8192, bias=True)
(gelu_impl): BloomGelu()
(dense_4h_to_h): Linear(in_features=8192, out_features=2048, bias=True)
)
)
)
(ln_f): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
)
(lm_head): Linear(in_features=2048, out_features=46145, bias=False)
)
python
# 基础模型的参数总量
sum(param.numel() for param in base_model.parameters())
1303111680
Prompt tuning
PEFT Step1 配置文件
python
import peft
peft.__version__
'0.18.1'
python
# conda install peft --channel conda-forge
from peft import PromptTuningConfig, get_peft_model, TaskType, PromptTuningInit
python
# 随机初始化prompt
# config = PromptTuningConfig(task_type=TaskType.CAUSAL_LM, num_virtual_tokens=10) # 设置10个虚拟提示词
# config
# 使用具体词向量初始化prompt(加速收敛,且效果更好)
config = PromptTuningConfig(task_type=TaskType.CAUSAL_LM,
prompt_tuning_init=PromptTuningInit.TEXT,
prompt_tuning_init_text="下面是一段人与机器人的对话,",
num_virtual_tokens=len(tokenizer("下面是一段人与机器人的对话,")["input_ids"]),
tokenizer_name_or_path="Langboat/bloom-1b4-zh")
config
PromptTuningConfig(task_type=<TaskType.CAUSAL_LM: 'CAUSAL_LM'>, peft_type=<PeftType.PROMPT_TUNING: 'PROMPT_TUNING'>, auto_mapping=None, peft_version='0.18.1', base_model_name_or_path=None, revision=None, inference_mode=False, num_virtual_tokens=8, token_dim=None, num_transformer_submodules=None, num_attention_heads=None, num_layers=None, modules_to_save=None, prompt_tuning_init=<PromptTuningInit.TEXT: 'TEXT'>, prompt_tuning_init_text='下面是一段人与机器人的对话,', tokenizer_name_or_path='Langboat/bloom-1b4-zh', tokenizer_kwargs=None)
PEFT Step2 创建peft模型
python
model = get_peft_model(base_model, config)
model
PeftModelForCausalLM(
(base_model): BloomForCausalLM(
(transformer): BloomModel(
(word_embeddings): Embedding(46145, 2048)
(word_embeddings_layernorm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(h): ModuleList(
(0-23): 24 x BloomBlock(
(input_layernorm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(self_attention): BloomAttention(
(query_key_value): Linear(in_features=2048, out_features=6144, bias=True)
(dense): Linear(in_features=2048, out_features=2048, bias=True)
(attention_dropout): Dropout(p=0.0, inplace=False)
)
(post_attention_layernorm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(mlp): BloomMLP(
(dense_h_to_4h): Linear(in_features=2048, out_features=8192, bias=True)
(gelu_impl): BloomGelu()
(dense_4h_to_h): Linear(in_features=8192, out_features=2048, bias=True)
)
)
)
(ln_f): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
)
(lm_head): Linear(in_features=2048, out_features=46145, bias=False)
)
(prompt_encoder): ModuleDict(
(default): PromptEmbedding(
(embedding): Embedding(8, 2048)
)
)
(word_embeddings): Embedding(46145, 2048)
)
python
# 可训练参数
model.print_trainable_parameters()
trainable params: 16,384 || all params: 1,303,128,064 || trainable%: 0.0013
Step5 配置训练参数
python
args = TrainingArguments(
output_dir="./chatbot", # 输出文件夹存储模型的预测结果和模型文件checkpoints
per_device_train_batch_size=1, # 默认8, 对于训练的时候每个 GPU核或者CPU 上面对应的一个批次的样本数
gradient_accumulation_steps=8, # 默认1, 在执行反向传播/更新参数之前, 对应梯度计算累积了多少次
logging_steps=10, # 每隔10迭代落地一次日志
num_train_epochs=1 # 整体上数据集让模型学习多少遍
)
Step6 创建训练器
python
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_ds,
# 构建一个个批次数据所需要的
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)
)
Step7 模型训练(耗时)
python
trainer.train()
加载训练好的 PEFT 模型
python
from peft import PeftModel
python
# 这里需要特别注意,传入的model参数对应的是原始的base模型,所以需要先重新加载一下原始模型。这里"./checkpoint-1000/"是训练好的模型参数文件夹
peft_model = PeftModel.from_pretrained(model=base_model, model_id="./checkpoint-1000/")
Step8 模型推理
python
peft_model.device
device(type='cpu')
python
# 按训练时的数据格式,构造输入数据
ipt = tokenizer("Human: {}\n{}".format("保持健康的三个提示?", "").strip() + "\n\nAssistant: ", return_tensors="pt").to(model.device)
python
# 把model输出的response结果再次转为文本
output = peft_model.generate(
input_ids=ipt["input_ids"],
attention_mask=ipt["attention_mask"],
max_length=256,
do_sample=True,
pad_token_id=tokenizer.pad_token_id # 显式指定 pad_token_id
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
Human: 保持健康的三个提示?
Assistant: 保持健康的提示包括
保持体重。人们应该保持体重,这样身体才能满足自己的需求。
控制饮食。人们的饮食习惯也会影响到健康。人们不应该每天都吃零食。吃零食会增加食欲,同时还会造成能量过剩。
不要吸烟。吸烟会导致心脏病和其他慢性疾病的风险。吸烟也会给人们造成慢性疲劳,影响人们的健康和精神健康。
不要长时间坐。人们可以坐的时间太长会导致血压升高,导致心脏病的发生。
不要过量饮酒,因为过量饮酒会让人们感觉疲惫不堪且容易生病。
训练好的模型文件是什么样的?
训练好的模型文件不含基础模型,因此体量很小,示例如下:
