Qwen 微调脚本分析

Qwen 微调脚本分析

AI学习交流qq群873673497
官网turingevo.com
邮箱wmx@turingevo.com
githubhttps://github.com/turingevo
huggingfacehttps://huggingface.co/turingevo

Qwen/finetune.py :

# 基于fastchat和tatsu-lab/stanford_alpaca的修订代码,用于训练语言模型
# 提供使用LoRA(低秩适应)和量化(QLoRA)压缩的选项,以及使用DeepSpeed的分布式训练支持

# 导入各种必要的库和模块
from dataclasses import dataclass, field
import json
import math
import logging
import os
from typing import Dict, Optional, List
import torch
from torch.utils.data import Dataset
from deepspeed import zero
from deepspeed.runtime.zero.partition_parameters import ZeroParamStatus
import transformers
from transformers import Trainer, GPTQConfig, deepspeed
from transformers.trainer_pt_utils import LabelSmoother
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from accelerate.utils import DistributedType

# 忽略标记ID,来自LabelSmoother的忽略索引
IGNORE_TOKEN_ID = LabelSmoother.ignore_index

# 模型参数类,定义模型相关的参数
@dataclass
class ModelArguments:
    model_name_or_path: Optional[str] = field(default="Qwen/Qwen-7B")

# 数据参数类,定义数据集相关的参数
@dataclass
class DataArguments:
    data_path: str = field(
        default=None, metadata={"help": "Path to the training data."}
    )
    eval_data_path: str = field(
        default=None, metadata={"help": "Path to the evaluation data."}
    )
    lazy_preprocess: bool = False

# 训练参数类,继承自transformers的TrainingArguments,定义训练相关的参数
@dataclass
class TrainingArguments(transformers.TrainingArguments):
    cache_dir: Optional[str] = field(default=None)
    optim: str = field(default="adamw_torch")
    model_max_length: int = field(
        default=8192,
        metadata={
            "help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."
        },
    )
    use_lora: bool = False

# Lora参数类,定义Lora模型压缩相关的参数
@dataclass
class LoraArguments:
    lora_r: int = 64
    lora_alpha: int = 16
    lora_dropout: float = 0.05
    lora_target_modules: List[str] = field(
        default_factory=lambda: ["c_attn", "c_proj", "w1", "w2"]
    )
    lora_weight_path: str = ""
    lora_bias: str = "none"
    q_lora: bool = False

# 将参数从DeepSpeed的Zero优化器转换为可用于其他操作的形式
def maybe_zero_3(param):
    if hasattr(param, "ds_id"):
        assert param.ds_status == ZeroParamStatus.NOT_AVAILABLE
         # 如果使用Zero-3,将参数转移到CPU并克隆
        with zero.GatheredParameters([param]):
            param = param.data.detach().cpu().clone()
    else:
     	# 否则,直接将参数转移到CPU并克隆
        param = param.detach().cpu().clone()
    return param

# 从模型中获取PEFT状态字典,可能涉及Zero优化器的转换
def get_peft_state_maybe_zero_3(named_params, bias):
    # 根据bias参数选择要返回的参数部分
    if bias == "none":
        to_return = {k: t for k, t in named_params if "lora_" in k}
    elif bias == "all":
        to_return = {k: t for k, t in named_params if "lora_" in k or "bias" in k}
    elif bias == "lora_only":
        to_return = {}
        maybe_lora_bias = {}
        lora_bias_names = set()
        for k, t in named_params:
            if "lora_" in k:
                to_return[k] = t
                bias_name = k.split("lora_")[0] + "bias"
                lora_bias_names.add(bias_name)
            elif "bias" in k:
                maybe_lora_bias[k] = t
        for k, t in maybe_lora_bias:
            if bias_name in lora_bias_names:
                to_return[bias_name] = t
    else:
        raise NotImplementedError
    # 对选取的参数进行必要的转换
    to_return = {k: maybe_zero_3(v) for k, v in to_return.items()}
    return to_return

# 获取本地排名,如果未设置则为None
local_rank = None

# 在本地排名为0时打印信息
def rank0_print(*args):
    if local_rank == 0:
        print(*args)
        
