视觉问答 (VQA, Visual Question Answering)¶
视觉问答(Visual Question Answering,简称 VQA)是一项基于图像回答开放式问题的任务。支持此任务的模型的输入通常是图像和问题的组合,输出是以自然语言表达的回答。
VQA 的一些值得注意的用例包括:
- 为视障人士提供无障碍应用。
- 教育:对讲座或教科书中呈现的视觉材料提出问题。VQA 还可用于互动博物馆展览或历史遗址。
- 客户服务和电子商务:VQA 可以通过让用户询问有关产品的问题来提升用户体验。
- 图像检索:VQA 模型可用于检索具有特定特征的图像。例如,用户可以问“有狗吗?”以从一组图像中找到所有包含狗的图像。
在本指南中,你将学习如何:
- 在
Graphcore/vqa数据集上微调分类 VQA 模型,特别是 ViLT。 - 使用微调后的 ViLT 进行推理。
- 使用生成模型(如 BLIP-2)运行零样本 VQA 推理。
微调 ViLT¶
ViLT 模型将文本嵌入到视觉 Transformer(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"
from datasets import load_dataset
dataset = load_dataset("Graphcore/vqa", split="validation[:200]")
print(dataset)
让我们看一下示例,以了解数据集的特征:
print(dataset[0])
与任务相关的特征包括:
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.show()
由于问题和答案的模糊性,这样的数据集被视为多标签分类问题(因为多个答案可能有效)。此外,不是简单地创建一个独热编码向量,而是根据某个答案在注释中出现的次数创建软编码。
例如,在上面的示例中,因为答案“向下”被选中的次数远远多于其他答案,所以它的得分(在数据集中称为 weight)为 1.0,其余答案的得分小于 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()
print(flat_dataset.features)
预处理数据¶
下一步是加载 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
processed_dataset = flat_dataset.map(preprocess_data, batched=True, remove_columns=['question','question_type', 'question_id', 'image_id', 'answer_type', 'label.ids', 'label.weights'])
print(processed_dataset)
最后一步,使用 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,
processing_class=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)
print(pipe(image, question, top_k=1))
尽管不太自信,但模型确实学到了一些东西。通过更多的示例和更长的训练,你将得到更好的结果!
如果你愿意,你还可以手动复制管道的结果:
- 取一个图像和问题,使用模型中的处理器为模型准备它们。
- 将预处理的结果或输入通过模型。
- 从 logits 中获取最可能的答案的 id,并在
id2label中找到实际的答案。
processor = ViltProcessor.from_pretrained("MariaK/vilt_finetuned_200")
image = Image.open(example['image_id'])
question = example['question']
inputs = processor(image, question, return_tensors="pt")
model = ViltForQuestionAnswering.from_pretrained("MariaK/vilt_finetuned_200")
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
idx = logits.argmax(-1).item()
print("Predicted answer:", model.config.id2label[idx])
零样本 VQA¶
前面的模型将 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)
如你所见,该模型识别了人群,以及脸部的方向(向下看),但它似乎忽略了人群在滑板手后面的事实。不过,在无法获得人工注释数据集的情况下,这种方法可以快速产生有用的结果。