시각 질의응답
Visual Question Answering(VQA)은 이미지를 기반으로 열린 형식의 질문에 답하는 작업입니다. 이 작업을 지원하는 모델의 입력은 일반적으로 이미지와 질문의 조합이며, 출력은 자연어로 표현된 답변입니다.
VQA의 주목할 만한 사용 사례 예시에는 다음이 포함됩니다:
- 시각 장애인을 위한 접근성 애플리케이션.
- 교육: 강의나 교재에서 제시된 시각 자료를 바탕으로 질문을 제기하는 것입니다. VQA는 인터랙티브 박물관 전시물이나 역사 유적지에서도 활용될 수 있습니다.
- 고객 서비스 및 전자상거래: VQA는 사용자가 제품에 대해 질문할 수 있도록 하여 사용자 경험을 향상시킬 수 있습니다.
- 이미지 검색: VQA 모델은 특정 특성을 가진 이미지를 검색하는 데 사용할 수 있습니다. 예를 들어, 사용자는 "개가 있나요?"라고 질문하여 이미지 세트에서 개가 포함된 모든 이미지를 찾을 수 있습니다.
이 가이드에서는 다음 내용을 배웁니다:
- 구체적으로 ViLT인 분류 VQA 모델을
Graphcore/vqa데이터셋으로 fine-tune합니다. - 파인튜닝한 ViLT를 추론에 사용하세요.
- BLIP-2와 같은 생성 모델로 zero-shot VQA 추론을 실행합니다.
ViLT 파인튜닝
ViLT 모델은 텍스트 임베딩을 Vision Transformer(ViT)에 통합하여 Vision-and-Language Pre-training(VLP)에 최소한의 설계를 갖도록 합니다. 이 모델은 여러 다운스트림 작업에 사용할 수 있습니다. VQA 작업의 경우, 분류기 헤드가 [CLS] 토큰의 최종 히든 상태 위에 있는 선형 레이어로) 최상단에 배치되며 무작위로 초기화됩니다. 따라서 Visual Question Answering은 분류 문제로 처리됩니다.
BLIP, BLIP-2, InstructBLIP와 같은 최근 모델들은 VQA를 생성형 작업으로 취급합니다. 이 가이드의 후반부에서 이러한 모델들을 사용하여 zero-shot VQA 추론을 수행하는 방법을 보여드리겠습니다.
시작하기 전에, 필요한 라이브러리가 모두 설치되어 있는지 확인하세요.
pip install -q transformers datasets
모델을 커뮤니티와 공유하시도록 권장합니다. 🤗 Hub에 업로드하려면 Hugging Face 계정에 로그인하세요. 프롬프트가 표시되면 로그인할 토큰을 입력하세요:
>>> from huggingface_hub import notebook_login
>>> notebook_login()모델 체크포인트를 전역 변수로 정의해 보겠습니다.
>>> model_checkpoint = "dandelin/vilt-b32-mlm"데이터 로드
설명 목적으로, 이 가이드에서는 주석 처리된 시각 질의응답 Graphcore/vqa 데이터셋의 매우 작은 샘플을 사용합니다.
전체 데이터셋은 🤗 Hub에서 찾을 수 있습니다.
Graphcore/vqa 데이터셋의 대안으로, 공식 VQA 데이터셋 페이지에서 동일한 데이터를 수동으로 다운로드할 수 있습니다. 사용자 정의 데이터로 튜토리얼을 진행하기를 원하신다면, 🤗 Datasets 문서의 이미지 데이터셋 만들기 가이드를 확인해 보세요.
validation 분할에서 첫 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라고 함)가 포함되어 있습니다.
이는 질문에 대한 답변이 주관적일 수 있기 때문입니다. 이 경우 질문은 “그는 어디를 보고 있나요?”입니다. 어떤 주석자는 “down”으로, 다른 주석자는 “at table”로, 또 다른 주석자는 “skateboard”로 주석했습니다.
이미지를 살펴보고 어떤 답변을 제시할지 생각해 보세요:
>>> from PIL import Image
>>> image = Image.open(dataset[0]['image_id'])
>>> image
질문과 답변의 모호성으로 인해, 이러한 데이터셋은 다중 레이블 분류 문제로 취급됩니다(여러 답변이 동시에 유효할 수 있기 때문이죠). 또한, 단순히 one-hot 인코딩 벡터를 생성하는 대신, 특정 답변이 어노테이션에 나타난 횟수에 기반한 soft 인코딩을 생성합니다.
예를 들어, 위 예시에서는 “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()}이제 매핑이 준비되었으므로, 문자열 답변을 해당 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를 사용하여 이미지와 질문을 인코딩해야 합니다. 프로세서가 텍스트를 토큰화하고 텍스트 데이터에 대한 input_ids, attention_mask, token_type_ids를 생성하는 데 BertTokenizerFast를 사용합니다.
이미지의 경우, 프로세서가 ViltImageProcessor를 활용하여 이미지를 리사이즈하고 정규화한 뒤 pixel_values와 pixel_mask를 생성합니다.
이 모든 전처리 단계는 내부적으로 수행되며, 우리는 processor만 호출하면 됩니다. 그러나 여전히 대상 라벨을 준비해야 합니다. 이 표현 방식에서 각 요소는 가능한 답변(라벨) 하나를 나타냅니다. 정답인 경우 해당 요소에는 각각의 점수(가중치)가 저장되고, 나머지 요소는 0으로 설정됩니다.
다음 함수는 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전체 데이터셋에 전처리 함수를 적용하려면 🤗 Datasets의 map 함수를 사용합니다. 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를 사용하여 예시 배치(batch)를 생성합니다:
>>> 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에 공유하세요:
>>> trainer.push_to_hub()추론
ViLT 모델을 파인튜닝하고 🤗 Hub에 업로드했으므로 이제 추론에 사용할 수 있습니다.
- 이미지와 질문을 가져온 후, 모델의 processor를 사용하여 모델에 맞게 준비합니다.
- 결과 또는 전처리 결과를 모델로 전달합니다.
- 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를 예로 들어 보겠습니다. BLIP-2는 사전 학습된 비전 인코더와 LLM의 임의의 조합을 사용할 수 있는 새로운 시각-언어 사전 학습 패러다임을 도입했습니다(BLIP-2 블로그 게시물에서 자세히 알아보세요). 이를 통해 시각 질의응답을 포함한 여러 시각-언어 작업에서 최첨단 결과를 달성할 수 있습니다.
이 모델로 VQA를 수행하는 방법을 살펴보겠습니다. 먼저 모델을 로드합니다. 여기서는 Trainer가 자동으로 처리해 주었기 때문에 학습 시에는 필요하지 않았던 GPU로 모델을 명시적으로 전송합니다(사용 가능한 경우):
>>> 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:"이제 모델의 processor로 이미지/프롬프트를 전처리하고, 전처리된 입력을 모델에 전달한 후 출력을 디코딩해야 합니다:
>>> 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