QwenVL 如何微调

概览

对着 README 文件, 看看 QwenVL 的基本情况, 以及我们需要了解什么

代码结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
qwenvl/
├── train/           # 训练核心模块
│   ├── trainer.py   # 自定义训练器(基于HuggingFace)
│   ├── train_qwen.py # 训练主入口
│   └── argument.py  # 参数定义类
├── data/            # 数据处理模块
│   ├── __init__.py  # 数据集配置注册
│   ├── data_qwen.py # 标准数据处理
│   ├── data_qwen_packed.py # 打包数据处理
│   └── rope2d.py    # 2D位置编码实现
└── tools/           # 辅助工具
    ├── process_bbox.ipynb # 目标检测数据转换
    └── pack_data.py # 数据打包工具

大概是这些, 和实际的有些出入
它给了一个 demo, 里面包含一些图片视频以及相应的 json
我准备先从 demo 入手

demo

这个文件夹里提供了图片, 视频, 以及对应的 json 文件

单图片训练数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "image": "demo/images/10095.png",     // 图像路径(相对路径)
  "conversations": [                    // 对话数组
    {
      "from": "human",                  // 用户角色
      "value": "Is the value of Favorable 38 in 2015?\n<image>"  // 问题+图像标记
    },
    {
      "from": "gpt",                    // 模型角色
      "value": "Yes"                    // 简洁的回答
    }
  ]
}
  1. 图片路径
  2. 对话, 其中包含
    1. 用户角色, 具体问题以及图像标记
    2. 模型角色, 以及响应的回答
  3. 此外, 也支持多个图像的训练, 就像下面这样
1
2
3
4
5
6
7
8
9
{
	"images": ["cats/001.jpg", "cats/002.jpg"],
	"conversations": [
	{
		"from": "human",
		"value": "<image>\n<image>\nWhat are the differences between these two cats?"
},

......

视频训练数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "video": "v_7bUu05RIksU.mp4",       // 视频文件名
    "conversations": [
        {
            "from": "human",
            "value": "<video>\nCan you give me an overview of the video content?"
        },
        {
            "from": "gpt", 
            "value": "The video showcases a group of men washing cars..."  // 详细描述
        }
    ]
}

其实跟图像差不多, 就是把 image 标记换成了 video

目标检测/定位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "image": "demo/COCO_train2014_000000580957.jpg",
    "conversations": [
        {
            "from": "human",
            "value": "<image>\nLocate house in this image and output the bbox coordinates in JSON format."
        },
        {
            "from": "gpt",
            "value": "{\n"bbox_2d": [135, 114, 1016, 672]\n}"
        }
    ]
}

这里 demo 没有给, 但是在 readme 里写出来了
这里 value 里的值是模型输出的物体边界框坐标

scripts

这是训练的核心配置

1
2
3
4
5
6
7
scripts/
├── sft.sh              # 3B模型训练脚本 (入门首选)
├── sft_7b.sh           # 7B模型训练脚本
├── sft_32b.sh          # 32B模型训练脚本  
├── zero2.json          # DeepSpeed ZeRO-2 配置
├── zero3.json          # DeepSpeed ZeRO-3 配置
└── zero3_offload.json  # DeepSpeed ZeRO-3 + CPU卸载配置

这里我准备就看 3B 的, 因为相对简单? 并且我们的模型也是用的 3B 模型

分布式训练

1
2
3
MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"}    # 主节点地址
MASTER_PORT=${MASTER_PORT:-$(shuf -i 20001-29999 -n 1)}  # 随机端口
NNODES=${WORLD_SIZE:-1}                     # 节点数量
  • MASTER_ADDR: 主节点地址,默认本地训练
  • MASTER_PORT: 通信端口,随机选择 20001-29999 范围内的端口
  • NNODES: 节点数量,默认单机训练

DeepSeed 配置

1
deepspeed=./scripts/zero3.json

使用 ZeRO Stage 3 优化策略,可以显著减少显存占用,支持更大模型训练
(不了解, 回头需要看一下)

模型配置

1
llm=Qwen/Qwen2.5-VL-3B-Instruct

使用 HuggingFace 上的 Qwen2.5-VL-3B-Instruct 预训练模型

超参数

