Llama-Factory 微调 Qwen2.5-VL:从数据集制作到模型合并
环境配置
- Ubuntu 24
- 3090 (24G)
- CUDA 12.9
数据集制作
我的数据集主要是对图像内容进行描述。
1. Label Studio 制作数据集
这是最原始的从零开始制作数据集的方法,不建议这样做! 安装完 label-studio 后,输入指令启动:
label-studio start
Llama-Factory 微调 Qwen2.5-VL:从数据集制作到模型合并 环境配置 Ubuntu 24 3090 (24G) CUDA 12.9 数据集制作 我的数据集主要是对图像内容进行描述。 Label Studio 制作数据集 这是最原始的从零开始制作数据集的方法,不建议这样做! 安装完 label-studio 后,输入指令启动: 进入浏览器界面。 利用 Qwen2.5-VL 半自动…
我的数据集主要是对图像内容进行描述。
这是最原始的从零开始制作数据集的方法,不建议这样做! 安装完 label-studio 后,输入指令启动:
label-studio start
进入浏览器界面。
既然 Qwen 本身具有较好的图像描述能力,那我们可以先使用 Qwen 进行图像描述,在此基础上进行复核修改,这样做可以减少人力成本。 脚本如下:
import torch
from modelscope import Qwen2_5_VLForConditionalGeneration, AutoTokenizer, AutoProcessor
from qwen_vl_utils import process_vision_info
import time
import os
from pathlib import Path
import json
def process_single_image(model, processor, image_path, prompt):
messages = [{"role": "user", "content": [{"type": "image", "image": image_path}, {"type": "text", "text": prompt}],}]
# 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")
time_start = time.time()
# Inference: Generation of the output
generated_ids = model.generate(**inputs, max_new_tokens=256, do_sample=False)
time_end = time.time()
print(f"Inference time for {Path(image_path).name}: {time_end - time_start:.2f}s")
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
)
return output_text[0]
def process_images_in_folder(model, processor, image_folder, prompt, output_file=None):
# 支持的图像格式
image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
# 获取文件夹中所有图像文件
image_files = []
for file in Path(image_folder).iterdir():
if file.suffix.lower() in image_extensions:
image_files.append(file)
image_files.sort()
if not image_files:
print(f"No image files found in {image_folder}")
return
print(f"Found {len(image_files)} image files")
# 存储结果
results = []
# 遍历处理每张图像
for image_file in image_files:
print(f"\nProcessing: {image_file.name}")
try:
result = process_single_image(model, processor, str(image_file), prompt)
print(f"Result: {result}")
# 保存结果
results.append({'image': image_file.name, 'path': str(image_file), 'result': result})
except Exception as e:
print(f"Error processing {image_file.name}: {e}")
results.append({'image': image_file.name, 'path': str(image_file), 'result': f"Error: {e}", 'error': True})
# 如果指定了输出文件,则保存为 JSONL 格式
if output_file:
with open(output_file, 'w', encoding='utf-8') as f:
for item in results:
# 构造 JSONL 格式的字典
json_line = {"image": item['path'], "text": item['result']}
# 写入一行 JSON
f.write(json.dumps(json_line, ensure_ascii=False) + '\n')
print(f"\nResults saved to {output_file}")
return results
if __name__ == '__main__':
# default: Load the model on the available device(s)
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
"/home/ct/work/BigModel/Qwen2.5-VL/models/Qwen2.5-VL-7B-Instruct",
torch_dtype="auto",
device_map="auto"
)
# The default range for the number of visual tokens per image in the model is 4-16384.
# You can set min_pixels and max_pixels according to your needs, such as a token range of 256-1280, to balance performance and cost.
min_pixels = 256 * 28 * 28
max_pixels = 1280 * 28 * 28
processor = AutoProcessor.from_pretrained(
"/home/ct/work/BigModel/Qwen2.5-VL/models/Qwen2.5-VL-7B-Instruct",
min_pixels=min_pixels,
max_pixels=max_pixels
)
# 设置图像文件夹路径和提示词
image_folder = "/home/ct/work/Label_tools/PICS/Flame/"
prompt = "查看图像中红色矩形框中是否存在烟火,判定存在烟火需要看到明显的烟雾和火焰,注意区分灯光、太阳光和一些其他的影响。"
output_file = "inference_results.jsonl" # 结果输出文件
# 处理文件夹中的所有图像
results = process_images_in_folder(model, processor, image_folder, prompt, output_file)
# 打印汇总信息
print(f"\nProcessing completed. Total images processed: {len(results)}")
配置运行后,将会生成推理结果的 JSONL 文件。主要包含图像路径和对应描述。其他任务主要修改以下提示词就可以。 接下来需人工复核图像与 Qwen2.5-VL 描述的一致性。
建议一边测试一边记录,为安全起见,建议使用 Anaconda 建立 LLaMA-Factory 虚拟环境。 (1)克隆 LLaMA-Factory 项目
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
(2)创建虚拟环境
# 使用 conda(推荐)
conda create -n llama-factory python=3.10
conda activate llama-factory
# 或使用 venv
# python -m venv venv
# source venv/bin/activate
(3)安装依赖
pip install -r requirements.txt
目前我们的数据格式大概是:
{"image": "/path/to/image.jpg", "text": "图像描述语句"}
而 LLaMA-Factory 对于多模态大模型的建议数据格式为:
{"images": ["/home/ct/work/Label_tools/PICS/Smoke/Smoke001.png"], "conversations": [{"content": "<image>\n请分析图像中红色矩形框内是否存在吸烟行为,并说明理由。", "from": "user"}, {"content": "红色矩形框中的人在吸烟。", "from": "assistant"}]}
转换脚本如下:
import json
# 读取原始文件
input_file = "/home/ct/work/LLaMA-Factory/inference_results_Smoke.jsonl"
output_file = "/home/ct/work/LLaMA-Factory/smoke_dataset.jsonl"
with open(input_file, 'r', encoding='utf-8') as infile:
lines = infile.readlines()
# 转换格式
converted_lines = []
for line in lines:
data = json.loads(line.strip())
# 构建新的数据结构
new_data = {
"images": [data["image"]],
"conversations": [
{"content": "<image>\n请分析图像中红色矩形框内是否存在吸烟行为,并说明理由。", "from": "user"},
{"content": data["text"], "from": "assistant"}
]
}
converted_lines.append(json.dumps(new_data, ensure_ascii=False) + '\n')
# 写入新文件
with open(output_file, 'w', encoding='utf-8') as outfile:
outfile.writelines(converted_lines)
print(f"转换完成!已保存到 {output_file}")
(1)下载模型 Hugging Face 下载较慢,建议去魔塔社区下载,下载后置于 LLaMA-Factory 根目录下,新建 models 文件夹。
(2)构建 dataset_info.json 在 LLaMA-Factory 的根目录下新建该文件,并写入:
{"smoke_dataset": {"file_name": "smoke_dataset.jsonl", "formatting": "sharegpt", "columns": {"messages": "conversations", "images": "images"}, "tags": {"role_tag": "from", "content_tag": "content", "user_tag": "user", "assistant_tag": "assistant"}}}
注意 smoke_dataset 和 smoke_dataset.jsonl 两者需要对应。
(3)启动微调
cd /home/ct/work/LLaMA-Factory
python src/train.py \
--stage sft \
--do_train \
--model_name_or_path /home/ct/work/LLaMA-Factory/models/Qwen2.5-VL-7B-Instruct \
--dataset smoke_dataset \
--dataset_dir . \
--template qwen2_vl \
--finetuning_type lora \
--lora_target all \
--output_dir saves/Qwen2.5-VL-7B-Instruct-lora \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 8 \
--lr_scheduler_type cosine \
--logging_steps 10 \
--save_steps 100 \
--learning_rate 5e-5 \
--num_train_epochs 3.0 \
--plot_loss \
--fp16
生成的权重文件在 LLaMA-Factory 根目录下的 saves 文件夹下。 显存占用约 22GB。
在模型微调训练后,会在 saves 文件夹下生成一系列的微调权重文件,我使用的 LoRA 微调。大小在 100~300MB 之间。需要与原始权重文件合并。 可以采用 LLaMA-Factory 和 PyTorch+Transformers 等多种方法进行合并,脚本如下:
# merge_lora_weights.py
import os
import torch
from transformers import AutoModelForVision2Seq, AutoTokenizer, AutoProcessor
from peft import PeftModel
def merge_lora_weights():
# 配置路径
base_model_path = "models/Qwen2.5-VL-7B-Instruct" # 原始模型路径
lora_weights_path = "saves/Qwen2.5-VL-7B-Instruct-lora/checkpoint-3520" # LoRA 权重路径
output_path = "./merged_qwen2.5-vl-finetuned" # 合并后模型保存路径
print("Loading base model...")
base_model = AutoModelForVision2Seq.from_pretrained(
base_model_path,
torch_dtype=torch.float16,
low_cpu_mem_usage=True,
trust_remote_code=True
)
print("Loading LoRA adapter...")
lora_model = PeftModel.from_pretrained(base_model, lora_weights_path)
print("Merging weights...")
merged_model = lora_model.merge_and_unload()
print("Saving merged model...")
# 创建输出目录
os.makedirs(output_path, exist_ok=True)
# 保存模型
merged_model.save_pretrained(output_path, safe_serialization=True, max_shard_size="5GB")
# 保存 tokenizer 和 processor
tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True)
tokenizer.save_pretrained(output_path)
# 保存 processor(对 VL 模型很重要)
processor = AutoProcessor.from_pretrained(base_model_path, trust_remote_code=True)
processor.save_pretrained(output_path)
print(f"Merged model saved to {output_path}")
if __name__ == "__main__":
merge_lora_weights()
合并后模型权重大小: 接下来就是测试了。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online