STEP 21:固有表現認識(NER)

🏷️ STEP 21: 固有表現認識(NER)

Named Entity Recognition – テキストから人名・地名・組織名を自動抽出

📋 このステップで学ぶこと

  • 固有表現認識(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」タグの意味は?

  1. Initial(最初)
  2. Inside(内部)
  3. Intermediate(中間)
  4. Independent(独立)
正解:b(Inside)

「I」はInside(内部)を意味します。

BIOタグスキーマ:

  • B (Begin): エンティティの開始
  • I (Inside): エンティティの内部(2番目以降)
  • O (Outside): エンティティ外

例: “New York” → [B-LOC, I-LOC]

問題2:NERの評価

NERで最も実用的な評価方法は?

  1. Token-level Accuracy
  2. Entity-level F1 Score
  3. 文全体のAccuracy
  4. 損失関数の値
正解:b(Entity-level F1 Score)

Entity-level F1 Scoreが最も実用的です。

理由:

  • エンティティ全体が正しく抽出できているかを評価
  • 部分的な抽出では意味がない
  • 実際の使用シーンに即している

例: “Barack Obama” → “Barack”だけ抽出しても不十分

問題3:タグ数の計算

3種類のエンティティタイプ(PER, LOC, ORG)がある場合、BIOタグの総数は?

  1. 3個
  2. 6個
  3. 7個
  4. 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”のラベルはどうすべき?

  1. 元のラベルをそのまま使用
  2. -100を付与(損失計算から除外)
  3. I-タグに変換
  4. 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実装
📝

学習メモ

自然言語処理(NLP) - Step 21

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE