Transformers documentation
Визуальный вопросно-ответ
Визуальный вопросно-ответ
Visual Question Answering (VQA) — это задача, в которой модель отвечает на открытые вопросы, основываясь на изображении. Входными данными для моделей, поддерживающих эту задачу, обычно являются сочетание изображения и вопроса, а выходом — ответ, сформулированный на естественном языке.
Среди примечательных примеров использования VQA можно выделить:
- Приложения для обеспечения доступности для людей с нарушениями зрения.
- Образование: постановка вопросов по визуальным материалам, представленным в лекциях или учебниках. VQA также может использоваться в интерактивных музейных экспозициях или на исторических объектах.
- Служба поддержки и электронная коммерция: VQA может улучшить пользовательский опыт, позволяя задавать вопросы о товарах.
- Поиск изображений: модели VQA можно использовать для поиска изображений с определёнными характеристиками. Например, пользователь может задать вопрос «Есть ли здесь собака?», чтобы найти все изображения с собаками из набора.
В этом руководстве вы узнаете, как:
- Настройте модель классификации VQA, а именно ViLT, на датасете
Graphcore/vqa. - Используйте дообученную модель ViLT для инференса.
- Выполните zero-shot VQA-инференс с генеративной моделью, например BLIP-2.
Доработка ViLT
Модель ViLT включает текстовые эмбеддинги в Vision Transformer (ViT), что позволяет использовать минималистичную архитектуру для предобучения на задачах зрения и языка (VLP). Эту модель можно применять для нескольких задач. Для задачи VQA на верхушку модели добавляется классификатор (линейный слой поверх финального скрытого состояния токена [CLS]), который инициализируется случайными значениями. Таким образом, Visual Question Answering рассматривается как задача классификации.
Более новые модели, такие как BLIP, BLIP-2 и InstructBLIP, рассматривают VQA как генеративную задачу. Позже в этом руководстве мы покажем, как использовать их для zero-shot-инференса 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. Если вы предпочитаете следовать учебнику, используя собственные данные, ознакомьтесь с руководством Создание набора данных изображений в документации 🤗 Datasets.
Загрузим первые 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
Из-за неоднозначности вопросов и ответов такие наборы данных рассматриваются как задача многометочной классификации (поскольку допустимо несколько правильных ответов). Более того, вместо создания одногорячего вектора (one-hot) формируется мягкое кодирование (soft encoding), основанное на количестве появлений конкретного ответа в аннотациях.
Например, в приведённом выше примере ответ «down» выбирается значительно чаще, чем остальные, поэтому его оценка (называемая 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()}Теперь, когда у нас есть соответствия, мы можем заменить строковые ответы на их идентификаторы и развернуть набор данных для более удобной дальнейшей предобработки.
>>> 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 из 🤗 Datasets. Вы можете ускорить map, установив batched=True, чтобы обрабатывать несколько элементов набора данных одновременно. На этом этапе можно удалить столбцы, которые вам не нужны.
>>> 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()Обучение модели
Теперь вы готовы приступить к обучению модели! Загрузите ViLT с помощью ViltForQuestionAnswering. Укажите количество меток, а также соответствия меток:
>>> 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 вместе с моделью, набором данных, процессором и data collator.
>>> 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:
>>> trainer.push_to_hub()Вывод
Теперь, когда вы дообучили модель ViLT и загрузили её в 🤗 Hub, вы можете использовать её для инференса.
- Возьмите изображение и вопрос, подготовьте их для модели с помощью процессора вашей модели.
- Передайте результат или результат предобработки в модель.
- По значениям 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: downZero-shot VQA
Предыдущая модель рассматривала VQA как задачу классификации. Некоторые современные модели, такие как BLIP, BLIP-2 и InstructBLIP, подходят к VQA как к генеративной задаче. Возьмём, например, BLIP-2. В ней была введена новая парадигма предобучения мультимодальных моделей, в которой можно использовать любую комбинацию предобученного визуального энкодера и LLM (подробнее в блог-посте о BLIP-2). Это позволяет достигать передовых результатов на множестве мультимодальных задач, включая визуальный вопросно-ответный диалог.
Покажем, как можно использовать эту модель для VQA. Сначала загрузим модель. Здесь мы явно перенесём модель на GPU, если он доступен, чего не требовалось делать ранее при обучении, поскольку Trainer выполняет это автоматически:
>>> from transformers import AutoProcessor, Blip2ForConditionalGeneration
from accelerate import Accelerator
>>> import torch
>>> processor = AutoProcessor.from_pretrained("Salesforce/blip2-opt-2.7b")
>>> model = Blip2ForConditionalGeneration.from_pretrained("Salesforce/blip2-opt-2.7b", dtype=torch.float16)
>>> device = Accelerator().device
>>> 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"Как видно, модель распознала толпу и направление взгляда (вниз), однако, похоже, она не учла, что толпа находится за скейтбордистом. Тем не менее, в случаях, когда получение размеченных человеком датасетов невозможно, этот подход позволяет быстро получить полезные результаты.
Update on GitHub