def safe_save_model_for_hf_trainer(trainer: transformers.Trainer, output_dir: str, bias="none"):
    """
	Collects the state dict and dump to disk.
    根据transformers.Trainer实例的安全方式保存模型到指定目录。
    
    参数:
    - trainer: transformers.Trainer 实例,代表一个训练器,用于获取模型状态等。
    - output_dir: str,指定保存模型的输出目录。
    - bias: str,默认为"none",表示处理模型偏置的方式,本函数暂不支持更改偏置。

    说明:
    - 该函数会根据是否启用zero3模式、是否使用LORA,来决定如何获取模型的状态字典,并保存至磁盘。
    - 仅当trainer的args.should_save标志为True且args.local_rank为0时,才会真正保存模型。
    """
    # 检查是否启用了zero3模式
    if deepspeed.is_deepspeed_zero3_enabled():
        state_dict = trainer.model_wrapped._zero3_consolidated_16bit_state_dict()
    else:
        # 根据是否使用LORA,选择不同的方式获取状态字典
        if trainer.args.use_lora:
            state_dict = get_peft_state_maybe_zero_3(
                trainer.model.named_parameters(), bias
            )
        else:
            state_dict = trainer.model.state_dict()
    # 条件满足时,执行模型保存操作
    if trainer.args.should_save and trainer.args.local_rank == 0:
        trainer._save(output_dir, state_dict=state_dict)

