【多模态微调实战】Qwen2-VL + LoRA + SwanLab:手把手教你在COCO数据集上微调通义千问多模态大模型
【多模态微调实战】Qwen2-VL + LoRA + SwanLab:手把手教你在COCO数据集上微调通义千问多模态大模型
前言
大家好,今天带来一篇保姆级的多模态大模型微调教程。我们的主角是阿里通义实验室刚推出的 Qwen2-VL-2B-Instruct 多模态大模型,搭配 LoRA(低秩自适应) 轻量化微调方案,在经典的 COCO 2014 图像描述数据集上做训练,同时用 SwanLab 做全程可视化监控。
先给刚入门的同学白话解释几个关键词:
- Qwen2-VL:通义千问的多模态版本,既能看懂图片,又能生成文字,相当于"会看图说话的AI"
- LoRA:轻量化微调神器,不用更新整个大模型的全部参数,只训练一点点"外挂"权重,显存占用直接砍半,单卡就能跑
- SwanLab:开源的训练可视化工具,自动帮你画loss曲线、记超参数、对比实验,调参一目了然
整个流程跑下来,一张24G显存的显卡就能搞定,非常适合个人研究者和小团队快速上手。
一、环境配置:三步搭好炼丹炉
工欲善其事,必先利其器。环境配置分三步走,照着敲命令就行。
1.1 基础要求
- 一张英伟达显卡(建议显存≥16G,24G最佳),装好CUDA环境
- Python 版本 ≥ 3.8,但需要小于3.12,实测有问题
- PyTorch 支持 CUDA 加速
1.2 一键安装依赖库
直接复制下面命令到终端,全部自动安装:
python -m pip install --upgrade pip
# 更换清华源,下载速度起飞
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install modelscope==1.18.0
pip install transformers==4.46.2
pip install sentencepiece==0.2.0
pip install accelerate==1.1.1
pip install datasets==2.18.0
pip install peft==0.13.2
pip install swanlab==0.3.25
pip install qwen-vl-utils==0.0.8
重点说明:
peft是 HuggingFace 官方的参数高效微调库,LoRA 全靠它swanlab负责训练可视化,和 Transformers 原生集成,一行代码接入qwen-vl-utils是 Qwen2-VL 专属的图像处理工具,别漏装了
二、数据集准备:把COCO数据改成模型爱吃的格式
我们使用 COCO 2014 Caption 数据集,这是图像描述领域的"高考真题",每张图都配有人工写的英文描述。
为了快速演示,我们只取前500张图来训练,完整数据集有40多万张,大家可以按需扩充。
2.1 数据集格式说明
Qwen2-VL 吃的是对话格式数据,长这样:
[
{
"id": "identity_1",
"conversations": [
{
"from": "user",
"value": "COCO Yes: <|vision_start|>图片路径<|vision_end|>"
},
{
"from": "assistant",
"value": "A snow skier assessing the mountain before starting to ski"
}
]
}
]
其中 <|vision_start|> 和 <|vision_end|> 是模型的"图像占位符",告诉模型:这中间放的是图片,好好看。
2.2 第一步:下载数据并转成CSV
新建 data2csv.py,复制以下代码:
from modelscope.msdatasets import MsDataset
import os
import pandas as pd
MAX_DATA_NUMBER = 500
if not os.path.exists('coco_2014_caption'):
# 从ModelScope下载COCO数据集
ds = MsDataset.load('modelscope/coco_2014_caption',
subset_name='coco_2014_caption',
split='train')
total = min(MAX_DATA_NUMBER, len(ds))
os.makedirs('coco_2014_caption', exist_ok=True)
image_paths, captions = [], []
for i in range(total):
item = ds[i]
image_id = item['image_id']
caption = item['caption']
image = item['image']
image_path = os.path.abspath(f'coco_2014_caption/{image_id}.jpg')
image.save(image_path)
image_paths.append(image_path)
captions.append(caption)
if (i + 1) % 50 == 0:
print(f'Processing {i+1}/{total} images ({(i+1)/total*100:.1f}%)')
df = pd.DataFrame({'image_path': image_paths, 'caption': captions})
df.to_csv('./coco-2024-dataset.csv', index=False)
print(f'数据处理完成,共处理了{total}张图片')
else:
print('目录已存在,跳过数据处理')
2.3 第二步:CSV转JSON对话格式
新建 csv2json.py:
import pandas as pd
import json
df = pd.read_csv('./coco-2024-dataset.csv')
conversations = []
for i in range(len(df)):
conversations.append({
"id": f"identity_{i+1}",
"conversations": [
{
"from": "user",
"value": f"COCO Yes: <|vision_start|>{df.iloc[i]['image_path']}<|vision_end|>"
},
{
"from": "assistant",
"value": df.iloc[i]['caption']
}
]
})
with open('data_vl.json', 'w', encoding='utf-8') as f:
json.dump(conversations, f, ensure_ascii=False, indent=2)
依次运行两个脚本,你会得到 data_vl.json,数据集准备完毕。
三、模型下载与加载:把Qwen2-VL请回家
我们用 ModelScope 下载模型,国内速度快,不用科学上网。
核心加载代码片段:
from modelscope import snapshot_download, AutoTokenizer
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
import torch
# 下载模型到本地
model_dir = snapshot_download("Qwen/Qwen2-VL-2B-Instruct",
cache_dir="./",
revision="master")
# 加载分词器和处理器
tokenizer = AutoTokenizer.from_pretrained(
"./Qwen/Qwen2-VL-2B-Instruct/",
use_fast=False,
trust_remote_code=True
)
processor = AutoProcessor.from_pretrained("./Qwen/Qwen2-VL-2B-Instruct")
# 加载模型,用bf16精度省显存
model = Qwen2VLForConditionalGeneration.from_pretrained(
"./Qwen/Qwen2-VL-2B-Instruct/",
device_map="auto",
torch_dtype=torch.bfloat16,
trust_remote_code=True
)
model.enable_input_require_grads() # 开启梯度检查点必须加这行
划重点:
- 2B版本模型大小约 4.5GB,下载5分钟左右
device_map="auto"自动分配显存,多卡也能用torch.bfloat16是炼丹标配,比float32省一半显存,精度基本不掉enable_input_require_grads()配合梯度检查点,进一步省显存

