使用PEFT微调ChatGLM3-6B

系列文章目录

该系列文章用于介绍使用peft库来进行大模型的微调
第一章 使用PEFT对ChatGLM3-6B进行LORA微调


文章目录

  • 系列文章目录
  • 前言
  • 一、准备工作
    • 1. 环境准备
    • 2. 大模型准备:
    • 3. 数据准备
  • 二、数据集准备和训练LORA
    • 1. 把对话数据转化为token化的id进行存储
    • 2.训练
  • 三、测试
    • 1. 将待转换语句填充到询问模板中并加载lora模型进行对话
    • 2.结果展示
  • 总结


前言

PEFT简介: PEFT(Parameter-Efficient Fine-Tuning)是一个库,用于有效地使大型预训练模型适应各种下游应用程序,而无需微调模型的所有参数,因为它的成本高得令人望而却步。PEFT方法仅微调少量(额外)模型参数 - 显着降低计算和存储成本 - 同时产生与完全微调模型相当的性能。这使得在消费者硬件上训练和存储大型语言模型 (LLM) 变得更加容易。

PEFT 与 Transformers、Diffusers 和 Accelerate 库集成,以更快、更简单的方式加载、训练和使用大型模型进行推理


声明:
本文实现主要参考自:https://github.com/Suffoquer-fang/LuXun-GPT
该库主要实现了一个从普通语句到鲁迅风格语句的转换的一个LORA微调。但其使用版本较老,已经难以复现。本文在其基础上进行了修改,以适应当前最新版本(peft和chatglm3)的微调,并整理到了该库:https://github.com/foolishortalent/AIGC/tree/main/LuXun%20trans%20chatglm。


一、准备工作

1. 环境准备

# int8
bitsandbytes
accelerate

# chatglm
modelscope # 国内用户用这个比较快
protobuf
transformers
icetk
cpm_kernels

torch

# 
datasets
peft>=0.7.1

2. 大模型准备:

我们这里使用的是ChatGLM3-6B,运行如下代码便会自行从Hugging Face加载模型。

from transformers import AutoTokenizer, AutoModel
model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)

国内同学可以通过魔搭社区下载,和从Hugging Face下载的是一样的。
魔搭:https://modelscope.cn/models/ZhipuAI/chatglm3-6b/summary

pip install modelscope
from modelscope import snapshot_download
model_dir = snapshot_download("ZhipuAI/chatglm3-6b", revision = "v1.0.0")

3. 数据准备

鲁迅说过:“有多少人工,便有多少智能。”
这也是挡在众多大语言模型微调开发者前面的一座大山,但好在有github:)
本博文整理的git库中包含的数据由对参考库中的example_data修改而来。
参考库中luxun_data.jsonl是selected_aug.jsonl经过与随机一个下面指令相结合生成的:

  • “将这句话改写成鲁迅风格的语言”
  • “用鲁迅的风格改写”
  • “你是一个非常熟悉鲁迅风格的作家,请用鲁迅的风格改写这句话”
  • “用鲁迅的风格改写这句话”

为了减少回答中出现英文的可能性,我们使用中文代替了原模版中的英文单词:
替代前 数据实例:

{"context": "Instruction: 将这句话改写成鲁迅风格的语言\nInput: 虽然打高尔夫、脱钩和参加豪门社交都是炫耀身份的行为,但如此疯狂追求名利地步就算被曝出丑闻,也不为过。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "Instruction: 将这句话改写成鲁迅风格的语言\nInput: 在追逐虚荣和地位的过程中,参加高尔夫球赛、脱离常规、参加豪门社交等活动已成为一种炫耀的方式,但这样的沉迷甚至即使被曝出丑闻也不为过。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "Instruction: 你是一个非常熟悉鲁迅风格的作家,请用鲁迅的风格改写这句话\nInput: 打高尔夫、脱掉脱钩、参加社交活动等的追求地位与名望的行为已经到了疯狂的程度,哪怕发生丑闻也不会有任何过错。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "Instruction: 用鲁迅的风格改写\nInput: 尽管打高尔夫、脱钩、参加豪门社交等活动是一种炫耀身份和社会地位的行为,但如此的沉迷到了这种地步,即使曝出丑闻也不算是过分。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}

替代后 数据实例:

{"context": "指令:将这句话改写成鲁迅风格的语言\n语句:虽然打高尔夫、脱钩和参加豪门社交都是炫耀身份的行为,但如此疯狂追求名利地步就算被曝出丑闻,也不为过。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "指令:将这句话改写成鲁迅风格的语言\n语句:在追逐虚荣和地位的过程中,参加高尔夫球赛、脱离常规、参加豪门社交等活动已成为一种炫耀的方式,但这样的沉迷甚至即使被曝出丑闻也不为过。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "指令:你是一个非常熟悉鲁迅风格的作家,请用鲁迅的风格改写这句话\n语句:打高尔夫、脱掉脱钩、参加社交活动等的追求地位与名望的行为已经到了疯狂的程度,哪怕发生丑闻也不会有任何过错。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "指令:用鲁迅的风格改写\n语句:尽管打高尔夫、脱钩、参加豪门社交等活动是一种炫耀身份和社会地位的行为,但如此的沉迷到了这种地步,即使曝出丑闻也不算是过分。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}

二、数据集准备和训练LORA

1. 把对话数据转化为token化的id进行存储

import json
from tqdm import tqdm

import datasets
import transformers

def preprocess(tokenizer, config, example, max_seq_length):
    prompt = example["context"]
    target = example["target"]
    prompt_ids = tokenizer.encode(prompt, max_length=max_seq_length, truncation=True)
    target_ids = tokenizer.encode(
        target,
        max_length=max_seq_length,
        truncation=True,
        add_special_tokens=False)
    input_ids = prompt_ids + target_ids + [config.eos_token_id]
    return {"input_ids": input_ids, "seq_len": len(prompt_ids)}


def read_jsonl(path, max_seq_length, skip_overlength=False):
    model_name = "ZhipuAI//chatglm-6b"
    tokenizer = transformers.AutoTokenizer.from_pretrained(
        model_name, trust_remote_code=True)
    config = transformers.AutoConfig.from_pretrained(
        model_name, trust_remote_code=True, device_map='auto')
    with open(path, "r") as f:
        for line in tqdm(f.readlines()):
            example = json.loads(line)
            feature = preprocess(tokenizer, config, example, max_seq_length)
            if skip_overlength and len(feature["input_ids"]) > max_seq_length:
                continue
            feature["input_ids"] = feature["input_ids"][:max_seq_length]
            yield feature

jsonl_path = "lunxun-style-data/luxun_data.jsonl"
save_path = "lunxun-style-data/luxun"
max_seq_length = 500
skip_overlength = False
dataset = datasets.Dataset.from_generator(
        lambda: read_jsonl(args.jsonl_path, args.max_seq_length, args.skip_overlength)
    )
dataset.save_to_disk(args.save_path)

我们通过preprocess方法来对luxun_data.jsonl中的每一条数据进行token化处理,将语句转化为代表词语编码序号的一系列序号id。最后返回的 **{“input_ids”: input_ids, “seq_len”: len(prompt_ids)}**中,input_ids由prompt_ids、target_ids、config.eos_token_id三部分拼接而成,seq_len则记录prompt_ids的长度。

2.训练

构建MyTrainingArguments类,用于接收训练所需参数:

  • max_steps: 训练轮数
  • save_steps: 每隔多少轮存储一次模型数据
  • learning_rate: 学习率
  • fp16: 是否使用fp16
  • remove_unused_columns: 是否移除不使用的列
  • logging_steps: 每多少轮打印一次日志
  • output_dir: lora模型存储路径
  • per_device_train_batch_size: 训练批次大小
  • gradient_accumulation_steps: 累计梯度轮次
  • dataset_path: dataset存放路径
  • lora_rank: lora的秩

构建MyTrainer类,用于训练。实现了父类的compute_losssave_model的函数。

# coding=utf-8
import sys

sys.path.append("./")
from dataclasses import dataclass, field
import os
from transformers import (

    TrainingArguments,
    Trainer,
)


@dataclass
class MyTrainingArguments(TrainingArguments):
    max_steps: int = field(default=5000)
    save_steps: int = field(default=1000)
    learning_rate: float = field(default=1e-4)
    fp16: bool = field(default=True)
    remove_unused_columns: bool = field(default=False)
    logging_steps: int = field(default=50)
    output_dir: str = field(default="LuXun-lora")
    per_device_train_batch_size: int = field(default=4)
    gradient_accumulation_steps: int = field(default=2)
    dataset_path: str = field(default="lunxun-style-data/luxun")
    lora_rank: int = field(default=8)


import torch


class MyTrainer(Trainer):
    def compute_loss(self, model, inputs, return_outputs=False):
        return model(
            input_ids=inputs["input_ids"],
            labels=inputs["labels"],
        ).loss

    def save_model(self, output_dir=None, _internal_call=False):
        from transformers.trainer import TRAINING_ARGS_NAME

        os.makedirs(output_dir, exist_ok=True)
        torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
        self.model.save_pretrained(output_dir)