def preprocess(
    sources,
    tokenizer: transformers.PreTrainedTokenizer,
    max_len: int,
    system_message: str = "You are a helpful assistant."
) -> Dict:

   """
    对输入源数据进行预处理,包括编码、填充等,以便于输入到模型中进行训练或推理。
    
    参数:
    - sources: 输入源数据列表,每个源数据是一个字典列表,表示对话中的不同角色的句子。
    - tokenizer: transformers.PreTrainedTokenizer,用于对文本进行编码的分词器。
    - max_len: int,指定编码后序列的最大长度。
    - system_message: str,默认为"You are a helpful assistant.",表示系统消息。

    返回值:
    - 预处理后数据的字典,包括input_ids(编码后的输入序列)、labels(对应的标签序列)和attention_mask(注意力掩码)。

    说明:
    - 该函数首先定义了不同角色的起始标记,然后根据输入源数据,应用模板并进行编码,同时生成对应的标签序列。
    - 最后,对编码后的序列进行填充至最大长度,并返回预处理后的数据。
    """
    
    # 定义不同角色的标记
    roles = {"user": "<|im_start|>user", "assistant": "<|im_start|>assistant"}

    # 获取tokenizer的开始和结束标记的ID
    im_start = tokenizer.im_start_id
    im_end = tokenizer.im_end_id
    # 将换行符转换为input_ids
    nl_tokens = tokenizer('\n').input_ids
    # 为不同的角色创建对应的标记序列
    _system = tokenizer('system').input_ids + nl_tokens
    _user = tokenizer('user').input_ids + nl_tokens
    _assistant = tokenizer('assistant').input_ids + nl_tokens

    # 初始化存储输入ID和目标ID的列表
    input_ids, targets = [], []
    
    # 遍历每个对话数据
    for i, source in enumerate(sources):
        # 如果对话的第一个消息不是用户的消息,则跳过它
        if roles[source[0]["from"]] != roles["user"]:
            source = source[1:]

        # 初始化当前对话的输入ID和目标ID列表
        input_id, target = [], []
        
        # 创建系统消息的输入ID序列,并添加到input_id
        system = [im_start] + _system + tokenizer(system_message).input_ids + [im_end] + nl_tokens
        input_id += system
        # 创建系统消息的目标ID序列,并添加到target,忽略序列中的某些部分
        target += [im_start] + [IGNORE_TOKEN_ID] * (len(system)-3) + [im_end] + nl_tokens
        # 确保input_id和target的长度相同
        assert len(input_id) == len(target)
        
        # 遍历对话中的每个句子
        for j, sentence in enumerate(source):
            role = roles[sentence["from"]]
            # 创建当前句子的输入ID序列
            _input_id = tokenizer(role).input_ids + nl_tokens + \
                tokenizer(sentence["value"]).input_ids + [im_end] + nl_tokens
            input_id += _input_id
            # 根据角色创建当前句子的目标ID序列,并添加到target
            if role == '<|im_start|>user':
                _target = [im_start] + [IGNORE_TOKEN_ID] * (len(_input_id)-3) + [im_end] + nl_tokens
            elif role == '<|im_start|>assistant':
                _target = [im_start] + [IGNORE_TOKEN_ID] * len(tokenizer(role).input_ids) + \
                    _input_id[len(tokenizer(role).input_ids)+1:-2] + [im_end] + nl_tokens
            else:
                raise NotImplementedError
            target += _target
        
        # 确保input_id和target的长度相同
        assert len(input_id) == len(target)
        
        # 如果input_id序列长度小于max_len,则用padding填充
        input_id += [tokenizer.pad_token_id] * (max_len - len(input_id))
        # 对应地,target序列也进行填充
        target += [IGNORE_TOKEN_ID] * (max_len - len(target))
        
        # 将当前对话的input_id和target添加到列表中
        input_ids.append(input_id[:max_len])
        targets.append(target[:max_len])
    
    # 将input_ids和targets列表转换为张量
    input_ids = torch.tensor(input_ids, dtype=torch.int)
    targets = torch.tensor(targets, dtype=torch.int)
    
    # 创建包含input_ids、labels和attention_mask的字典,并返回
    return dict(
        input_ids=input_ids,
        labels=targets,
        attention_mask=input_ids.ne(tokenizer.pad_token_id),  # 创建attention_mask,排除padding部分
    )

 
 class SupervisedDataset(Dataset):
    """Dataset for supervised fine-tuning."""
    # 构造函数,初始化SupervisedDataset类的实例
    def __init__(self, raw_data, tokenizer: transformers.PreTrainedTokenizer, max_len: int):
        super(SupervisedDataset, self).__init__()  # 调用基类的构造函数

        # 在rank 0进程中打印格式化输入信息
        rank0_print("Formatting inputs...")
        # 从原始数据中提取对话数据
        sources = [example["conversations"] for example in raw_data]
        # 使用preprocess函数预处理对话数据
        data_dict = preprocess(sources, tokenizer, max_len)

        # 将预处理后的数据赋值给实例变量
        self.input_ids = data_dict["input_ids"]
        self.labels = data_dict["labels"]
        self.attention_mask = data_dict["attention_mask"]

    # 返回数据集中样本的数量
    def __len__(self):
        return len(self.input_ids)

    # 根据索引i获取数据集中的样本
    def __getitem__(self, i) -> Dict[str, torch.Tensor]:
        return dict(
            input_ids=self.input_ids[i],
            labels=self.labels[i],
            attention_mask=self.attention_mask[i],
        )


class LazySupervisedDataset(Dataset):
    """Dataset for supervised fine-tuning."""
    # LazySupervisedDataset类继承自Dataset类,用于实现惰性加载的数据集

    def __init__(self, raw_data, tokenizer: transformers.PreTrainedTokenizer, max_len: int):
        super(LazySupervisedDataset, self).__init__()
        # 调用父类构造函数进行初始化

        self.tokenizer = tokenizer
        # 将传入的tokenizer对象保存为实例变量

        self.max_len = max_len
        # 将最大序列长度保存为实例变量

        rank0_print("Formatting inputs...Skip in lazy mode")
        # 在rank 0进程中打印提示信息,表明在惰性模式下跳过格式化输入
		self.tokenizer = tokenizer

        self.raw_data = raw_data
        # 将原始数据保存为实例变量

        self.cached_data_dict = {}
        # 初始化一个空字典,用于缓存预处理后的数据,以提高性能

    def __len__(self):
        return len(self.raw_data)
    # __len__方法返回数据集中样本的数量,即原始数据的长度

    def __getitem__(self, i) -> Dict[str, torch.Tensor):
        # __getitem__方法根据索引i获取数据集中的样本
        if i in self.cached_data_dict:
            # 如果索引i对应的数据已经在缓存中,则直接返回缓存的数据
            return self.cached_data_dict[i]

        # 对单个样本进行预处理
        ret = preprocess([self.raw_data[i]["conversations"]], self.tokenizer, self.max_len)
        # 预处理后的数据是一个包含input_ids, labels和attention_mask的字典

        # 从预处理结果中提取第一个样本的数据,并将其存储在新的字典中
        ret = dict(
            input_ids=ret["input_ids"][0],
            labels=ret["labels"][0],
            attention_mask=ret["attention_mask"][0],
        )

        # 将预处理的数据存储在缓存字典中,以备后续使用
        self.cached_data_dict[i] = ret

        # 返回预处理后的数据
        return ret


    # 定义一个函数,用于创建监督式微调的数据模块