四、集成SwanLab:训练过程一眼看穿
SwanLab 是个开源的训练可视化工具,跟 Transformers 已经做好了原生集成,加一个回调就行。
SwanLab官方文档:docs.swanlab.cn/guide_cloud/general/quick-start.html
4.1 接入代码
from swanlab.integration.transformers import SwanLabCallback
swanlab_callback = SwanLabCallback(
project="Qwen2-VL-finetune",
experiment_name="qwen2-vl-coco2014",
config={
"model": "Qwen2-VL-2B-Instruct",
"dataset": "coco_2014_caption",
"train_data_number": 496,
"lora_rank": 64,
"lora_alpha": 16,
"lora_dropout": 0.05,
},
)
然后在 Trainer 里加上 callbacks=[swanlab_callback] 就完事了。
4.2 首次使用
第一次用需要去 SwanLab 官网注册个账号,复制 API Key,训练开始时粘贴进去就行,后续自动登录。

它会自动帮你记录:
- 训练loss、验证loss曲线
- 学习率变化、梯度范数
- 所有超参数配置
- 甚至还能上传生成的图片做对比
运行下面的Python脚本,开启第一次训练
import swanlab
import random
# 创建一个SwanLab项目
swanlab.init(
# 设置项目名
project="my-awesome-project",
# 设置超参数
config={
"learning_rate": 0.02,
"architecture": "CNN",
"dataset": "CIFAR-100",
"epochs": 10
}
)
# 模拟一次训练
epochs = 10
offset = random.random() / 5
for epoch in range(2, epochs):
acc = 1 - 2 ** -epoch - random.random() / epoch - offset
loss = 2 ** -epoch + random.random() / epoch + offset
# 记录训练指标
swanlab.log({"acc": acc, "loss": loss})
# [可选] 完成训练,这在notebook环境中是必要的
swanlab.finish()

