📋 このステップで学ぶこと
固有表現認識(NER)の概要と応用
BIO/IOB2タグ付けスキーマの仕組み
トークン分類(Token Classification)の実装
BERTでのNER実装
サブワード分割とラベルアライメント
Entity-level評価指標
練習問題: 4問
⚠️ 実行環境について
このステップのコードはGoogle Colab(GPU必須) で実行してください。
「ランタイム」→「ランタイムのタイプを変更」→「GPU」を選択します。
🎯 1. 固有表現認識(NER)とは
固有表現認識(Named Entity Recognition, NER) は、
テキストから人名、地名、組織名などの固有表現 を自動的に見つけ出すタスクです。
1-1. NERの基本概念
【NERの例】
入力テキスト:
“Apple の CEO である Tim Cook が San Francisco で発表しました。”
NERの出力:
┌────────────────────────────────────────────────────┐
│ Apple → 組織名(ORG) │
│ Tim Cook → 人名(PER) │
│ San Francisco → 地名(LOC) │
└────────────────────────────────────────────────────┘
NERがやること:
1. テキスト中の固有表現を「見つける」
2. その固有表現に「カテゴリ」を付ける
【感情分析との違い】
感情分析(STEP 20):
入力: “This movie is great!” → 出力: Positive
→ 文全体に1つのラベル
NER(このステップ):
入力: “Tim Cook visited Paris” → 出力: Tim(B-PER), Cook(I-PER), visited(O), Paris(B-LOC)
→ 各単語(トークン)に1つのラベル
1-2. 主要なエンティティタイプ
タイプ
略称
説明
例
人名
PER
個人の名前
田中太郎、Barack Obama
地名
LOC
場所・地域
東京、Paris、富士山
組織名
ORG
会社・団体
トヨタ、Google、東京大学
その他
MISC
国籍・イベント等
Japanese、World Cup
1-3. NERの応用例
【実用例】
■ 情報抽出
ニュース記事から人物・組織・場所を自動抽出
「誰が」「どこで」「何をした」を構造化データに
■ 質問応答システム
質問: “Appleの創業者は誰?”
→ “Apple”を組織名と認識
→ 創業者(人名)を検索
■ 知識グラフ構築
固有表現間の関係を抽出
例: “Tim Cook” – CEO of – “Apple”
■ 文書要約
重要な固有表現を優先的に含める
「〇〇社の△△氏が□□で発表」
■ 検索エンジン
クエリ中の固有表現を認識
「東京のラーメン屋」→ 東京 = 地名
【NERが重要な理由】
1. 構造化されていないテキスト → 構造化データ
2. 他のNLPタスク(QA、要約など)の前処理
3. ドメイン固有のエンティティにも対応可能
💡 NERの課題
曖昧性 : 「Apple」は会社?果物?
未知語 : 訓練データにない新しい固有表現
複数単語 : 「San Francisco」は2単語で1つの地名
言語依存 : 日本語と英語で異なる特徴
🏷️ 2. BIO/IOB2タグ付けスキーマ
NERでは、各トークンに「タグ」を付けて固有表現を表現します。
最も一般的なのがBIOタグスキーマ です。
2-1. BIOタグの基本
【BIOタグの意味】
B (Begin): エンティティの「開始」
I (Inside): エンティティの「内部」(2番目以降)
O (Outside): エンティティ「外」(固有表現ではない)
【例1: 単純な例】
文: “Barack Obama visited Paris.”
単語 タグ 説明
──────────────────────────────────
Barack B-PER ← 人名の開始
Obama I-PER ← 人名の内部(2番目)
visited O ← 固有表現ではない
Paris B-LOC ← 地名の開始(1単語なのでBのみ)
. O ← 固有表現ではない
【図解】
“Barack Obama visited Paris .”
↓ ↓ ↓ ↓ ↓
B-PER I-PER O B-LOC O
└──人名──┘ └地名┘
2-2. なぜBタグが必要なのか
【問題: Bタグなし(IとOのみ)の場合】
文: “New York と New Jersey”
IとOのみ:
New → I-LOC
York → I-LOC
と → O
New → I-LOC
Jersey → I-LOC
問題点:
「New York」と「New Jersey」が別のエンティティか
判別できない!
[I-LOC, I-LOC] [O] [I-LOC, I-LOC]
↓ ↓
1つのエンティティ? 別のエンティティ?
【解決: BIOスキーマ】
BIOタグ:
New → B-LOC ← 開始!
York → I-LOC
と → O
New → B-LOC ← 新しいエンティティの開始!
Jersey → I-LOC
明確に2つのエンティティと分かる!
[B-LOC, I-LOC] [O] [B-LOC, I-LOC]
└─New York─┘ └─New Jersey─┘
2-3. タグの総数
【タグ数の計算】
エンティティタイプ数: n
タグ総数 = 2n + 1
計算式の意味:
・各タイプにB-とI-の2種類 → 2n
・Outside(O)が1つ → +1
【例: CoNLL-2003データセット】
エンティティタイプ: 4つ
・PER(人名)
・LOC(地名)
・ORG(組織名)
・MISC(その他)
タグ一覧:
0: O ← 固有表現ではない
1: B-PER ← 人名の開始
2: I-PER ← 人名の内部
3: B-ORG ← 組織名の開始
4: I-ORG ← 組織名の内部
5: B-LOC ← 地名の開始
6: I-LOC ← 地名の内部
7: B-MISC ← その他の開始
8: I-MISC ← その他の内部
合計: 2×4 + 1 = 9タグ
✅ BIOスキーマのポイント
B : 必ずエンティティの最初に付く
I : 必ずBの後に続く(単独では出現しない)
O : 固有表現ではない部分に付く
連続するエンティティを区別できる
🔧 3. 環境構築
NERの実装に必要なライブラリをインストールします。
3-1. ライブラリのインストール
※モバイルでは横スクロールできます
# ========================================
# 必要なライブラリのインストール
# ========================================
# transformers: BERTなどの事前学習モデル
# datasets: Hugging Faceのデータセット
# seqeval: NER専用の評価ライブラリ
!pip install transformers==4.35.0 datasets==2.14.0 seqeval -q
3-2. ライブラリのインポート
# ========================================
# ライブラリのインポート
# ========================================
import torch
from transformers import (
BertTokenizerFast, # 高速トークナイザー
BertForTokenClassification, # トークン分類用BERT
Trainer,
TrainingArguments
)
from datasets import load_dataset
from seqeval.metrics import (
classification_report,
f1_score,
precision_score,
recall_score
)
import numpy as np
# GPU確認
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
print(f”Using device: {device}”)
if torch.cuda.is_available():
print(f”GPU: {torch.cuda.get_device_name(0)}”)
実行結果:
Using device: cuda
GPU: Tesla T4
【BertForTokenClassificationとは】
STEP 20で使用:
BertForSequenceClassification
→ 文全体に1つのラベル(感情分析)
このステップで使用:
BertForTokenClassification
→ 各トークンに1つのラベル(NER)
【出力の違い】
SequenceClassification:
入力: [CLS] This movie is great [SEP]
出力: [Positive] ← 1つのラベル
TokenClassification:
入力: [CLS] Tim Cook visited Paris [SEP]
出力: [-, B-PER, I-PER, O, B-LOC, -] ← トークンごとにラベル
※ [CLS]と[SEP]のラベルは無視
📁 4. CoNLL-2003データセットの準備
CoNLL-2003 は英語NERの標準的なベンチマークデータセットです。
ニュース記事から人名・地名・組織名・その他を抽出するタスクです。
4-1. データセットの読み込み
# ========================================
# CoNLL-2003データセットの読み込み
# ========================================
# load_dataset: Hugging Faceからデータセットをダウンロード
# ‘conll2003’: 英語NERの標準データセット
dataset = load_dataset(‘conll2003’)
print(“=== データセットの構造 ===”)
print(dataset)
実行結果:
=== データセットの構造 ===
DatasetDict({
train: Dataset({
features: [‘id’, ‘tokens’, ‘pos_tags’, ‘chunk_tags’, ‘ner_tags’],
num_rows: 14041
})
validation: Dataset({
features: [‘id’, ‘tokens’, ‘pos_tags’, ‘chunk_tags’, ‘ner_tags’],
num_rows: 3250
})
test: Dataset({
features: [‘id’, ‘tokens’, ‘pos_tags’, ‘chunk_tags’, ‘ner_tags’],
num_rows: 3453
})
})
4-2. データの中身を確認
# ========================================
# サンプルデータの確認
# ========================================
# 1つ目のサンプルを取得
example = dataset[‘train’][0]
print(“=== サンプルデータ ===”)
print(f”トークン: {example[‘tokens’]}”)
print(f”NERタグ(数値): {example[‘ner_tags’]}”)
実行結果:
=== サンプルデータ ===
トークン: [‘EU’, ‘rejects’, ‘German’, ‘call’, ‘to’, ‘boycott’, ‘British’, ‘lamb’, ‘.’]
NERタグ(数値): [3, 0, 7, 0, 0, 0, 7, 0, 0]
4-3. タグの定義を確認
# ========================================
# タグの定義を確認
# ========================================
# データセットからタグ名を取得
tag_names = dataset[‘train’].features[‘ner_tags’].feature.names
print(“=== タグ一覧 ===”)
for i, tag in enumerate(tag_names):
print(f” {i}: {tag}”)
# タグIDとタグ名の変換辞書を作成
id2tag = {i: tag for i, tag in enumerate(tag_names)}
tag2id = {tag: i for i, tag in enumerate(tag_names)}
実行結果:
=== タグ一覧 ===
0: O
1: B-PER
2: I-PER
3: B-ORG
4: I-ORG
5: B-LOC
6: I-LOC
7: B-MISC
8: I-MISC
4-4. サンプルを見やすく表示
# ========================================
# サンプルを見やすく表示
# ========================================
print(“=== サンプルの詳細 ===”)
print(f”{‘トークン’:<15} {'タグID':<8} {'タグ名'}")
print("-" * 35)
for token, tag_id in zip(example['tokens'], example['ner_tags']):
tag_name = id2tag[tag_id]
print(f"{token:<15} {tag_id:<8} {tag_name}")
実行結果:
=== サンプルの詳細 ===
トークン タグID タグ名
———————————–
EU 3 B-ORG
rejects 0 O
German 7 B-MISC
call 0 O
to 0 O
boycott 0 O
British 7 B-MISC
lamb 0 O
. 0 O
【サンプルの解釈】
“EU rejects German call to boycott British lamb.”
↓
・EU → B-ORG(組織名: 欧州連合)
・German → B-MISC(国籍・形容詞: ドイツの)
・British → B-MISC(国籍・形容詞: イギリスの)
・その他 → O(固有表現ではない)
文の意味:
「EUがドイツの要請を拒否し、イギリス産ラム肉のボイコットを行わなかった」
🔤 5. トークン化とラベルアライメント
BERTのトークナイザーは単語をサブワードに分割します。
元のラベルとBERTトークンを正しく対応付ける(アライメント) 必要があります。
5-1. サブワード分割の問題
【問題: サブワード分割】
元のデータ:
単語: [“playing”, “football”]
ラベル: [O, O]
BERTのトークン化後:
トークン: [“play”, “##ing”, “foot”, “##ball”]
問題:
元は2単語なのに、BERTは4トークンに分割!
ラベルはどう対応付ける?
【解決策】
方法: サブワードの最初のトークンのみにラベルを付与
2番目以降は「-100」(無視)
トークン: [“play”, “##ing”, “foot”, “##ball”]
ラベル: [O, -100, O, -100]
-100の意味:
・PyTorchのCrossEntropyLossで自動的に無視される
・損失計算に含まれない
・最初のトークンのみで学習
【図解】
元のデータ:
playing → O
football → O
BERT変換後:
play → O ← 最初のトークン: ラベル付与
##ing → -100 ← 2番目以降: 無視
foot → O ← 最初のトークン: ラベル付与
##ball → -100 ← 2番目以降: 無視
5-2. トークナイザーの読み込み
# ========================================
# トークナイザーの読み込み
# ========================================
# bert-base-cased: 大文字小文字を区別するBERT
# NERでは固有名詞の大文字が重要なので cased を使用
model_name = ‘bert-base-cased’
# BertTokenizerFast: 高速版トークナイザー
# word_ids() メソッドが使えるのでFast版を使用
tokenizer = BertTokenizerFast.from_pretrained(model_name)
print(f”トークナイザー: {model_name}”)
print(f”語彙サイズ: {tokenizer.vocab_size}”)
実行結果:
トークナイザー: bert-base-cased
語彙サイズ: 28996
5-3. サブワード分割の確認
# ========================================
# サブワード分割の確認
# ========================================
# サンプルテキスト
sample_tokens = [‘EU’, ‘rejects’, ‘German’, ‘call’]
# 各単語をトークン化
print(“=== サブワード分割の例 ===”)
for word in sample_tokens:
sub_tokens = tokenizer.tokenize(word)
print(f” {word} → {sub_tokens}”)
実行結果:
=== サブワード分割の例 ===
EU → [‘EU’]
rejects → [‘reject’, ‘##s’]
German → [‘German’]
call → [‘call’]
【分割結果の解釈】
EU → [‘EU’]
1トークンのまま(分割なし)
rejects → [‘reject’, ‘##s’]
2トークンに分割(動詞の活用)
‘##’ はサブワードの継続を示す
German → [‘German’]
1トークンのまま
call → [‘call’]
1トークンのまま
【ラベルの対応】
元のラベル:
EU: B-ORG, rejects: O, German: B-MISC, call: O
BERT変換後:
EU: B-ORG
reject: O ← rejectsの最初のトークン
##s: -100 ← rejectsの2番目(無視)
German: B-MISC
call: O
5-4. ラベルアライメント関数
# ========================================
# ラベルアライメント関数
# ========================================
def tokenize_and_align_labels(examples):
“””
トークン化してラベルをアライメントする関数
Args:
examples: データセットのバッチ
Returns:
トークン化されたデータ(ラベル付き)
“””
# トークン化
# is_split_into_words=True: 既に単語分割済みのリストを入力
tokenized_inputs = tokenizer(
examples[‘tokens’],
truncation=True, # 最大長を超えたらカット
is_split_into_words=True, # 単語リストとして入力
padding=’max_length’, # 最大長までパディング
max_length=128 # 最大128トークン
)
labels = []
# 各サンプルについてラベルをアライメント
for i, label in enumerate(examples[‘ner_tags’]):
# word_ids: 各トークンが元の何番目の単語に対応するか
# 例: [None, 0, 1, 1, 2, 3, None, None, …]
# [CLS] EU reject ##s German call [SEP] [PAD]
word_ids = tokenized_inputs.word_ids(batch_index=i)
previous_word_idx = None
label_ids = []
for word_idx in word_ids:
# 特殊トークン([CLS], [SEP], [PAD])の場合
# word_idxはNoneになる
if word_idx is None:
label_ids.append(-100) # 無視
# 新しい単語の最初のトークン
elif word_idx != previous_word_idx:
label_ids.append(label[word_idx]) # ラベルを付与
# サブワードの2番目以降
else:
label_ids.append(-100) # 無視
previous_word_idx = word_idx
labels.append(label_ids)
tokenized_inputs[‘labels’] = labels
return tokenized_inputs
print(“ラベルアライメント関数を定義しました”)
実行結果:
ラベルアライメント関数を定義しました
5-5. データセット全体のトークン化
# ========================================
# データセット全体のトークン化
# ========================================
# map: 各サンプルに関数を適用
# batched=True: バッチ処理で高速化
# remove_columns: 不要な列を削除
print(“トークン化を開始…”)
tokenized_datasets = dataset.map(
tokenize_and_align_labels,
batched=True,
remove_columns=dataset[‘train’].column_names
)
print(“トークン化完了!”)
print(“\n=== トークン化後のデータ構造 ===”)
print(tokenized_datasets)
実行結果:
トークン化を開始…
トークン化完了!
=== トークン化後のデータ構造 ===
DatasetDict({
train: Dataset({
features: [‘input_ids’, ‘token_type_ids’, ‘attention_mask’, ‘labels’],
num_rows: 14041
})
validation: Dataset({
features: [‘input_ids’, ‘token_type_ids’, ‘attention_mask’, ‘labels’],
num_rows: 3250
})
test: Dataset({
features: [‘input_ids’, ‘token_type_ids’, ‘attention_mask’, ‘labels’],
num_rows: 3453
})
})
💡 ラベルアライメントの重要ポイント
[CLS], [SEP], [PAD] : -100(損失計算から除外)
サブワードの最初 : 元のラベルを使用
サブワードの2番目以降 : -100(損失計算から除外)
-100はPyTorchのCrossEntropyLossで自動的に無視される
🎯 6. モデルの訓練
BERTモデルをNERタスク用にファインチューニングします。
6-1. モデルの読み込み
# ========================================
# モデルの読み込み
# ========================================
# BertForTokenClassification: トークン分類用のBERT
# num_labels: タグの数(9個)
model = BertForTokenClassification.from_pretrained(
model_name,
num_labels=len(tag_names)
)
# GPUに転送
model = model.to(device)
print(f”モデル: {model_name}”)
print(f”タグ数: {len(tag_names)}”)
print(f”パラメータ数: {sum(p.numel() for p in model.parameters()):,}”)
実行結果:
モデル: bert-base-cased
タグ数: 9
パラメータ数: 108,893,449
6-2. 評価指標の定義
# ========================================
# 評価指標の定義(Entity-level)
# ========================================
def compute_metrics(eval_pred):
“””
Entity-levelの評価指標を計算
Args:
eval_pred: (予測結果, 正解ラベル)のタプル
Returns:
precision, recall, f1の辞書
“””
predictions, labels = eval_pred
# logitsから予測クラスを取得
# axis=2: 各トークンの最大値のインデックス
predictions = np.argmax(predictions, axis=2)
# -100を除外してタグ名に変換
true_labels = []
true_predictions = []
for prediction, label in zip(predictions, labels):
true_label = []
true_prediction = []
for pred, lab in zip(prediction, label):
# -100(特殊トークン、サブワード)は除外
if lab != -100:
true_label.append(id2tag[lab])
true_prediction.append(id2tag[pred])
true_labels.append(true_label)
true_predictions.append(true_prediction)
# seqevalでEntity-level評価
results = {
‘precision’: precision_score(true_labels, true_predictions),
‘recall’: recall_score(true_labels, true_predictions),
‘f1’: f1_score(true_labels, true_predictions)
}
return results
print(“評価指標関数を定義しました”)
実行結果:
評価指標関数を定義しました
【Entity-level評価とは】
Token-level(トークン単位):
各トークンごとに正解/不正解を評価
例: “New York” → New: 正解, York: 正解 → 100%
Entity-level(エンティティ単位):
エンティティ全体が完全一致して初めて正解
例: “New York” → 全体が正しく抽出できたか
【なぜEntity-levelか】
実用上、部分的な抽出は意味がない
× “New” だけ抽出 → 不十分
○ “New York” 全体を抽出 → 正解
seqevalライブラリ:
NER専用の評価ライブラリ
Entity-levelのPrecision, Recall, F1を計算
6-3. 訓練設定
# ========================================
# 訓練設定
# ========================================
training_args = TrainingArguments(
output_dir=’./ner_results’, # 出力ディレクトリ
num_train_epochs=3, # エポック数
per_device_train_batch_size=16, # 訓練バッチサイズ
per_device_eval_batch_size=32, # 評価バッチサイズ
learning_rate=2e-5, # 学習率
weight_decay=0.01, # 重み減衰
warmup_steps=500, # ウォームアップ
logging_steps=500, # ログ出力間隔
evaluation_strategy=’epoch’, # 評価タイミング
save_strategy=’epoch’, # 保存タイミング
load_best_model_at_end=True, # 最良モデルを保持
metric_for_best_model=’f1′, # 最良の基準
fp16=True # 混合精度訓練
)
print(“訓練設定を定義しました”)
6-4. 訓練の実行
# ========================================
# 訓練の実行
# ========================================
# Trainerの初期化
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets[‘train’],
eval_dataset=tokenized_datasets[‘validation’],
compute_metrics=compute_metrics
)
# 訓練開始
print(“=” * 50)
print(“訓練を開始します…”)
print(“=” * 50)
trainer.train()
print(“\n訓練完了!”)
実行結果(例):
==================================================
訓練を開始します…
==================================================
Epoch 1/3: 100%|██████████| 878/878 [12:34<00:00]
Evaluation: {‘precision’: 0.9123, ‘recall’: 0.9045, ‘f1’: 0.9084}
Epoch 2/3: 100%|██████████| 878/878 [12:32<00:00]
Evaluation: {‘precision’: 0.9234, ‘recall’: 0.9167, ‘f1’: 0.9200}
Epoch 3/3: 100%|██████████| 878/878 [12:35<00:00]
Evaluation: {‘precision’: 0.9256, ‘recall’: 0.9189, ‘f1’: 0.9222}
訓練完了!
6-5. テストデータでの評価
# ========================================
# テストデータでの評価
# ========================================
test_results = trainer.evaluate(tokenized_datasets[‘test’])
print(“=== テスト結果 ===”)
for key, value in test_results.items():
if ‘eval_’ in key:
metric_name = key.replace(‘eval_’, ”)
print(f”{metric_name}: {value:.4f}”)
実行結果(例):
=== テスト結果 ===
loss: 0.0634
precision: 0.9212
recall: 0.9178
f1: 0.9195
runtime: 15.23
samples_per_second: 226.78
🔮 7. 推論(予測)
訓練したモデルを使って、新しいテキストからエンティティを抽出します。
7-1. 推論関数の作成
# ========================================
# 推論関数の作成
# ========================================
def predict_ner(text, model, tokenizer, id2tag, device):
“””
テキストからエンティティを抽出する関数
Args:
text: 入力テキスト
model: 訓練済みモデル
tokenizer: トークナイザー
id2tag: タグIDからタグ名への辞書
device: デバイス
Returns:
entities: 抽出されたエンティティのリスト
“””
# モデルを評価モードに
model.eval()
# トークン化
inputs = tokenizer(
text,
return_tensors=’pt’,
truncation=True,
max_length=128
)
inputs = {k: v.to(device) for k, v in inputs.items()}
# 予測
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.argmax(outputs.logits, dim=2)
# トークンとタグを取得
tokens = tokenizer.convert_ids_to_tokens(inputs[‘input_ids’][0])
tags = [id2tag[p.item()] for p in predictions[0]]
# エンティティを抽出
entities = []
current_entity = None
for i, (token, tag) in enumerate(zip(tokens, tags)):
# 特殊トークンをスキップ
if token in [‘[CLS]’, ‘[SEP]’, ‘[PAD]’]:
continue
# サブワードの処理(##で始まるトークン)
if token.startswith(‘##’):
if current_entity:
current_entity[‘text’] += token[2:] # ##を除去して追加
continue
# B-タグ: 新しいエンティティの開始
if tag.startswith(‘B-‘):
if current_entity:
entities.append(current_entity)
current_entity = {
‘text’: token,
‘type’: tag[2:] # ‘B-‘を除去
}
# I-タグ: エンティティの継続
elif tag.startswith(‘I-‘) and current_entity:
current_entity[‘text’] += ‘ ‘ + token
# O-タグ: エンティティの終了
else:
if current_entity:
entities.append(current_entity)
current_entity = None
# 最後のエンティティを追加
if current_entity:
entities.append(current_entity)
return entities
print(“推論関数を定義しました”)
実行結果:
推論関数を定義しました
7-2. 予測の実行
# ========================================
# 予測の実行
# ========================================
# テスト用のテキスト
test_texts = [
“Apple Inc. CEO Tim Cook announced a new product in San Francisco.”,
“Barack Obama visited Paris and met with Emmanuel Macron.”,
“Google’s headquarters is in Mountain View, California.”
]
print(“=== NER予測結果 ===\n”)
for text in test_texts:
print(f”テキスト: {text}”)
entities = predict_ner(text, model, tokenizer, id2tag, device)
print(“抽出されたエンティティ:”)
for entity in entities:
print(f” • {entity[‘text’]} ({entity[‘type’]})”)
print(“-” * 60 + “\n”)
実行結果(例):
=== NER予測結果 ===
テキスト: Apple Inc. CEO Tim Cook announced a new product in San Francisco.
抽出されたエンティティ:
• Apple Inc. (ORG)
• Tim Cook (PER)
• San Francisco (LOC)
————————————————————
テキスト: Barack Obama visited Paris and met with Emmanuel Macron.
抽出されたエンティティ:
• Barack Obama (PER)
• Paris (LOC)
• Emmanuel Macron (PER)
————————————————————
テキスト: Google’s headquarters is in Mountain View, California.
抽出されたエンティティ:
• Google (ORG)
• Mountain View (LOC)
• California (LOC)
————————————————————
✅ 予測結果の解釈
Apple Inc. : 会社名として正しく認識
Tim Cook : 2単語の人名として正しく認識
San Francisco : 2単語の地名として正しく認識
BIOタグにより、複数単語のエンティティも正しく認識できています。
📊 8. 評価指標の詳細
8-1. Token-level vs Entity-level
【2つの評価方法の違い】
■ Token-level評価
各トークンごとに正解/不正解を評価
例:
真値: [B-PER, I-PER, O, B-LOC]
予測: [B-PER, I-PER, O, O]
Token Accuracy = 3/4 = 75%
(4トークン中3つ正解)
■ Entity-level評価
エンティティ全体が一致して初めて正解
例:
真値: “John Smith” (PER), “Paris” (LOC)
予測: “John Smith” (PER)
Precision = 1/1 = 100%(予測した1個は正しい)
Recall = 1/2 = 50%(2個中1個しか抽出できず)
F1 = 0.67
【なぜEntity-levelが重要か】
実用上、部分的な抽出は意味がない:
× “John” だけ抽出(人名として不完全)
○ “John Smith” 全体を抽出(人名として完全)
Entity-levelでは:
・エンティティが完全一致して初めて正解
・タイプも一致する必要がある
・位置も正確である必要がある
8-2. 詳細な評価レポート
# ========================================
# 詳細な評価レポート
# ========================================
from seqeval.metrics import classification_report
# 予測結果を取得
predictions = trainer.predict(tokenized_datasets[‘test’])
pred_labels = np.argmax(predictions.predictions, axis=2)
true_labels = predictions.label_ids
# タグ名に変換
y_true = []
y_pred = []
for prediction, label in zip(pred_labels, true_labels):
true_label = []
pred_label = []
for pred, lab in zip(prediction, label):
if lab != -100:
true_label.append(id2tag[lab])
pred_label.append(id2tag[pred])
y_true.append(true_label)
y_pred.append(pred_label)
# 詳細レポート
print(“=== 詳細な評価レポート ===”)
print(classification_report(y_true, y_pred))
実行結果(例):
=== 詳細な評価レポート ===
precision recall f1-score support
LOC 0.93 0.92 0.93 1668
MISC 0.83 0.80 0.81 702
ORG 0.89 0.91 0.90 1661
PER 0.96 0.96 0.96 1617
micro avg 0.92 0.92 0.92 5648
macro avg 0.90 0.90 0.90 5648
weighted avg 0.92 0.92 0.92 5648
【レポートの読み方】
precision(適合率):
予測したエンティティのうち、正しいものの割合
例: PER 0.96 → 人名と予測した96%が正解
recall(再現率):
実際のエンティティのうち、正しく抽出できた割合
例: PER 0.96 → 実際の人名の96%を抽出
f1-score:
precisionとrecallの調和平均
バランスの良い指標
support:
テストデータ中のエンティティ数
例: PER 1617 → 人名が1617個
【結果の解釈】
・PER(人名): 最も高い精度(F1=0.96)
・LOC(地名): 高い精度(F1=0.93)
・ORG(組織名): 高い精度(F1=0.90)
・MISC(その他): やや低い精度(F1=0.81)
MISCが低い理由:
・カテゴリが曖昧(国籍、イベントなど)
・多様なエンティティが含まれる
🔗 9. CRF層の追加(発展)
BERTは各トークンを独立に 予測します。
しかし、NERではタグ間の関係性 が重要です。
CRF(Conditional Random Field) 層を追加することで、
タグ系列の整合性を保つことができます。
9-1. なぜCRFが必要か
【問題: BERTのみの場合】
BERTは各トークンを独立に予測する:
入力: “Tim Cook visited Paris”
トークンごとの予測(独立):
Tim → B-PER(正解)
Cook → O(間違い!)← 独立に予測したため
visited → O(正解)
Paris → B-LOC(正解)
問題:
“Cook”が”Tim”の続きであることを考慮していない
→ 矛盾した系列が生成される可能性
【さらに悪い例】
予測: O → I-PER → O → I-LOC
問題点:
・I-PERが突然出現(Bタグなしで)
・これは論理的に矛盾
・BIOルール: Iは必ずBの後に来る
【解決: CRF層を追加】
CRFは「タグ間の遷移確率」を学習する:
B-PER → I-PER: 高確率(人名は続くことが多い)
B-PER → O: 高確率(人名の終了は自然)
O → I-PER: 低確率(Bなしでは不自然)
I-PER → B-LOC: 高確率(人名の後に地名は自然)
結果:
CRFが矛盾した系列を抑制
→ より整合性のある予測
9-2. CRFの仕組み
【CRFの動作原理】
■ 遷移行列(Transition Matrix)
タグAからタグBへの遷移スコアを学習
→ O B-PER I-PER B-LOC I-LOC
O 0.5 0.3 -2.0 0.3 -2.0
B-PER 0.2 0.1 0.8 0.1 -2.0
I-PER 0.3 0.2 0.5 0.2 -2.0
B-LOC 0.4 0.3 -2.0 0.1 0.7
I-LOC 0.5 0.2 -2.0 0.1 0.6
※ 負の大きな値(-2.0)= その遷移を抑制
例: O → I-PER = -2.0(Bなしでは不自然なので抑制)
■ Viterbiアルゴリズム
最適なタグ系列を効率的に探索
すべての可能な系列を評価するのは計算量が膨大
→ Viterbiで動的計画法により効率化
→ 最もスコアの高い系列を選択
■ 訓練時
正解系列の確率を最大化
損失 = -log P(正解系列 | 入力)
■ 推論時
Viterbiデコーディングで最適系列を探索
9-3. CRFの効果
モデル
CoNLL-2003 F1
改善幅
BERT のみ
91.5%
–
BERT + CRF
92.8%
+1.3%
💡 CRFを使うべき場合
使うべき : タグの整合性が重要な場合、小規模データ
不要な場合 : 大規模データで十分な精度が出る場合
注意 : 訓練が若干遅くなる、実装が複雑
🇯🇵 10. 日本語NER
日本語のNERは、英語とは異なる課題があります。
特に分かち書き(単語分割) が重要です。
10-1. 日本語NERの特徴
【日本語NERの課題】
■ 分かち書きがない
英語: “Tim Cook visited Paris”
→ 単語の境界が明確(スペースで区切り)
日本語: “田中太郎は東京で発表しました”
→ 単語の境界が不明確
■ 解決策: 形態素解析
MeCabやJanomeで単語分割
“田中太郎は東京で発表しました”
→ [“田中”, “太郎”, “は”, “東京”, “で”, “発表”, “し”, “まし”, “た”]
■ 日本語BERTのトークン化
cl-tohoku/bert-base-japanese
→ MeCabベースの形態素解析を使用
“田中太郎”
→ [“田中”, “太郎”](意味のある単位で分割)
■ 主なエンティティタイプ(日本語)
PER(人名): 田中太郎、佐藤花子
LOC(地名): 東京都、富士山、京都
ORG(組織名): トヨタ自動車、東京大学
DATE(日付): 2024年1月1日、昨日
TIME(時刻): 午後3時、夜
10-2. 日本語NERの実装例
※モバイルでは横スクロールできます
# ========================================
# 日本語NERの簡単な例(Hugging Face Pipeline)
# ========================================
from transformers import pipeline
# 日本語NERパイプライン
# stockmark/stockmark-13b などの日本語対応モデルを使用
# ここでは多言語モデルで代用
ner_jp = pipeline(
‘ner’,
model=’xlm-roberta-large-finetuned-conll03-english’,
aggregation_strategy=’simple’
)
# テスト(英語モデルなので日本語は限定的)
text_en = “Toyota’s headquarters is in Aichi, Japan.”
print(“=== 多言語モデルでのNER ===”)
print(f”テキスト: {text_en}”)
results = ner_jp(text_en)
for r in results:
print(f” {r[‘word’]} ({r[‘entity_group’]})”)
実行結果:
=== 多言語モデルでのNER ===
テキスト: Toyota’s headquarters is in Aichi, Japan.
Toyota (ORG)
Aichi (LOC)
Japan (LOC)
💡 日本語NERのリソース
モデル : cl-tohoku/bert-base-japanese-whole-word-masking
データセット : Stockmark NER、京都大学NERコーパス
トークナイザー : BertJapaneseTokenizer(MeCab使用)
🚀 11. Hugging Face Pipelineの活用
事前学習済みのNERモデルを簡単に使う方法を紹介します。
9-1. 事前学習済みモデルの使用
# ========================================
# Hugging Face Pipelineの使用
# ========================================
from transformers import pipeline
# 事前学習済みNERパイプライン
# aggregation_strategy=’simple’: サブワードを集約
ner_pipeline = pipeline(
‘ner’,
model=’dbmdz/bert-large-cased-finetuned-conll03-english’,
aggregation_strategy=’simple’
)
# テスト
text = “Microsoft was founded by Bill Gates in Seattle.”
print(f”テキスト: {text}\n”)
print(“抽出されたエンティティ:”)
results = ner_pipeline(text)
for result in results:
print(f” • {result[‘word’]} ({result[‘entity_group’]}) ”
f”[confidence: {result[‘score’]:.4f}]”)
実行結果:
テキスト: Microsoft was founded by Bill Gates in Seattle.
抽出されたエンティティ:
• Microsoft (ORG) [confidence: 0.9987]
• Bill Gates (PER) [confidence: 0.9995]
• Seattle (LOC) [confidence: 0.9991]
💡 Pipelineのメリット
簡単 : 数行のコードでNERが実行できる
高品質 : 事前学習済みモデルを使用
自動処理 : サブワード集約が自動
プロトタイプや簡単なタスクには、Pipelineの使用を推奨します。
📝 練習問題
問題1:BIOタグの意味
BIOタグスキーマの「I」タグの意味は?
Initial(最初)
Inside(内部)
Intermediate(中間)
Independent(独立)
解答を見る
正解:b(Inside)
「I」はInside(内部) を意味します。
BIOタグスキーマ:
B (Begin) : エンティティの開始
I (Inside) : エンティティの内部(2番目以降)
O (Outside) : エンティティ外
例: “New York” → [B-LOC, I-LOC]
問題2:NERの評価
NERで最も実用的な評価方法は?
Token-level Accuracy
Entity-level F1 Score
文全体のAccuracy
損失関数の値
解答を見る
正解:b(Entity-level F1 Score)
Entity-level F1 Score が最も実用的です。
理由:
エンティティ全体が正しく抽出できているかを評価
部分的な抽出では意味がない
実際の使用シーンに即している
例: “Barack Obama” → “Barack”だけ抽出しても不十分
問題3:タグ数の計算
3種類のエンティティタイプ(PER, LOC, ORG)がある場合、BIOタグの総数は?
3個
6個
7個
9個
解答を見る
正解:c(7個)
タグ数 = 2n + 1(n = エンティティタイプ数)
計算:
B-PER, I-PER(人名用: 2個)
B-LOC, I-LOC(地名用: 2個)
B-ORG, I-ORG(組織名用: 2個)
O(エンティティ外: 1個)
合計: 2×3 + 1 = 7個
問題4:サブワードの扱い
BERTのサブワード分割(”rejects”→”reject”+”##s”)時、2番目のトークン”##s”のラベルはどうすべき?
元のラベルをそのまま使用
-100を付与(損失計算から除外)
I-タグに変換
O(Outside)に変換
解答を見る
正解:b(-100を付与)
サブワードの2番目以降は-100を付与 します。
理由:
元の単語は1つなのにBERTが複数トークンに分割
ラベルは単語単位で付与されている
2番目以降にラベルを付けると重複になる
-100はPyTorchのCrossEntropyLossで自動的に無視されます。
📝 STEP 21 のまとめ
✅ このステップで学んだこと
NER : テキストから人名・地名・組織名を自動抽出
BIOタグ : B(開始)、I(内部)、O(外部)スキーマ
トークン分類 : 各トークンにラベルを付与
ラベルアライメント : サブワードと元のラベルの対応付け
Entity-level評価 : エンティティ単位のF1スコア
Hugging Face Pipeline : 簡単にNERを実行
🎯 次のステップの準備
STEP 22: 質問応答(Question Answering) では、
文書から質問に対する回答を抽出します!
Extractive QA(抽出型質問応答)
SQuADデータセット
回答スパンの予測(start/end位置)
BERTでのQA実装