[翻訳] Hugging Face transformersにおける質問応答

[Question answering](https://huggingface.co/docs/transformers/tasks/question_answering)の翻訳です。

:::note warn
本書は抄訳であり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
:::

https://youtu.be/ajPx5LwJD-I

質問応答タスクは、指定された質問に対する回答を返却します。Alexa、Siri、Googleのようなバーチャルアシスタントに天気を尋ねたことがあるのであれば、以前に質問応答モデルを使ったことがあることになります。質問応答タスクには2つの一般的なタイプがあります:

- Extractive: 指定されたコンテキストから回答を抽出します。
- Abstractive: コンテキストから質問に正確に答える回答を生成します。

このガイドでは、以下の方法を説明します:

1. extractiveな質問応答のために[SQuAD](https://huggingface.co/datasets/squad)データセットで[DistilBERT](https://huggingface.co/distilbert-base-uncased)をファインチューニングします。
1. ファインチューニングしたモデルを推論で使用します。

:::note
このチュートリアルで説明されるタスクは以下のモデルアーキテクチャをサポートしています:

ALBERT, BART, BERT, BigBird, BigBird-Pegasus, BLOOM, CamemBERT, CANINE, ConvBERT, Data2VecText, DeBERTa, DeBERTa-v2, DistilBERT, ELECTRA, ERNIE, ErnieM, FlauBERT, FNet, Funnel Transformer, OpenAI GPT-2, GPT Neo, GPT NeoX, GPT-J, I-BERT, LayoutLMv2, LayoutLMv3, LED, LiLT, Longformer, LUKE, LXMERT, MarkupLM, mBART, MEGA, Megatron-BERT, MobileBERT, MPNet, MVP, Nezha, Nyströmformer, OPT, QDQBert, Reformer, RemBERT, RoBERTa, RoBERTa-PreLayerNorm, RoCBert, RoFormer, Splinter, SqueezeBERT, XLM, XLM-RoBERTa, XLM-RoBERTa-XL, XLNet, X-MOD, YOSO
:::

始める前に、必要なすべてのライブラリがインストールされていることを確認してください:

```
pip install transformers datasets evaluate
```

コミュニティーにモデルをアップロードし、共有できる様にHugging Faceアカウントにログインすることをお勧めします。

```py:Python
from huggingface_hub import notebook_login

notebook_login()
```

# SQuADデータセットのロード

🤗 DatasetsライブラリからSQuADデータセットの小規模なサブセットをロードすることからスタートします。これによって、データセット全体でのトレーニングでより多くの時間を費やす前に、実験を行い、すべてが動作することを確認することができます。

```py:Python
from datasets import load_dataset

squad = load_dataset("squad", split="train[:5000]")
```

[train\_test\_split](https://huggingface.co/docs/datasets/v2.12.0/en/package_reference/main_classes#datasets.Dataset.train_test_split)メソッドを用いて、データセットの`train`スプリットをトレーニングセットとテストセットに分割します:

```py:Python
squad = squad.train_test_split(test_size=0.2)
```

サンプルを見てみましょう:

```py:Python
squad["train"][0]
```
```json:JSON
{'answers': {'answer_start': [515], 'text': ['Saint Bernadette Soubirous']},
 'context': 'Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.',
 'id': '5733be284776f41900661182',
 'question': 'To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?',
 'title': 'University_of_Notre_Dame'
}
```

ここにはいくつかの重要なフィールドがあります:

- `answers`: 回答のトークンの開始位置と回答のテキスト。
- `context`: モデルが回答を抽出するために必要な背景情報。
- `question`: モデルが回答すべき質問。

# 前処理

https://youtu.be/qgaM0weJHpA

次のステップでは、`question`と`context`フィールドを処理するために、DistilBERTのトークナイザーをロードします:

```py:Python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
```

注意すべき質問応答タスクに固有な前処理ステップがいくつか存在します:

1. データセットのいくつかのサンプルには、モデルの最大入力長を超える非常に長い`context`が含まれる場合があります。長いシーケンスを取り扱うには、`truncation="only_second"`を設定することで`context`のみを切り取ります。
1. 次に、`return_offset_mapping=True`を設定することで、オリジナルの`context`に対する回答の開始位置、終了位置をマッピングします。
1. マッピングを行うことで、回答の開始トークンと終了トークンを特定することができる様になります。オフセットのどの部分が`question`に対応し、どの部分が`context`に対応するのかを特定するために[sequence\_ids](https://huggingface.co/docs/tokenizers/v0.13.3/en/api/encoding#tokenizers.Encoding.sequence_ids)を活用します。

こちらが、切り取りを行い、`answer`の開始トークンと終了トークンを`context`にマッピングする関数の作成方法です:

```py:Python
def preprocess_function(examples):
    questions = [q.strip() for q in examples["question"]]
    inputs = tokenizer(
        questions,
        examples["context"],
        max_length=384,
        truncation="only_second",
        return_offsets_mapping=True,
        padding="max_length",
    )

    offset_mapping = inputs.pop("offset_mapping")
    answers = examples["answers"]
    start_positions = []
    end_positions = []

    for i, offset in enumerate(offset_mapping):
        answer = answers[i]
        start_char = answer["answer_start"][0]
        end_char = answer["answer_start"][0] + len(answer["text"][0])
        sequence_ids = inputs.sequence_ids(i)

        # Find the start and end of the context
        idx = 0
        while sequence_ids[idx] != 1:
            idx += 1
        context_start = idx
        while sequence_ids[idx] == 1:
            idx += 1
        context_end = idx - 1

        # If the answer is not fully inside the context, label it (0, 0)
        if offset[context_start][0] > end_char or offset[context_end][1] < start_char:
            start_positions.append(0)
            end_positions.append(0)
        else:
            # Otherwise it's the start and end token positions
            idx = context_start
            while idx <= context_end and offset[idx][0] <= start_char:
                idx += 1
            start_positions.append(idx - 1)

            idx = context_end
            while idx >= context_start and offset[idx][1] >= end_char:
                idx -= 1
            end_positions.append(idx + 1)

    inputs["start_positions"] = start_positions
    inputs["end_positions"] = end_positions
    return inputs
```

データセット全体に前処理関数を適用するために、🤗 Datasetsの[map](https://huggingface.co/docs/datasets/v2.12.0/en/package_reference/main_classes#datasets.Dataset.map)関数を使用します。一度にデータセットの複数の要素を処理するために`batched=True`を設定することで、`map`関数をスピードアップすることができます。不要なすべてのカラムを削除します:

```py:Python
tokenized_squad = squad.map(preprocess_function, batched=True, remove_columns=squad["train"].column_names)
```

次に、[DefaultDataCollator](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/data_collator#transformers.DefaultDataCollator)を用いてサンプルのバッチを作成します。🤗 Transformersの他のdata collatorと異なり、[DefaultDataCollator](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/data_collator#transformers.DefaultDataCollator)はパディングの様な追加の前処理は適用しません。

## Pytorch

```py:Python
from transformers import DefaultDataCollator

data_collator = DefaultDataCollator()
```

## TensorFlow

```py:Python
from transformers import DefaultDataCollator

data_collator = DefaultDataCollator(return_tensors="tf")
```

# トレーニング

## Pytorch

:::note
[Trainer](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.Trainer)を用いたモデルのファインチューニングに馴染みがないのであれば、[こちら](https://qiita.com/taka_yayoi/items/597c6b1c36abaf9c1b3c)の基本的なチュートリアルをご覧ください！
:::

これで、モデルのトレーニングをスタートする準備ができました！[AutoModelForQuestionAnswering](https://huggingface.co/docs/transformers/v4.29.1/en/model_doc/auto#transformers.AutoModelForQuestionAnswering)を用いてDistilBERTをロードします。

```py:Python
from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer

model = AutoModelForQuestionAnswering.from_pretrained("distilbert-base-uncased")
```

この時点で、残りは3つのステップです:

1. [TrainingArguments](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.TrainingArguments)でトレーニングのハイパーパラメーターを定義します。必要な唯一のパラメーターは、モデルを保存する場所を指定する`output_dir`です。`push_to_hub=True`を設定することで、このモデルをHubにプッシュします(モデルをアップロードするにはHugging Faceにサインインする必要があります)。
1. モデル、データセット、トークナイザー、データコレーターとともにトレーニングの引数を[Trainer](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.Trainer)に引き渡します。
1. モデルをファインチューンするために[train\(\)](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.Trainer.train)を呼び出します。

```py:Python
training_args = TrainingArguments(
    output_dir="my_awesome_qa_model",
    evaluation_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=True,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_squad["train"],
    eval_dataset=tokenized_squad["test"],
    tokenizer=tokenizer,
    data_collator=data_collator,
)

trainer.train()
```

トレーニングが完了したら、このモデルを誰でも使える様に、[push\_to\_hub\(\)](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.Trainer.push_to_hub)メソッドを用いてHubにモデルを共有します。

```py:Python
trainer.push_to_hub()
```

## TensorFlow

:::note
[Trainer](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.Trainer)を用いたモデルのファインチューニングに馴染みがないのであれば、[こちら](https://qiita.com/taka_yayoi/items/597c6b1c36abaf9c1b3c)の基本的なチュートリアルをご覧ください！
:::

TensorFlowでモデルをファインチューンにするには、optimizer関数、学習率のスケジュール、いくつかのトレーニングハイパーパラメーターをセットアップするところからスタートします:

```py:Python
from transformers import create_optimizer

batch_size = 16
num_epochs = 2
total_train_steps = (len(tokenized_squad["train"]) // batch_size) * num_epochs
optimizer, schedule = create_optimizer(
    init_lr=2e-5,
    num_warmup_steps=0,
    num_train_steps=total_train_steps,
)
```

次に、[TFAutoModelForQuestionAnswering](https://huggingface.co/docs/transformers/v4.29.1/en/model_doc/auto#transformers.TFAutoModelForQuestionAnswering)でDistilBERTをロードします。

```py:Python
from transformers import TFAutoModelForQuestionAnswering

model = TFAutoModelForQuestionAnswering("distilbert-base-uncased")
```

[prepare\_tf\_dataset\(\)](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/model#transformers.TFPreTrainedModel.prepare_tf_dataset)を用いて、データセットを`tf.data.Dataset`に変換します:

```py:Python
tf_train_set = model.prepare_tf_dataset(
    tokenized_squad["train"],
    shuffle=True,
    batch_size=16,
    collate_fn=data_collator,
)

tf_validation_set = model.prepare_tf_dataset(
    tokenized_squad["test"],
    shuffle=False,
    batch_size=16,
    collate_fn=data_collator,
)
```

[compile](https://keras.io/api/models/model_training_apis/#compile-method)を用いてトレーニングするモデルを設定します:

```py:Python
import tensorflow as tf

model.compile(optimizer=optimizer)
```

トレーニングをスタートする前にセットアップする最後のポイントは、Hubにモデルをプッシュする方法を指定することです。これは、[PushToHubCallback](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/keras_callbacks#transformers.PushToHubCallback)で、モデルをトークナイザーをどこにプッシュするのかを指定することで実現できます。

```py:Python
from transformers.keras_callbacks import PushToHubCallback

callback = PushToHubCallback(
    output_dir="my_awesome_qa_model",
    tokenizer=tokenizer,
)
```

最後に、モデルのトレーニングを開始できる準備ができました！モデルをファインチューンするために、トレーニングデータセット、検証データセット、エポック数を指定して[fit](https://keras.io/api/models/model_training_apis/#fit-method)を呼び出します:

```py:Python
model.fit(x=tf_train_set, validation_data=tf_validation_set, epochs=3, callbacks=[callback])
```

トレーニングが完了すると、誰でも使用できる様に自動でモデルがHubにアップロードされます！

:::note
質問応答のモデルのファインチューンのより詳細なサンプルに関しては、対応する[PyTorch notebook](https://colab.research.google.com/github/huggingface/notebooks/blob/main/examples/question_answering.ipynb)や[TensorFlow notebook](https://colab.research.google.com/github/huggingface/notebooks/blob/main/examples/question_answering-tf.ipynb)をご覧ください。
:::

# 評価

質問応答の評価には、膨大な後処理が必要となります。あまり皆様が時間をかけないで済む様に、このガイドでは評価ステップをスキップします。しかし、[Trainer](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/trainer#transformers.Trainer)は、トレーニング過程で評価lossを計算するので、モデルのパフォーマンスが全くわからないということはありません。

あなたに時間の余裕があり、質問応答のモデルの評価方法に興味があるのであれば、🤗 Hugging Faceコースの[Question answering](https://huggingface.co/course/chapter7/7?fw=pt#postprocessing)をご覧ください！

# 推論

すばらしいです。これでモデルをファインチューンしたので推論で活用することができます！

モデルに予測させたい質問といくつかのコンテキストを考えだします:

```py:Python
question = "How many programming languages does BLOOM support?"
context = "BLOOM has 176 billion parameters and can generate text in 46 languages natural languages and 13 programming languages."
```

推論でファインチューンしたモデルをトライする最もシンプルな方法は、[pipeline\(\)](https://huggingface.co/docs/transformers/v4.29.1/en/main_classes/pipelines#transformers.pipeline)を活用することです。モデルの質問応答のための`pipeline`のインスタンスを作成し、テキストを引き渡します:

```py:Python
from transformers import pipeline

question_answerer = pipeline("question-answering", model="my_awesome_qa_model")
question_answerer(question=question, context=context)
```
```
{'score': 0.2058267742395401,
 'start': 10,
 'end': 95,
 'answer': '176 billion parameters and can generate text in 46 languages natural languages and 13'}
```

あるいは、必要であれば`pipeline`の結果を手動で再現することができます:

## Pytorch

テキストをトークナイズしPyTorchのtensorを返却します:

```py:Python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("my_awesome_qa_model")
inputs = tokenizer(question, context, return_tensors="pt")
```

モデルへの入力を渡して、`logits`を返却します:

```py:Python
from transformers import AutoModelForQuestionAnswering

model = AutoModelForQuestionAnswering.from_pretrained("my_awesome_qa_model")
with torch.no_grad():
    outputs = model(**inputs)
```

開始位置、終了位置に関するモデル出力で最も高い確率を取得します:

```py:Python
answer_start_index = outputs.start_logits.argmax()
answer_end_index = outputs.end_logits.argmax()
```

回答を取得するために予測したトークンをデコードします:

```py:Python
predict_answer_tokens = inputs.input_ids[0, answer_start_index : answer_end_index + 1]
tokenizer.decode(predict_answer_tokens)
```
```
'176 billion parameters and can generate text in 46 languages natural languages and 13'
```

## TensorFlow

テキストをトークナイズしPyTorchのtensorを返却します:

```py:Python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("my_awesome_qa_model")
inputs = tokenizer(question, text, return_tensors="tf")
```

モデルへの入力を渡して、`logits`を返却します:

```py:Python
from transformers import TFAutoModelForQuestionAnswering

model = TFAutoModelForQuestionAnswering.from_pretrained("my_awesome_qa_model")
outputs = model(**inputs)
```

開始位置、終了位置に関するモデル出力で最も高い確率を取得します:

```py:Python
answer_start_index = int(tf.math.argmax(outputs.start_logits, axis=-1)[0])
answer_end_index = int(tf.math.argmax(outputs.end_logits, axis=-1)[0])
```

回答を取得するために予測したトークンをデコードします:

```py:Python
predict_answer_tokens = inputs.input_ids[0, answer_start_index : answer_end_index + 1]
tokenizer.decode(predict_answer_tokens)
```
```
'176 billion parameters and can generate text in 46 languages natural languages and 13'
```