コンテンツにスキップ

11.4.5 NER 実践

NER プロジェクトのエンティティ評価ループ図

  • 最小限の NER プロジェクトの範囲を定義できるようになる
  • token ラベルからエンティティを復元できるようになる
  • エンティティ単位のエラー分析ができるようになる
  • 実行できる例を通して、情報抽出プロジェクトの骨組みを作れるようになる

NER の実践は、「ラベル -> エンティティ -> 評価 -> 改善」という順番で理解すると分かりやすいです。

flowchart LR
A["エンティティタイプを定義する"] --> B["BIO ラベルを設計する"]
B --> C["モデルがラベル列を出力する"]
C --> D["エンティティ span を復元する"]
D --> E["エンティティ単位の評価とエラー分析を行う"]

この節で本当に解決したいのは、次の点です。

  • NER プロジェクトはなぜ「ラベル予測」だけではないのか
  • なぜエンティティ復元とエラー分析のほうが、実際のプロジェクトに近いのか

一、まずプロジェクトの問題をはっきり決める

Section titled “一、まずプロジェクトの問題をはっきり決める”

入力:

  • 履歴書や候補者プロフィールのテキスト

出力:

  • 名前
  • 学校
  • スキル

なぜ「なんとなくエンティティを抜く」より練習に向いているのか?

Section titled “なぜ「なんとなくエンティティを抜く」より練習に向いているのか?”

理由は、境界がはっきりしているからです。

  • クラス数が多くない
  • エンティティタイプが明確
  • 結果を業務の文脈で説明しやすい

最初に大事なのはモデルではなく、ラベル体系

Section titled “最初に大事なのはモデルではなく、ラベル体系”

たとえば:

  • 张三 -> B-NAME
  • 清华大学 -> B-SCHOOL I-SCHOOL ...
  • Python -> B-SKILL

ここがあいまいだと、その後のモデル学習も評価も、すべて崩れやすくなります。

初心者向けの分かりやすい比喩

Section titled “初心者向けの分かりやすい比喩”

NER は、次のように考えると理解しやすいです。

  • テキストの中で、蛍光ペンで重要情報を囲む

難しいのは、ただ囲むことではなくて、次の点です。

  • どこから囲み始めるか
  • どこで囲み終わるか
  • その部分はどの種類に属するのか

こう考えると、NER が境界の問題でつまずきやすい理由が自然に見えてきます。


二、まずは実行できるアノテーションとデコードの閉ループを作る

Section titled “二、まずは実行できるアノテーションとデコードの閉ループを作る”

次の例では、3 つのことをします。

  1. 小さなサンプルを用意する
  2. BIO ラベルをデコードしてエンティティに戻す
  3. 簡単な予測比較とエラー分析をする
