Minicpm-V LoRA微调

857 阅读9分钟

(‌‍⁠​‬​​​​⁠‌⁠‍​‬‌​​​​​​​‌​​​​⁠⁠​​‌​​​​​​⁠​⁠‬​​​​MiniCPM-V 2.6 训练指南 - 飞书云文档 (feishu.cn))

@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."}
    )
  
  • data_path:训练数据的路径
  • eval_data_apth:评估数据的路径
  • field 是一个函数,用于定义类属性并提供元数据,通常用于数据类(Python 3.7+ 的 dataclasses 库)中,帮助设置默认值和附加信息,如文档说明,这有助于增强代码可读性和维护性。在这个例子中,field 设置了属性的默认值和帮助信息。
    """
@dataclass
class TrainingArguments(transformers.TrainingArguments):
    cache_dir: Optional[str] = field(default=None)
    optim: str = field(default="adamw_torch")
    model_max_length: int = field(
        default=2048,
        metadata={
            "help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."
        },
    )
    tune_vision: Optional[bool] = field(default=True)
    tune_llm: Optional[bool] = field(default=True)
    llm_type: str = field(default="minicpm")
    use_lora: Optional[bool] = field(default=False)
    max_slice_nums: Optional[int] = field(default=9)

TrainingArguments继承自transformers.TrainingArguments,并扩展了以下字段:

  • cache_dir:缓存目录路径,默认为None。
  • optim:优化器类型,默认为adamw_torch
  • model_max_length:模型的最大序列长度,默认为2048,并会在必要时对序列进行右填充或截断。
  • tune_vision和tune_llm:分别表示是否调优视觉模型和语言模型,默认均为True。
  • llm_type:语言模型类型,默认为minicpm。
  • use_lora:是否使用LoRA,默认为False。
  • max_slice_nums:最大切片数量,默认为9。
@dataclass
class LoraArguments:
    lora_r: int = 64
    lora_alpha: int = 64
    lora_dropout: float = 0.05
    lora_target_modules: str = r"llm..*layers.\d+.self_attn.(q_proj|k_proj|v_proj)"
    lora_weight_path: str = ""
    lora_bias: str = "none"
    q_lora: bool = False
    lora_modules_to_save: str = ""
    lora_layer_replication: Optional[List[Tuple[int, int]]] = None
    lora_layers_to_transform: Optional[List[int]] = None
    lora_layers_pattern: Optional[str] = None

定义了LoRA(低秩适应)参数,用于微调模型。各属性功能如下:

  • lora_r: LoRA秩,默认为64。
  • lora_alpha: 矩阵分解中的缩放因子,默认为64。
  • lora_dropout: LoRA dropout比率,默认为0.05。
  • lora_target_modules: 要应用LoRA的目标模块正则表达式。
  • lora_weight_path: LoRA权重路径。
  • lora_bias: 偏置选项,默认为"none"。
  • q_lora: 是否启用Q-LoRA,默认为False。
  • lora_modules_to_save: 需要保存的模块名称。
  • lora_layer_replication: 层复制配置。
  • lora_layers_to_transform: 需要转换的层列表。
  • lora_layers_pattern: 层模式匹配字符串。
local_rank = None
def rank0_print(*args):
    if local_rank == 0:
        print(*args)


def safe_save_model_for_hf_trainer(trainer, output_dir: str, bias="none"):
    """Collects the state dict and dump to disk."""
    if trainer.args.should_save and trainer.args.local_rank == 0:
        trainer.save_model(output_dir,)

local_rank 通常在分布式训练中用于标识当前进程(或称为rank)的本地排名,特别是在多GPU环境中。 其主要作用如下:

  • 进程标识:帮助区分不同GPU上的进程,确保某些操作只在一个特定的进程中执行,比如日志记录或模型保存。
  • 条件执行:如示例中的 rank0_print 和 safe_save_model_for_hf_trainer 函数所示,仅当 local_rank 为 0 时才执行特定操作,避免在多个进程中重复执行相同任务。
def make_supervised_data_module(
    tokenizer: transformers.PreTrainedTokenizer,
    data_args,
    transform,
    data_collator=None,
    llm_type="minicpm",
    slice_config=None,
    patch_size=14,
    query_nums=64,
    batch_vision=False,
    max_length=2048,
) -> Dict:
    """Make dataset and collator for supervised fine-tuning."""
    dataset_cls = SupervisedDataset

    rank0_print("Loading data...")

    train_json = json.load(open(data_args.data_path, "r"))
    train_dataset = dataset_cls(
        train_json,
        transform,
        tokenizer,
        slice_config=slice_config,
        llm_type=llm_type,
        patch_size=patch_size,
        query_nums=query_nums,
        batch_vision=batch_vision,
        max_length=max_length,
    )

    if data_args.eval_data_path:
        eval_json = json.load(open(data_args.eval_data_path, "r"))
        eval_dataset = dataset_cls(
            eval_json,
            transform,
            tokenizer,
            slice_config=slice_config,
            llm_type=llm_type,
            patch_size=patch_size,
            query_nums=query_nums,
            batch_vision=batch_vision,
            max_length=max_length,
        )
    else:
        eval_dataset = None

    return dict(
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        data_collator= partial(data_collator, max_length=max_length),
    )

该函数用于生成监督微调所需的数据集和数据合并器。主要功能包括:

  • 读取训练和评估数据(JSON格式);
  • 使用指定参数初始化训练数据集和(可选的)评估数据集;
  • 返回包含训练数据集、评估数据集(如存在)及数据合并器的字典。
def build_transform():
    IMAGENET_INCEPTION_MEAN = (0.5, 0.5, 0.5) # timm.data.IMAGENET_INCEPTION_MEAN
    IMAGENET_INCEPTION_STD = (0.5, 0.5, 0.5)  # timm.data.IMAGENET_INCEPTION_STD
    return transforms.Compose(
            [
                transforms.ToTensor(),
                transforms.Normalize(
                    mean=IMAGENET_INCEPTION_MEAN, std=IMAGENET_INCEPTION_STD
                ),
            ]
        )

图像预处理

def get_parameter_number(model):
    trainable_params, all_param = 0, 0
    for param in model.parameters():
        num_params = param.numel()
        # if using DS Zero 3 and the weights are initialized empty
        if num_params == 0 and hasattr(param, "ds_numel"):
            num_params = param.ds_numel

        all_param += num_params
        if param.requires_grad:
            trainable_params += num_params
        
    return {'Total': all_param, 'Trainable': trainable_params}

用于统计模型的参数数量,包括总参数数和可训练参数数。具体功能如下:

  • 遍历模型的所有参数,并计算每个参数的数量。
  • 如果参数初始化为空且使用了DS Zero 3,则使用ds_numel属性获取参数数量。
  • 统计所有参数总数(all_param)。
  • 如果参数需要梯度更新,则累加到可训练参数总数(trainable_params)。
  • 返回一个字典,包含总参数数和可训练参数数。
local_rank = 0
def train():
    global local_rank
    # 使用 HfArgumentParser 解析命令行参数或配置文件中的参数,并将它们分别解析到四个类中:
    # ModelArguments、DataArguments、TrainingArguments 和 LoraArguments。
    # 这有助于组织和管理模型、数据、训练及 LoRA 相关的参数设置。
    parser = transformers.HfArgumentParser(
        (ModelArguments, DataArguments, TrainingArguments, LoraArguments)
    )
    # 该行代码将命令行参数解析为四个不同的数据类:
    # model_args:模型相关参数。
    # data_args:数据集相关参数。
    # training_args:训练过程相关参数。
    # lora_args:LoRA(低秩适应)技术相关参数。这通常用于微调大模型。
    (
        model_args,
        data_args,
        training_args,
        lora_args,
    ) = parser.parse_args_into_dataclasses()
    # 该段代码功能如下:
    # 检查training_args对象中是否包含deepspeed属性。
    # 如果该属性存在,则设置training_args.distributed_state.distributed_type为DistributedType.DEEPSPEED,指示使用DeepSpeed进行分布式训练。
    if getattr(training_args, "deepspeed", None) : 
        training_args.distributed_state.distributed_type = DistributedType.DEEPSPEED
    # 该函数根据training_args中的配置选择计算时使用的数据类型:
    # 如果fp16为真,则使用半精度浮点数(torch.float16)。
    # 否则,如果bf16为真,则使用bfloat16格式的浮点数(torch.bfloat16)。
    # 默认使用单精度浮点数(torch.float32)。
    compute_dtype = (
        torch.float16
        if training_args.fp16
        else (torch.bfloat16 if training_args.bf16 else torch.float32)
    )
    # 获取本地排名 local_rank,用于分布式训练。
    # 从环境变量获取世界大小 WORLD_SIZE(进程数),默认为1。
    # 判断是否使用分布式数据并行(DDP)训练,若进程数大于1则使用DDP。
    # 初始化 device_map 为 None,用于指定模型和张量的设备映射。
    local_rank = training_args.local_rank
    world_size = int(os.environ.get("WORLD_SIZE", 1))
    ddp = world_size != 1
    device_map = None
    # 检查是否启用QLoRA训练。
    # 根据分布式训练环境设置设备映射。
    # 若检测到使用FSDP或ZeRO3,会发出警告,提示它们与QLoRA不兼容。
    if lora_args.q_lora:
        device_map = {"": int(os.environ.get("LOCAL_RANK") or 0)} if ddp else None
        if len(training_args.fsdp) > 0 or deepspeed.is_deepspeed_zero3_enabled():
            logging.warning(
                "FSDP or ZeRO3 are not incompatible with QLoRA."
            )
    # 加载模型-允许多卡-计算的数据类型-参数映射的设备
    model = AutoModel.from_pretrained(
        model_args.model_name_or_path,
        trust_remote_code=True,
        torch_dtype=compute_dtype,
        device_map=device_map,
    )
    # 从预训练模型路径加载一个自动分词器(tokenizer)
    tokenizer = AutoTokenizer.from_pretrained(
        model_args.model_name_or_path, trust_remote_code=True
    )
    # 根据training_args中的tune_vision和tune_llm值决定是否冻结模型的部分参数:
    # 若tune_vision为False,则冻结model.vpm部分的梯度;
    # 若tune_llm为False,则冻结model.llm部分的梯度。
    if not training_args.tune_vision:
        model.vpm.requires_grad_(False)
    if not training_args.tune_llm:
        model.llm.requires_grad_(False)
    # 检查training_args中的use_lora和tune_llm配置:
    # 若同时启用use_lora和tune_llm,则抛出ValueError,因为模型不能同时调整LLM参数并应用LoRA。
    # 若仅启用use_lora,则打印信息表明正在使用LoRA微调MiniCPM-V模型。
    if training_args.use_lora:
        if training_args.use_lora and training_args.tune_llm:
            raise ValueError("The model cannot simultaneously adjust LLM parameters and apply LoRA.")
            
        rank0_print("Currently using LoRA for fine-tuning the MiniCPM-V model.")
        # 冻结为model.llm的模型参数
        for name, param in model.llm.named_parameters():
            param.requires_grad = False
        #     定义了一个列表modules_to_save,其中包含两个字符串元素'embed_tokens'和'resampler'。
        #     如果training_args.tune_vision为真(True),则向modules_to_save列表中添加一个新的字符串元素'vpm'。
        # 作用:指定哪些模块需要被保存或处理
        modules_to_save = ['embed_tokens','resampler']
        if training_args.tune_vision:
            modules_to_save.append('vpm')
        #     创建一个LoraConfig对象,配置LoRA(Low-Rank Adaptation)模型的参数
        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,
            layers_to_transform=lora_args.lora_layers_to_transform,
            modules_to_save=modules_to_save,
        )
        # 为model对象动态添加了一个名为get_input_embeddings的方法。此方法的功能是从model的llm属性中获取输入嵌入(input_embeddings)。具体步骤如下:
        # 检查model是否已有get_input_embeddings方法。若无,则定义一个新方法并绑定到model上。
        if not hasattr(model, 'get_input_embeddings'):
            def get_input_embeddings(self):
                return self.llm.get_input_embeddings()
            model.get_input_embeddings = MethodType(get_input_embeddings, model)
        # 若lora_args.q_lora为真,则准备模型进行K - bit训练,并根据需要设置梯度检查点。
        if lora_args.q_lora:
            model = prepare_model_for_kbit_training(
                model, use_gradient_checkpointing=training_args.gradient_checkpointing
            )
        # 使用LoRA(低秩适应)方法对模型进行微调,应用指定的LoRA配置。
        model = get_peft_model(model, lora_config)
        # 检查training_args中是否设置了gradient_checkpointing。
        # 如果启用了gradient_checkpointing,则调用模型的enable_input_require_grads方法,
        # 推测是为了在训练过程中启用输入梯度计算的优化。
        if training_args.gradient_checkpointing:
            model.enable_input_require_grads()
    # 在 Orank上打印模型参数
    rank0_print(get_parameter_number(model))

    llm_type = training_args.llm_type    
    
    rank0_print(f'llm_type={llm_type}')

    
    # Load data
    # 检查模型配置中是否存在slice_config属性。
    # 如果存在,则设置其max_slice_nums值,并将其转换为字典;否则直接设置模型配置的max_slice_nums值,并将整个配置转换为字典。
    if hasattr(model.config, "slice_config"):
        model.config.slice_config.max_slice_nums = training_args.max_slice_nums
        slice_config = model.config.slice_config.to_dict()
    else:
        model.config.max_slice_nums = training_args.max_slice_nums
        slice_config = model.config.to_dict()
    # 检查模型配置中是否存在batch_vision_input属性。
    # 如果存在,则将其值赋给batch_vision;否则设置batch_vision为False。
    if hasattr(model.config, "batch_vision_input"):
        batch_vision = model.config.batch_vision_input
    else:
        batch_vision = False
    # 根据传入的多个配置和模型参数,构建一个用于训练的数据模块。
    transform_func = build_transform()
    data_module = make_supervised_data_module(
        tokenizer=tokenizer,
        data_args=data_args,
        transform=transform_func,
        data_collator=data_collator,
        slice_config=slice_config,
        llm_type=llm_type,
        patch_size=model.config.patch_size,
        query_nums=model.config.query_num,
        batch_vision=batch_vision,
        max_length=training_args.model_max_length,
    )
    
    training_args.gradient_checkpointing_kwargs={"use_reentrant":False}
    # 创建了一个CPMTrainer实例,用于训练模型。具体功能包括:
    # 将传入的模型、分词器及训练参数等初始化为训练器的属性。
    # model:待训练的模型。
    # tokenizer:用于文本分词。
    # args:训练参数配置。
    # data_module:数据相关设置,如训练集、验证集等。
    trainer = CPMTrainer(
        model=model,
        tokenizer=tokenizer,
        args=training_args,
        **data_module,
    )

    trainer.train()
    trainer.save_state()
    # 安全地保存模型以供Hugging Face Trainer使用,参数如下:
    # trainer:训练器对象。
    # output_dir:模型保存的输出目录路径。
    # bias:LoRA偏置设置。
    # 函数主要执行模型的安全保存操作,并应用LoRA微调技术中的特定偏置处理。
    safe_save_model_for_hf_trainer(
        trainer=trainer,
        output_dir=training_args.output_dir,
        bias=lora_args.lora_bias)


if __name__ == "__main__":
    train()

其中

model = get_peft_model(model, lora_config)

实现了LoRA微调