通过build_model来构建待训练的peft模型,通过LoraConfig来设置所需训练的Lora参数:

  • task_type=TaskType.CAUSAL_LM: 任务类型:因果大模型
  • inference_mode=False:
  • r=training_args.lora_rank: 所添加lora的秩
  • lora_alpha=32: lora模型的系数
  • lora_dropout=0.1: lora模型的dropout比例
  • target_modules=[“query_key_value”]: 需要添加lora的层,query_key_value即模块中包含该名称
from transformers import HfArgumentParser
from transformers import AutoTokenizer, AutoModel

import datasets

from peft import get_peft_model, LoraConfig, TaskType
from utils import get_data_collator
from training_arguments import MyTrainingArguments, MyTrainer


def build_model(training_args):
    print("#> Building model...")
    model = AutoModel.from_pretrained(
        "ZhipuAI/chatglm3-6b", load_in_8bit=True, trust_remote_code=True, device_map="auto"
    )
    model.gradient_checkpointing_enable()
    model.enable_input_require_grads()
    model.is_parallelizable = True
    model.model_parallel = True
    model.config.use_cache = (
        False  # silence the warnings. Please re-enable for inference!
    )

    peft_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        inference_mode=False,
        r=training_args.lora_rank,
        lora_alpha=32,
        lora_dropout=0.1,
        target_modules=["query_key_value"]
    )
    model = get_peft_model(model, peft_config)

    print("#> Model built.")
    print("#> Total Trainable Parameters:", sum(p.numel() for p in model.parameters() if p.requires_grad))
    print("#> Total Parameters:", sum(p.numel() for p in model.parameters()), "\n")

    return model


def main():
    # parse args
    training_args = HfArgumentParser(MyTrainingArguments).parse_args_into_dataclasses()[0]

    training_args.remove_unused_columns = False

    print("#> Loading dataset...")

    dataset = datasets.load_from_disk(training_args.dataset_path)
    dataset.set_format(
        type=dataset.format["type"],
        columns=list(dataset.features.keys()),
    )

    print("#> Dataset loaded.", "Total samples:", len(dataset), "\n")

    # build model

    model = build_model(training_args)
    tokenizer = AutoTokenizer.from_pretrained("ZhipuAI/chatglm3-6b", trust_remote_code=True)

    print("#> Start training...")
    # start train
    trainer = MyTrainer(
        model=model,
        train_dataset=dataset,
        args=training_args,
        data_collator=get_data_collator(tokenizer),
    )
    trainer.train()
    model.save_pretrained(training_args.output_dir)

    print("#> Training finished.")
    print("#> Model saved to:", training_args.output_dir)


if __name__ == "__main__":
    main()

utils中的get_data_collator返回一个数据校对器,用于将所有训练数据的token id、标签数据的token id补齐为相同长度。

def get_data_collator(tokenizer: AutoTokenizer):
    def data_collator(features: list) -> dict:
        len_ids = [len(feature["input_ids"]) for feature in features]
        longest = max(len_ids)
        input_ids = []
        labels_list = []
        for ids_l, feature in sorted(zip(len_ids, features), key=lambda x: -x[0]):
            ids = feature["input_ids"]
            seq_len = feature["seq_len"]
            labels = (
                [-100] * (longest-ids_l+seq_len) + ids[seq_len:]
            )
            ids = [tokenizer.pad_token_id] * (longest - ids_l) + ids
            _ids = torch.LongTensor(ids)
            labels_list.append(torch.LongTensor(labels))
            input_ids.append(_ids)
        input_ids = torch.stack(input_ids)
        labels = torch.stack(labels_list)
        return {
            "input_ids": input_ids,
            "labels": labels,
        }
    return data_collator

训练指令:

python lora_finetune.py 

三、测试

1. 将待转换语句填充到询问模板中并加载lora模型进行对话

from transformers import AutoModel
import torch

from transformers import AutoTokenizer

from peft import PeftModel
import argparse


def generate(instruction, text):
    with torch.no_grad():
        input_text = f"指令:{instruction}\n语句:{text}\n答:"
        ids = tokenizer.encode(input_text)
        input_ids = torch.LongTensor([ids]).cuda()
        output = peft_model.generate(
            input_ids=input_ids,
            max_length=500,
            do_sample=False,
            temperature=0.0,
            num_return_sequences=1
        )
        output = tokenizer.decode(output[0])
        answer = output.split("答:")[-1]
    return answer.strip()


