【大模型LLMs】PEFT实战:基于LoRA/P-Tuning/Prefix Tuning/Prompt Tuning微调Qwen2-7B-Instruct大模型完整链路构建

  • 大模型底座:Qwen2-7B-Instruct
  • 数据集:shibing624/alpaca-zh
  • 微调:LoRA/P-tuning/Prefix-Tuning/Prompt-Tuning

1. 环境/数据准备

安装项目相关依赖

pip install datasets
pip install peft
pip install transformers

从Hugging Face安装将要使用的LLMs以及embedding model,这里我们选择Qwen/Qwen2-7B-Instruct作为大模型底座,选择shibing624/alpaca-zh作为数据集
这里介绍快速下载huggingface模型的命令行方法:

1. 首先安装依赖
pip install -U huggingface_hub
pip install -U hf-transfer 
1. 设置环境变量(设置hf环境变量为1用于提升下载速度;设置镜像站地址)
export HF_HUB_ENABLE_HF_TRANSFER=1
export HF_ENDPOINT="https://hf-mirror.com"
1. 安装相应的模型(以Qwen2-7B-Instruct为例,前面是huggingface上的模型名,后面是本地下载的路径)
huggingface-cli download Qwen/Qwen2-7B-Instruct --local-dir ./Qwen2-7B-Instruct
huggingface-cli download --repo-type dataset shibing624/alpaca-zh --local-dir ./data/

该数据集是基于GPT4得到的self-instruct数据,遵循Alpaca格式,json文件中每个条目包括:

  • 用户指令(Instruct,必填)
  • 用户输入(Input,额外的补充文本,选填)
  • 模型回答(Output,必填)
  • 系统提示词(选填,shibing624/alpaca-zh数据集中没有该字段)
  • 对话历史(选填,shibing624/alpaca-zh数据集中没有该字段)
[
  {
    "instruction": "辨认不同之处。 ",
    "input": "输入:Twitter,Instagram,Telegram。",
    "output": "Twitter,Instagram 和 Telegram 都是流行的社交媒体平台,但它们之间也有一些不同之处:\n\n- Twitter: 是一个微博社交媒体平台,用户可以发布 280 字符以内的消息(即推文)并与其他用户互动。它主要关注文字交流,尽管用户也可以分享照片和视频。\n- Instagram: 是一个图片和视频分享的社交媒体平台,用户可以在 Instagram 上发布照片和视频并与其他用户互动。它主要关注图片和视频的分享,尽管用户也可以添加文字说明。\n- Telegram: 是一个即时通讯和社交媒体平台,用户可以通过文本、语音、图片和视频与其他用户即时交流。与 Twitter 和 Instagram 不同,Telegram 更侧重于即时通讯,它还提供了端到端加密的私人聊天功能。"
  },
  ...
]

在这里插入图片描述

2. 训练集/测试集划分

在该节,我们编写了两个函数:

  • get_alpaca_dataset: 使用datasets加载json文件,并按照0.9:0.1的比例构建训练集和测试集
  • get_tokenizer_dataset: 对输入的dataset,使用tokenizer获取input_ids, attention_mask, labels;同时该函数会考虑传入datasets和tokenizer为空的情况,通过json_path和tokenizer_path自动加载datasets和tokenizer,如果上面两个参数也没有,则raise报错
from transformers import AutoTokenizer
from datasets import load_dataset


def get_alpaca_dataset(json_path: str, test_size: float=0.1):
    dataset = load_dataset(
        'json', 
        data_files=json_path,
        split="train"
    )
    dataset = dataset.train_test_split(test_size=test_size)
    return dataset


def get_tokenizer_dataset(
        dataset, 
        tokenizer,
        max_length: int=256,
        json_path: str="",
        tokenizer_path: str="",
    ):

    def process_sample(sample):
        input_ids, attention_mask, labels = [], [], []
        instruction = tokenizer(
            "\n".join([
                "Human:" + sample["instruction"],
                sample["input"]
            ]).strip()
            + "\n\nAssistant: "
        )
        responese = tokenizer(sample["output"] + tokenizer.eos_token)
        input_ids = instruction["input_ids"] + responese["input_ids"]
        attention_mask = instruction["attention_mask"] + responese["attention_mask"]
        labels = [-100] * len(instruction["input_ids"]) + responese["input_ids"]
        # 最大长度截断
        if len(input_ids) > max_length:
            input_ids = input_ids[:max_length]
            attention_mask = attention_mask[:max_length]
            labels = labels[:max_length]
        # 返回结果
        return {
            "input_ids": input_ids,
            "attention_mask": attention_mask,
            "labels": labels
        }
    
    # 如果没有传入dataset
    if dataset is None:
        # 如果传入json_path,则自动执行get_alpaca_dataset获取dataset
        if json_path != "":
            dataset = get_alpaca_dataset(json_path=json_path, test_size=0.1)
        # 否则,直接报错
        else:
            raise ValueError("错误参数:dataset不能为空")

    # 如果没有传入tokenizer
    if tokenizer is None:
        # 如果传入tokenizer_path,则加载tokenizer
        if tokenizer_path != "":
            tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
        # 否则,直接报错
        else:
            raise ValueError("错误参数:tokenizer不能为空")
        
    return dataset.map(process_sample, remove_columns=dataset['train'].column_names)

