视觉问答
视觉问答 (VQA) 是一项基于图像回答开放式问题的任务。支持此任务的模型的输入通常是图像和问题的组合,输出是以自然语言表达的答案。
一些值得注意的 VQA 使用案例包括
- 视障人士的无障碍应用。
- 教育:对讲座或教科书中提供的视觉材料提出问题。VQA 还可以用于互动博物馆展览或历史遗址。
- 客户服务和电子商务:VQA 可以通过允许用户询问有关产品的问题来增强用户体验。
- 图像检索:VQA 模型可用于检索具有特定特征的图像。例如,用户可以询问“有狗吗?”以从一组图像中查找所有包含狗的图像。
在本指南中,您将学习如何
- 微调分类 VQA 模型,特别是 ViLT,在
Graphcore/vqa
数据集上。 - 使用您微调的 ViLT 进行推理。
- 使用生成模型(如 BLIP-2)运行零样本 VQA 推理。
微调 ViLT
ViLT 模型将文本嵌入整合到视觉转换器 (ViT) 中,使其能够拥有用于视觉和语言预训练 (VLP) 的最小设计。此模型可用于多个下游任务。对于 VQA 任务,分类器头放置在顶部([CLS]
标记的最终隐藏状态顶部的线性层)并随机初始化。因此,视觉问答被视为一个**分类问题**。
更新的模型,如 BLIP、BLIP-2 和 InstructBLIP,将 VQA 视为一个生成任务。在本指南的后面,我们将说明如何将它们用于零样本 VQA 推理。
在开始之前,请确保您已安装所有必要的库。
pip install -q transformers datasets
我们鼓励您与社区共享您的模型。登录您的 Hugging Face 帐户将其上传到 🤗 Hub。出现提示时,输入您的令牌以登录
>>> from huggingface_hub import notebook_login
>>> notebook_login()
让我们将模型检查点定义为全局变量。
>>> model_checkpoint = "dandelin/vilt-b32-mlm"
加载数据
为了说明目的,在本指南中,我们使用了带注释的视觉问答 Graphcore/vqa
数据集的一个非常小的样本。您可以在 🤗 Hub 上找到完整的数据集。
作为 Graphcore/vqa
数据集 的替代方案,您可以从官方的 VQA 数据集页面 手动下载相同的数据。如果您希望使用自定义数据遵循本教程,请查看 🤗 数据集文档中有关如何 创建图像数据集 的指南。
让我们加载验证拆分中的前 200 个示例并探索数据集的特征
>>> from datasets import load_dataset
>>> dataset = load_dataset("Graphcore/vqa", split="validation[:200]")
>>> dataset
Dataset({
features: ['question', 'question_type', 'question_id', 'image_id', 'answer_type', 'label'],
num_rows: 200
})
让我们来看一个例子,以了解数据集的特征
>>> dataset[0]
{'question': 'Where is he looking?',
'question_type': 'none of the above',
'question_id': 262148000,
'image_id': '/root/.cache/huggingface/datasets/downloads/extracted/ca733e0e000fb2d7a09fbcc94dbfe7b5a30750681d0e965f8e0a23b1c2f98c75/val2014/COCO_val2014_000000262148.jpg',
'answer_type': 'other',
'label': {'ids': ['at table', 'down', 'skateboard', 'table'],
'weights': [0.30000001192092896,
1.0,
0.30000001192092896,
0.30000001192092896]}}
与任务相关的特征包括
question
:要从图像中回答的问题image_id
:问题所引用的图像的路径label
:注释
我们可以删除其余的特征,因为它们将不再需要
>>> dataset = dataset.remove_columns(['question_type', 'question_id', 'answer_type'])
如您所见,label
特征包含多个由不同人类注释者收集的针对相同问题的答案(此处称为 ids
)。这是因为问题的答案可能是主观的。在这种情况下,问题是“他在看哪里?”。有些人将其注释为“向下”,另一些人注释为“桌子”,另一个人注释为“滑板”等。
看看图像,考虑一下您会给出什么答案
>>> from PIL import Image
>>> image = Image.open(dataset[0]['image_id'])
>>> image
由于问题和答案的模糊性,此类数据集被视为多标签分类问题(因为多个答案可能有效)。此外,与其只创建一个独热编码向量,不如根据某个答案在注释中出现的次数创建一个软编码。
例如,在上面的示例中,因为答案“向下”比其他答案选择得更多,所以它具有 1.0 的分数(在数据集中称为 weight
),其余答案的分数 < 1.0。
为了以后使用合适的分类器头实例化模型,让我们创建两个字典:一个将标签名称映射到整数,反之亦然
>>> import itertools
>>> labels = [item['ids'] for item in dataset['label']]
>>> flattened_labels = list(itertools.chain(*labels))
>>> unique_labels = list(set(flattened_labels))
>>> label2id = {label: idx for idx, label in enumerate(unique_labels)}
>>> id2label = {idx: label for label, idx in label2id.items()}
现在我们有了映射,我们可以用它们的 id 替换字符串答案,并展平数据集以方便进一步预处理。
>>> def replace_ids(inputs):
... inputs["label"]["ids"] = [label2id[x] for x in inputs["label"]["ids"]]
... return inputs
>>> dataset = dataset.map(replace_ids)
>>> flat_dataset = dataset.flatten()
>>> flat_dataset.features
{'question': Value(dtype='string', id=None),
'image_id': Value(dtype='string', id=None),
'label.ids': Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None),
'label.weights': Sequence(feature=Value(dtype='float64', id=None), length=-1, id=None)}
预处理数据
下一步是加载 ViLT 处理器以准备模型的图像和文本数据。ViltProcessor 将 BERT 分词器和 ViLT 图像处理器包装到一个方便的单个处理器中
>>> from transformers import ViltProcessor
>>> processor = ViltProcessor.from_pretrained(model_checkpoint)
要预处理数据,我们需要使用 ViltProcessor 对图像和问题进行编码。处理器将使用 BertTokenizerFast 对文本进行标记并为文本数据创建 input_ids
、attention_mask
和 token_type_ids
。至于图像,处理器将利用 ViltImageProcessor 调整图像大小并对其进行归一化,并创建 pixel_values
和 pixel_mask
。
所有这些预处理步骤都在后台完成,我们只需要调用 processor
即可。但是,我们仍然需要准备目标标签。在此表示中,每个元素对应于一个可能的答案(标签)。对于正确答案,该元素包含其相应的分数(权重),而其余元素设置为零。
以下函数将 processor
应用于图像和问题,并如上所述格式化标签
>>> import torch
>>> def preprocess_data(examples):
... image_paths = examples['image_id']
... images = [Image.open(image_path) for image_path in image_paths]
... texts = examples['question']
... encoding = processor(images, texts, padding="max_length", truncation=True, return_tensors="pt")
... for k, v in encoding.items():
... encoding[k] = v.squeeze()
... targets = []
... for labels, scores in zip(examples['label.ids'], examples['label.weights']):
... target = torch.zeros(len(id2label))
... for label, score in zip(labels, scores):
... target[label] = score
... targets.append(target)
... encoding["labels"] = targets
... return encoding
要将预处理函数应用于整个数据集,请使用 🤗 数据集的 map
函数。您可以通过设置 batched=True
来加快 map
的速度,以便一次处理数据集的多个元素。此时,您可以随意删除不需要的列。
>>> processed_dataset = flat_dataset.map(preprocess_data, batched=True, remove_columns=['question','question_type', 'question_id', 'image_id', 'answer_type', 'label.ids', 'label.weights'])
>>> processed_dataset
Dataset({
features: ['input_ids', 'token_type_ids', 'attention_mask', 'pixel_values', 'pixel_mask', 'labels'],
num_rows: 200
})
作为最后一步,使用 DefaultDataCollator 创建一批示例
>>> from transformers import DefaultDataCollator
>>> data_collator = DefaultDataCollator()
训练模型
您现在可以开始训练您的模型了!使用 ViltForQuestionAnswering 加载 ViLT。指定标签数量以及标签映射
>>> from transformers import ViltForQuestionAnswering
>>> model = ViltForQuestionAnswering.from_pretrained(model_checkpoint, num_labels=len(id2label), id2label=id2label, label2id=label2id)
此时,只剩下三个步骤
- 在 TrainingArguments 中定义您的训练超参数
>>> from transformers import TrainingArguments
>>> repo_id = "MariaK/vilt_finetuned_200"
>>> training_args = TrainingArguments(
... output_dir=repo_id,
... per_device_train_batch_size=4,
... num_train_epochs=20,
... save_steps=200,
... logging_steps=50,
... learning_rate=5e-5,
... save_total_limit=2,
... remove_unused_columns=False,
... push_to_hub=True,
... )
- 将训练参数传递给 Trainer 以及模型、数据集、处理器和数据整理器。
>>> from transformers import Trainer
>>> trainer = Trainer(
... model=model,
... args=training_args,
... data_collator=data_collator,
... train_dataset=processed_dataset,
... tokenizer=processor,
... )
- 调用 train() 以微调您的模型。
>>> trainer.train()
训练完成后,使用 push_to_hub() 方法将模型共享到 Hub,以在 🤗 Hub 上共享您的最终模型
>>> trainer.push_to_hub()
推理
现在您已经微调了一个 ViLT 模型,并将其上传到 🤗 Hub,您可以使用它进行推理。尝试使用微调模型进行推理的最简单方法是在 Pipeline 中使用它。
>>> from transformers import pipeline
>>> pipe = pipeline("visual-question-answering", model="MariaK/vilt_finetuned_200")
本指南中的模型仅在 200 个示例上进行了训练,因此不要期望它能做太多。让我们看看它是否至少从数据中学到了一些东西,并以数据集中的第一个示例来说明推理过程。
>>> example = dataset[0]
>>> image = Image.open(example['image_id'])
>>> question = example['question']
>>> print(question)
>>> pipe(image, question, top_k=1)
"Where is he looking?"
[{'score': 0.5498199462890625, 'answer': 'down'}]
尽管置信度不高,但模型确实学到了一些东西。使用更多示例和更长的训练,您将获得更好的结果!
如果您愿意,也可以手动复制 pipeline 的结果。
- 获取一张图像和一个问题,使用模型的处理器为模型准备它们。
- 将预处理结果输入模型。
- 从 logits 中获取最可能的答案的 id,并在
id2label
中找到实际的答案。
>>> processor = ViltProcessor.from_pretrained("MariaK/vilt_finetuned_200")
>>> image = Image.open(example['image_id'])
>>> question = example['question']
>>> # prepare inputs
>>> inputs = processor(image, question, return_tensors="pt")
>>> model = ViltForQuestionAnswering.from_pretrained("MariaK/vilt_finetuned_200")
>>> # forward pass
>>> with torch.no_grad():
... outputs = model(**inputs)
>>> logits = outputs.logits
>>> idx = logits.argmax(-1).item()
>>> print("Predicted answer:", model.config.id2label[idx])
Predicted answer: down
零样本视觉问答
之前的模型将 VQA 视为一个分类任务。一些最新的模型,例如 BLIP、BLIP-2 和 InstructBLIP 将 VQA 视为一个生成任务。让我们以 BLIP-2 为例。它引入了一种新的视觉语言预训练范式,其中可以使用任何预训练的视觉编码器和 LLM 的组合(在 BLIP-2 博客文章 中了解更多信息)。这使得能够在包括视觉问答在内的多个视觉语言任务上取得最先进的结果。
让我们说明如何将此模型用于 VQA。首先,让我们加载模型。在这里,如果可用,我们将显式地将模型发送到 GPU,这在训练时我们不需要这样做,因为 Trainer 会自动处理。
>>> from transformers import AutoProcessor, Blip2ForConditionalGeneration
>>> import torch
>>> processor = AutoProcessor.from_pretrained("Salesforce/blip2-opt-2.7b")
>>> model = Blip2ForConditionalGeneration.from_pretrained("Salesforce/blip2-opt-2.7b", torch_dtype=torch.float16)
>>> device = "cuda" if torch.cuda.is_available() else "cpu"
>>> model.to(device)
模型以图像和文本作为输入,因此让我们使用 VQA 数据集中第一个示例中完全相同的图像/问题对。
>>> example = dataset[0]
>>> image = Image.open(example['image_id'])
>>> question = example['question']
要将 BLIP-2 用于视觉问答任务,文本提示必须遵循特定的格式:Question: {} Answer:
。
>>> prompt = f"Question: {question} Answer:"
现在我们需要使用模型的处理器预处理图像/提示,将处理后的输入传递给模型,并解码输出。
>>> inputs = processor(image, text=prompt, return_tensors="pt").to(device, torch.float16)
>>> generated_ids = model.generate(**inputs, max_new_tokens=10)
>>> generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
>>> print(generated_text)
"He is looking at the crowd"
如您所见,模型识别了人群和面部的朝向(向下看),但是,它似乎忽略了人群在滑冰者后面的事实。尽管如此,在无法获取人工标注数据集的情况下,这种方法可以快速产生有用的结果。
< > 在 GitHub 上更新