1
2
3
lr=2e-7          # 学习率:非常小的学习率,适合微调
batch_size=4     # 批次大小
grad_accum_steps=4  # 梯度累积步数,有效批次大小 = 4×4 = 16

训练入口

1
entry_file=qwenvl/train/train_qwen.py

这是训练脚本的主入口文件,包含:

  • 模型加载逻辑:加载 Qwen2.5-VL 预训练模型
  • 数据预处理:处理图像和文本的输入格式
  • 训练循环:前向传播、反向传播、参数更新
  • DeepSpeed 集成:分布式训练和显存优化
  • 检查点保存:模型权重和训练状态的保存

数据集配置

1
datasets=public_dataset1,public_dataset2

当前状态:这是占位符,需要替换为实际数据集
数据集要求

  • 图像-文本对话格式
  • JSON 或类似结构化格式
  • 支持指令跟随的对话数据

核心训练参数

模型组件微调配置

1
2
3
--tune_mm_vision False    # 不微调视觉编码器
--tune_mm_mlp True       # 微调多模态连接层
--tune_mm_llm True       # 微调语言模型部分
我的疑问: 为什么不微调视觉编码器?

是因为即使微调了, 相比于大厂的 pre-train 的效果也微乎其微?
还是说这是 mllm 的一些微调要求?
../source/QwenVL 如何微调\_关于视觉编码器.png

数据处理参数

1
2
3
--max_pixels 50176       # 最大像素数(约224×224)
--min_pixels 784         # 最小像素数(约28×28)
--model_max_length 8192  # 最大序列长度

这里的序列是指模型输入的总token数量限制, 包括文本 token 和图像 token

[系统prompt tokens] + [图像tokens] + [用户文本tokens] + [助手回复tokens] = 总长度 ≤ 8192

优化器配置

1
2
3
4
--learning_rate ${lr}
--weight_decay 0         # 不使用权重衰减
--warmup_ratio 0.03      # 3% 步数用于学习率预热
--lr_scheduler_type "cosine"  # 余弦学习率调度

保存和日志

1
2
3
4
--save_strategy "steps"
--save_steps 1000        # 每1000步保存一次
--save_total_limit 1     # 最多保存1个检查点
--report_to wandb        # 使用 Weights & Biases 记录训练过程

启动命令

1
2
3
4
torchrun --nproc_per_node=${NPROC_PER_NODE} \
         --master_addr=${MASTER_ADDR} \
         --master_port=${MASTER_PORT} \
         ${entry_file} ${args}

使用 PyTorch 分布式启动器运行训练脚本
看不懂

qwenvl

1
2
3
qwenvl/
├── data/          # 数据处理模块
├── train/         # 训练相关模块

train

1
2
3
4
train/
├── argument.py      # 1️⃣ 训练参数定义
├── trainer.py       # 2️⃣ 自定义训练器
└── train_qwen.py    # 3️⃣ 主训练脚本
  • argument.py:了解所有可配置的训练参数
  • trainer.py:理解训练流程和损失计算
  • train_qwen.py:主入口,整合所有组件

argument.py

这个文件定义了训练 Qwen2.5-VL 模型的三类参数配置
这里对于三种参数类, 都使用了dataclass装饰器, 代码变得简洁许多

train_qwen.py

这是主训练脚本

导入和路径
1
2
3
4
5
import sys
from pathlib import Path

project_root = Path(__file__).parent.parent.parent
sys.path.append(str(project_root))

获取项目根目录, 添加到 python 的路径中, 确保模块正确导入

导入模块
1
2
3
4
5
6
7
import qwenvl.train.trainer  # 自定义训练器增强
from trainer import replace_qwen2_vl_attention_class  # 注意力机制优化

from transformers import (
    Qwen2VLForConditionalGeneration,      # Qwen2-VL 模型
    Qwen2_5_VLForConditionalGeneration,   # Qwen2.5-VL 模型
)

这里的 trainer 是 train 文件夹中的另一个文件, 待会看一下

工具函数
打印控制
1
2
3
def rank0_print(*args):
    if local_rank == 0:
        print(*args)

作用:在多GPU训练时,只有主进程(rank 0)打印信息,避免重复输出