if __name__ == "__main__":

    base_model="ZhipuAI/chatglm3-6b"
    lora="LuXun-lora"
    instruction="你是一个非常熟悉鲁迅风格的作家,用鲁迅风格的积极正面的语言改写,保持原来的意思:"

    model = AutoModel.from_pretrained(base_model, trust_remote_code=True, load_in_8bit=True, device_map="auto")
    tokenizer = AutoTokenizer.from_pretrained(base_model, trust_remote_code=True)

    if args.lora == "":
        print("#> No lora model specified, using base model.")
        peft_model = model.eval()
    else:
        print("#> Using lora model:", lora)
        peft_model = PeftModel.from_pretrained(model, lora).eval()
    torch.set_default_tensor_type(torch.cuda.FloatTensor)

    texts = [
        "你好",
        "有多少人工,便有多少智能。",
        "落霞与孤鹜齐飞,秋水共长天一色。",
        "我去买几个橘子,你就站在这里,不要走动。",
        "学习计算机技术,是没有办法救中国的。",
        "我怎么样都起不了床,我觉得我可能是得了抑郁症吧。",
        "它是整个系统的支撑架构,连接处理器、内存、存储、显卡和外围端口等所有其他组件。",
        "古巴导弹危机和越南战争是20世纪最大、最致命的两场冲突。古巴导弹危机涉及美国和苏联之间的僵局,因苏联在古巴设立核导弹基地而引发,而越南战争则是北方(由苏联支持)和南方(由美国支持)之间在印度支那持续的军事冲突。",
        "齿槽力矩是指旋转设备受到齿轮牙齿阻力时施加的扭矩。",
        "他的作品包括蒙娜丽莎和最后的晚餐,两者都被认为是杰作。",
        "滑铁卢战役发生在1815年6月18日,是拿破仑战争的最后一场重大战役。"
    ]

    for text in texts:
        print(text)
        print(generate(args.instruction, text), "\n")

2.结果展示

你好
您好,有什么事吗? 

有多少人工,便有多少智能。
倘说:这便是智能的时代,而已。

落霞与孤鹜齐飞,秋水共长天一色。
秋天的景色,例如落霞,给天空带来美丽的画面,它却也只需要在天空飞翔,在秋水面上飞翔,在所谓美丽天空上飞翔。 

我去买几个橘子,你就站在这里,不要走动。
我便咬定:我去买橘子,你站在此处,不要移动。 

学习计算机技术,是没有办法救中国的。
倘要说学计算机技术,便只能说:“我国尚未成功”,而已。 

我怎么样都起不了床,我觉得我可能是得了抑郁症吧。
因为我要爬不起床来,我才能觉得我是个抑郁症患者。 

它是整个系统的支撑架构,连接处理器、内存、存储、显卡和外围端口等所有其他组件。
它诚然是这个系统的关键部分,一切处理器,一切内存,一切存储,都需要通过它来连接和传输,便在于它的连接和传输。 

古巴导弹危机涉及美国和苏联之间的僵局,因苏联在古巴设立核导弹基地而引发,而越南战争则是北方(由苏联支持)和南方(由美国支持)之间在印度支那持续的军事冲突。
古巴导弹危机,大抵是古巴导弹的僵局,或者说是美国导弹的僵局;而越南战争,则放任北方支持,或者说是北方军事占领,或者是什么像越南的印度支那般持续军事对峙。 

齿槽力矩是指旋转设备受到齿轮牙齿阻力时施加的扭矩。
齿轮的旋转,显然需要一定的扭矩。也许 toothless rotation 才是旋转设备遇到的问题。 

他的作品包括蒙娜丽莎和最后的晚餐,两者都被认为是杰作。
他的作品,如以蒙娜丽莎为准,说是艺术杰作;以最后的晚餐为准,则称之為杰作也不為過。 

滑铁卢战役发生在1815年6月18日,是拿破仑战争的最后一场重大战役。
滑铁卢战役的胜利,是拿破仑战争中的最后一次胜利, accordingly它也是拿破仑战争中最有意义的战役。 

总结

正如先生所言:“倘说:这便是智能的时代,而已。”
数据的优劣决定了模型的优质与否。智能的时代是数据的时代。而像lora、p-tuning等微调技术均可以由peft这类开源库完成。
算法专家的工作在于了解其算法原理,能够搭建模型、处理数据、训练模型、在多卡情况下训练模型、在终端设备上部署模型、优化模型(速度和精度)。

你可能感兴趣的:(AIGC,深度学习,人工智能,python)