掘金 人工智能 2024年07月05日
使用PEFT库进行ChatGLM3-6B模型的QLORA高效微调
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了使用QLORA技术微调ChatGLM3-6B模型的方法,包括安装相关库、准备数据集、数据处理、加载量化模型、预处理量化模型、配置LoRA适配器、训练超参数配置等步骤。QLORA是LoRA的扩展版本,它在微调过程中引入了量化技术,进一步提高了参数效率,并使用4位NormalFloat(NF4)量化和双重量化技术,减少了存储和计算资源的使用。

😄 **安装相关库** 安装必要的库,包括datasets、pandas、transformers、peft、accelerate、bitsandbytes和autoawq。这些库提供了必要的工具来处理数据集、加载模型、进行量化训练和微调模型。 python pip install datasets==2.17.0 pandas transformers==4.37.2 peft==0.8.0 accelerate==0.27.0 bitsandbytes autoawq optimum

😊 **准备数据集** 准备数据集是指令集构建过程,LLM的微调一般指指令微调过程。指令微调是指使用特定格式和形式的微调数据。 该数据集应该包含用户指令、用户输入和模型回答三个部分,其中用户指令告知模型需要完成的任务,用户输入是完成用户指令所必须的输入内容,模型回答是模型应该给出的输出。 [ { "instruction": "内退条件是什么?", "input": "", "output": "内退条件包括与公司签订正式劳动合同并连续工作满20年及以上,以及距离法定退休年龄不足5年。特殊工种符合国家相关规定可提前退休的也可在退休前5年内提出内退申请。" }, ]

😎 **数据处理** 数据处理是将原始数据转换为模型可接受的格式,包括将输入文本编码为input_ids,将输出文本编码为labels。 使用`AutoTokenizer`加载指定模型的tokenizer,并定义一个预处理函数`tokenize_func`,对每一个样本编码其输入、输出文本并返回一个编码后的字典。 python from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True) def tokenize_func(example, tokenizer, ignore_label_id=-100): # ... return {'input_ids': input_ids, 'labels': labels}

😉 **加载量化模型** 使用`BitsAndBytesConfig`配置量化参数,并使用`AutoModel`加载量化后的模型。 python from transformers import AutoModel, BitsAndBytesConfig q_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type='nf4', bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.float16) model = AutoModel.from_pretrained('./chatglm3-6b', quantization_config=q_config, device_map='auto', trust_remote_code=True)

😍 **预处理量化模型** 使用`prepare_model_for_kbit_training`函数对量化后的模型进行预处理,使其可以支持低精度微调训练。 python from peft import prepare_model_for_kbit_training kbit_model = prepare_model_for_kbit_training(model)


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)

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

QLORA ChatGLM3-6B 微调 量化 低秩适配器
相关文章