模型安全保存
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def safe_save_model_for_hf_trainer(trainer: transformers.Trainer, output_dir: str):
    if trainer.deepspeed:
        torch.cuda.synchronize()    # 同步所有CUDA操作
        trainer.save_model(output_dir)  # DeepSpeed专用保存
        return

    state_dict = trainer.model.state_dict()
    if trainer.args.should_save:
        cpu_state_dict = {key: value.cpu() for key, value in state_dict.items()}
        del state_dict  # 释放GPU内存
        trainer._save(output_dir, state_dict=cpu_state_dict)
模型微调
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def set_model(model_args, model):
    # 1. 视觉编码器控制
    if model_args.tune_mm_vision:
        for n, p in model.visual.named_parameters():
            p.requires_grad = True    # 开启梯度计算
    else:
        for n, p in model.visual.named_parameters():
            p.requires_grad = False   # 冻结参数

    # 2. 多模态连接层控制  
    if model_args.tune_mm_mlp:
        for n, p in model.visual.merger.named_parameters():
            p.requires_grad = True
    else:
        for n, p in model.visual.merger.named_parameters():
            p.requires_grad = False

    # 3. 语言模型控制
    if model_args.tune_mm_llm:
        for n, p in model.model.named_parameters():
            p.requires_grad = True
        model.lm_head.requires_grad = True
    else:
        for n, p in model.model.named_parameters():
            p.requires_grad = False
        model.lm_head.requires_grad = False

这里根据传入参数的情况, 控制哪些模块需要冻结, 哪些可以微调, 然后相应的调整梯度的设置

主训练函数
参数解析与环境设置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def train(attn_implementation="flash_attention_2"):
    global local_rank

    parser = transformers.HfArgumentParser(
        (ModelArguments, DataArguments, TrainingArguments)
    )
    model_args, data_args, training_args = parser.parse_args_into_dataclasses()

    local_rank = training_args.local_rank
    os.makedirs(training_args.output_dir, exist_ok=True)

命令行参数 → 三个dataclass对象 → 全局变量设置

模型版本自动检测与加载
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if "qwen2.5" in model_args.model_name_or_path.lower():
    # Qwen2.5-VL 分支
    model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
        model_args.model_name_or_path,
        cache_dir=training_args.cache_dir,
        attn_implementation=attn_implementation,  # 使用Flash Attention 2
        torch_dtype=(torch.bfloat16 if training_args.bf16 else None),
    )
    data_args.image_processor = AutoProcessor.from_pretrained(
        model_args.model_name_or_path,
    ).image_processor
    data_args.model_type = "qwen2.5vl"
else:
    # Qwen2-VL 分支(向后兼容)
    model = Qwen2VLForConditionalGeneration.from_pretrained(...)
    data_args.image_processor = Qwen2VLImageProcessor.from_pretrained(...)
    data_args.model_type = "qwen2vl"

根据模型路径名称自动选择正确的模型类和处理器

性能优化

这里不懂

分析器设置
1
2
3
4
5
6
7
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,  # 8192
    padding_side="right",  # 右侧填充
    use_fast=False,       # 使用Python版本(更稳定)
)
模型参数设置与监控
1
2
3
4
5
set_model(model_args, model)  # 设置可训练参数

if torch.distributed.get_rank() == 0:
    model.visual.print_trainable_parameters()    # 打印视觉模块训练状态
    model.model.print_trainable_parameters()     # 打印语言模块训练状态
训练器初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
trainer = Trainer(
    model=model, 
    processing_class=tokenizer,  # HF新API
    args=training_args, 
    **data_module
)

# 断点恢复检测
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
    logging.info("checkpoint found, resume training")
    trainer.train(resume_from_checkpoint=True)
else:
    trainer.train()
训练完成后的处理
1
2
3
4
5
6
trainer.save_state()  # 保存训练状态
data_args.image_processor.save_pretrained(training_args.output_dir)  # 保存图像处理器

model.config.use_cache = True  # 推理时启用缓存

safe_save_model_for_hf_trainer(trainer=trainer, output_dir=training_args.output_dir)

data

1
2
3
4
5
6
data/
├── __init__.py
├── data_qwen.py         # 4️⃣ 基础数据处理
├── data_qwen_packed.py  # 5️⃣ 优化的数据处理(重点)
├── rope2d.py           # 6️⃣ 2D位置编码
└── data_list.py        # 7️⃣ 数据集列表配置
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计