def make_supervised_data_module(
    tokenizer: transformers.PreTrainedTokenizer,  # 用于分词的tokenizer对象
    data_args,  # 数据参数,包含数据路径和是否使用惰性加载等配置
    max_len,  # 输入序列的最大长度
) -> Dict:
    
    # 根据是否采用惰性加载选择数据集类
    dataset_cls = (
        LazySupervisedDataset if data_args.lazy_preprocess else SupervisedDataset
    )
    
    # 在rank 0进程中打印加载数据信息
    rank0_print("Loading data...")

    # 打开并加载训练数据的JSON文件
    train_json = json.load(open(data_args.data_path, "r"))
    # 创建训练数据集实例
    train_dataset = dataset_cls(train_json, tokenizer=tokenizer, max_len=max_len)

    # 如果提供了评估数据路径,则加载评估数据集
    if data_args.eval_data_path:
        eval_json = json.load(open(data_args.eval_data_path, "r"))
        eval_dataset = dataset_cls(eval_json, tokenizer=tokenizer, max_len=max_len)
    else:
        # 如果没有提供评估数据路径,则设置评估数据集为None
        eval_dataset = None

    # 返回包含训练数据集和评估数据集的字典
    return dict(train_dataset=train_dataset, eval_dataset=eval_dataset)
           
   # 这段代码定义了一个名为 `train` 的函数,它是启动模型训练流程的主函数。下面是对函数中每一行的逐行注释:

