QLORA微调ChatGLM3-6B模型 – 提高参数效率的新技术

释放双眼,带上耳机,听听看~!
了解QLORA如何通过低秩矩阵和量化技术提高ChatGLM3-6B模型的参数效率,为特定任务调整和专门化提供新方法。

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)

QLORA微调ChatGLM3-6B模型 - 提高参数效率的新技术

模型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])

QLORA微调ChatGLM3-6B模型 - 提高参数效率的新技术

数据集处理

需要使用一个数据收集器,可以使用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 方法,在各类大模型上的微调适配模块。

QLORA微调ChatGLM3-6B模型 - 提高参数效率的新技术

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模型 - 提高参数效率的新技术

模型推理

使用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)

查看合并文件
QLORA微调ChatGLM3-6B模型 - 提高参数效率的新技术

使用微调后的模型

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)

QLORA微调ChatGLM3-6B模型 - 提高参数效率的新技术

本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

MarsCode初体验:远程VSCode开发环境体验rust开发

2024-7-16 19:17:00

AI教程

Vue 3构建GPT聊天应用教程 | 单页应用路由配置、TailwindCSS样式设计、OpenAI API集成

2024-7-17 6:35:00

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索