Transformers documentation
Ответы на вопросы по документам
Ответы на вопросы по документам
Document Question Answering, также известный как Document Visual Question Answering, — это задача, предполагающая предоставление ответов на вопросы, заданные по изображениям документов. Входом для моделей, поддерживающих эту задачу, обычно является комбинация изображения и вопроса, а выходом — ответ, сформулированный на естественном языке. Эти модели используют несколько модальностей, включая текст, позиции слов (bounding boxes) и само изображение.
В этом руководстве показано, как:
- Настройте LayoutLMv2 на наборе данных DocVQA.
- Используйте дообученную модель для инференса.
Чтобы увидеть все архитектуры и контрольные точки, совместимые с этой задачей, рекомендуем обратиться к странице задачи
LayoutLMv2 решает задачу вопросно-ответной обработки документов, добавляя вопросно-ответный слой поверх финальных скрытых состояний токенов для предсказания позиций начального и конечного токенов ответа. Иными словами, задача рассматривается как извлекающий вопросно-ответный процесс: по заданному контексту извлекается фрагмент информации, отвечающий на вопрос. Контекст поступает с выхода OCR-движка, в данном случае — Tesseract от Google.
Прежде чем начать, убедитесь, что у вас установлены все необходимые библиотеки. LayoutLMv2 зависит от detectron2, torchvision и tesseract.
pip install -q transformers datasets
pip install 'git+https://github.com/facebookresearch/detectron2.git'
pip install torchvisionsudo apt install tesseract-ocr
pip install -q pytesseractПосле установки всех зависимостей перезапустите среду выполнения.
Мы рекомендуем делиться вашей моделью с сообществом. Войдите в свой аккаунт Hugging Face, чтобы загрузить её на 🤗 Hub. При запросе введите токен для входа:
>>> from huggingface_hub import notebook_login
>>> notebook_login()Определим несколько глобальных переменных.
>>> model_checkpoint = "microsoft/layoutlmv2-base-uncased"
>>> batch_size = 4Загрузка данных
В этом руководстве используется небольшой набор предварительно обработанных данных DocVQA, который можно найти на 🤗 Hub. Если вы хотите использовать полный датасет DocVQA, вы можете зарегистрироваться и скачать его на главной странице DocVQA. В этом случае, чтобы продолжить работу с этим руководством, ознакомьтесь с тем, как загрузить файлы в датасет 🤗.
>>> from datasets import load_dataset
>>> dataset = load_dataset("nielsr/docvqa_1200_examples")
>>> dataset
DatasetDict({
train: Dataset({
features: ['id', 'image', 'query', 'answers', 'words', 'bounding_boxes', 'answer'],
num_rows: 1000
})
test: Dataset({
features: ['id', 'image', 'query', 'answers', 'words', 'bounding_boxes', 'answer'],
num_rows: 200
})
})Как видно, набор данных уже разбит на обучающую и тестовую выборки. Посмотрите на случайный пример, чтобы ознакомиться с признаками.
>>> dataset["train"].featuresВот что означают отдельные поля:
id: идентификатор примераimage: объект PIL.Image.Image, содержащий изображение документаquery: строка с вопросом — вопрос на естественном языке, на нескольких языкахanswers: список правильных ответов, предоставленных людьми-аннотаторамиwordsиbounding_boxes: результаты OCR, которые мы здесь не будем использоватьanswer: ответ, найденный другой моделью, который мы здесь не будем использовать
Оставим только вопросы на английском языке и уберём признак answer, который, по всей видимости, содержит предсказания другой модели.
Также возьмём первый ответ из набора, предоставленного аннотаторами. Альтернативно, вы можете выбрать его случайным образом.
>>> updated_dataset = dataset.map(lambda example: {"question": example["query"]["en"]}, remove_columns=["query"])
>>> updated_dataset = updated_dataset.map(
... lambda example: {"answer": example["answers"][0]}, remove_columns=["answer", "answers"]
... )Обратите внимание, что контрольная точка LayoutLMv2, используемая в этом руководстве, обучена с параметром max_position_embeddings = 512 (эту информацию можно найти в файле config.json контрольной точки).
Мы можем усечь примеры, но чтобы избежать ситуации, когда ответ находится в конце большого документа и оказывается усеченным, здесь мы удалим несколько примеров, в которых эмбеддинг, вероятно, превысит длину 512.
Если большинство документов в вашем наборе данных длинные, вы можете реализовать стратегию скользящего окна — подробности смотрите в этом ноутбуке.
>>> updated_dataset = updated_dataset.filter(lambda x: len(x["words"]) + len(x["question"].split()) < 512)На этом этапе давайте также удалим из этого набора данных признаки OCR. Они являются результатом OCR для дообучения другой модели. Если бы мы хотели их использовать, потребовалась бы дополнительная обработка, так как они не соответствуют требованиям к входным данным модели, используемой в этом руководстве. Вместо этого мы можем применить LayoutLMv2Processor к исходным данным как для OCR, так и для токенизации. Таким образом, мы получим входные данные, соответствующие ожидаемому формату модели. Если вы хотите обрабатывать изображения вручную, обратитесь к документации модели LayoutLMv2, чтобы узнать, какой формат входных данных ожидает модель.
>>> updated_dataset = updated_dataset.remove_columns("words")
>>> updated_dataset = updated_dataset.remove_columns("bounding_boxes")Наконец, исследование данных не будет завершено, если мы не посмотрим на пример изображения.
>>> updated_dataset["train"][11]["image"]
Предобработка данных
Задача Document Question Answering является мультимодальной, поэтому необходимо убедиться, что входные данные каждого модальности предобработаны в соответствии с ожиданиями модели. Начнём с загрузки LayoutLMv2Processor, который внутренне объединяет процессор изображений, способный обрабатывать данные изображений, и токенизатор, способный кодировать текстовые данные.
>>> from transformers import AutoProcessor
>>> processor = AutoProcessor.from_pretrained(model_checkpoint)Предобработка изображений документов
Сначала подготовим изображения документов для модели с помощью image_processor из процессора.
По умолчанию image processor изменяет размер изображений до 224x224, обеспечивает правильный порядок каналов цвета,
применяет OCR с tesseract для получения слов и нормализованных ограничивающих прямоугольников. В этом учебнике все эти значения по умолчанию как раз то, что нам нужно.
Напишите функцию, которая применяет обработку изображений по умолчанию к пакету изображений и возвращает результаты OCR.
>>> image_processor = processor.image_processor
>>> def get_ocr_words_and_boxes(examples):
... images = [image.convert("RGB") for image in examples["image"]]
... encoded_inputs = image_processor(images)
... examples["image"] = encoded_inputs.pixel_values
... examples["words"] = encoded_inputs.words
... examples["boxes"] = encoded_inputs.boxes
... return examplesЧтобы применить эту предобработку ко всему набору данных быстро, используйте map.
>>> dataset_with_ocr = updated_dataset.map(get_ocr_words_and_boxes, batched=True, batch_size=2)Предобработка текстовых данных
После применения OCR к изображениям необходимо закодировать текстовую часть набора данных, чтобы подготовить её для модели.
Это включает преобразование слов и рамок, полученных на предыдущем шаге, в токены уровня input_ids, attention_mask, token_type_ids и bbox. Для предобработки текста нам понадобится tokenizer из процессора.
>>> tokenizer = processor.tokenizerПомимо описанной выше предобработки, нам также необходимо добавить метки для модели. Для моделей xxxForQuestionAnswering
в 🤗 Transformers метки состоят из start_positions и end_positions, которые указывают, какой токен находится в
начале, а какой — в конце ответа.
Начнём с этого. Определим вспомогательную функцию, которая находит подпоследовательность (ответ, разбитый на слова) в более крупном списке (списке слов).
This function will take two lists as input, words_list and answer_list. It will then iterate over the words_list and check
if the current word in the words_list (words_list[i]) is equal to the first word of answer_list (answer_list[0]) and if
the sublist of words_list starting from the current word and of the same length as answer_list is equal to answer_list.
If this condition is true, it means that a match has been found, and the function will record the match, its starting index (idx),
and its ending index (idx + len(answer_list) - 1). If more than one match was found, the function will return only the first one.
If no match is found, the function returns (None, 0, and 0).
>>> def subfinder(words_list, answer_list):
... matches = []
... start_indices = []
... end_indices = []
... for idx, i in enumerate(range(len(words_list))):
... if words_list[i] == answer_list[0] and words_list[i : i + len(answer_list)] == answer_list:
... matches.append(answer_list)
... start_indices.append(idx)
... end_indices.append(idx + len(answer_list) - 1)
... if matches:
... return matches[0], start_indices[0], end_indices[0]
... else:
... return None, 0, 0Чтобы проиллюстрировать, как эта функция находит позицию ответа, давайте применим её к примеру:
>>> example = dataset_with_ocr["train"][1]
>>> words = [word.lower() for word in example["words"]]
>>> match, word_idx_start, word_idx_end = subfinder(words, example["answer"].lower().split())
>>> print("Question: ", example["question"])
>>> print("Words:", words)
>>> print("Answer: ", example["answer"])
>>> print("start_index", word_idx_start)
>>> print("end_index", word_idx_end)
Question: Who is in cc in this letter?
Words: ['wie', 'baw', 'brown', '&', 'williamson', 'tobacco', 'corporation', 'research', '&', 'development', 'internal', 'correspondence', 'to:', 'r.', 'h.', 'honeycutt', 'ce:', 't.f.', 'riehl', 'from:', '.', 'c.j.', 'cook', 'date:', 'may', '8,', '1995', 'subject:', 'review', 'of', 'existing', 'brainstorming', 'ideas/483', 'the', 'major', 'function', 'of', 'the', 'product', 'innovation', 'graup', 'is', 'to', 'develop', 'marketable', 'nove!', 'products', 'that', 'would', 'be', 'profitable', 'to', 'manufacture', 'and', 'sell.', 'novel', 'is', 'defined', 'as:', 'of', 'a', 'new', 'kind,', 'or', 'different', 'from', 'anything', 'seen', 'or', 'known', 'before.', 'innovation', 'is', 'defined', 'as:', 'something', 'new', 'or', 'different', 'introduced;', 'act', 'of', 'innovating;', 'introduction', 'of', 'new', 'things', 'or', 'methods.', 'the', 'products', 'may', 'incorporate', 'the', 'latest', 'technologies,', 'materials', 'and', 'know-how', 'available', 'to', 'give', 'then', 'a', 'unique', 'taste', 'or', 'look.', 'the', 'first', 'task', 'of', 'the', 'product', 'innovation', 'group', 'was', 'to', 'assemble,', 'review', 'and', 'categorize', 'a', 'list', 'of', 'existing', 'brainstorming', 'ideas.', 'ideas', 'were', 'grouped', 'into', 'two', 'major', 'categories', 'labeled', 'appearance', 'and', 'taste/aroma.', 'these', 'categories', 'are', 'used', 'for', 'novel', 'products', 'that', 'may', 'differ', 'from', 'a', 'visual', 'and/or', 'taste/aroma', 'point', 'of', 'view', 'compared', 'to', 'canventional', 'cigarettes.', 'other', 'categories', 'include', 'a', 'combination', 'of', 'the', 'above,', 'filters,', 'packaging', 'and', 'brand', 'extensions.', 'appearance', 'this', 'category', 'is', 'used', 'for', 'novel', 'cigarette', 'constructions', 'that', 'yield', 'visually', 'different', 'products', 'with', 'minimal', 'changes', 'in', 'smoke', 'chemistry', 'two', 'cigarettes', 'in', 'cne.', 'emulti-plug', 'te', 'build', 'yaur', 'awn', 'cigarette.', 'eswitchable', 'menthol', 'or', 'non', 'menthol', 'cigarette.', '*cigarettes', 'with', 'interspaced', 'perforations', 'to', 'enable', 'smoker', 'to', 'separate', 'unburned', 'section', 'for', 'future', 'smoking.', '«short', 'cigarette,', 'tobacco', 'section', '30', 'mm.', '«extremely', 'fast', 'buming', 'cigarette.', '«novel', 'cigarette', 'constructions', 'that', 'permit', 'a', 'significant', 'reduction', 'iretobacco', 'weight', 'while', 'maintaining', 'smoking', 'mechanics', 'and', 'visual', 'characteristics.', 'higher', 'basis', 'weight', 'paper:', 'potential', 'reduction', 'in', 'tobacco', 'weight.', '«more', 'rigid', 'tobacco', 'column;', 'stiffing', 'agent', 'for', 'tobacco;', 'e.g.', 'starch', '*colored', 'tow', 'and', 'cigarette', 'papers;', 'seasonal', 'promotions,', 'e.g.', 'pastel', 'colored', 'cigarettes', 'for', 'easter', 'or', 'in', 'an', 'ebony', 'and', 'ivory', 'brand', 'containing', 'a', 'mixture', 'of', 'all', 'black', '(black', 'paper', 'and', 'tow)', 'and', 'ail', 'white', 'cigarettes.', '499150498']
Answer: T.F. Riehl
start_index 17
end_index 18Однако после кодирования примеры будут выглядеть так:
>>> encoding = tokenizer(example["question"], example["words"], example["boxes"])
>>> tokenizer.decode(encoding["input_ids"])
[CLS] who is in cc in this letter? [SEP] wie baw brown & williamson tobacco corporation research & development ...Нам нужно найти позицию ответа в закодированном входе.
token_type_idsуказывает, какие токены относятся к вопросу, а какие — к словам документа.tokenizer.cls_token_idпоможет найти специальный токен в начале входных данных.word_idsпоможет сопоставить ответ, найденный в исходныхwords, с тем же ответом в полном закодированном входе и определить начальную/конечную позицию ответа в закодированном входе.
С учётом этого давайте создадим функцию для кодирования батча примеров из набора данных:
>>> def encode_dataset(examples, max_length=512):
... questions = examples["question"]
... words = examples["words"]
... boxes = examples["boxes"]
... answers = examples["answer"]
... # encode the batch of examples and initialize the start_positions and end_positions
... encoding = tokenizer(questions, words, boxes, max_length=max_length, padding="max_length", truncation=True)
... start_positions = []
... end_positions = []
... # loop through the examples in the batch
... for i in range(len(questions)):
... cls_index = encoding["input_ids"][i].index(tokenizer.cls_token_id)
... # find the position of the answer in example's words
... words_example = [word.lower() for word in words[i]]
... answer = answers[i]
... match, word_idx_start, word_idx_end = subfinder(words_example, answer.lower().split())
... if match:
... # if match is found, use `token_type_ids` to find where words start in the encoding
... token_type_ids = encoding["token_type_ids"][i]
... token_start_index = 0
... while token_type_ids[token_start_index] != 1:
... token_start_index += 1
... token_end_index = len(encoding["input_ids"][i]) - 1
... while token_type_ids[token_end_index] != 1:
... token_end_index -= 1
... word_ids = encoding.word_ids(i)[token_start_index : token_end_index + 1]
... start_position = cls_index
... end_position = cls_index
... # loop over word_ids and increase `token_start_index` until it matches the answer position in words
... # once it matches, save the `token_start_index` as the `start_position` of the answer in the encoding
... for id in word_ids:
... if id == word_idx_start:
... start_position = token_start_index
... else:
... token_start_index += 1
... # similarly loop over `word_ids` starting from the end to find the `end_position` of the answer
... for id in word_ids[::-1]:
... if id == word_idx_end:
... end_position = token_end_index
... else:
... token_end_index -= 1
... start_positions.append(start_position)
... end_positions.append(end_position)
... else:
... start_positions.append(cls_index)
... end_positions.append(cls_index)
... encoding["image"] = examples["image"]
... encoding["start_positions"] = start_positions
... encoding["end_positions"] = end_positions
... return encodingТеперь, когда у нас есть эта функция предобработки, мы можем закодировать весь набор данных:
>>> encoded_train_dataset = dataset_with_ocr["train"].map(
... encode_dataset, batched=True, batch_size=2, remove_columns=dataset_with_ocr["train"].column_names
... )
>>> encoded_test_dataset = dataset_with_ocr["test"].map(
... encode_dataset, batched=True, batch_size=2, remove_columns=dataset_with_ocr["test"].column_names
... )Проверим, как выглядят признаки закодированного набора данных:
>>> encoded_train_dataset.features
{'image': Sequence(feature=Sequence(feature=Sequence(feature=Value(dtype='uint8', id=None), length=-1, id=None), length=-1, id=None), length=-1, id=None),
'input_ids': Sequence(feature=Value(dtype='int32', id=None), length=-1, id=None),
'token_type_ids': Sequence(feature=Value(dtype='int8', id=None), length=-1, id=None),
'attention_mask': Sequence(feature=Value(dtype='int8', id=None), length=-1, id=None),
'bbox': Sequence(feature=Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None), length=-1, id=None),
'start_positions': Value(dtype='int64', id=None),
'end_positions': Value(dtype='int64', id=None)}Оценка
Оценка для задачи документного вопросно-ответного моделирования требует значительного количества постобработки. Чтобы не отнимать у вас слишком много времени, в этом руководстве шаг оценки пропущен. Trainer всё равно вычисляет потерю оценки во время обучения, поэтому вы не остаётесь в полной неопределённости относительно производительности вашей модели. Экстрактивное вопросно-ответное моделирование обычно оценивается с помощью метрик F1/точное совпадение. Если вы хотите реализовать это самостоятельно, загляните в главу о вопросно-ответном моделировании курса Hugging Face в качестве источника вдохновения.
Обучение
Поздравляем! Вы успешно прошли самую сложную часть этого руководства и теперь готовы обучать собственную модель. Обучение включает следующие шаги:
- Загрузите модель с помощью AutoModelForDocumentQuestionAnswering, используя тот же чекпоинт, что и на этапе предобработки.
- Определите гиперпараметры обучения в TrainingArguments.
- Определите функцию для объединения примеров в батчи; здесь DefaultDataCollator вполне подойдёт
- Передайте аргументы обучения в Trainer вместе с моделью, набором данных и data collator.
- Вызовите train(), чтобы дообучить модель.
>>> from transformers import AutoModelForDocumentQuestionAnswering
>>> model = AutoModelForDocumentQuestionAnswering.from_pretrained(model_checkpoint)В TrainingArguments используйте output_dir, чтобы указать, куда сохранять модель, и настройте гиперпараметры по своему усмотрению.
Если вы хотите поделиться моделью с сообществом, установите push_to_hub в значение True (для загрузки модели необходимо войти в аккаунт Hugging Face).
В этом случае output_dir также станет именем репозитория, в который будет загружена контрольная точка вашей модели.
>>> from transformers import TrainingArguments
>>> # REPLACE THIS WITH YOUR REPO ID
>>> repo_id = "MariaK/layoutlmv2-base-uncased_finetuned_docvqa"
>>> training_args = TrainingArguments(
... output_dir=repo_id,
... per_device_train_batch_size=4,
... num_train_epochs=20,
... save_steps=200,
... logging_steps=50,
... eval_strategy="steps",
... learning_rate=5e-5,
... save_total_limit=2,
... remove_unused_columns=False,
... push_to_hub=True,
... )Определите простой коллаборатор данных для объединения примеров в батчи.
>>> from transformers import DefaultDataCollator
>>> data_collator = DefaultDataCollator()Наконец, соберите всё вместе и вызовите train():
>>> from transformers import Trainer
>>> trainer = Trainer(
... model=model,
... args=training_args,
... data_collator=data_collator,
... train_dataset=encoded_train_dataset,
... eval_dataset=encoded_test_dataset,
... processing_class=processor,
... )
>>> trainer.train()Чтобы добавить итоговую модель в 🤗 Hub, создайте карточку модели и вызовите push_to_hub:
>>> trainer.create_model_card()
>>> trainer.push_to_hub()Вывод
Теперь, когда вы дообучили модель LayoutLMv2 и загрузили её в 🤗 Hub, вы можете использовать её для инференса. Самый простой способ проверить дообученную модель на инференсе — использовать её в Pipeline.
Рассмотрим пример:
>>> example = dataset["test"][2]
>>> question = example["query"]["en"]
>>> image = example["image"]
>>> print(question)
>>> print(example["answers"])
'Who is ‘presiding’ TRRF GENERAL SESSION (PART 1)?'
['TRRF Vice President', 'lee a. waller']Далее создайте экземпляр конвейера для ответов на вопросы по документу с вашей моделью и передайте в него комбинацию изображения и вопроса.
>>> from transformers import pipeline
>>> qa_pipeline = pipeline("document-question-answering", model="MariaK/layoutlmv2-base-uncased_finetuned_docvqa")
>>> qa_pipeline(image, question)
[{'score': 0.9949808120727539,
'answer': 'Lee A. Waller',
'start': 55,
'end': 57}]Вы также можете вручную воспроизвести результаты конвейера, если хотите:
- Возьмите изображение и вопрос, подготовьте их для модели с помощью процессора вашей модели.
- Передайте результат или результат предобработки в модель.
- Модель возвращает
start_logitsиend_logits, которые указывают, какой токен находится в начале ответа, а какой — в конце. Оба имеют форму (batch_size, sequence_length). - Выполните argmax по последней размерности как для
start_logits, так и дляend_logits, чтобы получить предсказанныеstart_idxиend_idx. - Декодируйте ответ с помощью токенизатора.
>>> import torch
>>> from transformers import AutoProcessor
>>> from transformers import AutoModelForDocumentQuestionAnswering
>>> processor = AutoProcessor.from_pretrained("MariaK/layoutlmv2-base-uncased_finetuned_docvqa")
>>> model = AutoModelForDocumentQuestionAnswering.from_pretrained("MariaK/layoutlmv2-base-uncased_finetuned_docvqa")
>>> with torch.no_grad():
... encoding = processor(image.convert("RGB"), question, return_tensors="pt")
... outputs = model(**encoding)
... start_logits = outputs.start_logits
... end_logits = outputs.end_logits
... predicted_start_idx = start_logits.argmax(-1).item()
... predicted_end_idx = end_logits.argmax(-1).item()
>>> processor.tokenizer.decode(encoding.input_ids.squeeze()[predicted_start_idx : predicted_end_idx + 1])
'lee a. waller'