# 定义训练函数
def train():
    global local_rank  # 声明local_rank为全局变量,以便在函数内部修改其值

    # 创建一个参数解析器,用于解析命令行参数到数据类
    parser = transformers.HfArgumentParser(
        (ModelArguments, DataArguments, TrainingArguments, LoraArguments)
    )
    # 解析命令行参数到对应的数据类实例
    (
        model_args,
        data_args,
        training_args,
        lora_args,
    ) = parser.parse_args_into_dataclasses()

    # 如果使用DeepSpeed并且是单GPU环境,设置分布式类型为DeepSpeed
    if getattr(training_args, 'deepspeed', None) and int(os.environ.get("WORLD_SIZE", 1))==1:
        training_args.distributed_state.distributed_type = DistributedType.DEEPSPEED

    # 设置local_rank为训练参数中的local_rank值
    local_rank = training_args.local_rank

    # 初始化设备映射和世界大小
    device_map = None
    world_size = int(os.environ.get("WORLD_SIZE", 1))
    ddp = world_size != 1
    # 如果使用QLoRA并且是分布式数据并行(DDP)环境,设置设备映射
    if lora_args.q_lora:
        device_map = {"": int(os.environ.get("LOCAL_RANK") or 0)} if ddp else "auto"
        # 如果同时使用FSDP或ZeRO-3和QLoRA,输出警告信息
        if len(training_args.fsdp) > 0 or deepspeed.is_deepspeed_zero3_enabled():
            logging.warning(
                "FSDP or ZeRO3 are incompatible with QLoRA."
            )

    # 检查模型是否为聊天模型
    is_chat_model = 'chat' in model_args.model_name_or_path.lower()
    # 如果使用LoRA并且启用了ZeRO-3,但不是聊天模型,抛出错误
    if (
            training_args.use_lora
            and not lora_args.q_lora
            and deepspeed.is_deepspeed_zero3_enabled()
            and not is_chat_model
    ):
        raise RuntimeError("ZeRO3 is incompatible with LoRA when finetuning on base model.")

    # 设置模型加载参数
    model_load_kwargs = {
        'low_cpu_mem_usage': not deepspeed.is_deepspeed_zero3_enabled(),
    }

    # 加载模型配置
    config = transformers.AutoConfig.from_pretrained(
        model_args.model_name_or_path,
        cache_dir=training_args.cache_dir,
        trust_remote_code=True,
    )
    # 禁用缓存
    config.use_cache = False

    # 加载模型和分词器
    model = transformers.AutoModelForCausalLM.from_pretrained(
        model_args.model_name_or_path,
        config=config,
        cache_dir=training_args.cache_dir,
        device_map=device_map,
        trust_remote_code=True,
        # 如果使用LoRA和QLoRA,设置量化配置
        quantization_config=GPTQConfig(
            bits=4, disable_exllama=True
        )
        if training_args.use_lora and lora_args.q_lora
        else None,
        **model_load_kwargs,
    )
    tokenizer = transformers.AutoTokenizer.from_pretrained(
        model_args.model_name_or_path,
        cache_dir=training_args.cache_dir,
        model_max_length=training_args.model_max_length,
        padding_side="right",
        use_fast=False,
        trust_remote_code=True,
    )
    # 设置分词器的填充标记ID
    tokenizer.pad_token_id = tokenizer.eod_id

    # 如果使用LoRA
    if training_args.use_lora:
        if lora_args.q_lora or is_chat_model:
            modules_to_save = None
        else:
            modules_to_save = ["wte", "lm_head"]
        # 创建LoRA配置
        lora_config = LoraConfig(
            r=lora_args.lora_r,
            lora_alpha=lora_args.lora_alpha,
            target_modules=lora_args.lora_target_modules,
            lora_dropout=lora_args.lora_dropout,
            bias=lora_args.lora_bias,
            task_type="CAUSAL_LM",
            modules_to_save=modules_to_save  # 用于添加新标记的参数
        )
        # 如果使用QLoRA,准备模型进行k-bit训练
        if lora_args.q_lora:
            model = prepare_model_for_kbit_training(
                model, use_gradient_checkpointing=training_args.gradient_checkpointing
            )

        # 应用LoRA配置到模型
        model = get_peft_model(model, lora_config)

        # 打印LoRA可训练参数
        model.print_trainable_parameters()

        # 如果启用梯度检查点,启用模型的输入梯度
        if training_args.gradient_checkpointing:
            model.enable_input_require_grads()

    # 加载数据模块
    data_module = make_supervised_data_module(
        tokenizer=tokenizer, data_args=data_args, max_len=training_args.model_max_length
    )

    # 创建训练器
    trainer = Trainer(
        model=model, tokenizer=tokenizer, args=training_args, **data_module
    )

    # 开始训练
    trainer.train()
    # 保存训练状态
    trainer.save_state()

    # 安全地保存模型到硬盘
    safe_save_model_for_hf_trainer(trainer=trainer, output_dir=training_args.output_dir, bias=lora_args.lora_bias)

if __name__ == "__main__":
    train()



使用微调脚本 finetune_lora_single_gpu.sh

Qwen/finetune/finetune_lora_single_gpu.sh :

#!/bin/bash

export CUDA_DEVICE_MAX_CONNECTIONS=1
# 设置环境变量,用于控制每个GPU设备的最大连接数

# Set the path if you do not want to load from huggingface directly
MODEL="/media/huggingface_cache/Qwen-1_8B-Chat" 
# 设置模型的路径,如果不想直接从huggingface加载模型,可以在这里指定本地模型路径

# ATTENTION: specify the path to your training data, which should be a json file consisting of a list of conversations.
# 提示:指定训练数据的路径,它应该是一个包含对话列表的json文件。
# See the section for finetuning in README for more information.
# 有关微调的更多信息,请参见README的相关部分。
# DATA="path_to_data"
DATA=/media/huggingface_cache/DISC-Law-SFT/train_data_law.json
# 设置训练数据的路径

