Transformers documentation

ドキュメント質問応答

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

ドキュメント質問応答

Document Question Answering(Document Visual Question Answeringとも呼ばれる)は、ドキュメントの画像に関する質問に対して回答を提供するタスクです。このタスクに対応するモデルへの入力としては、通常、画像と質問の組み合わせが使用され、出力は自然言語で表現された回答となります。これらのモデルは、テキスト、単語の位置(バウンディングボックス)、画像自体といった複数のモダリティを利用します。

このガイドでは、次の方法について説明します。

このタスクと互換性のあるすべてのアーキテクチャとチェックポイントを確認するには、タスクページを参照することをお勧めします。

LayoutLMv2は、トークンの最終的な隠れ状態の上に質問応答ヘッドを追加し、回答の開始トークンと終了トークンの位置を予測することで、文書質問応答タスクを解決します。言い換えれば、この問題は抽出型質問応答として扱われます。つまり、コンテキストが与えられたとき、質問に答える情報としてどの部分を抽出するかを決定します。コンテキストはOCRエンジンの出力から得られ、ここではGoogleのTesseractが使用されています。

開始する前に、必要なライブラリがすべてインストールされていることを確認してください。LayoutLMv2 は detectron2、torchvision、tesseract に依存しています。

pip install -q transformers datasets
pip install 'git+https://github.com/facebookresearch/detectron2.git'
pip install torchvision
sudo apt install tesseract-ocr
pip install -q pytesseract

すべての依存関係のインストールが完了したら、ランタイムを再起動してください。

モデルをコミュニティと共有することをお勧めします。🤗 Hub にアップロードするには、Hugging Face アカウントにログインしてください。 プロンプトが表示されたら、ログイン用のトークンを入力してください:

>>> from huggingface_hub import notebook_login

>>> notebook_login()

いくつかのグローバル変数を定義しましょう。

>>> model_checkpoint = "microsoft/layoutlmv2-base-uncased"
>>> batch_size = 4

データの読み込み

このガイドでは、🤗 Hub で入手できる前処理済みの DocVQA の小さなサンプルを使用します。DocVQA の完全なデータセットを使用したい場合は、DocVQA のホームページで登録してダウンロードできます。その場合、このガイドを続行するには、🤗 dataset にファイルを読み込む方法を参照してください。

>>> 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
    })
})

ご覧のとおり、データセットはすでにtrainセットとtestセットに分割されています。特徴量を把握するために、ランダムな例を確認してみましょう。

>>> dataset["train"].features

各フィールドの意味は以下の通りです。

  • id: 例の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"]
DocVQA Image Example

データを前処理する

Document Question Answering タスクはマルチモーダルなタスクであり、各モダリティからの入力がモデルの期待する形式で前処理されていることを確認する必要があります。まず、画像データを処理できる画像プロセッサと、テキストデータをエンコードできるトークナイザーを内部で組み合わせる LayoutLMv2Processor を読み込みましょう。

>>> from transformers import AutoProcessor

>>> processor = AutoProcessor.from_pretrained(model_checkpoint)

ドキュメント画像の前処理

まず、プロセッサの image_processor を使用して、モデル用のドキュメント画像を準備します。 デフォルトでは、image processor は画像を 224x224 にリサイズし、カラーチャネルの順序が正しいことを確認した上で、tesseract を用いた OCR を適用して単語と正規化されたバウンディングボックスを取得します。本チュートリアルでは、これらのデフォルト設定がそのまま必要とされる設定です。 画像のバッチにデフォルトの画像処理を適用し、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

上記の前処理に加えて、モデル用のラベルも追加する必要があります。🤗 Transformers の xxxForQuestionAnswering モデルでは、ラベルは 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コースのQuestion Answering chapterを参考にしてください。

学習

おめでとうございます!このガイドで最も難しい部分を無事に終え、いよいよご自身のモデルの学習に取り組める段階になりました。 学習には、以下の手順が含まれます。

  • 前処理で使用したのと同じチェックポイントで、AutoModelForDocumentQuestionAnsweringを使用してモデルを読み込みます。
  • TrainingArguments で、学習ハイパーパラメータを定義します。
  • 例をバッチにまとめる関数を定義します。ここでは、DefaultDataCollatorを使用すれば十分です。
  • モデル、データセット、データコラクターとともに、学習引数をTrainerに渡します。
  • 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}]

必要に応じて、パイプラインの結果を手動で再現することもできます:

  1. 画像と質問を受け取り、モデルのを使ってモデルへの入力として準備します。
  2. 結果または前処理をモデルに渡します。
  3. モデルはstart_logitsとend_logitsを返します。これらは、回答の開始位置と終了位置にあるトークンを示します。どちらも形状は(batch_size, sequence_length)です。
  4. start_logits と end_logits の最後の次元に対して argmax を適用し、予測された start_idx と end_idx を取得します。
  5. tokenizerで回答をデコードします。
>>> 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'
Update on GitHub