[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()清理和释放分布式训练中使用到的资源
至此,模型训练篇讲解完毕,后续可能就会更新模型的实战篇,以及其他模型的源码内容辽~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值