(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微调