打开网址,可以看到记录的信息
五、开始微调:LoRA参数全解析
重头戏来了。完整的训练脚本 train.py 我放在下面,先讲几个核心参数,别瞎调。
5.1 LoRA配置怎么设
from peft import LoraConfig, TaskType, get_peft_model
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
# 这7个模块是注意力和MLP的核心,都加上LoRA效果最好
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
inference_mode=False,
r=64, # LoRA秩,越大参数量越多,效果越好但越慢
lora_alpha=16, # 缩放系数,一般设为r的1/4或1/2
lora_dropout=0.05, # 防止过拟合
bias="none",
)
peft_model = get_peft_model(model, config)
白话解释:
- r=64:LoRA矩阵的秩。r=8是极简版,r=64效果更饱满,2B模型用64完全扛得住
- lora_alpha=16:相当于给LoRA输出乘个系数,控制LoRA对原模型的"影响力"
- target_modules:指定在哪些层上加LoRA。注意力的q/k/v/o和MLP的三层全加上,覆盖最全面
5.2 训练参数配置
args = TrainingArguments(
output_dir="./output/Qwen2-VL-2B",
per_device_train_batch_size=4, # 每张卡batch size
gradient_accumulation_steps=4, # 梯度累积,等效batch=16
logging_steps=10,
num_train_epochs=2, # 小数据集2个epoch足够,多了过拟合
save_steps=100,
learning_rate=1e-4, # LoRA学习率比全参微调大一个量级
gradient_checkpointing=True, # 显存救星,用时间换空间
report_to="none", # 关掉wandb,我们用SwanLab
)
5.3 完整训练代码
import torch
import json
import swanlab
from datasets import Dataset
from modelscope import snapshot_download, AutoTokenizer
from swanlab.integration.transformers import SwanLabCallback
from qwen_vl_utils import process_vision_info
from peft import LoraConfig, TaskType, get_peft_model, PeftModel
from transformers import (
TrainingArguments, Trainer, DataCollatorForSeq2Seq,
Qwen2VLForConditionalGeneration, AutoProcessor,
)
def process_func(example):
"""数据预处理:把对话格式转换成模型输入张量"""
MAX_LENGTH = 8192
conversation = example["conversations"]
input_content = conversation[0]["value"]
output_content = conversation[1]["value"]
file_path = input_content.split("<|vision_start|>")[1].split("<|vision_end|>")[0]
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"image": file_path,
"resized_height": 280,
"resized_width": 280,
},
{"type": "text", "text": "COCO Yes:"},
],
}
]
text = processor.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
image_inputs, _ = process_vision_info(messages)
inputs = processor(
text=[text],
images=image_inputs,
padding=True,
return_tensors="pt"
)
response = tokenizer(
output_content,
add_special_tokens=False,
return_tensors="pt"
)
prompt_input_ids = inputs["input_ids"][0]
prompt_attention_mask = inputs["attention_mask"][0]
response_input_ids = response["input_ids"][0]
response_attention_mask = response["attention_mask"][0]
pad_id = torch.tensor([tokenizer.pad_token_id], dtype=prompt_input_ids.dtype)
pad_mask = torch.tensor([1], dtype=prompt_attention_mask.dtype)
# 拼接输入与输出;label 只计算回答部分,指令部分用 -100 掩码
input_ids = torch.cat(
[prompt_input_ids, response_input_ids, pad_id],
dim=0
)
attention_mask = torch.cat(
[prompt_attention_mask, response_attention_mask, pad_mask],
dim=0
)
labels = torch.cat(
[
torch.full((prompt_input_ids.size(0),), -100, dtype=prompt_input_ids.dtype),
response_input_ids,
pad_id
],
dim=0
)
if input_ids.size(0) > MAX_LENGTH:
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels,
"pixel_values": inputs["pixel_values"].squeeze(0),
"image_grid_thw": inputs["image_grid_thw"].squeeze(0),
}
def predict(messages, model):
"""推理函数,训练完用来测效果"""
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
image_inputs, _ = process_vision_info(messages)
inputs = processor(text=[text], images=image_inputs,
padding=True, return_tensors="pt").to("cuda")
generated_ids = model.generate(**inputs, max_new_tokens=128)
generated_ids_trimmed = [
out_ids[len(in_ids):]
for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
return processor.batch_decode(
generated_ids_trimmed,
skip_special_tokens=True,
clean_up_tokenization_spaces=False
)[0]
# ========== 加载模型 ==========
model_dir = snapshot_download("Qwen/Qwen2-VL-2B-Instruct",
cache_dir="./", revision="master")
tokenizer = AutoTokenizer.from_pretrained(
"./Qwen/Qwen2-VL-2B-Instruct/", use_fast=False, trust_remote_code=True
)
processor = AutoProcessor.from_pretrained("./Qwen/Qwen2-VL-2B-Instruct")
model = Qwen2VLForConditionalGeneration.from_pretrained(
"./Qwen/Qwen2-VL-2B-Instruct/",
device_map="auto", torch_dtype=torch.bfloat16, trust_remote_code=True
)
model.enable_input_require_grads()
# ========== 划分训练/测试集 ==========
with open("data_vl.json", 'r') as f:
data = json.load(f)
train_data = data[:-4] # 496条训练
test_data = data[-4:] # 4条留着看效果
with open("data_vl_train.json", "w") as f:
json.dump(train_data, f)
with open("data_vl_test.json", "w") as f:
json.dump(test_data, f)
train_ds = Dataset.from_json("data_vl_train.json")
train_dataset = train_ds.map(process_func)
# ========== 配置LoRA ==========
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
inference_mode=False,
r=64, lora_alpha=16, lora_dropout=0.05, bias="none",
)
peft_model = get_peft_model(model, config)
# ========== 训练参数 ==========
args = TrainingArguments(
output_dir="./output/Qwen2-VL-2B",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
logging_steps=10,
logging_first_step=5,
num_train_epochs=2,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True,
report_to="none",
)
# ========== SwanLab回调 ==========
swanlab_callback = SwanLabCallback(
project="Qwen2-VL-finetune",
experiment_name="qwen2-vl-coco2014",
config={
"model": "Qwen2-VL-2B-Instruct",
"dataset": "coco_2014_caption",
"train_data_number": len(train_data),
"lora_rank": 64,
"lora_alpha": 16,
"lora_dropout": 0.05,
},
)
# ========== 开始训练 ==========
trainer = Trainer(
model=peft_model,
args=args,
train_dataset=train_dataset,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
callbacks=[swanlab_callback],
)
trainer.train()
# ========== 推理测试并上传结果 ==========
val_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
inference_mode=True,
r=64, lora_alpha=16, lora_dropout=0.05, bias="none",
)
val_peft_model = PeftModel.from_pretrained(
model, model_id="./output/Qwen2-VL-2B/checkpoint-62", config=val_config
)
test_image_list = []
with open("data_vl_test.json", "r") as f:
test_dataset = json.load(f)
for item in test_dataset:
origin_image_path = item["conversations"][0]["value"].split(
"<|vision_start|>")[1].split("<|vision_end|>")[0]
messages = [{
"role": "user",
"content": [
{"type": "image", "image": origin_image_path},
{"type": "text", "text": "COCO Yes:"}
]
}]
response = predict(messages, val_peft_model)
print(f"生成结果:{response}")
test_image_list.append(swanlab.Image(origin_image_path, caption=response))
swanlab.log({"Prediction": test_image_list})
swanlab.finish()
运行 python train.py,看到进度条就说明炼丹开始了!
六、训练结果分析:从曲线里看出门道
训练完成后打开 SwanLab 面板,能看到几张关键图表:


loss 曲线:随着训练步数增加,loss 稳步下降,说明模型确实在学习图像和文本的对应关系。
学习率曲线:线性衰减策略,训练初期大步走,后期小步微调,这是标准操作。
grad_norm 梯度范数:训练后期逐渐上升,这是个预警信号——说明模型开始在小数据集上记答案了,有过拟合风险。所以我们只训2个epoch刚刚好,再训下去泛化能力反而下降。
调参经验:小数据集上别贪多epoch,loss平稳了就收手;想看泛化能力一定要留验证集。
七、推理演示:看看微调后的模型看图说话水平
训练结束后,模型会在 ./output/Qwen2-VL-2B/ 目录下生成 checkpoint,里面就是 LoRA 权重文件,一般只有几十MB,非常轻便。
加载LoRA做推理的核心代码:
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
from peft import PeftModel, LoraConfig, TaskType
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=True,
r=64, # Lora 秩
lora_alpha=16, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=0.05, # Dropout 比例
bias="none",
)
# default: Load the model on the available device(s)
model = Qwen2VLForConditionalGeneration.from_pretrained(
"./Qwen/Qwen2-VL-2B-Instruct", torch_dtype="auto", device_map="auto"
)
model = PeftModel.from_pretrained(model, model_id="./output/Qwen2-VL-2B/checkpoint-62", config=config)
processor = AutoProcessor.from_pretrained("./Qwen/Qwen2-VL-2B-Instruct")
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"image": "测试图像路径",
},
{"type": "text", "text": "COCO Yes:"},
],
}
]
# Preparation for inference
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(
text=[text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt",
)
inputs = inputs.to("cuda")
# Inference: Generation of the output
generated_ids = model.generate(**inputs, max_new_tokens=128)
generated_ids_trimmed = [
out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
output_text = processor.batch_decode(
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)
print(output_text)
然后给一张新图片,模型就能输出对应的英文描述。SwanLab 面板里的 Prediction 标签页也能看到测试集的生成效果,图文对照非常直观。
八、补充:硬件要求与注意事项
8.1 推荐硬件配置
| 配置项 | 最低要求 | 推荐配置 |
|---|---|---|
| 显存 | 16GB | 24GB+ |
| 显卡 | RTX 3090 | RTX 4090 / A10 |
| 训练时长(500张,2epoch) | ~30分钟 | ~15分钟 |
8.2 避坑指南
- 一定要用
bfloat16:float32 显存直接翻倍,还没明显收益 gradient_checkpointing必开:省30%-40%显存,速度只慢一点点- 图像别太大:我们统一缩到280x280,又快又够用
- LoRA别加在视觉编码器上:这次只调语言部分,视觉编码器冻住效果就不错
enable_input_require_grads()别忘了:开梯度检查点不加这行会报错
九、总结
整篇教程跑下来,你应该掌握了这几项技能:
- ✅ Qwen2-VL 多模态模型的加载与数据预处理
- ✅ 使用 LoRA 对大模型进行轻量化微调
- ✅ SwanLab 可视化监控训练全过程
- ✅ 训练完成后加载 LoRA 权重做推理
- ✅ 看懂 loss 曲线,判断是否过拟合
这套流程可以直接迁移到你自己的数据集上,不管是做工业缺陷图文描述、文档VQA还是其他多模态任务,换个数据格式就能直接开训。
2B 只是入门款,想效果更好可以上 7B 版本,原理完全一样,就是显存要求高一些。祝大家炼丹顺利!
参考资料:
更多推荐


所有评论(0)