3. 微调finetune

3.1 导入依赖

我们主要使用transformers框架用于加载大模型,使用peft库进行高效微调

import torch
import logging
import warnings
from transformers import (
    AutoTokenizer, 
    AutoModel, 
    AutoModelForCausalLM, 
    DataCollatorForSeq2Seq,
    Trainer, 
    TrainingArguments,
    HfArgumentParser
)
from peft import (
    LoraConfig, 
    PromptEncoderConfig,
    PrefixTuningConfig,
    PromptTuningConfig,
    PromptTuningInit,
    TaskType, 
    get_peft_model
)
from dataclasses import dataclass, field
from dataset_load import get_alpaca_dataset, get_tokenizer_dataset

3.2 设置logger

我们初始化一个日志记录logger,用于记录运行过程中的信息

import logging

# 设置logger
logging.basicConfig(
    level=logging.DEBUG,
    filename='../out/output.log',  # 替换成保存日志的本地目录
    datefmt='%Y/%m/%d %H:%M:%S',
    format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s'
)
logger = logging.getLogger(__name__)

3.3 加载命令行参数

我们需要从sh运行文件中解析需要的参数(包括大模型路径、peft类型、结果保存路径以及max_length等超参数),这里我们基于transformers的HfArgumentParser自动解析命令行的参数,并保存至TrainingArguments(源于transformers库)FinetuneArguments(自定义)

# 设置模型微调的参数类
@dataclass
class FinetuneArguments:
    peft_type: str = field(default="lora")
    llm_model_name: str = field(default="Qwen")
    llm_model_path: str = field(default="../../../model/Qwen2-7B-Instruct")
    dataset_path: str = field(default="../data/alpaca_gpt4_data_zh.json")
    log_path: str = field(default="../log/lora_output.log")
    max_length: int = field(default=256)
    lora_rank: int = field(default=8)

# 加载命令行参数
finetune_args, training_args = HfArgumentParser(
    (FinetuneArguments, TrainingArguments)
).parse_args_into_dataclasses()

3.4 加载大模型

我们的代码中允许用户使用Qwen、ChatGLM以及BaiChuan三类模型,并且按照模型类型设置model的加载方式(AutoModelForCausalLM、AutoModel),对于其他模型,我们直接raise报错(当然你也可以手动对其他模型进行扩充),后文我们都将使用Qwen2-7B-Instruct模型

同时在该代码中,我们也初始化了tokenizer(基于transformers的AutoTokenizer)

# 加载LLMs model/tokenizer
def get_base_llm_model_tokenizer(finetune_args):
    # 读取模型类型
    llm_model_name = finetune_args.llm_model_name
    llm_model_path = finetune_args.llm_model_path

    # 加载llm_model
    if llm_model_name == "Qwen" or llm_model_name == "BaiChuan":
        model = AutoModelForCausalLM.from_pretrained(
            llm_model_path, 
            low_cpu_mem_usage=True, 
            torch_dtype=torch.half
        )
    elif llm_model_name == "ChatGLM":
        model = AutoModel.from_pretrained(
            llm_model_path, 
            low_cpu_mem_usage=True, 
            torch_dtype=torch.half
        )
    # 模型不是为本项目支持的模型
    else:
        logger.error("错误参数:底座模型必须是Qwen/ChatGLM/BaiChuan")
        raise ValueError("错误参数:底座模型必须是Qwen/ChatGLM/BaiChuan")

    # 配置模型
    if finetune_args.peft_type != "prefix-tuning":
        model.gradient_checkpointing_enable()
    model.enable_input_require_grads()
    model.is_parallelizable = True
    model.model_parallel = True
    # 加载tokenizer
    tokenizer = AutoTokenizer.from_pretrained(llm_model_path)

    return model, tokenizer

3.5 设置PEFT的配置config

这里我们同样考虑了四种常见的peft方法:LoRA、p-tuning、prefix-tuning,prompt-tuning。并按照peft_type类型判断当前使用的peft方法,设置相应的配置文件config。如果命令行中传入的peft_type不在上述四种方法中,我们也会raise报错

