QLORA微调ChatGLM3-6B模型
LoRA的核心思想是将可调整的低秩矩阵注入到Transformer架构的每一层中,充当”适配器”的作用。这样可以使模型针对特定任务进行调整和专门化,同时最大限度地减少额外的参数数量,提高参数效率。
QLoRA是LoRA的扩展版本,在微调过程中引入了量化技术,以进一步提高参数效率。QLoRA利用LoRA的原理,并引入了4位NormalFloat(NF4)量化和双重量化技术,进一步减少了存储和计算资源的使用。
安装相关库
pip install datasets==2.17.0 pandas transformers==4.37.2 peft==0.8.0 accelerate==0.27.0 bitsandbytes autoawq optimum
使用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_bytes / (1024 ** 3)
print(f"{memory_gb :.2f}GB")
注意:
与实际进程占用有差异,差值为预留给PyTorch的显存
准备数据集
准备数据集其实就是指令集构建,LLM的微调一般指指令微调过程。所谓指令微调,就是使用的微调数据格式、形式。
训练目标是让模型具有理解并遵循用户指令的能力。因此在指令集构建时,应该针对目标任务,针对性的构建任务指令集。
[
{
"instruction": "用户指令(必填)",
"input": "用户输入(选填)",
"output": "模型回答(必填)",
}
]
instruction:用户指令,告知模型其需要完成的任务
input:用户输入,是完成用户指令所必须的输入内容
output:模型回答,模型应该给出的输出
这里根据企业私有文档数据,生成相关格式的训练数据集,大概格式如下:
[
{
"instruction": "内退条件是什么?",
"input": "",
"output": "内退条件包括与公司签订正式劳动合同并连续工作满20年及以上,以及距离法定退休年龄不足5年。特殊工种符合国家相关规定可提前退休的也可在退休前5年内提出内退申请。"
},
]
作者:CodeDevMaster
链接:juejin.cn/post/738463…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
加载数据集
from datasets import load_dataset # 导入 load_dataset 函数
# 加载处理好的训练数据集
dataset = load_dataset("json", data_files="./train_data.json")
print(dataset) # 打印加载的数据集对象
DatasetDict({
train: Dataset({
features: ['instruction', 'output'],
num_rows: 20182
})
})
数据处理
Lora训练数据是需要经过tokenize编码处理,然后后再输入模型进行训练。一般需要将输入文本编码为input_ids,将输出文本编码为labels,编码之后的结果都是多维的向量。
加载AutoTokenizer
from transformers import AutoTokenizer # 导入 AutoTokenizer 类
# 使用 AutoTokenizer 类的 from_pretrained 方法加载指定模型的 tokenizer
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
需要定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典。
# 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}
可以从从原始数据集中提取出一个较小规模的训练子集,以便稍后对这个子集进行处理、训练或其他操作。
from datasets import DatasetDict # 导入 DatasetDict 类
small_dataset = DatasetDict() # 创建一个空的 DatasetDict 对象
# 在 small_dataset 中存储一个部分数据集,包括以下步骤:
# 1. 从原始数据集的 'train' 部分中随机打乱数据(使用随机种子 16)
# 2. 选择前 1000 条样本构成一个小的数据集
small_dataset["train"] = dataset["train"].shuffle(seed=16).select(range(1000))
# 打印数据集
print(small_dataset)
进行数据映射处理,同时删除特定列
# 获取 'train' 部分的列名
column_names = dataset['train'].column_names
# 使用lambda函数调用tokenize_func函数,并传入example和tokenizer作为参数
tokenized_dataset = small_dataset['train'].map(
lambda example: tokenize_func(example, tokenizer),
batched=False, # 不按批次处理
remove_columns=column_names # 移除特定列(column_names中指定的列)
)
查看处理结果
print(tokenized_dataset[0])
数据集处理
需要使用一个数据收集器,可以使用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)
加载量化模型-4bit
对于加载4bit量化模型,需要设置多个参数,具体参数如下:使用nf4量化数据类型加载模型,开启双量化配置,以bf16混合精度训练
以4-bits量化加载ChatGLM3-6B模型,只需要大约4GB左右显存。
from transformers import AutoModel, BitsAndBytesConfig
# QLoRA量化配置
q_config = BitsAndBytesConfig(load_in_4bit=True, # 是否在4位精度下加载模型
bnb_4bit_quant_type='nf4', # 4位精度量化的类型,表示使用nf4量化类型
bnb_4bit_use_double_quant=True, # 是否使用双精度量化
bnb_4bit_compute_dtype=torch.float16) # 4位精度计算的数据类型,使用半精度浮点数
model = AutoModel.from_pretrained('./chatglm3-6b',
quantization_config=q_config,
device_map='auto',
trust_remote_code=True)
# 优化内存使用和计算效率
model.supports_gradient_checkpointing = True
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
model.config.use_cache = False
预处理量化模型
预处理量化后的模型,使其可以支持低精度微调训练
peft库中的
prepare_model_for_kbit_training
函数是用于为模型执行量化准备的工具。这个函数通常用于准备在低比特宽度(如8-bit, 4-bit, 2-bit等)下进行模型训练或微调。
该函数的主要功能是调整模型的权重和激活,使其适应在指定的比特位宽度下进行有效的训练。
from peft import prepare_model_for_kbit_training
kbit_model = prepare_model_for_kbit_training(model)
配置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 方法,在各类大模型上的微调适配模块。
lora_config = LoraConfig(
target_modules=target_modules,
r=4,# LoRA秩
lora_alpha=32,
lora_dropout=0.05,
bias='none',
inference_mode=False,
task_type=TaskType.CAUSAL_LM
)
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
qlora_model = get_peft_model(kbit_model, lora_config)
# 打印出模型中可训练的参数
qlora_model.print_trainable_parameters()
训练超参数配置
训练总步数steps的计算:
epoch: 1个epoch表示对全部训练集样本进行一次完整的训练
num_train_epochs:多少个epoch, 表示进行多少个、多少轮完整数据集的训练
每个epoch的训练步数:
steps/epoch = num_train_examples / (batch_size * gradient_accumulation_steps)
训练总步数:
total_steps = steps/epoch * num_train_epochs
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="models/chatglm3-6b", # 输出目录
per_device_train_batch_size=4, # 每个设备的训练批量大小
gradient_accumulation_steps=4, # 梯度累积步数
# per_device_eval_batch_size=8, # 每个设备的评估批量大小
learning_rate=1e-3, # 学习率
num_train_epochs=1, # 训练轮数
lr_scheduler_type="linear", # 学习率调度器类型
warmup_ratio=0.1, # 预热比例
logging_steps=10, # 日志记录步数
save_strategy="steps", # 模型保存策略
save_steps=100, # 模型保存步数
# evaluation_strategy="steps", # 评估策略
# eval_steps=500, # 评估步数
optim="adamw_torch", # 优化器类型
fp16=True, # 是否使用混合精度训练
)
启用指标评估
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="models/chatglm3-6b", # 输出目录
per_device_train_batch_size=4, # 每个设备的训练批量大小
gradient_accumulation_steps=4, # 梯度累积步数
learning_rate=1e-3, # 学习率
max_steps=100, # 训练步数
lr_scheduler_type="linear", # 学习率调度器类型
warmup_ratio=0.1, # 预热比例
logging_steps=10, # 日志记录步数
save_strategy="steps", # 模型保存策略
save_steps=20, # 模型保存步数
optim="adamw_torch", # 优化器类型
fp16=True, # 是否使用混合精度训练
)
开始训练
trainer = Trainer(
model=qlora_model,
args=training_args,
train_dataset=tokenized_dataset,
data_collator=data_collator
)
trainer.train()
保存LoRA模型
lora_model_path = "lora/chatglm3-6b"
trainer.model.save_pretrained(lora_model_path )
#model.save_pretrained(lora_model_path )
模型推理
使用QLoRA微调后的ChatGLM3-6B模型进行对比模型推理
import torch
from transformers import AutoModel, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel, PeftConfig
# QLoRA量化配置
q_config = BitsAndBytesConfig(load_in_4bit=True,
bnb_4bit_quant_type='nf4',
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.float32)
# 加载量化后的原始模型
base_model = AutoModel.from_pretrained('./chatglm3-6b',
quantization_config=q_config,
trust_remote_code=True,
device_map='auto')
# 优化
base_model.requires_grad_(False)
base_model.eval()
# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True)
使用原始ChatGLM3-6B模型进行推理测试
input_text = 'xxxx?'
response, history = base_model.chat(tokenizer=tokenizer, query=input_text)
print(f'ChatGLM3-6B 微调前:n{response}')
使用PeftModel合并源model与PEFT微调后的参数,然后进行推理测试
input_text = 'xxxx?'
# 合并
model = PeftModel.from_pretrained(base_model, './chatglm3-6b-lora')
response, history = model.chat(tokenizer=tokenizer, query=input_text)
print(f'ChatGLM3-6B 微调后: n{response}')
合并模型
将lora权重合并到大模型中,将模型参数加载为16位浮点数
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_path="./chatglm3-6b"
peft_model_path="./lora/chatglm3-6b"
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)