从零到一 | CV转多模态大模型 | week10 | Multitask Data构建多任务数据支持(Caption + VQA + QA)
Multitask Data
摘要: 本文详细介绍了如何构建多任务数据集来训练多模态大模型。首先,将不同格式的数据(如 COCO Caption、VQA)统一转换为 LLaVA 风格的对话格式。然后,通过配置采样权重平衡各数据集的训练比例,并使用 MultiTaskLlavaDataset 和 WeightedRandomSampler 实现按权重的有放回采样。最终,模型能够同时学习图像描述、视觉问答等多种任务,为后续增加多轮对话能力打下基础。
文中代码: https://github.com/wz940216/From0to1-MLLM-StudyLog.git
上一篇重新梳理llava的结构细节,从源码出发再次理解llava的核心原理。
从零到一 | CV转多模态大模型 | week09 | Minillava Refactor结合手搓和llava源码深入理解多模态大模型原理
第十周,开始让模型支持多任务数据,整体思路是先将所有数据格式转换成统一 llava conversation 格式,在按特定比例抽样,让模型同时训练多任务数据。
COCO caption 和 QA 格式:
caption 是看图生成一句描述,QA 指令是看图后按问题回答。
COCO Caption 格式
{
"image": "xxx.jpg",
"caption": "A dog is running on the grass."
}
训练目标通常是:
输入:图像
输出:A dog is running on the grass.
特点:
- 没有明确问题
- 输出是完整自然语言描述
- 偏向图像整体内容描述
- 一张图通常有多条 caption
- 适合训练图像描述能力
QA 指令格式
{
"image": "xxx.jpg",
"conversations": [
{
"from": "human",
"value": "<image>\nWhat is the dog doing?"
},
{
"from": "gpt",
"value": "The dog is running on the grass."
}
]
}
训练目标通常是:
输入:图像 + 问题
输出:The dog is running on the grass.
特点:
- 有明确 instruction/question
- 输出针对问题,不一定描述整张图
- 可以是问答、判断、计数、推理、定位等
- 更接近多模态 Chat / Instruction Tuning
- 适合训练模型按用户指令回答
我们要把 COCO caption 转成 QA 指令格式,只需要在 caption中 加一个固定问题即可:
{
"image": "xxx.jpg",
"conversations": [
{
"from": "human",
"value": "<image>\nDescribe this image."
},
{
"from": "gpt",
"value": "A dog is running on the grass."
}
]
}
也可以用中文:
{
"from": "human",
"value": "<image>\n请描述这张图片。"
}
本质上,caption 数据可以看成一种特殊的 QA 数据:问题固定为请描述图片,答案就是 caption。
我们再来看一下 llava 的数据格式:
单轮对话
{
"id": "0000001",
"image": "0000001.jpg",
"conversations": [
{
"from": "human",
"value": "<image>\n请描述这张图片。"
},
{
"from": "gpt",
"value": "图片中有一只橘猫趴在沙发上。"
}
]
}
多轮对话
{
"id": "2",
"image": "dog.jpg",
"conversations": [
{
"from": "human",
"value": "<image>\n图里有什么?"
},
{
"from": "gpt",
"value": "一只狗。"
},
{
"from": "human",
"value": "它在做什么?"
},
{
"from": "gpt",
"value": "它正在草地上奔跑。"
}
]
}
VQA(Visual Question Answering,视觉问答)
给定一张图片和一个问题,模型需要根据图片回答问题。
VQA中一般有两个json文件,一个用来存储问题,一个用来存储答案。
答案 annotations 字段含义:
{
"question_type": "is it",
"multiple_choice_answer": "yes",
"answers": [...],
"image_id": 28940,
"answer_type": 'yes/no',
"question_id": 289402
}
image_id: 28940
对应图片的 ID。数据集中会有一张图片编号为 28940。
question_id: 289402
这个问题的唯一 ID。通常一个图片可以对应多个问题。
question_type:‘is it’
问题类型,表示这个问题大概率是以 “Is it …?” 开头,比如:
Is it raining?
Is it a dog?
answer_type: ‘yes/no’
答案类型是“是/否”问题。
multiple_choice_answer: ‘yes’
官方整理后的标准答案是 yes。
answers: [{…}, {…}, …]
多个人类标注者给出的答案。VQA 通常会让 10 个人回答同一个问题,所以这里看到很多 {…}。里面一般类似:
{
"answer": "yes",
"answer_confidence": "yes",
"answer_id": 1
}
也就是说,不同标注者可能都回答了 yes,也可能有人回答 no 或其他近似答案。
问题 questions 字段含义:
{
"question_id": 289402,
"image_id": 28940,
"question": "Is it ...?"
}
question_id :对应 annotations 中的 question_id。
question: 对应问题
可以通过脚本查看,感受一下vqa数据集的格式:
import json
ann_path = "dataset/VQA/abstract_v002_val2017_annotations.json"
ques_path = "dataset/VQA/OpenEnded_abstract_v002_val2017_questions.json"
with open(ann_path, "r") as f:
anns = json.load(f)["annotations"]
with open(ques_path, "r") as f:
ques = json.load(f)["questions"]
qid = 289402
ann = next(x for x in anns if x["question_id"] == qid)
q = next(x for x in ques if x["question_id"] == qid)
print("image_id:", ann["image_id"])
print("question:", q["question"])
print("answer:", ann["multiple_choice_answer"])
print("all answers:", [a["answer"] for a in ann["answers"]])
构建多任务数据集
将各个任务的数据集整理成统一格式后,如果直接放在 dataset 中加载,会产生数据集数量多的采样概率大,数据及数量小的,被采样的概率变小的问题。
最好在加载数据集时,人为设置采样权重,平衡各个数据集的采样率。
在配置文件里设置各个数据集任务的数据地址和采样率:
DATA:
TRAIN_DATASET:
TASK_NAME:
- "LLaVA-CC3M-Pretrain"
- "COCOCaption"
- "VQA"
PATH:
- "dataset/LLaVA-CC3M-Pretrain-595K"
- "dataset/COCOCaption"
- "dataset/VQA"
IMAGE_DIR:
- "images"
- "val2017"
- "scene_img_abstract_v002_val2017"
ANNOTATION_FILE:
- "chat.json"
- "annotations/captions_val2017_qa.json"
- "abstract_v002_val2017_qa.json"
SAMPLE_RATE:
- 0.4
- 0.3
- 0.3
这里每个 list 的同一位置对应一个任务:
第 0 个任务:
name = LLaVA-CC3M-Pretrain
path = dataset/LLaVA-CC3M-Pretrain-595K
image_dir = images
annotation = chat.json
sample_rate = 0.4
第 1 个任务:
name = COCOCaption
path = dataset/COCOCaption
image_dir = val2017
annotation = annotations/captions_val2017_qa.json
sample_rate = 0.3
第 2 个任务:
name = VQA
path = dataset/VQA
image_dir = scene_img_abstract_v002_val2017
annotation = abstract_v002_val2017_qa.json
sample_rate = 0.3
train.py 里的 build_train_dataset() 会把这些 list 拆开,逐个创建 LlavaPretrainDataset。单个数据源仍然还是由LlavaPretrainDataset 来处理,我们只需要合并它们,并且给予对应的采样权重即可。
合并方法:
class MultiTaskLlavaDataset(Dataset):
"""把多个 LLaVA 风格数据集合并成一个训练集。
该类只负责把多个子数据集拼接起来;真正的按比例采样交给
train.py 中的 WeightedRandomSampler,这样数据读取和采样策略保持解耦。
"""
def __init__(self, datasets, task_names=None, sample_rates=None):
if not datasets:
raise ValueError("MultiTaskLlavaDataset 至少需要一个子数据集。")
self.datasets = list(datasets)
self.task_names = task_names or [
getattr(dataset, "task_name", f"task_{idx}")
for idx, dataset in enumerate(self.datasets)
]
self.sample_rates = sample_rates
self.concat_dataset = ConcatDataset(self.datasets)
self.sample_to_task = []
for task_idx, dataset in enumerate(self.datasets):
self.sample_to_task.extend([task_idx] * len(dataset))
def __len__(self):
return len(self.concat_dataset)
def __getitem__(self, index):
sample = self.concat_dataset[index]
task_idx = self.sample_to_task[index]
sample.setdefault("task_name", self.task_names[task_idx])
sample["task_index"] = task_idx
return sample
def build_sample_weights(self):
"""为 WeightedRandomSampler 构造逐样本权重。
每个任务内所有样本共享同一权重:任务采样率 / 任务样本数。
因此按 replacement 抽样时,抽中某个任务的总概率接近 SAMPLE_RATE。
"""
if self.sample_rates is None:
return None
if len(self.sample_rates) != len(self.datasets):
raise ValueError("SAMPLE_RATE 数量必须和数据集数量一致。")
total_rate = sum(float(rate) for rate in self.sample_rates)
if total_rate <= 0:
raise ValueError("SAMPLE_RATE 之和必须大于 0。")
weights = []
for dataset, rate in zip(self.datasets, self.sample_rates):
dataset_len = len(dataset)
if dataset_len == 0:
raise ValueError("子数据集不能为空。")
task_weight = float(rate) / total_rate / dataset_len
weights.extend([task_weight] * dataset_len)
return weights
使用 PyTorch 中的工具函数拼接索引空间
ConcatDataset(self.datasets)
生成每个样本的权重
一个很直觉的算法就是:
某个任务中每条样本的权重 = 该任务采样比例 / 所有采样比例之和 / 该任务样本数
例如 COCO 单条样本的权重会比 LLaVA 单条样本大很多,因为 COCO 样本少,但整个 COCO 任务仍然按照配置文件设置的,只占 30% 的抽样概率。
采样时,使用 PyTorch 的工具函数,传入事先准备的 sample_weights ,并设置可放回采样 replacement=True ,表示同一个样本在一个 epoch 里可能被抽到多次,也可能一次都没抽到。
WeightedRandomSampler(
weights=sample_weights,
num_samples=len(dataset),
replacement=True
)
来看一下整体流程:
Caption / VQA / LLaVA chat
↓
统一成 conversations
↓
LlavaPretrainDataset
↓
MultiTaskLlavaDataset + ConcatDataset
↓
按 SAMPLE_RATE 生成逐样本权重
↓
WeightedRandomSampler 有放回采样
↓
LlavaCollator 构造 input_ids / labels
↓
MiniLLaVA 训练
多任务对话完成后,我们可以用这种方法联合训练各种开源数据集,下周计划再增加多轮对话能力,构建多轮对话数据集。
以上笔记来源于我的仓库: https://github.com/wz940216/From0to1-MLLM-StudyLog.git
我正在连载一个从零到一的多模态大模型学习笔记。
如果你对多模态大模型感兴趣,或者也在准备往大模型方向转
可以点赞/Fork我的仓库: https://github.com/wz940216/From0to1-MLLM-StudyLog.git
也可评论区留言交流,后面我会继续把每周的学习记录、踩坑经验陆续更新到仓库和这里。
更多推荐
所有评论(0)