[CLIP-VIT-L + Qwen] 多模态大模型源码阅读 - 模型训练篇
参考repo:WatchTower-Liu/VLM-learning; url: VLLM-BASE
前情提要
有关多模态大模型架构中的语言模型部分(MQwen.py)的代码请看(多模态大模型源码阅读 - 1、 多模态大模型源码阅读 - 2, 多模态大模型源码阅读 - 3,多模态大模型源码阅读 - 4)
多模态大模型架构中的视觉模型(visual/CLIP-VIT.py)部分请看多模态大模型源码阅读 - 5
多模态大模型架构中的trainer(trainer.py)部分请看多模态大模型源码阅读 - 6
多模态大模型架构中的MultiModal融合部分(MultiModal.py)部分请看多模态大模型源码阅读 - MultiModal篇。
多模态大模型架构中的Dataset部分请看多模态大模型源码阅读 - Dataset篇
观前提醒,本文中介绍的多模态模型架构来源于github项目WatchTower-Liu/VLM-learning,对Qwen模型的前向传播代码进行重写,并通过中间投影层将视觉特征与文本映射到同一向量空间。投影层原理参考LLAVA
本节介绍的是模型训练部分。将视觉模型的参数冻结,并采用LoRA对语言模型进行微调。训练参数包括语言模型中LoRA的参数和中间投影层参数。
其中投影层参数为初始化参数,为了平衡模型参数优化速度,这里为映射层设定了比Lora部分更大的学习率。
源码阅读
完整代码
import os
import json
import torch
from typing import Optional
from functools import partial
from trainer import MultiModalTrainer
from model.model import MMultiModal, LanguageConfig, VisualConfig, MultiModalConfig
from dataset.image_caption_dataset import ImageCaptionDataset, data_collate
import transformers
from transformers import HfArgumentParser, AutoTokenizer
from dataclasses import dataclass, field
from qwen.modeling_qwen import QWenLMHeadModel
from accelerate import Accelerator
# from peft import LoraConfig, TaskType, get_peft_model, PeftModel
# from einops import rearrange
@dataclass
class FinetuneArguments:
lora_rank: int = field(default=8)
lora_dropout: float = field(default=0.1)
previous_lora_weights: Optional[str] = field(default=None)
target_modules: str = field(default="W_pack")
image_map: str = field(default="data/image_map_b.json", metadata={"help": "图像文件与索引ID"})
captions_file: str = field(default="data/captions_b.json", metadata={"help": "ID与caption的对应"})
@dataclass
class TrainingArguments(transformers.TrainingArguments):
feature_proj_lr: Optional[float] = None
def train():
finetune_args, training_args = HfArgumentParser(
(FinetuneArguments, TrainingArguments)
).parse_args_into_dataclasses()
base_language_model = "Qwen/Qwen-7B-Chat"
# base_language_model = "openbmb/MiniCPM-2B-history"
# base_value_model = "openai/clip-vit-large-patch14"
base_value_model = "google/siglip-so400m-patch14-384"
tokenizer = AutoTokenizer.from_pretrained(base_language_model, trust_remote_code=True)
replace_token_id = tokenizer.convert_tokens_to_ids("<|extra_0|>")
# Check file paths
if not os.path.exists(finetune_args.image_map):
raise FileNotFoundError(f"Image map file not found: {finetune_args.image_map}")
if not os.path.exists(finetune_args.captions_file):
raise FileNotFoundError(f"Captions file not found: {finetune_args.captions_file}")
# Load and check file contents
with open(finetune_args.image_map, 'r') as f:
image_map = json.load(f)
print(f"Image map contains {len(image_map)} entries")
with open(finetune_args.captions_file, 'r') as f:
captions = json.load(f)
print(f"Captions file contains {len(captions)} entries")
model = MMultiModal(
LanguageConfig(model_path=base_language_model),
VisualConfig(model_path=base_value_model),
MultiModalConfig(replace_token_id=replace_token_id),
finetune_args,
train=True
).cuda()
model.train()
model.LLM.config.use_cache = False
dataset = ImageCaptionDataset(
tokenizer,
finetune_args.image_map,
finetune_args.captions_file,
VisualConfig(model_path=base_value_model),
max_train_data_item=300000
)
# Add debug information
print(f"Dataset length: {len(dataset)}")
if len(dataset) == 0:
raise ValueError("The dataset is empty. Please check the dataset files and paths.")
print(training_args)
# Initialize Accelerator
accelerator = Accelerator()
# Create DataLoader
train_dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=training_args.per_device_train_batch_size,
shuffle=True,
collate_fn=partial(data_collate, tokenizer=tokenizer, black_token_length=MultiModalConfig.image_context_length)
)
trainer = MultiModalTrainer(
model=model,
data_collator=partial(data_collate, tokenizer=tokenizer, black_token_length=MultiModalConfig.image_context_length),
train_dataset=dataset,
args=training_args
)
# Prepare the trainer and dataloader with the accelerator
trainer, train_dataloader = accelerator.prepare(trainer, train_dataloader)
trainer.train()
def main():
torch.distributed.init_process_group(backend='nccl')
train()
torch.distributed.destroy_process_group()
if __name__ == "__main__":
main()
导包
import os
import json
import torch
from typing import Optional
from functools import partial
from trainer import MultiModalTrainer
from model.model import MMultiModal, LanguageConfig, VisualConfig, MultiModalConfig
from dataset.image_caption_dataset import ImageCaptionDataset, data_collate
import transformers
from transformers import HfArgumentParser, AutoTokenizer
from dataclasses import dataclass, field
from qwen.modeling_qwen import QWenLMHeadModel
from accelerate import Accelerator
逐行解读
部分老生常谈和之前讲过的包就不再赘述辽~
partial:主要用于预先传入函数的部分参数,这样在后续调用的时候只需补充剩余的函数即可。
如函数Multiply(x,y),实现的是x * y的操作。定义func = partial(Multiply,x = 2),那么后续调用时使用func(3)就能实现原函数Multiply(2,3)的效果。通常也用于固定部分参数。
MultiModalTrainer:用于多模态模型的训练。
HfArgumentParser:用于解析命令行参数和脚本参数,并将解析的参数返回为制定的配置类型,如MultiModalConfig,VisualConfig等。
AutoTokenizer:用于根据传入模型路径加载制定的分词器。
field:用于定义数据类中变量的属性和额外信息。
Accelerator:用于简化和加速模型在GPU等硬件上的训练过程、
配置类
FinetuneArguments
@dataclass
class FinetuneArguments:
lora_rank: int = field(default=8)
lora_dropout: float = field(default=0.1)
previous_lora_weights: Optional[str] = field(default=None)
target_modules: str = field(default="W_pack")
image_map: str = field(default="data/image_map_b.json", metadata={"help": "图像文件与索引ID"})
captions_file: str = field(default="data/captions_b.json", metadata={"help": "ID与caption的对应"})
整体含义
微调参数的数据类,管理和存储微调模型的参数和类型等数据。
逐行解读
lora_rank:代表LoRA参数的秩,LoRA通过在保持预训练模型结构不变的情况下,引入低秩矩阵增加模型的可训练参数。默认为int类型,值为8
lora_dropout: LoRA参数的dropout比例,防止过拟合。
previous_lora_weights:预先训练好的lora权重,如果存在的话,就在该权重的基础上进行进一步的微调。
target_modules: 需要应用微调技术的模块。
image_map:图像和id的映射信息,用于根据图像获得对应的id。
captions_file: id和图像描述(字幕)的映射信息,用于根据id获取图像描述。
TrainingArguments
@dataclass
class TrainingArguments(transformers.TrainingArguments):
feature_proj_lr: Optional[float] = None
整体含义
训练参数的数据类,继承自transformers.TrainingArguments,并添加了一个新的参数feature_proj_lr,用于调整中间映射层的学习率。
train函数
def train():
finetune_args, training_args = HfArgumentParser(
(FinetuneArguments, TrainingArguments)
).parse_args_into_dataclasses()
base_language_model = "Qwen/Qwen-7B-Chat"
# base_language_model = "openbmb/MiniCPM-2B-history"
# base_value_model = "openai/clip-vit-large-patch14"
base_value_model = "google/siglip-so400m-patch14-384"
tokenizer = AutoTokenizer.from_pretrained(base_language_model, trust_remote_code=True)
replace_token_id = tokenizer.convert_tokens_to_ids("<|extra_0|>")
# Check file paths
if not os.path.exists(finetune_args.image_map):
raise FileNotFoundError(f"Image map file not found: {finetune_args.image_map}")
if not os.path.exists(finetune_args.captions_file):
raise FileNotFoundError(f"Captions file not found: {finetune_args.captions_file}")
# Load and check file contents
with open(finetune_args.image_map, 'r') as f:
image_map = json.load(f)
print(f"Image map contains {len(image_map)} entries")
with open(finetune_args.captions_file, 'r') as f:
captions = json.load(f)
print(f"Captions file contains {len(captions)} entries")
model = MMultiModal(
LanguageConfig(model_path=base_language_model),
VisualConfig(model_path=base_value_model),
MultiModalConfig(replace_token_id=replace_token_id),
finetune_args,
train=True
).cuda()
model.train()
model.LLM.config.use_cache = False
dataset = ImageCaptionDataset(
tokenizer,
finetune_args.image_map,
finetune_args.captions_file,
VisualConfig(model_path=base_value_model),
max_train_data_item=300000
)
# Add debug information
print(f"Dataset length: {len(dataset)}")
if len(dataset) == 0:
raise ValueError("The dataset is empty. Please check the dataset files and paths.")
print(training_args)
# Initialize Accelerator
accelerator = Accelerator()
# Create DataLoader
train_dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=training_args.per_device_train_batch_size,
shuffle=True,
collate_fn=partial(data_collate, tokenizer=tokenizer, black_token_length=MultiModalConfig.image_context_length)
)
trainer = MultiModalTrainer(
model=model,
data_collator=partial(data_collate, tokenizer=tokenizer, black_token_length=MultiModalConfig.image_context_length),
train_dataset=dataset,
args=training_args
)
# Prepare the trainer and dataloader with the accelerator
trainer, train_dataloader = accelerator.prepare(trainer, train_dataloader)
trainer.train()
整体含义
多模态模型的训练代码,将视觉模型的参数冻结,并采用LoRA对语言模型进行微调。训练参数包括语言模型中LoRA的参数和中间投影层参数。
逐行解读
def train():
finetune_args, training_args = HfArgumentParser(
(FinetuneArguments, TrainingArguments)
).parse_args_into_dataclasses()
首先使用HfArgumentParser的parse_args_into_dataclasses()函数解析命令行参数,并将参数转换为相应的数据类。这里传入的参数是一个元组,包含之前初始化的数据类FinetuneArguments和TrainingArguments,返回两个包含了解析后参数的数据类实例finetune_args和training_args。
base_language_model = "Qwen/Qwen-7B-Chat"
base_value_model = "openai/clip-vit-large-patch14"
初始化语言模型和视觉模型的模型路径,这里可以根据自己的需求更换模型,前提是重构了目标模型的方法以适应多模态任务的需求。
tokenizer = AutoTokenizer.from_pretrained(base_language_model, trust_remote_code=True)
replace_token_id = tokenizer.convert_tokens_to_ids("<|extra_0|>")
根据语言模型路径初始化分词器,并利用分词器的token2id函数将"<|extra_0|>"转换为数字id索引,将这个转换后的索引位置作为图像信息的插入位置。
if not os.path.exists(finetune_args.image_map):
raise FileNotFoundError(f"Image map file not found: {finetune_args.image_map}")
if not os.path.exists(finetune_args.captions_file):
raise FileNotFoundError(f"Captions file not found: {finetune_args.captions_file}")
为了防止后续代码读取图像索引映射文件(image_map)和索引图像描述映射文件(captions_file),这里用os.path.exists进行路径存在性检验,如果不存在则报错。
with open(finetune_args.image_map, 'r') as f:
image_map = json.load(f)
print(f"Image map contains {len(image_map)} entries")
with open(finetune_args.captions_file, 'r') as f:
captions = json.load(f)
print(f"Captions file contains {len(captions)} entries")
分别打开并读取image_map和captions_file文件,并赋值给对应的变量。打印出每个文件的长度信息(这一步用来debug)
model = MMultiModal(
LanguageConfig(model_path=base_language_model),
VisualConfig(model_path=base_value_model),
MultiModalConfig(replace_token_id=replace_token_id),
finetune_args,
train=True
).cuda()
model.train()
model.LLM.config.use_cache = False
实例化自定义的多模态模型类,传入语言模型配置。视觉模型配置和多模态模型配置等参数,将模型转移到cuda设备上,并设定模型为训练模式,启用dropout。
设置使用缓存为False,表示在训练过程中不缓存过去计算得到的键值对信息,这是为了启用梯度检查点。梯度检查点和键值对缓存互相冲突。
dataset = ImageCaptionDataset(
tokenizer,
finetune_args.image_map,
finetune_args.captions_file,
VisualConfig(model_path=base_value_model),
max_train_data_item=300000
)
初始化一个ImageCaptionDataset类的实例,有关ImageCaptionDataset的代码可以参考多模态大模型源码阅读-Dataset篇,传入初始化好的分词器,微调参数配置中的image_map和caption_file地址,视觉模型配置参数以及最大训练数据量限制。
print(f"Dataset length: {len(dataset)}")
if len(dataset) == 0:
raise ValueError("The dataset is empty. Please check the dataset files and paths.")
print(training_args)
这是一段debug用代码,以防初始化后的dataset为空,在后续操作时出现问题。
# Initialize Accelerator
accelerator = Accelerator()
# Create DataLoader
train_dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=training_args.per_device_train_batch_size,
shuffle=True,
collate_fn=partial(data_collate, tokenizer=tokenizer, black_token_length=MultiModalConfig.image_context_length)
)
初始化一个Accelerator对象,以便后续使用。
初始化一个DataLoader对象,用于加载数据,传入之前初始化好的dataset,dataset中包含了训练需要使用的数据。
根据训练配置参数获取batch_size,启用shuffle,这样能够在每个训练周期开始时,随机打乱训练数据,防止模型过拟合。
collate_fn传入之前导入的data_collate函数,对训练数据进行统一的批处理,并利用partial函数固定tokenizer,black_token_length等参数,有关data_collate函数的细节请参考多模态大模型源码阅读-Dataset篇。
trainer = MultiModalTrainer(
model=model,
data_collator=partial(data_collate, tokenizer=tokenizer, black_token_length=MultiModalConfig.image_context_length),
train_dataset=dataset,
args=training_args
)
初始化多模态模型训练器,这里的MultiModalTrainer内部实现参考多模态模型源码阅读-trainer篇,传入整合好的多模态模型和数据批量处理函数,这里的data_collater类似之前代码的collate_fn,都用了partial函数固定部分参数。传入dataset作为训练数据集,training_args作为训练用配置参数。
trainer, train_dataloader = accelerator.prepare(trainer, train_dataloader)
trainer.train()
使用accelerator.prepare自动将模型和数据迁移到正确的设备上,并调整数据加载器以保证可以在分布式环境下加载数据。
trainer.train():开始训练迭代,进行加载数据,前向传播,计算损失,反向传播,更新模型权重等一系列操作。
def main():
torch.distributed.init_process_group(backend='nccl')
train()
torch.distributed.destroy_process_group()
torch.distributed.init_process_group(backend=‘nccl’)初始化一个分布式进程组,支持多个GPU之间进行数据交换
train启用训练进程
torch.distributed.destroy_process_group()清理和释放分布式训练中使用到的资源
至此,模型训练篇讲解完毕,后续可能就会更新模型的实战篇,以及其他模型的源码内容辽~