highlight: androidstudiotheme: cyanosis
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, AutoModelmodel_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()# 转换为GBmemory_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链接:https://juejin.cn/post/7384636970033938482来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
加载数据集
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 方法加载指定模型的 tokenizertokenizer = 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 DataCollatorForSeq2Seqdata_collator = DataCollatorForSeq2Seq( tokenizer, model=model, label_pad_token_id=-100, pad_to_multiple_of=None, padding=True)
或者自定义实现一个数据收集器
import torchfrom 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_trainingkbit_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_MAPPINGtarget_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, Trainertraining_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, Trainertraining_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 torchfrom transformers import AutoModel, AutoTokenizer, BitsAndBytesConfigfrom 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.requiresgrad(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 PeftModelfrom transformers import AutoModelForCausalLM, AutoTokenizerimport 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, AutoModeltokenizer = 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)