# 根据peft类型返回相应的config
def get_peft_config(finetune_args, tokenizer):
    # 读取peft类型
    peft_type = finetune_args.peft_type
    if peft_type == "lora":
        peft_config = LoraConfig(
            task_type=TaskType.CAUSAL_LM,
            target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
            inference_mode=False,
            r=finetune_args.lora_rank,
            lora_alpha=32,
            lora_dropout=0.1,
        )
    elif peft_type == "p-tuning":
        peft_config = PromptEncoderConfig(
            task_type=TaskType.CAUSAL_LM, 
            num_virtual_tokens=10,
            encoder_reparameterization_type=PromptEncoderReparameterizationType.MLP,
            encoder_hidden_size=1024
        )
    elif peft_type == "prefix-tuning":
        peft_config = PrefixTuningConfig(
            task_type=TaskType.CAUSAL_LM, 
            num_virtual_tokens=10,
            prefix_projection=True
        )
    elif peft_type == "prompt-tuning":
        peft_config = PromptTuningConfig(
            task_type=TaskType.CAUSAL_LM,
            prompt_tuning_init=PromptTuningInit.TEXT,
            prompt_tuning_init_text = "你是百科全书智能问答机器人。",
            num_virtual_tokens = len(tokenizer("你是百科全书智能问答机器人。")["input_ids"]),
            tokenizer_name_or_path = finetune_args.llm_model_path
        )
    else:
        logger.error("错误参数:peft类型必须为lora/p-tuning/prefix-tuning/prompt-tuning")
        raise ValueError("错误参数:peft类型必须为lora/p-tuning/prefix-tuning/prompt-tuning")

    return peft_config

3.6 进行模型微调

模型微调则使用get_peft_model方法,该函数传入大模型model以及peft的config,使用transformers的Trainer类完成大模型的微调任务

# 微调函数
def finetune_train(model, peft_config, tokenizer, dataset, train_args):
    model = get_peft_model(model=model, peft_config=peft_config)
    trainer = Trainer(
        model=model,
        args=train_args,
        train_dataset=dataset["train"],
        data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)
    )
    trainer.train()

3.7 main函数

这里我们编写了main函数,穿起前面提到的各个流程:

  • 加载命令行参数
  • 设置logger(采用全局变量global)
  • 加载LLM的model和tokenizer
  • 获取peft_config参数
  • 加载微调所使用的数据(基于第二节的get_alpaca_dataset和get_tokenizer_dataset)
  • 完成微调
def main():
    # 忽略警告
    warnings.filterwarnings("ignore")

    # 加载命令行参数
    finetune_args, training_args = HfArgumentParser(
        (FinetuneArguments, TrainingArguments)
    ).parse_args_into_dataclasses()

    # 设置logger
    logging.basicConfig(
        level=logging.DEBUG,
        filename=finetune_args.log_path,  #保存日志的本地目录
        datefmt='%Y/%m/%d %H:%M:%S',
        format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s'
    )
    # 将logger声明为全局变量
    global logger
    logger = logging.getLogger(__name__)
    logger.debug("命令行参数")
    logger.debug("finetune_args:")
    logger.debug(finetune_args.__repr__())
    logger.debug("training_args:")
    logger.debug(training_args.__repr__())

    # 加载模型
    llm_model, llm_tokenizer = get_base_llm_model_tokenizer(finetune_args)
    logger.info('Base LLMs {} load successfully! LLM path::: {}'.format(finetune_args.llm_model_name, finetune_args.llm_model_path))

    # 获取peft_config参数
    peft_config = get_peft_config(finetune_args)
    logger.info('Peft {} config load successfully!'.format(finetune_args.peft_type))

    # 加载数据
    dataset = get_alpaca_dataset(finetune_args.dataset_path, test_size=0.1)
    logger.info('dataset build successfully!')
    tokenizer_dataset = get_tokenizer_dataset(dataset, llm_tokenizer, max_length=finetune_args.max_length)
    logger.info('tokenizer dataset build successfully!')

    # 开始训练
    logger.info('Train start!')
    finetune_train(model=llm_model, peft_config=peft_config, tokenizer=llm_tokenizer, dataset=tokenizer_dataset, train_args=training_args)
    logger.info('Train end! LoRA model saves in the path:::{}'.format(training_args.output_dir))


if __name__ == "__main__":
    main()

在sh文件中,我们传入peft所需要的所有参数:

  • finetune_args:

    • peft类型
    • 大模型名字、大模型本地路径(相对)
    • 数据集本地路径(相对)
    • log文档保存路径(相对)
    • 文本最大长度max_length
    • LoRA的秩
  • training_args:

    • 模型保存路径
    • 训练阶段的batch size
    • 验证阶段的batch size
    • 训练轮数
    • 最大步长
    • 保存checkoutpoint周期
    • 学习率等

sh文件:

python3 finetune.py \
    --peft_type lora \
    --llm_model_name Qwen \
    --llm_model_path ../../../model/Qwen2-7B-Instruct \
    --dataset_path ../data/alpaca_gpt4_data_zh.json \
    --log_path ../out/lora_output.log \
    --max_length 256 \
    --lora_rank 8 \
    --output_dir ../out/lora_peft \
    --per_device_train_batch_size 1 \   
    --per_device_eval_batch_size 1 \
    --num_train_epochs 1 \
    --learning_rate 1e-4 \
    --max_steps 2400 \
    --save_steps 240 \
    --save_total_limit 10 \
    --logging_steps 10 \
    --gradient_accumulation_steps 16 \

参考

  1. LoRA微调Qwen参考教程
  2. 本项目链接
  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值