PEFT库进行ChatGLM3-6B模型LORA高效微调
- LORA微调ChatGLM3-6B模型
- 安装相关库
- 使用ChatGLM3-6B
- 模型GPU显存占用
- 准备数据集
- 加载模型
- 加载数据集
- 数据处理
- 数据集处理
- 配置LoRA
- 配置训练超参数
- 开始训练
- 保存LoRA模型
- 模型推理
- 从新加载
- 合并模型
- 使用微调后的模型
LORA微调ChatGLM3-6B模型
本文基于transformers、peft等框架,对ChatGLM3-6B模型进行Lora微调。
LORA(Low-Rank Adaptation)是一种高效的模型微调技术,它可以通过在预训练模型上添加额外的低秩权重矩阵来微调模型,从而仅需更新很少的参数即可获得良好的微调性能。这相比于全量微调大幅减少了训练时间和计算资源的消耗。
安装相关库
pip install ransformers==4.37.2 peft==0.8.0 accelerate==0.27.0 bitsandbytes
使用ChatGLM3-6B
直接调用ChatGLM3-6B模型来生成对话
from transformers import AutoTokenizer, AutoModel
model_id = "/root/work/chatglm3-6b"
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
#model = AutoModel.from_pretrained(model_id, trust_remote_code=True).half().cuda()
model = AutoModel.from_pretrained(model_id, trust_remote_code=True, device='cuda')
model = model.eval()
response, history = model.chat(tokenizer, "你好", history=history)
print(response)
模型GPU显存占用
默认情况下,模型以半精度(float16)加载,模型权重需要大概 13GB显存。
获取当前模型占用的GPU显存
memory_bytes = model.get_memory_footprint()
# 转换为GB
memory_gb = memory_footprint_bytes / (1024 ** 3)
print(f"{memory_gb :.2f}GB")
注意:与实际进程占用有差异,差值为预留给PyTorch的显存
准备数据集
准备数据集其实就是指令集构建,LLM的微调一般指指令微调过程。所谓指令微调,就是使用的微调数据格式、形式。
训练目标是让模型具有理解并遵循用户指令的能力。因此在指令集构建时,应该针对目标任务,针对性的构建任务指令集。
这里使用alpaca格式的数据集,格式形式如下:
[
{
"instruction": "用户指令(必填)",
"input": "用户输入(选填)",
"output": "模型回答(必填)",
},
"system": "系统提示词(选填)",
"history": [
["第一轮指令(选填)", "第一轮回答(选填)"],
["第二轮指令(选填)", "第二轮回答(选填)"]
]
]
instruction:用户指令,要求AI执行的任务或问题
input:用户输入,是完成用户指令所必须的输入内容,就是执行指令所需的具体信息或上下文
output:模型回答,根据给定的指令和输入生成答案
这里根据企业私有文档数据,生成相关格式的训练数据集,大概格式如下:
[
{
"instruction": "内退条件是什么?",
"input": "",
"output": "内退条件包括与公司签订正式劳动合同并连续工作满20年及以上,以及距离法定退休年龄不足5年。特殊工种符合国家相关规定可提前退休的也可在退休前5年内提出内退申请。"
},
]
加载模型
from transformers import AutoModel, AutoTokenizer
model_id = "/root/work/chatglm3-6b"
model = AutoModel.from_pretrained(model_id, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
加载数据集
from datasets import load_dataset
data_id="/root/work/jupyterlab/zd.json"
dataset = load_dataset("json", data_files=data_id)
print(dataset["train"])
数据处理
Lora训练数据是需要经过tokenize编码处理,然后后再输入模型进行训练。一般需要将输入文本编码为
input_ids
,将输出文本编码为labels
,编码之后的结果都是多维的向量。
需要定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典。
# tokenize_func 函数
def tokenize_func(example, tokenizer, ignore_label_id=-100):
"""
对单个数据样本进行tokenize处理。
参数:
example (dict): 包含'content'和'summary'键的字典,代表训练数据的一个样本。
tokenizer (transformers.PreTrainedTokenizer): 用于tokenize文本的tokenizer。
ignore_label_id (int, optional): 在label中用于填充的忽略ID,默认为-100。
返回:
dict: 包含'tokenized_input_ids'和'labels'的字典,用于模型训练。
"""
prompt_text = '' # 所有数据前的指令文本
max_input_length = 512 # 输入的最大长度
max_output_length = 1536 # 输出的最大长度
# 构建问题文本
question = prompt_text + example['instruction']
if example.get('input', None) and example['input'].strip():
question += f'\n{example["input"]}'
# 构建答案文本
answer = example['output']
# 对问题和答案文本进行tokenize处理
q_ids = tokenizer.encode(text=question, add_special_tokens=False)
a_ids = tokenizer.encode(text=answer, add_special_tokens=False)
# 如果tokenize后的长度超过最大长度限制,则进行截断
if len(q_ids) > max_input_length - 2: # 保留空间给gmask和bos标记
q_ids = q_ids[:max_input_length - 2]
if len(a_ids) > max_output_length - 1: # 保留空间给eos标记
a_ids = a_ids[:max_output_length - 1]
# 构建模型的输入格式
input_ids = tokenizer.build_inputs_with_special_tokens(q_ids, a_ids)
question_length = len(q_ids) + 2 # 加上gmask和bos标记
# 构建标签,对于问题部分的输入使用ignore_label_id进行填充
labels = [ignore_label_id] * question_length + input_ids[question_length:]
return {'input_ids': input_ids, 'labels': labels}
进行数据映射处理,同时删除特定列
# 获取 'train' 部分的列名
column_names = dataset['train'].column_names
# 使用lambda函数调用tokenize_func函数,并传入example和tokenizer作为参数
tokenized_dataset = dataset['train'].map(
lambda example: tokenize_func(example, tokenizer),
batched=False, # 不按批次处理
remove_columns=column_names # 移除特定列(column_names中指定的列)
)
执行print(tokenized_dataset[0])
,打印tokenize处理结果
数据集处理
还需要使用一个数据收集器,可以使用transformers 中的DataCollatorForSeq2Seq数据收集器
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(
tokenizer,
model=model,
label_pad_token_id=-100,
pad_to_multiple_of=None,
padding=True
)
或者自定义实现一个数据收集器
import torch
from typing import List, Dict, Optional
# DataCollatorForChatGLM 类
class DataCollatorForChatGLM:
"""
用于处理批量数据的DataCollator,尤其是在使用 ChatGLM 模型时。
该类负责将多个数据样本(tokenized input)合并为一个批量,并在必要时进行填充(padding)。
属性:
pad_token_id (int): 用于填充(padding)的token ID。
max_length (int): 单个批量数据的最大长度限制。
ignore_label_id (int): 在标签中用于填充的ID。
"""
def __init__(self, pad_token_id: int, max_length: int = 2048, ignore_label_id: int = -100):
"""
初始化DataCollator。
参数:
pad_token_id (int): 用于填充(padding)的token ID。
max_length (int): 单个批量数据的最大长度限制。
ignore_label_id (int): 在标签中用于填充的ID,默认为-100。
"""
self.pad_token_id = pad_token_id
self.ignore_label_id = ignore_label_id
self.max_length = max_length
def __call__(self, batch_data: List[Dict[str, List]]) -> Dict[str, torch.Tensor]:
"""
处理批量数据。
参数:
batch_data (List[Dict[str, List]]): 包含多个样本的字典列表。
返回:
Dict[str, torch.Tensor]: 包含处理后的批量数据的字典。
"""
# 计算批量中每个样本的长度
len_list = [len(d['input_ids']) for d in batch_data]
batch_max_len = max(len_list) # 找到最长的样本长度
input_ids, labels = [], []
for len_of_d, d in sorted(zip(len_list, batch_data), key=lambda x: -x[0]):
pad_len = batch_max_len - len_of_d # 计算需要填充的长度
# 添加填充,并确保数据长度不超过最大长度限制
ids = d['input_ids'] + [self.pad_token_id] * pad_len
label = d['labels'] + [self.ignore_label_id] * pad_len
if batch_max_len > self.max_length:
ids = ids[:self.max_length]
label = label[:self.max_length]
input_ids.append(torch.LongTensor(ids))
labels.append(torch.LongTensor(label))
# 将处理后的数据堆叠成一个tensor
input_ids = torch.stack(input_ids)
labels = torch.stack(labels)
return {'input_ids': input_ids, 'labels': labels}
data_collator = DataCollatorForChatGLM(pad_token_id=tokenizer.pad_token_id)
配置LoRA
在peft中使用LoRA非常简单。借助PeftModel抽象,可以快速将低秩适配器(LoRA)应用到任意模型中。
在初始化相应的微调配置类(LoraConfig)时,需要显式指定在哪些层新增适配器(Adapter),并将其设置正确。
ChatGLM3-6B模型通过以下方式获取需要训练的模型层的名字
from peft.utils import TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING
target_modules = TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING['chatglm']
在PEFT库的 constants.py 文件中定义了不同的 PEFT 方法,在各类大模型上的微调适配模块。
主要是配置LoraConfig类,其中可以设置很多参数,但主要参数只有几个
# 从peft库导入LoraConfig和get_peft_model函数
from peft import LoraConfig, get_peft_model, TaskType
# 创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
r=8, # LoRA的秩,影响LoRA矩阵的大小
lora_alpha=32, # LoRA适应的比例因子
# 指定需要训练的模型层的名字,不同模型对应层的名字不同
# target_modules=["query_key_value"],
target_modules=target_modules,
lora_dropout=0.05, # 在LoRA模块中使用的dropout率
bias="none", # 设置bias的使用方式,这里没有使用bias
# task_type="CAUSAL_LM" # 任务类型,这里设置为因果(自回归)语言模型
task_type=TaskType.CAUSAL_LM
)
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
model = get_peft_model(model, config)
# 打印出模型中可训练的参数
model.print_trainable_parameters()
配置训练超参数
配置训练超参数使用TrainingArguments类,可配置参数同样有很多,但主要参数也是只有几个
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir=f"{model_id}-lora", # 指定模型输出和保存的目录
per_device_train_batch_size=4, # 每个设备上的训练批量大小
learning_rate=2e-4, # 学习率
fp16=True, # 启用混合精度训练,可以提高训练速度,同时减少内存使用
logging_steps=20, # 指定日志记录的步长,用于跟踪训练进度
save_strategy="steps", # 模型保存策略
save_steps=50, # 模型保存步数
# max_steps=50, # 最大训练步长
num_train_epochs=1 # 训练的总轮数
)
查看添加LoRA模块后的模型
print(model)
开始训练
配置model、参数、数据集后就可以进行训练了
trainer = Trainer(
model=model, # 指定训练时使用的模型
train_dataset=tokenized_dataset, # 指定训练数据集
args=training_args,
data_collator=data_collator,
)
model.use_cache = False
# trainer.train()
with torch.autocast("cuda"):
trainer.train()
注意:
执行
trainer.train()
时出现异常,参考:bitsandbytes的issues
保存LoRA模型
lora_model_path = "lora/chatglm3-6b-int8"
trainer.model.save_pretrained(lora_model_path )
#model.save_pretrained(lora_model_path )
模型推理
使用LoRA模型,进行模型推理
lora_model = trainer.model
1.文本补全
text = "人力资源部根据各部门人员"
inputs = tokenizer(text, return_tensors="pt").to(0)
out = lora_model.generate(**inputs, max_new_tokens=500)
print(tokenizer.decode(out[0], skip_special_tokens=True))
2.问答对话
from peft import PeftModel
input_text = '公司的招聘需求是如何提出的?'
model.eval()
response, history = lora_model.chat(tokenizer=tokenizer, query=input_text)
print(f'ChatGLM3-6B 微调后回答: \n{response}')
从新加载
加载源model与tokenizer,使用PeftModel合并源model与PEFT微调后的参数,然后进行推理测试。
from peft import PeftModel
from transformers import AutoModel, AutoTokenizer
model_path="/root/work/chatglm3-6b"
peft_model_checkpoint_path="./chatglm3-6b-lora/checkpoint-50"
model = AutoModel.from_pretrained(model_path, trust_remote_code=True, low_cpu_mem_usage=True)
tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True)
# 将训练所得的LoRa权重加载起来
p_model = PeftModel.from_pretrained(model, model_id=peft_model_checkpoint_path)
p_model = p_model.cuda()
response, history = p_model.chat(tokenizer, "内退条件是什么?", history=[])
print(response)
合并模型
将lora权重合并到大模型中,将模型参数加载为16位浮点数
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_path="/root/work/chatglm3-6b"
peft_model_path="./lora/chatglm3-6b-int8"
save_path = "chatglm3-6b-lora"
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, low_cpu_mem_usage=True, torch_dtype=torch.float16, device_map="auto")
model = PeftModel.from_pretrained(model, peft_model_path)
model = model.merge_and_unload()
tokenizer.save_pretrained(save_path)
model.save_pretrained(save_path)
查看合并文件
使用微调后的模型
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("chatglm3-6b-lora", trust_remote_code=True)
model = AutoModel.from_pretrained("chatglm3-6b-lora", trust_remote_code=True, device='cuda')
model = model.eval()
response, history = model.chat(tokenizer, "内退条件是什么?", history=[])
print(response)