samples = [
{
"tokens": ["田中太郎", "", "東京大学", "を卒業し", "Python", "", "PyTorch", "に詳しい"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O", "B-SKILL", "O"],
"pred_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O", "B-SKILL", "O"],
},
{
"tokens": ["佐藤花子", "", "京都大学", "出身で", "Java", "を使えます"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O"],
"pred_tags": ["B-NAME", "O", "O", "O", "B-SKILL", "O"],
},
]
def decode_entities(tokens, tags):
entities = []
current_tokens = []
current_type = None
for token, tag in zip(tokens, tags):
if tag == "O":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = []
current_type = None
continue
prefix, entity_type = tag.split("-", 1)
if prefix == "B":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
elif prefix == "I" and current_type == entity_type:
current_tokens.append(token)
else:
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
if current_tokens:
entities.append(("".join(current_tokens), current_type))
return entities
for sample in samples:
gold_entities = decode_entities(sample["tokens"], sample["gold_tags"])
pred_entities = decode_entities(sample["tokens"], sample["pred_tags"])
print("tokens:", sample["tokens"])
print("gold :", gold_entities)
print("pred :", pred_entities)
print("miss :", [x for x in gold_entities if x not in pred_entities])
print()

実行結果の例:

Terminal window
tokens: ['田中太郎', 'は', '東京大学', 'を卒業し', 'Python', 'と', 'PyTorch', 'に詳しい']
gold : [('田中太郎', 'NAME'), ('東京大学', 'SCHOOL'), ('Python', 'SKILL'), ('PyTorch', 'SKILL')]
pred : [('田中太郎', 'NAME'), ('東京大学', 'SCHOOL'), ('Python', 'SKILL'), ('PyTorch', 'SKILL')]
miss : []
tokens: ['佐藤花子', 'は', '京都大学', '出身で', 'Java', 'を使えます']
gold : [('佐藤花子', 'NAME'), ('京都大学', 'SCHOOL'), ('Java', 'SKILL')]
pred : [('佐藤花子', 'NAME'), ('Java', 'SKILL')]
miss : [('京都大学', 'SCHOOL')]

NER gold pred miss の漏れ確認図

2つ目のサンプルでは学校エンティティを見逃しています。だから NER プロジェクトでは、token ラベルだけでなく復元されたエンティティを確認する必要があります。

なぜこのコードが「プロジェクトの最小閉ループ」なのか?

Section titled “なぜこのコードが「プロジェクトの最小閉ループ」なのか?”

すでに次の要素が入っているからです。

  • データ表現
  • 予測結果
  • エンティティ復元
  • エラー分析

これは、ただラベルを並べるだけよりも、実際のプロジェクトにかなり近い形です。

なぜ token ではなく、エンティティ単位で比べるのか?

Section titled “なぜ token ではなく、エンティティ単位で比べるのか?”

業務で本当に知りたいのは、たいてい次の点だからです。

  • エンティティをきちんと抽出できたか
  • タイプは正しいか

どの token が当たったかだけを見ても、実際の価値は見えにくいです。

最小の「エンティティログ」の例

Section titled “最小の「エンティティログ」の例”
sample = {
"tokens": ["佐藤花子", "", "京都大学", "出身で", "Java", "を使えます"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O"],
"pred_tags": ["B-NAME", "O", "O", "O", "B-SKILL", "O"],
}
def decode_entities(tokens, tags):
entities = []
current_tokens = []
current_type = None
for token, tag in zip(tokens, tags):
if tag == "O":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = []
current_type = None
continue
prefix, entity_type = tag.split("-", 1)
if prefix == "B":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
elif prefix == "I" and current_type == entity_type:
current_tokens.append(token)
if current_tokens:
entities.append(("".join(current_tokens), current_type))
return entities
gold_entities = decode_entities(sample["tokens"], sample["gold_tags"])
pred_entities = decode_entities(sample["tokens"], sample["pred_tags"])
print(
{
"text": "".join(sample["tokens"]),
"gold_entities": gold_entities,
"pred_entities": pred_entities,
}
)

実行結果の例:

Terminal window
{'text': '佐藤花子は京都大学出身でJavaを使えます', 'gold_entities': [('佐藤花子', 'NAME'), ('京都大学', 'SCHOOL'), ('Java', 'SKILL')], 'pred_entities': [('佐藤花子', 'NAME'), ('Java', 'SKILL')]}

このログは、初心者にとても向いています。なぜなら、抽象的なラベルタスクを、実際のプロジェクトっぽい出力に変えてくれるからです。

  • 元のテキストは何か
  • 正しいエンティティは何か
  • システムは何を見逃したのか

三、NER プロジェクトで最初に見るべき指標は何か?

Section titled “三、NER プロジェクトで最初に見るべき指標は何か?”

エンティティ単位の Precision / Recall / F1

Section titled “エンティティ単位の Precision / Recall / F1”

これは、もっとも一般的で、もっとも意味のある指標のセットです。

なぜ token accuracy では足りないのか?

Section titled “なぜ token accuracy では足りないのか?”

系列の中には、O がたくさん出てくることが多いからです。

token accuracy だけを見ると、数値は高く見えやすいです。 でも、実際のエンティティ抽出性能が良いとは限りません。

とても簡単なエンティティ Recall の例

Section titled “とても簡単なエンティティ Recall の例”
samples = [
{
"tokens": ["田中太郎", "", "東京大学", "を卒業し", "Python", "", "PyTorch", "に詳しい"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O", "B-SKILL", "O"],
"pred_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O", "B-SKILL", "O"],
},
{
"tokens": ["佐藤花子", "", "京都大学", "出身で", "Java", "を使えます"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "B-SKILL", "O"],
"pred_tags": ["B-NAME", "O", "O", "O", "B-SKILL", "O"],
},
]
def decode_entities(tokens, tags):
entities = []
current_tokens = []
current_type = None
for token, tag in zip(tokens, tags):
if tag == "O":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = []
current_type = None
continue
prefix, entity_type = tag.split("-", 1)
if prefix == "B":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
elif prefix == "I" and current_type == entity_type:
current_tokens.append(token)
if current_tokens:
entities.append(("".join(current_tokens), current_type))
return entities
def entity_recall(gold_entities, pred_entities):
if not gold_entities:
return 1.0
hit = sum(entity in pred_entities for entity in gold_entities)
return hit / len(gold_entities)
for sample in samples:
gold_entities = decode_entities(sample["tokens"], sample["gold_tags"])
pred_entities = decode_entities(sample["tokens"], sample["pred_tags"])
print(entity_recall(gold_entities, pred_entities))

実行結果の例:

Terminal window
1.0
0.6666666666666666

1つ目のサンプルではすべてのエンティティを復元できています。2つ目では3つのうち2つだけなので、entity-level recall は下がります。多くの O が正しくても、実体の見逃しは残ります。

NER プロジェクトを始めるときの、いちばん安定した順番

Section titled “NER プロジェクトを始めるときの、いちばん安定した順番”

一般的には、次の順番が安定しています。

  1. まずエンティティタイプを絞る
  2. まずラベル規則を明確にする
  3. まずエンティティ復元とエンティティ単位の評価を作る
  4. そのあとで、より強いモデルに切り替える

最初から BERT を急いで入れるより、こちらのほうがプロジェクトを安定させやすいです。


四、NER プロジェクトでよくある失敗

Section titled “四、NER プロジェクトでよくある失敗”

たとえば、学校名を半分しか抽出できないケースです。

たとえば、スキルを学校として認識してしまうケースです。

たとえば、サンプル 2 で 北京大学 を取りこぼしているケースです。

なぜエラー分析に向いているのか?

Section titled “なぜエラー分析に向いているのか?”

NER のミスはかなり具体的なので、 1 件ずつ見たり、種類ごとに直したりしやすいからです。

初心者がまず使うとよいエラー分類

Section titled “初心者がまず使うとよいエラー分類”

最初のエラー分析では、まず次の 3 つに分けるのがおすすめです。

  1. 境界ミス
  2. タイプミス
  3. 見逃し

この 3 つだけでも、次のどれが原因か見えてきます。

  • データアノテーションの問題
  • モデル表現の問題
  • 後処理ルール不足の問題

五、実際のプロジェクトでは次に何をすべきか?

Section titled “五、実際のプロジェクトでは次に何をすべきか?”

特に次のようなサンプルを増やすとよいです。

  • 長いエンティティ
  • まれなエンティティ
  • 混同しやすいタイプ

ルールや古典的モデルから、より強いモデルへ進む

Section titled “ルールや古典的モデルから、より強いモデルへ進む”

たとえば:

  • BiLSTM + CRF
  • BERT token classification

多くの業務プロジェクトでは、 適切な後処理ルールだけでもエンティティ品質をかなり改善できます。

これをプロジェクトとして見せるなら、何を見せるとよいか

Section titled “これをプロジェクトとして見せるなら、何を見せるとよいか”

見せる価値が高いのは、たいてい次のようなものです。

  • ラベルの予測結果の一覧

ではなく、

  1. 元のテキスト
  2. gold エンティティ
  3. 予測エンティティ
  4. 抽出漏れと誤抽出の例
  5. 次にどの種類のエラーを優先して直すか

こうすると、見る人に次のことが伝わりやすくなります。

  • ただの系列ラベリングではなく、情報抽出プロジェクトをやっている
  • モデルを学習しただけの半完成品ではない

勘違い 1:token レベルの指標だけを見る

Section titled “勘違い 1:token レベルの指標だけを見る”

NER では、エンティティレベルの結果をもっと重視すべきです。

勘違い 2:最初からすべてのエンティティタイプをカバーしようとする

Section titled “勘違い 2:最初からすべてのエンティティタイプをカバーしようとする”

より安定した方法は、たいてい次のとおりです。

  • まず 2〜4 種類のコアなエンティティをしっかり作る

勘違い 3:最初にラベル体系をきちんと決めない

Section titled “勘違い 3:最初にラベル体系をきちんと決めない”

ラベルの境界があいまいだと、データも評価も一緒にぶれます。


このページを終えたら、この evidence card を残します。

スキーマ
エンティティ型、BIO タグ、またはシーケンスラベル規則
予測
トークン単位のラベルと抽出スパン
指標
エンティティの precision/recall/F1 と境界ケース
失敗確認
span 境界、入れ子のエンティティ、未知語、または不一致なアノテーション
期待される成果
少なくとも1つの miss がある、gold と predicted の span 表

この節でいちばん大切なのは、実践の習慣を身につけることです。

NER プロジェクトでは、まずエンティティタイプ、ラベル体系、エンティティ復元、エンティティ単位のエラー分析をしっかり作り、そのうえで、より複雑なモデルを目指す。

こうすると、残るのは本当に説明できて、改善もできる情報抽出プロジェクトです。 ただ学習スクリプトを動かせるだけの未完成品にはなりません。


  1. サンプルに ORGTITLE のエンティティタイプを追加して、データを拡張してみましょう。
  2. 考えてみましょう。なぜ NER プロジェクトでは、token accuracy よりエンティティ単位の指標を見るほうがよいのでしょうか?
  3. システムが長い学校名をいつも半分しか抽出できない場合、まずデータ、モデル、後処理のどれを直しますか? なぜですか?
  4. この履歴書抽出プロジェクトを、さらにポートフォリオ用の成果物としてどう発展させますか?
プロジェクト参考とレビュー観点
  1. ORGTITLE を追加するときは、まず boundary rule を決めます。organization name、job title、周辺修飾語を entity に含めるかを明確にします。
  2. NER は entity-level metrics を使うべきです。ユーザーが受け取るのは孤立した token label ではなく、抽出された entity だからです。
  3. 長い学校名が半分だけ抽出されるなら、まず annotation consistency を見ます。その後、例を増やすか post-processing を足し、target が明確になってから model を変えます。
  4. portfolio では label schema、examples、entity-level metrics、error buckets、fixes、小さな before/after improvement log を見せると説得力があります。