辞書とルールで固有表現抽出器を作れるライブラリfunerを公開しました

概要

こんにちは@kajyuuenです。 辞書とルールによる固有表現抽出(Named Entity Recognition; NER)を実現するライブラリfunerを作りました。

github.com

辞書とルールによって抽出した固有表現をこんな感じで確認できます。

tokens       東京    出身    の   吉田     は    4       月        から   JR      で    働く    。
=============================================================================================
gold_label   B-LOC   O      O    B-PER   O    B-DATE   I-DATE   O      B-ORG   O    O      O 
---------------------------------------------------------------------------------------------
person_f     O       O      O    B-PER   O    O        O        O      O       O    O      O 
month_f      O       O      O    O       O    B-DATE   I-DATE   O      O       O    O      O 
company_f    O       O      O    O       O    O        O        O      B-ORG   O    O      O 
pref_f       B-LOC   O      O    O       O    O        O        O      O       O    O      O 
---------------------------------------------------------------------------------------------
aggregate    B-LOC   O      O    B-PER   O    B-DATE   I-DATE   O      B-ORG   O    O      O 

モチベーション

funerは以下のモチベーションを満たすために作ったライブラリです。

  • 辞書やルールベースを用いた固有表現抽出モデルを作りたい
  • 辞書やルールからタグ付きコーパスを自動的に構築したい
  • 辞書とルールで付与したラベルと人手でつけたアノテーションを見比べて、ルールとアノテーションデータを同時に改善したい

はじめに

固有表現抽出とは

固有表現抽出とは人名や組織名などの固有名詞や、日付や時間等の固有表現を抽出するタスクです。 固有表現抽出器は現在、機械学習によって作られるものが主流です。 これは辞書やルールベースによる固有表現抽出に比べて、高い精度が期待できるからです。

辞書やルールベースで固有表現抽出モデルを作る意味

機械学習での固有表現抽出モデルが高い性能を出せる今、辞書やルールベースで固有表現抽出モデルを作る必要は特にないように思われます。 しかし、いくつかの場合において辞書&ルールベース固有表現抽出モデルを作る意味があると考えています。

1つ目は取り組むタスクが簡単な場合です。これへは機械学習ベースの固有表現抽出を使うまでもないときに、すぐに用意できる辞書とルールでモデルを作るという状況です。

2つ目はアノテーションコストがかけられない場合です。 機械学習を用いて固有表現抽出モデルを作るとき、教師データである固有表現タグ付きコーパスが学習に必要です。 コーパスの構築には多くの時間がかかります。また途中でアノテーションガイドライン、つまりラベルを付与する際の判断基準を変更したくなったり、抽出したい固有表現が増えたりした場合は、アノテーションをやり直さなければいけません。 一方、辞書やルールベースで構築する固有表現抽出モデルはタグ付きコーパスが不要なため、モデルの構築にかかる時間は比較的早いです。またアノテーションガイドラインを変更する場合も、変更の影響がある辞書やルールの修正だけで済みます。 それ以外にも辞書やルールベースによって作られた固有表現抽出モデルはDistant supervisionやWeakly supervisionといった手法に転用可能です。

これらの理由から辞書やルールベースで固有表現抽出モデルを作る意味は現在でもあると考えています。

解決したい課題

辞書やルールによって固有表現抽出を実現できるライブラリはいくつかありましたが、次の点をすべて満たすようなライブラリは探したところ見つかりませんでした。

  • どのルールが抽出に成功していて、どのルールが失敗しているのかひと目で分かる
  • 辞書やシンプルなルール以外だけではなく、細かいルールによる抽出が可能
  • トークナイザー依存がない

特に最初の項目は自分にとって必要不可欠でした。 そこで、これらを満たすライブラリfunerを作成しました。

使い方

まずはpipでfunerをインストールします。

pip install funer

Documentの作成

分析対象のテキストを次のようにDocumentとして作成します。 既にアノテーションが終わっているタグ付きコーパスはgold_labelとしてBIO形式でアノテーションを追加します。

from funer.document import Document

labeled_document_1 = Document(
    tokens=["東京", "出身", "の", "吉田", "は", "4", "月", "から", "JR", "で", "働く", "。"],
    gold_label=["B-LOC", "O", "O", "B-PER", "O", "B-DATE", "I-DATE", "O", "B-ORG", "O", "O", "O"]
)
labeled_document_2 = Document(
    tokens=["9", "月", "から", "東京", "大学", "に", "通う"],
    gold_label=["B-DATE", "I-DATE", "O", "B-ORG", "I-ORG", "O", "O"],
)
nolabeled_document = Document(
    tokens=["8", "月", "に", "東京", "の", "大学", "に", "通う"],
)
documents = [labeled_document_1, labeled_document_2, nolabeled_document]

分かち書きをしてないテキストはspaCyを用いてDocumentを作成できます。

import spacy

nlp = spacy.load('ja_ginza')
document = Document.from_spacy_doc(nlp("東京出身の吉田は4月からJRで働く。"))
print(document.tokens)
# > ["東京", "出身", "の", "吉田", "は", "4", "月", "から", "JR", "で", "働く", "。"],

ラベリング関数の定義

funerではトークン単位・文字単位・辞書を用いたラベリング関数を記述することができます。