function usage() {
    echo '
Usage: bash finetune/finetune_lora_single_gpu.sh [-m MODEL_PATH] [-d DATA_PATH]
'
    # 定义usage函数,用于显示脚本的使用方法
}

while [[ "$1" != "" ]]; do
    # 循环处理命令行参数
    case $1 in
        -m | --model )
            shift
            MODEL=$1
            # 如果命令行参数是-m或--model,则设置MODEL变量为下一个参数的值
            ;;
        -d | --data )
            shift
            DATA=$1
            # 如果命令行参数是-d或--data,则设置DATA变量为下一个参数的值
            ;;
        -h | --help )
            usage
            exit 0
            # 如果命令行参数是-h或--help,则显示使用方法并退出脚本
            ;;
        * )
            echo "Unknown argument ${1}"
            exit 1
            # 对于未知的命令行参数,打印错误信息并退出脚本
            ;;
    esac
    shift
    # 移动到下一个命令行参数
done

export CUDA_VISIBLE_DEVICES=0
# 设置环境变量,指定要使用的GPU设备编号

python finetune.py \
--model_name_or_path $MODEL
# 指定模型的路径或名称。如果是基于预训练模型的微调,可以是模型的标识符;如果是本地模型,可以是模型文件的路径。

--data_path $DATA
# 指定训练数据的路径。训练数据应该是一个JSON文件,包含用于微调的对话列表。

--bf16 False
# 指定是否使用bfloat16进行训练。bfloat16是一种16位浮点数格式,可以提供与32位浮点数相似的精度,同时减少内存使用。

--output_dir output_qwen
# 指定模型和训练输出(如检查点、配置文件等)的保存目录。

--num_train_epochs 5
# 指定训练的总轮数(epoch)。每个epoch都会遍历一次完整的训练数据集。

--per_device_train_batch_size 8
# 指定每个设备(GPU/CPU)上用于训练的批次大小。

--per_device_eval_batch_size 1
# 指定每个设备(GPU/CPU)上用于评估的批次大小。

--gradient_accumulation_steps 8
# 指定梯度累积的步数。这允许模型在每个更新之前累积梯度,可以减少内存使用并允许更大的批次大小。

--evaluation_strategy "no"
# 指定评估策略。"no"表示不进行评估。

--save_strategy "steps"
# 指定保存模型的策略,"steps"表示每训练一定步数后保存模型。

--save_steps 200
# 指定每训练多少步后保存模型。

--save_total_limit 10
# 指定最多保存的模型检查点数量。

--learning_rate 3e-4
# 指定模型训练的初始学习率。

--weight_decay 0.1
# 指定权重衰减的量,这是一种正则化技术,用于防止过拟合。

--adam_beta2 0.95
# 指定Adam优化器的beta2参数,它影响优化器的动量项。

--warmup_ratio 0.01
# 指定学习率预热(warmup)的比例。预热期间,学习率会从0逐渐增加到初始学习率。

--lr_scheduler_type "cosine"
# 指定学习率调度器的类型,这里使用的是余弦退火调度器。

--logging_steps 1
# 指定记录日志的步数,即每训练多少步记录一次信息。

--report_to "none"
# 指定不向任何外部系统报告训练进度。

--model_max_length 512
# 指定模型输入序列的最大长度。

--lazy_preprocess True
# 指定是否使用惰性预处理。惰性预处理可以在处理大型数据集时节省内存。

--gradient_checkpointing
# 指定是否使用梯度检查点技术,这是一种节省内存的技术,通过丢弃一些中间激活来减少内存使用。

--use_lora
# 指定是否使用LoRA(Low-Rank Adaptation)技术,这是一种微调大型模型的技术,通过在模型的特定部分引入低秩结构来减少参数数量。
# 运行python脚本finetune.py,传入一系列参数用于配置微调过程

# If you use fp16 instead of bf16, you should use deepspeed
# --fp16 True --deepspeed finetune/ds_config_zero2.json
# 注释掉的部分是关于使用fp16代替bf16时的建议,应该使用deepspeed,并指定配置文件

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值