Qwen-2.5-7B-VL-SFT with Official Code
本文介绍了如何使用官方代码对 Qwen-2.5-7B-VL(-Instruct) 进行 SFT(Supervised Fine-Tuning)。提供了详细的环境配置、数据集准备、训练脚本编写等步骤,希望帮助大家顺利完成模型微调。
Source
对 Qwen2.5-VL 的微调我们只需要他库里的 Qwen2.5-VL/qwen-vl-fintune 文件夹即可。一般来说我们只需要对 qwenvl/data/__init__.py 和 scripts/sft.sh(我是在 sft_7b.sh 上更改的) 这两个脚本稍稍修改就能顺利运行。读取文件还有特殊步骤的就要在 qwenvl/data/data_qwen.py 和 qwenvl/data/data_qwen_packed.py 做同样修改即可。
环境配置
官方有给出必要的各个包的版本要求,我的 python 版本是 3.11,方便参考,我也把我的 requirements.txt 放在这里。
数据集
查看你的数据格式是否满足官方的格式,数据的格式根据官方提供的示例更改即可。重点说一下 dataset dictionary 的格式,以我的 dataset dictionary 为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"dataset1": {
"data_path": "/data/my_dataset/images/",
"annotation_path": "/data/my_dataset/annotations1.jsonl",
"length": 11739,
"sampleing_rate": 0.05437
},
"dataset2": {
"data_path": "/data/my_dataset/images/",
"annotation_path": "/data/my_dataset/annotations2.jsonl",
"length": 33456,
"sampling_rate": 0.567
},
}
如果你有和我一样的数据集汇总文件 Dataset-Dictionary.json,那么你在文件 qwenvl/data/__init__.py中就可以直接读取你的 Dataset-Dictionary.json 文件作为你的data_dict,从而不需要像官方的一样在 qwenvl/data/__init__.py 文件里一条条列出来。这是我在文件里添加的读取为 data_dict 的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 假设你的 JSON 文件路径为 'Dataset-Dictionary.json'
json_file_path = '/path/to/your/Dataset-Dictionary.json'
# 读取 JSON 文件并解析为 python 字典
with open(json_file_path, 'r', encoding='utf-8') as f:
raw_data = json.load(f)
# 筛选出每个数据项中需要的两个字段,此处按需更改,为了方便我就只要这两个属性
data_dict = {
key: {
'annotation_path': value['annotation_path'],
'data_path': value['data_path']
}
for key, value in raw_data.items()
}
注意正常我们的
Dataset-Dictionary.json里会提供每个数据的repeat_time或者sampling_rate,这个属性我在上面的代码里丢掉了,后面我会在sft.sh脚本里加上。
训练脚本
Model Path
1
2
3
4
5
6
7
llm=/path/to/your/model #你的模型路径
#对应下面的
args="
...
--model_name_or_path "${llm}" \
...
"
Datasets
1
2
3
4
5
6
7
8
datasets=$(jq -r 'to_entries | map("\(.key)%\((.value.repeat_time * 100 + 0.5) | floor)") | join(",")' /path/to/your/Dataset_Dictionary.json)
#对应下面的
...
args="
...
--dataset_use ${datasets} \
...
"
此处的脚本是为了读取 dataset dictionary 所有的数据集名称以及repeat_time 或者 sampling_rate形成该参数需要传入的格式。
训练前一定要在终端 echo 出你的
datasets检查是否满足要求的格式。
记得检查有没有安装 jq 这个工具,没有的话记得安装:
1
apt-get update && apt-get install -y jq
当然你也可以手动输入各个数据集的名称,格式如下(如果你训练的数据集数量少的话):
1
datasets=your_dataset1%100,your_dataset2%76,your_dataset3%1
Output Configuration
运行名称和 checkpoint 保存路径自行设置即可。
1
2
3
4
5
6
7
8
9
run_name=Qwen2.5-VL-7B
output_dir=/path/to/your/model/checkpoint
...
args="
...
--output_dir ${output_dir} \
--run_name ${run_name} \
...
"
注意每次从头开始训的
output_dir要不一样。
WANDB
实验数据的可视化与记录集成记录在 WANDB 平台工具,使用 wandb 首先要在网站上创建 team,然后在 team下创建 project,然后 project 下会记录每个实验的详细数据。创建 project 也可以在训练的时候一起创建,run_name 是当前创建的 project 里每个 run 的名称,一个 project 可以有很多 run。
1
2
3
4
5
6
7
8
9
10
11
export WANDB_PROJECT=Name-of-your-project
export WANDB_API_KEY=you-can-find-in-website
# 上面两行添加在脚本前面
...
run_name=your-run-name
...
args="
...
--run_name ${run_name} \
...
"
注意这里的
run_name和上面Output Configuration里的run_name是 同一个变量。
Hyperparameters
Learning Rate
官方给的脚本里的学习率是 2e-7(我个人觉得有点小,不过大家后续调参的时候可以先试试 2e-5)。
1
2
3
4
5
6
7
lr=2e-7
...
args="
...
--learning_rate ${lr} \
...
"
Batch Size
1
2
3
4
5
6
7
8
9
10
batch_size=4
grad_accum_steps=4
...
args="
...
--per_device_train_batch_size ${batch_size} \
--per_device_eval_batch_size $((batch_size*2)) \
--gradient_accumulation_steps ${grad_accum_steps} \
...
"
注意这里的
batch_size是指每张卡上的batch_size,所以真实的Batch Size = batch_size × grad_accum_steps × 卡的数量,比如我现在有16张卡,那我真实的Batch Size = 4 × 4 × 16 = 256。
Epoch
1
2
3
4
5
6
7
epoch=3
...
args="
...
--num_train_epochs ${epoch} \
...
"
Max Pixels
官方设置里的 max_pixels 是50176,我训练的时候翻一倍(100352)会好一点,这个也是因人而异调整。
1
2
3
4
5
args="
...
--max_pixels 100352 \
...
"
Others
其余的超参数各位按需调整。
特殊要求
如果你的数据和我一样不能直接读取,而是存储在 ceph 里的 bucket 上,那就需要在 qwenvl/data/data_qwen.py 和 qwenvl/data/data_qwen_packed.py 中的函数 process_image_unifiedvideo_decord 和 video_torchcodec 做特殊的更改。
Issue
官方的代码貌似有一个 bug:issue
在 qwenvl/data/data_qwen.py #450 行的代码(qwenvl/data/data_qwen_packed.py也一样的问题):
1
2
3
4
5
6
if "image" not in sources[0] and "video" not in sources[0]:
grid_thw_merged = None
sources = copy.deepcopy([e["conversations"] for e in sources])
data_dict = preprocess_qwen_2_visual(
sources, self.tokenizer, grid_thw=grid_thw_merged
)
原因是因为 preprocess_qwen_2_visual 这个函数并不接受 grid_thw 这个参数:
1
2
3
4
5
6
def preprocess_qwen_2_visual(
sources,
tokenizer: transformers.PreTrainedTokenizer,
grid_thw_image: List = [],
grid_thw_video: List = [],
) -> Dict:
修改成下面的代码就可以:
1
2
3
4
5
6
if "image" not in sources[0] and "video" not in sources[0]:
grid_thw_merged = None
sources = copy.deepcopy([e["conversations"] for e in sources])
data_dict = preprocess_qwen_2_visual(
sources, self.tokenizer, grid_thw_image=grid_thw_merged, grid_thw_video=grid_thw_merged
)