トークン単位でのラベリング関数を作成する場合、TokensConditionAnnotatorを用いてルールを記述できます。 次のコードは、文中に現れる「吉田」というトークンを人名(PER)として抽出する例です。

from funer.annotators.token_condition_annotator import TokensConditionAnnotator

def detect_name(tokens: list[str]):
    for i in range(len(tokens) - 1):
        if tokens[i:i + 1] == ["吉田"]:
            yield i, i + 1

f1 = TokensConditionAnnotator(
    name="person_f",
    f=detect_name,
    label="PER"
)

先程の例ではdetect_name関数を自作しましたが、funerではトークン単位でのラベリング関数を簡単に構築するためのgenerate_token_conditions_function関数を用意しています。

generate_token_conditions_functionを用いて、1つ目のトークンが「正規表現で1~12までの数字」かつ、2つ目のトークンが「月」であるとき日時(DATE)として抽出するコードは次の通りです。

from funer.annotators.token_condition_annotator import generate_token_conditions_function

f2 = TokensConditionAnnotator(
    name="month_f",
    f=generate_token_conditions_function([
        lambda token_1: re.search(r"[1-9]|1[0-2]", token_1) is not None,
        lambda token_2: token_2 == "月",
    ]),
    label="DATE"
)

また文字単位のでラベリング関数はSpanConditionAnnotatorを用いて作成ができます。 次のコードは文中にある「JR」という文字列を組織名(ORG)として抽出する例です。

from funer.annotators.span_condition_annotator import SpanConditionAnnotator

def span_condition_function(text: str):
    for m in re.finditer(r"JR", text):
        yield m.start(), m.end()

f3 = SpanConditionAnnotator(
    name="company_f",
    f=span_condition_function,
    label="ORG"
)

辞書を用いたラベリング関数にはDictionaryAnnotatorを用いて作成できます。 次のコードでは「東京」、「神奈川」、「大阪」を場所(LOC)として抽出するラベリング関数を作成しています。

from funer.annotators.dictionary_annotator import DictionaryAnnotator

loc_dictionary = ["東京", "神奈川", "大阪"]

f4 = DictionaryAnnotator(
    name="pref_f",
    words=loc_dictionary,
    label="LOC"
)

ラベリング関数の適用

funerではLabelingFunctionApplierを用いてラベリング関数を適用し、MajorityVotingAggregatorによってマッチした結果を集めます。

from funer.labeling_function_applier import LabelingFunctionApplier
from funer.aggregators.majority_voting_aggregators import MajorityVotingAggregator

# ラベリング関数の適用
lf_applier = LabelingFunctionApplier(lfs=[f1, f2, f3, f4])
documents = lf_applier.apply(documents)

# ラベリング結果の統合
aggregator = MajorityVotingAggregator()
documents = aggregator.aggregate(documents)

MajorityVotingAggregatorによってつけられたラベルはshow_labels関数によって閲覧できます。 これによってタグ付きコーパスとの違いを確認することができます。

from funer.utils import show_labels

print(show_labels(documents[1]))
#> tokens       9        月       から    東京    大学     に   通う
#> ===============================================================
#> gold_label   B-DATE   I-DATE   O      B-ORG   I-ORG   O    O   
#> ---------------------------------------------------------------
#> month_f      B-DATE   I-DATE   O      O       O       O    O   
#> pref_f       O        O        O      B-LOC   O       O    O   
#> ---------------------------------------------------------------
#> aggregate    B-DATE   I-DATE   O      B-LOC   O       O    O   

もちろんタグが付いていないデータに対する抽出結果も出力できます。

print(show_labels(documents[2]))
#> tokens       8        月       に   東京    の   大学   に   通う
#> =================================================================
#> gold_label   -        -        -    -       -    -      -    -   
#> -----------------------------------------------------------------
#> month_f      B-DATE   I-DATE   O    O       O    O      O    O   
#> pref_f       O        O        O    B-LOC   O    O      O    O   
#> -----------------------------------------------------------------
#> aggregate    B-DATE   I-DATE   O    B-LOC   O    O      O    O   

aggregateした結果をBIO形式とSpan形式で出力することも可能です。

print(documents[2].export_span_labels())
#> [EntitySpan(start_offset=0, end_offset=2, label='DATE'), EntitySpan(start_offset=3, end_offset=5, label='LOC')]

print(documents[2].export_bio_label())
#> ['B-DATE', 'I-DATE', 'O', 'B-LOC', 'O', 'O', 'O', 'O']

ラベリング関数の統計

ラベリング関数の性能改善を行いたいときはLabelingFunctionApplier.show_stats()が使えます。 これによってラベリング関数がタグ付きコーパスの固有表現を抽出成功している数と失敗している数を確認できます。

print(lf_applier.show_stats())
#> f_name    | pos | neg | hit
#> ==========+=====+=====+====
#> person_f  | 1   | 0   | 1  
#> month_f   | 2   | 0   | 2  
#> company_f | 1   | 0   | 1  
#> pref_f    | 1   | 1   | 2  

おわりに

funerという辞書&ルールベースの固有表現抽出ライブラリを作りました。 今後はラベリング関数と人手によるアノテーションを行き来がより簡単になるようにfunerをWebUIから操作できるようなアノテーションツールを作りたいと思っています。

欲しい機能やバグ報告がありましたらIssueで報告していただけると幸いです。

https://github.com/kajyuuen/funer