📋 このステップで学ぶこと
質問応答(QA)の種類と特徴
Extractive QA(抽出型質問応答)の仕組み
SQuADデータセットの構造
回答スパンの予測(start/end位置)
BERTでのQA実装
評価指標(Exact Match、F1)
練習問題: 4問
⚠️ 実行環境について
このステップのコードはGoogle Colab(GPU必須) で実行してください。
「ランタイム」→「ランタイムのタイプを変更」→「GPU」を選択します。
🎯 1. 質問応答(QA)とは
質問応答(Question Answering, QA) は、
与えられた文書から質問に対する回答を見つけ出すタスクです。
検索エンジンやチャットボットの中核技術として広く使われています。
1-1. QAシステムの種類
【質問応答の主な種類】
■ Extractive QA(抽出型)← このステップで学ぶ
文書から回答を「抽出」する
回答は必ず文書内に存在する
例:
文書: “富士山の高さは3,776メートルです。”
質問: “富士山の高さは?”
回答: “3,776メートル” ← 文書から抽出
特徴:
✅ 回答の根拠が明確
✅ 訓練が比較的簡単
❌ 文書にない情報は回答不可
■ Generative QA(生成型)
回答を「生成」する
文書を参考に自由に回答を作成
例:
文書: “富士山の高さは3,776メートルです。”
質問: “富士山の高さは?”
回答: “富士山は約3,776mの高さがあります。” ← 生成
特徴:
✅ 柔軟な回答が可能
✅ 複数の情報を統合できる
❌ ハルシネーション(嘘)のリスク
■ Open-domain QA(オープンドメイン)
特定の文書に限定されない
大規模知識ベースから回答
例: Wikipedia全体から回答を検索
■ Closed-domain QA(クローズドドメイン)
特定ドメインに特化
例: 医療FAQ、製品マニュアルQA
1-2. Extractive QAの仕組み
【Extractive QAの基本】
入力:
Context(文書): “Albert Einstein was born in Germany in 1879.”
Question(質問): “Where was Einstein born?”
出力:
Answer(回答): “Germany”
【どうやって回答を見つけるか?】
文書中の回答位置を特定する:
“Albert Einstein was born in Germany in 1879.”
0 1 2 3 4 5 6 7
回答 “Germany” の位置:
・開始位置(start): 5
・終了位置(end): 5
→ モデルは「開始位置」と「終了位置」を予測する
【複数単語の回答の場合】
“Albert Einstein was born in New York in 1950.”
0 1 2 3 4 5 6 7 8
回答 “New York” の位置:
・開始位置(start): 5
・終了位置(end): 6
→ position 5〜6 のトークンを抽出 = “New York”
💡 QAタスクの本質
Extractive QAは「回答の位置を当てる」 タスクです。
モデルは「開始位置」と「終了位置」の2つを予測します。
1-3. QAの応用例
応用分野
具体例
入力
検索エンジン
Google Feature Snippets
Web検索結果 + 質問
カスタマーサポート
FAQチャットボット
FAQ文書 + ユーザー質問
企業内検索
社内文書検索
マニュアル + 質問
教育
読解問題の自動採点
教科書 + 問題
📚 2. SQuADデータセット
SQuAD(Stanford Question Answering Dataset) は、
QAの標準的なベンチマークデータセットです。
Wikipedia記事から作成された質問と回答のペアで構成されています。
2-1. SQuADのデータ形式
【SQuADのデータ構造】
各サンプルに含まれる情報:
1. context(文書)
回答が含まれるWikipedia記事の段落
2. question(質問)
文書に対する質問文
3. answers(回答)
・text: 回答テキスト
・answer_start: 回答の開始位置(文字単位)
【具体例】
{
“context”: “Albert Einstein was born in Germany in 1879.”,
“question”: “Where was Einstein born?”,
“answers”: {
“text”: [“Germany”],
“answer_start”: [31]
}
}
文字位置の確認:
“Albert Einstein was born in Germany in 1879.”
0 6 14 18 23 27 31 38 41
answer_start = 31 → “Germany” の “G” の位置
【SQuADのバージョン】
SQuAD 1.1(2016年):
・100,000以上の質問
・回答は必ず文書内に存在
SQuAD 2.0(2018年):
・SQuAD 1.1 + 回答不可能な質問を追加
・「回答なし」を判定する必要がある
2-2. データセットの読み込み
※モバイルでは横スクロールできます
# ========================================
# 必要なライブラリのインストール
# ========================================
!pip install transformers==4.35.0 datasets==2.14.0 -q
# ========================================
# ライブラリのインポート
# ========================================
import torch
from transformers import BertTokenizerFast, BertForQuestionAnswering
from transformers import Trainer, TrainingArguments
from datasets import load_dataset
import numpy as np
# GPU確認
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
print(f”Using device: {device}”)
実行結果:
Using device: cuda
# ========================================
# SQuADデータセットの読み込み
# ========================================
# load_dataset: Hugging Faceからデータセットをダウンロード
# ‘squad’: SQuAD v1.1データセット
squad = load_dataset(‘squad’)
print(“=== データセットの構造 ===”)
print(squad)
実行結果:
=== データセットの構造 ===
DatasetDict({
train: Dataset({
features: [‘id’, ‘title’, ‘context’, ‘question’, ‘answers’],
num_rows: 87599
})
validation: Dataset({
features: [‘id’, ‘title’, ‘context’, ‘question’, ‘answers’],
num_rows: 10570
})
})
2-3. サンプルデータの確認
# ========================================
# サンプルデータの確認
# ========================================
# 1つ目のサンプルを取得
example = squad[‘train’][0]
print(“=== サンプルデータ ===”)
print(f”タイトル: {example[‘title’]}”)
print(f”\n文書(context):”)
print(f” {example[‘context’][:300]}…”)
print(f”\n質問: {example[‘question’]}”)
print(f”\n回答: {example[‘answers’][‘text’][0]}”)
print(f”回答の開始位置: {example[‘answers’][‘answer_start’][0]}”)
実行結果:
=== サンプルデータ ===
タイトル: University_of_Notre_Dame
文書(context):
Architecturally, the most striking features of the campus are its
Gothic Revival architecture, a style popularized in the mid-19th
century in England, and a neo-Gothic style of architecture that
originated in the United States in the 1890s…
質問: To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?
回答: Saint Bernadette Soubirous
回答の開始位置: 515
2-4. 回答位置の確認
# ========================================
# 回答位置の確認
# ========================================
context = example[‘context’]
answer_text = example[‘answers’][‘text’][0]
answer_start = example[‘answers’][‘answer_start’][0]
# 回答の終了位置を計算
answer_end = answer_start + len(answer_text)
print(“=== 回答位置の確認 ===”)
print(f”回答テキスト: ‘{answer_text}'”)
print(f”開始位置: {answer_start}”)
print(f”終了位置: {answer_end}”)
# 実際に抽出してみる
extracted = context[answer_start:answer_end]
print(f”\n抽出結果: ‘{extracted}'”)
print(f”一致確認: {extracted == answer_text}”)
実行結果:
=== 回答位置の確認 ===
回答テキスト: ‘Saint Bernadette Soubirous’
開始位置: 515
終了位置: 541
抽出結果: ‘Saint Bernadette Soubirous’
一致確認: True
✅ SQuADデータの特徴
サイズ : 訓練87,599件、検証10,570件
言語 : 英語(Wikipediaベース)
形式 : 文書 + 質問 + 回答位置
特徴 : 回答は必ず文書内に存在
🤖 3. BERTのQAモデルの仕組み
BERTを使ったQAモデルは、文書中の回答の開始位置と終了位置 を予測します。
3-1. 入力形式
【BERTへの入力形式】
入力: [CLS] Question [SEP] Context [SEP]
具体例:
質問: “Where was Einstein born?”
文書: “Einstein was born in Germany.”
BERTへの入力:
[CLS] Where was Einstein born ? [SEP] Einstein was born in Germany . [SEP]
0 1 2 3 4 5 6 7 8 9 10 11 12 13
トークンインデックス:
0: [CLS] ← 特殊トークン(分類用)
1-5: 質問のトークン
6: [SEP] ← 区切り
7-12: 文書のトークン
13: [SEP] ← 終了
【出力】
BERTは各トークンに対して2つのスコアを出力:
・Start logits: 各トークンが「回答の開始」である確率
・End logits: 各トークンが「回答の終了」である確率
例:
Start logits: [低, 低, 低, 低, 低, 低, 低, 低, 低, 低, 低, 高, 低, 低]
↑
position 11 (Germany)
End logits: [低, 低, 低, 低, 低, 低, 低, 低, 低, 低, 低, 高, 低, 低]
↑
position 11 (Germany)
回答: position 11 〜 11 = “Germany”
3-2. モデルの構造
【BertForQuestionAnsweringの構造】
┌─────────────────────────────────────┐
│ 入力: [CLS] Question [SEP] Context │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ BERT Encoder │
│ (12層のTransformer) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 各トークンの隠れ状態(768次元) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 線形層(768 → 2) │
│ 各トークンに2つのスコアを出力 │
│ ・Start score │
│ ・End score │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Start position: argmax(start) │
│ End position: argmax(end) │
└─────────────────────────────────────┘
↓
回答を抽出
【損失関数】
訓練時:
Start位置の損失 = CrossEntropy(予測, 正解start位置)
End位置の損失 = CrossEntropy(予測, 正解end位置)
合計損失 = Start損失 + End損失
💡 QAモデルのポイント
入力 : [CLS] + 質問 + [SEP] + 文書 + [SEP]
出力 : 各トークンのStart/Endスコア
予測 : 最もスコアが高い位置を選択
制約 : Start ≤ End でなければならない
🔤 4. トークン化と位置のアライメント
SQuADの回答位置は文字単位 ですが、
BERTはトークン単位 で予測します。
この変換(アライメント)が重要です。
4-1. 文字位置からトークン位置への変換
【位置変換の問題】
SQuADのデータ:
context: “Einstein was born in Germany.”
answer_start: 21(文字位置)
answer: “Germany”
BERTのトークン化後:
tokens: [‘Einstein’, ‘was’, ‘born’, ‘in’, ‘Germany’, ‘.’]
index: 0 1 2 3 4 5
必要な変換:
文字位置 21 → トークン位置 4
【変換方法: offset_mapping】
tokenizer(…, return_offsets_mapping=True) で
各トークンの「文字範囲」を取得できる
offset_mapping:
[(0, 8), # ‘Einstein’ は文字0-8
(9, 12), # ‘was’ は文字9-12
(13, 17), # ‘born’ は文字13-17
(18, 20), # ‘in’ は文字18-20
(21, 28), # ‘Germany’ は文字21-28 ← ここ!
(28, 29)] # ‘.’ は文字28-29
answer_start = 21 は (21, 28) の範囲内
→ トークン位置 4 が回答の開始位置
4-2. トークナイザーの準備
# ========================================
# トークナイザーの準備
# ========================================
# bert-base-uncased: 小文字化された英語BERT
model_name = ‘bert-base-uncased’
# BertTokenizerFast: 高速版トークナイザー
# offset_mappingを取得するためにFast版が必要
tokenizer = BertTokenizerFast.from_pretrained(model_name)
print(f”トークナイザー: {model_name}”)
print(f”語彙サイズ: {tokenizer.vocab_size}”)
実行結果:
トークナイザー: bert-base-uncased
語彙サイズ: 30522
4-3. トークン化の例
# ========================================
# トークン化の例
# ========================================
# サンプルデータ
question = “Where was Einstein born?”
context = “Albert Einstein was born in Germany in 1879.”
# トークン化
# return_offsets_mapping=True: 各トークンの文字範囲を取得
inputs = tokenizer(
question,
context,
return_offsets_mapping=True
)
# トークンを確認
tokens = tokenizer.convert_ids_to_tokens(inputs[‘input_ids’])
print(“=== トークン化の結果 ===”)
print(f”トークン数: {len(tokens)}”)
print(f”\nトークン一覧:”)
for i, (token, offset) in enumerate(zip(tokens, inputs[‘offset_mapping’])):
print(f” {i:2d}: {token:15s} 文字範囲: {offset}”)
実行結果:
=== トークン化の結果 ===
トークン数: 16
トークン一覧:
0: [CLS] 文字範囲: (0, 0)
1: where 文字範囲: (0, 5)
2: was 文字範囲: (6, 9)
3: einstein 文字範囲: (10, 18)
4: born 文字範囲: (19, 23)
5: ? 文字範囲: (23, 24)
6: [SEP] 文字範囲: (0, 0)
7: albert 文字範囲: (0, 6)
8: einstein 文字範囲: (7, 15)
9: was 文字範囲: (16, 19)
10: born 文字範囲: (20, 24)
11: in 文字範囲: (25, 27)
12: germany 文字範囲: (28, 35)
13: in 文字範囲: (36, 38)
14: 1879 文字範囲: (39, 43)
15: . 文字範囲: (43, 44)
16: [SEP] 文字範囲: (0, 0)
【結果の解釈】
質問部分(位置1-5):
where, was, einstein, born, ?
文書部分(位置7-15):
albert, einstein, was, born, in, germany, in, 1879, .
回答 “Germany” の位置:
文書中の文字位置: 28-35
→ offset_mapping で (28, 35) を探す
→ トークン位置: 12
つまり:
start_position = 12
end_position = 12
4-4. 前処理関数の作成
# ========================================
# 前処理関数の作成
# ========================================
def prepare_train_features(examples):
“””
訓練データの前処理を行う関数
Args:
examples: データセットのバッチ
Returns:
トークン化されたデータ(start/end位置付き)
“””
# トークン化
tokenized = tokenizer(
examples[‘question’], # 質問
examples[‘context’], # 文書
truncation=’only_second’, # 文書のみ切り詰め
max_length=384, # 最大384トークン
stride=128, # 長い文書は128トークンずつオーバーラップ
return_overflowing_tokens=True, # 長い文書を分割
return_offsets_mapping=True, # 文字位置を取得
padding=’max_length’ # 最大長までパディング
)
# 分割されたサンプルと元のサンプルの対応
sample_mapping = tokenized.pop(‘overflow_to_sample_mapping’)
offset_mapping = tokenized.pop(‘offset_mapping’)
tokenized[‘start_positions’] = []
tokenized[‘end_positions’] = []
for i, offsets in enumerate(offset_mapping):
# 元のサンプルのインデックス
sample_idx = sample_mapping[i]
answers = examples[‘answers’][sample_idx]
# 回答がない場合は[CLS]位置(0)に設定
if len(answers[‘answer_start’]) == 0:
tokenized[‘start_positions’].append(0)
tokenized[‘end_positions’].append(0)
continue
# 回答の文字位置
start_char = answers[‘answer_start’][0]
end_char = start_char + len(answers[‘text’][0])
# 文書部分の開始トークンを探す
# token_type_ids: 0=質問, 1=文書
sequence_ids = tokenized.sequence_ids(i)
# 文書の開始・終了インデックスを取得
context_start = 0
while sequence_ids[context_start] != 1:
context_start += 1
context_end = len(sequence_ids) – 1
while sequence_ids[context_end] != 1:
context_end -= 1
# 回答が文書範囲外の場合は[CLS]位置に設定
if offsets[context_start][0] > end_char or offsets[context_end][1] < start_char:
tokenized['start_positions'].append(0)
tokenized['end_positions'].append(0)
continue
# 開始トークン位置を探す
token_start = context_start
while token_start <= context_end and offsets[token_start][0] <= start_char:
token_start += 1
token_start -= 1
# 終了トークン位置を探す
token_end = context_end
while token_end >= context_start and offsets[token_end][1] >= end_char:
token_end -= 1
token_end += 1
tokenized[‘start_positions’].append(token_start)
tokenized[‘end_positions’].append(token_end)
return tokenized
print(“前処理関数を定義しました”)
実行結果:
前処理関数を定義しました
4-5. データセット全体の前処理
# ========================================
# データセット全体の前処理
# ========================================
# 小さいサブセットで試す(開発用)
small_train = squad[‘train’].select(range(1000))
small_valid = squad[‘validation’].select(range(200))
print(“前処理を開始…”)
tokenized_train = small_train.map(
prepare_train_features,
batched=True,
remove_columns=small_train.column_names
)
tokenized_valid = small_valid.map(
prepare_train_features,
batched=True,
remove_columns=small_valid.column_names
)
print(“前処理完了!”)
print(f”\n訓練データ: {len(tokenized_train)} サンプル”)
print(f”検証データ: {len(tokenized_valid)} サンプル”)
実行結果:
前処理を開始…
前処理完了!
訓練データ: 1024 サンプル
検証データ: 208 サンプル
⚠️ サンプル数が増える理由
長い文書は複数のサンプルに分割されるため、
元の1000件が1024件に増えています。
stride=128でオーバーラップしながら分割しています。
🎯 5. モデルの訓練
5-1. モデルの読み込み
# ========================================
# モデルの読み込み
# ========================================
# BertForQuestionAnswering: QA用のBERTモデル
model = BertForQuestionAnswering.from_pretrained(model_name)
# GPUに転送
model = model.to(device)
print(f”モデル: {model_name}”)
print(f”パラメータ数: {sum(p.numel() for p in model.parameters()):,}”)
実行結果:
モデル: bert-base-uncased
パラメータ数: 109,482,240
5-2. 訓練設定
# ========================================
# 訓練設定
# ========================================
training_args = TrainingArguments(
output_dir=’./qa_results’, # 出力ディレクトリ
num_train_epochs=3, # エポック数
per_device_train_batch_size=8, # バッチサイズ(メモリに応じて調整)
per_device_eval_batch_size=16, # 評価時バッチサイズ
learning_rate=3e-5, # 学習率
weight_decay=0.01, # 重み減衰
warmup_steps=100, # ウォームアップ
logging_steps=50, # ログ出力間隔
evaluation_strategy=’epoch’, # 評価タイミング
save_strategy=’epoch’, # 保存タイミング
load_best_model_at_end=True, # 最良モデルを保持
fp16=True # 混合精度訓練
)
print(“訓練設定を定義しました”)
5-3. 訓練の実行
# ========================================
# 訓練の実行
# ========================================
# Trainerの初期化
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_train,
eval_dataset=tokenized_valid
)
# 訓練開始
print(“=” * 50)
print(“訓練を開始します…”)
print(“=” * 50)
trainer.train()
print(“\n訓練完了!”)
実行結果(例):
==================================================
訓練を開始します…
==================================================
Epoch 1/3: 100%|██████████| 128/128 [01:45<00:00]
Epoch 2/3: 100%|██████████| 128/128 [01:44<00:00]
Epoch 3/3: 100%|██████████| 128/128 [01:44<00:00]
訓練完了!
🔮 6. 推論(予測)
訓練したモデルを使って、新しい質問に回答します。
6-1. 推論関数の作成
# ========================================
# 推論関数の作成
# ========================================
def answer_question(question, context, model, tokenizer, device):
“””
質問に対する回答を予測する関数
Args:
question: 質問文
context: 文書(回答が含まれるテキスト)
model: 訓練済みモデル
tokenizer: トークナイザー
device: デバイス
Returns:
回答テキストと信頼度
“””
# モデルを評価モードに
model.eval()
# トークン化
inputs = tokenizer(
question,
context,
return_tensors=’pt’, # PyTorchテンソルで返す
max_length=384,
truncation=’only_second’, # 文書のみ切り詰め
padding=’max_length’
)
# GPUに転送
inputs = {k: v.to(device) for k, v in inputs.items()}
# 予測
with torch.no_grad():
outputs = model(**inputs)
# 開始・終了位置のスコアを取得
start_logits = outputs.start_logits[0]
end_logits = outputs.end_logits[0]
# 最もスコアが高い位置を取得
start_idx = torch.argmax(start_logits).item()
end_idx = torch.argmax(end_logits).item()
# start <= end の制約をチェック
if start_idx > end_idx:
# 制約違反の場合、次点の候補を探す
start_idx, end_idx = end_idx, start_idx
# スコア(信頼度)
confidence = (start_logits[start_idx] + end_logits[end_idx]).item()
# トークンから回答を取得
tokens = tokenizer.convert_ids_to_tokens(inputs[‘input_ids’][0])
answer_tokens = tokens[start_idx:end_idx + 1]
# トークンをテキストに変換
answer = tokenizer.convert_tokens_to_string(answer_tokens)
# [CLS]や[SEP]などの特殊トークンを除去
answer = answer.replace(‘[CLS]’, ”).replace(‘[SEP]’, ”).strip()
return {
‘answer’: answer,
‘confidence’: confidence,
‘start_idx’: start_idx,
‘end_idx’: end_idx
}
print(“推論関数を定義しました”)
実行結果:
推論関数を定義しました
6-2. 予測の実行
# ========================================
# 予測の実行
# ========================================
# テスト用の文書
context = “””
Albert Einstein was a German-born theoretical physicist who
developed the theory of relativity. He was born in Ulm, Germany
in 1879 and died in Princeton, New Jersey in 1955. Einstein is
best known for his mass-energy equivalence formula E = mc².
“””
# 質問リスト
questions = [
“Where was Einstein born?”,
“When was Einstein born?”,
“When did Einstein die?”,
“What is Einstein known for?”
]
print(“=== 質問応答の結果 ===\n”)
for question in questions:
result = answer_question(question, context, model, tokenizer, device)
print(f”質問: {question}”)
print(f”回答: {result[‘answer’]}”)
print(f”信頼度: {result[‘confidence’]:.2f}”)
print(“-” * 50)
実行結果(例):
=== 質問応答の結果 ===
質問: Where was Einstein born?
回答: ulm, germany
信頼度: 12.45
————————————————–
質問: When was Einstein born?
回答: 1879
信頼度: 11.23
————————————————–
質問: When did Einstein die?
回答: 1955
信頼度: 10.87
————————————————–
質問: What is Einstein known for?
回答: mass-energy equivalence formula e = mc²
信頼度: 9.56
————————————————–
✅ 予測結果の解釈
回答 : 文書から抽出されたテキスト
信頼度 : Start + Endスコアの合計(高いほど自信あり)
回答は必ず文書内のテキストと一致
🚀 7. Hugging Face Pipelineの活用
事前学習済みのQAモデルを簡単に使う方法を紹介します。
7-1. Pipelineの使用
# ========================================
# Hugging Face Pipelineの使用
# ========================================
from transformers import pipeline
# QAパイプライン(事前学習済みモデル)
# distilbert-base-cased-distilled-squad: 軽量で高速なモデル
qa_pipeline = pipeline(
‘question-answering’,
model=’distilbert-base-cased-distilled-squad’,
device=0 if torch.cuda.is_available() else -1
)
print(“QAパイプラインを読み込みました”)
実行結果:
QAパイプラインを読み込みました
# ========================================
# Pipelineでの予測
# ========================================
context = “””
The Apollo 11 mission was the first manned mission to land on
the Moon. It was launched on July 16, 1969, and Neil Armstrong
became the first person to step onto the lunar surface on July 20, 1969.
“””
questions = [
“When was Apollo 11 launched?”,
“Who was the first person on the Moon?”,
“What was Apollo 11?”
]
print(“=== Pipeline QA ===\n”)
for question in questions:
# Pipelineは辞書形式で入力
result = qa_pipeline(question=question, context=context)
print(f”質問: {question}”)
print(f”回答: {result[‘answer’]}”)
print(f”スコア: {result[‘score’]:.4f}”)
print(“-” * 50)
実行結果:
=== Pipeline QA ===
質問: When was Apollo 11 launched?
回答: July 16, 1969
スコア: 0.9234
————————————————–
質問: Who was the first person on the Moon?
回答: Neil Armstrong
スコア: 0.9856
————————————————–
質問: What was Apollo 11?
回答: the first manned mission to land on the Moon
スコア: 0.8765
————————————————–
💡 Pipelineのメリット
簡単 : 数行のコードでQAが実行できる
高品質 : 事前学習済みモデルを使用
高速 : DistilBERTは軽量で速い
📊 8. 評価指標
QAシステムの性能はExact Match(EM) とF1スコア で評価します。
8-1. Exact Match(EM)
【Exact Match(完全一致率)】
定義:
予測と正解が完全に一致した割合
計算:
EM = 完全一致数 / 総質問数
例:
正解: “Albert Einstein”
予測: “Albert Einstein” → 一致(1点)
予測: “Einstein” → 不一致(0点)
予測: “albert einstein” → 正規化後一致(1点)
特徴:
・厳しい指標
・1文字でも違えば0点
・大文字小文字、冠詞などは正規化して比較
8-2. F1スコア
【F1スコア(トークンベース)】
定義:
予測と正解のトークンの重なりを評価
計算:
Precision = 共通トークン数 / 予測トークン数
Recall = 共通トークン数 / 正解トークン数
F1 = 2 × Precision × Recall / (Precision + Recall)
例:
正解: “Albert Einstein” → [“albert”, “einstein”]
予測: “Einstein” → [“einstein”]
共通トークン: [“einstein”] → 1個
Precision = 1/1 = 1.0(予測した1個は全て正解)
Recall = 1/2 = 0.5(正解2個中1個のみ予測)
F1 = 2 × 1.0 × 0.5 / 1.5 = 0.67
特徴:
・EMより緩い指標
・部分一致でもポイントがもらえる
・実用的な性能を反映
8-3. 評価関数の実装
# ========================================
# 評価関数の実装
# ========================================
import string
from collections import Counter
def normalize_answer(text):
“””
回答を正規化する関数
– 小文字化
– 句読点除去
– 冠詞除去
– 空白正規化
“””
# 小文字化
text = text.lower()
# 句読点除去
text = ”.join(ch for ch in text if ch not in string.punctuation)
# 冠詞除去
articles = [‘a’, ‘an’, ‘the’]
words = text.split()
words = [w for w in words if w not in articles]
# 空白正規化
text = ‘ ‘.join(words)
return text
def compute_exact_match(prediction, ground_truth):
“””
Exact Matchを計算
“””
return int(normalize_answer(prediction) == normalize_answer(ground_truth))
def compute_f1(prediction, ground_truth):
“””
F1スコアを計算
“””
pred_tokens = normalize_answer(prediction).split()
truth_tokens = normalize_answer(ground_truth).split()
# 共通トークン
common = Counter(pred_tokens) & Counter(truth_tokens)
num_common = sum(common.values())
if len(pred_tokens) == 0 or len(truth_tokens) == 0:
return int(pred_tokens == truth_tokens)
if num_common == 0:
return 0
precision = num_common / len(pred_tokens)
recall = num_common / len(truth_tokens)
f1 = 2 * precision * recall / (precision + recall)
return f1
print(“評価関数を定義しました”)
実行結果:
評価関数を定義しました
8-4. 評価例
# ========================================
# 評価例
# ========================================
# テストケース
test_cases = [
{“prediction”: “Albert Einstein”, “ground_truth”: “Albert Einstein”},
{“prediction”: “Einstein”, “ground_truth”: “Albert Einstein”},
{“prediction”: “the theory of relativity”, “ground_truth”: “theory of relativity”},
{“prediction”: “1879”, “ground_truth”: “1879”},
{“prediction”: “Germany”, “ground_truth”: “Ulm, Germany”},
]
print(“=== 評価例 ===\n”)
for case in test_cases:
pred = case[“prediction”]
truth = case[“ground_truth”]
em = compute_exact_match(pred, truth)
f1 = compute_f1(pred, truth)
print(f”予測: ‘{pred}'”)
print(f”正解: ‘{truth}'”)
print(f”EM: {em}, F1: {f1:.4f}”)
print(“-” * 40)
実行結果:
=== 評価例 ===
予測: ‘Albert Einstein’
正解: ‘Albert Einstein’
EM: 1, F1: 1.0000
—————————————-
予測: ‘Einstein’
正解: ‘Albert Einstein’
EM: 0, F1: 0.6667
—————————————-
予測: ‘the theory of relativity’
正解: ‘theory of relativity’
EM: 1, F1: 1.0000
—————————————-
予測: ‘1879’
正解: ‘1879’
EM: 1, F1: 1.0000
—————————————-
予測: ‘Germany’
正解: ‘Ulm, Germany’
EM: 0, F1: 0.6667
—————————————-
【結果の解釈】
1. “Albert Einstein” vs “Albert Einstein”
→ EM=1, F1=1.0(完全一致)
2. “Einstein” vs “Albert Einstein”
→ EM=0(部分一致はカウントされない)
→ F1=0.67(1/1 × 1/2 の調和平均)
3. “the theory of relativity” vs “theory of relativity”
→ EM=1(冠詞 “the” は正規化で除去)
→ F1=1.0
4. “Germany” vs “Ulm, Germany”
→ EM=0(”Ulm”が欠けている)
→ F1=0.67(”germany”のみ一致)
【SQuADベンチマーク(参考)】
人間: EM=82.3%, F1=91.2%
BERT-Large: EM=84.1%, F1=90.9%
→ BERTは人間と同等以上の性能!
指標
特徴
用途
Exact Match
厳しい(完全一致のみ)
正確性の評価
F1 Score
緩い(部分一致も評価)
実用的な性能評価
📝 練習問題
問題1:QAの種類
文書から回答を直接抽出するQAのタイプは?
Generative QA
Extractive QA
Open-domain QA
Closed-domain QA
解答を見る
正解:b(Extractive QA)
Extractive QA(抽出型) は文書から回答を直接抽出します。
特徴:
回答は必ず文書内に存在
開始位置と終了位置を予測
根拠が明確
対比: Generative QAは回答を「生成」する(文書の表現と異なる場合がある)
問題2:評価指標
QAで厳しい評価指標は?
F1 Score
Exact Match
BLEU Score
ROUGE Score
解答を見る
正解:b(Exact Match)
Exact Match が最も厳しい評価指標です。
理由:
F1 Scoreは部分一致でもポイントがもらえるため、より緩い指標です。
問題3:BERTのQAモデル
BERTのQAモデルが予測するものは?
回答テキストそのもの
回答の開始位置と終了位置
質問の類似度
文書の関連度
解答を見る
正解:b(回答の開始位置と終了位置)
BERTのQAモデルは開始位置と終了位置 を予測します。
仕組み:
入力: [CLS] Question [SEP] Context [SEP]
出力: 各トークンのStart/Endスコア
最もスコアが高い位置を選択
該当トークンを抽出して回答
問題4:F1スコアの計算
正解 “Albert Einstein”、予測 “Einstein” の場合のF1スコアは?
0.0
0.5
0.67
1.0
解答を見る
正解:c(0.67)
計算:
正解トークン: [“albert”, “einstein”] → 2個
予測トークン: [“einstein”] → 1個
共通トークン: [“einstein”] → 1個
Precision = 1/1 = 1.0
Recall = 1/2 = 0.5
F1 = 2 × 1.0 × 0.5 / 1.5 = 0.67
📝 STEP 22 のまとめ
✅ このステップで学んだこと
QAの種類 : Extractive、Generative、Open/Closed-domain
SQuADデータセット : 文書 + 質問 + 回答位置
BERTのQAモデル : 開始・終了位置の予測
トークン化 : 文字位置からトークン位置への変換
評価指標 : Exact Match(厳しい)、F1(緩い)
Pipeline : 簡単にQAを実行する方法
🎯 次のステップの準備
STEP 23: テキスト生成と要約 では、
GPT-2を使ったテキスト生成を学びます!
生成戦略(Greedy、Beam Search、Sampling)
GPT-2でのテキスト生成
文書要約(T5、BART)
評価指標(ROUGE、BLEU)