辞書とルールで固有表現抽出器を作れるライブラリ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

日本語文書分類・固有表現抽出タスクに対するData Augmentationの性能検証

概要

こんにちは@kajyuuenです。 本記事では日本語データセットに対して、Data Augmentation(データ拡張)を行い、モデルの性能が向上するか検証します。

今回は文書分類と固有表現抽出の2つのタスクに対して、実験を行いました。 その結果、文書分類タスクでは学習に用いた文章数が500文, 2000文, 5000文のどの場合においても性能が向上し、Data Augmentationなしのモデルと比べて、最大2.5ポイントAccuracyが向上しました。 固有表現抽出タスクでは50文, 150文, 500文に対してData Augmentationの効果を検証しました。 文章数によっては性能が下がった場合もありましたが、Data Augmentationによって最も性能が向上したケースではF1が2.7ポイント向上しました。

データ拡張に用いたライブラリdaajaやData Augmentation手法の詳細に関しては下記の記事をご覧ください。

kajyuuen.hatenablog.com

文書分類

実験に用いたコードはこちらです。

データセット

今回、文書分類タスクに用いたデータセットlivedoorニュースコーパスです。 これは9個のトピックのニュース記事をまとめたデータセットで、全7378記事から構成されています。

全7378記事のうちランダムに500記事, 2000記事, 5000記事を抽出してサブセットを作成し、これらをtrainセットとしました。またtrainセットに含まれていない2378記事を二分割したものをvalidセットとtestセットとしてパラメータ探索と評価に利用しました。

Data Augmentation

文書分類のData Augmentation手法にはEDA: Easy Data Augmentation Techniques for Boosting Performance on Text Classification Tasksを用いました。

EDAではどのくらいの確率で文章中の単語を置き換えるか、EDAによって一つの文から何個データを生成するかといったパラメータを決める必要があります。 今回の実験では論文の著者が推奨するパラメータをそのまま採用しています。

EDA: Easy Data Augmentation Techniques for Boosting Performance on Text Classification Tasksより引用

daajaではラベルと文章が\tで区切られたtsvに対して、EDAによってデータを増やすコマンドを用意しているので、これを用いてかさ増したデータセットを作成します。

python -m daaja.eda.run --input input.tsv --output output.tsv --alpha_sr 0.05 --alpha_rd 0.05 --alpha_ri 0.05 --alpha_rs 0.05 --num_aug 16

モデル

モデルにはBertForSequenceClassificationを用いています。 事前学習モデルにはcl-tohoku/bert-base-japanese-whole-word-maskingを選びました。

from transformers import BertForSequenceClassification
MODEL_NAME = 'cl-tohoku/bert-base-japanese-whole-word-masking'
model = BertForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=9)

結果

testデータに対するAccuracyは以下の表のとおりです。

文章数 500 2000 5000
BERT 77.1% 84.6% 88.8%
BERT+EDA 78.4% 87.1% 91.0%
EDAによる効果 +1.3ポイント +2.5ポイント +2.2ポイント

今回の結果ではすべての文章数において、性能が改善しています。

この結果を見る限り、対象のドメインやタスクの複雑さにもよりますが、文書分類タスクでデータが少ないときはとりあえずEDAを適用してみてもいいのかもしれません。

固有表現抽出

実験に用いたコードはこちらです。

データセット

固有表現抽出タスクに用いたデータセットja.wikipedia.conllです。このデータはWikipedia日本語版から抽出した全1,000文から構成されている固有表現抽出データセットです。

1,000文のうちランダムに取り出したサブセット50, 150, 500文をtrainセットとし、trainセットに含まれていない500文をそれぞれ半分づつvalidセットとtestセットとしました。

Data Augmentation

固有表現抽出ではAn Analysis of Simple Data Augmentation for Named Entity Recognition(SDA)で提案されている手法を用いてデータの拡張を行います。

SDAでは著者が推奨するパラメータがないため、論文同様に$p = \lbrace 0.1, 0.3, 0.5, 0.7 \rbrace $と$n_{\rm aug} = \lbrace 1, 3, 6, 10 \rbrace$に対してパラメータ探索を行い、最も良いモデルを採用しました。

EDA: Easy Data Augmentation Techniques for Boosting Performance on Text Classification Tasksより引用

トークンとBIOラベルが\tで区切られたconllフォーマットに対してデータ拡張を行うdaajaのコマンドは次の通りです。

python -m daaja.ner_sda.run --input input.tsv--output output.tsv --p_lwtr 0.7 --p_mr 0.7 --p_sis 0.7 --p_sr 0.7 --num_aug 10

モデル

モデルにはBertForTokenClassificationを用いています。 事前学習モデルにはnlp-waseda/roberta-base-japaneseを選びました。

from transformers import BertForTokenClassification
MODEL_NAME = 'nlp-waseda/roberta-base-japanese'
model = BertForTokenClassification.from_pretrained(MODEL_NAME, num_labels=22)

結果

testデータに対するF1は以下の表のとおりです。

文章数 50 150 500
BERT 65.5% 77.4% 81.3%
BERT+SDA 68.2% 76.7% 82.0%
SDAによる効果 +2.7ポイント -0.7ポイント +0.7ポイント

150文のときSDAを用いたモデルの性能が下がっていますが、それ以外の文章数ではF1が向上しています。

各文章数に対して、最も良かったパラメータは以下の通りです。

文章数 $p$ $n_{\rm aug}$
50 0.5 3
150 0.7 3
500 0.7 3

まとめと今後

今回の実験を通じて、Data Augmentationは日本語においても有効なことがわかりました。 実験で使ったData Augmentation手法はどれも単語の削除や交換、シノニムの追加といった単純な操作ですが、文書分類と固有表現抽出の両タスクの殆どの状況で性能向上を達成できているのは驚きです。

今後はシンプルなData Augmentation手法だけではなくWord2VecやTransformerを用いた手法をdaajaに実装し、性能の検証をしていきたいです。

参考

日本語自然言語処理のData Augmentationライブラリdaajaを作りました

概要

こんにちは@kajyuuenです。 日本語自然言語処理のData Augmentationライブラリdaajaを作成しました。 この記事ではdaajaが実装しているData Augmentation手法についての解説とその使い方について紹介します。

ソースコードは以下のリポジトリで公開しています。

github.com

また、このライブラリはPyPIに公開しているのでpip install daajaでインストールが可能です。

はじめに

Data Augmentationとは

Data Augmentationとは元のデータから新しいデータを生成し、データ数を増やす手法です。 日本語ではデータ拡張という名前で知られています。 ラベル付きデータを擬似的に増やすことによって、アノテーションコストを必要とせずにモデルの汎化性能や精度の向上が期待できます。

対応している手法

現在daajaは、次の2つの論文で紹介されているData Augmentation手法を実装しています。

EDAが文書分類タスク向け、SDAが固有表現抽出向けのData Augmentation手法です。それぞれの手法について、詳しく説明していきます。

EDA: Easy Data Augmentation Techniques for Boosting Performance on Text Classification Tasks

この論文では自然言語処理の分類タスクに使える4つのData Augmentation手法について紹介しています。それぞれの手法について、詳細とdaajaを使った呼び出し方を記載します。

各手法について

Synonym Replacement

これは文章中のストップワードを除いた$N$つの単語を同義語に置き換える手法です。ハイパーパラメータ$\alpha$と文章中の単語数$l$によって、この$N$は定まり、$N=l\alpha$と計算されます。$N$はこのあと紹介する手法のRandom Insertion, Synonym Replacementでも同じように算出されます。

from daaja.augmentors.sentence.synonym_replace_augmentor import \
    SynonymReplaceAugmentor
augmentor = SynonymReplaceAugmentor(alpha=0.1)
text = "日本語でデータ拡張を行う"
print(augmentor.augment(text)) # => 日本語でデータ拡張をする

Random Insertion

文章中のストップワードを除いた$N$つの単語の同義語をランダムに挿入する手法です。

from daaja.augmentors.sentence.randam_insert_augmentor import \
    RandamInsertAugmentor
augmentor = RandamInsertAugmentor(alpha=0.1)
text = "日本語でデータ拡張を行う"
print(augmentor.augment(text)) # => 日本語でデータ拡張押し広げるを行う

Random Swap

文章中の2つの単語をランダムに選び、入れ替える処理を$N$回行う手法です。

from daaja.augmentors.sentence.randam_swap_augmentor import RandamSwapAugmentor
augmentor = RandamSwapAugmentor(alpha=0.1)
text = "日本語でデータ拡張を行う"
print(augmentor.augment(text)) # => データで日本語拡張を行う

Random Deletion

文章中の各単語を確率$p$でランダムに削除する手法です。この$p$はハイパーパラメータで、論文中では$p=\alpha$としています。

from daaja.augmentors.sentence.randam_delete_augmentor import \
    RandamDeleteAugmentor
augmentor = RandamDeleteAugmentor(p=0.1)
text = "日本語でデータ拡張を行う"
print(augmentor.augment(text)) # => 日本語でデータを行う

EasyDataAugmentor

EasyDataAugmentorはそれぞれの手法のハイパーパラメータ$\alpha$と生成する文章数$n$を指定し、今まで紹介した4つの手法を一度に実行するクラスです。

from daaja.methods.eda.easy_data_augmentor import EasyDataAugmentor
augmentor = EasyDataAugmentor(alpha_sr=0.1, alpha_ri=0.1, alpha_rs=0.1, p_rd=0.1, num_aug=4)
text = "日本語でデータ拡張を行う"
print(augmentor.augments(text))
# => ['日本語でを拡張データ行う', '日本語でデータ押広げるを行う', '日本語でデータ拡張を行う', '日本語で智見拡張を行う', '日本語でデータ拡張を行う']

An Analysis of Simple Data Augmentation for Named Entity Recognition

この論文では自然言語処理の固有表現抽出タスクに使える4つのData Augmentation手法について紹介しています。

各手法について

Label-wise token replacement (LwTR)

文章中の各単語を二項分布${\rm Bin}(p)$に基づいて別の単語に置き換えます。 置き換え先の単語はデータセット中の同じラベルを持つ他の単語から選択されます。

from daaja.augmentors.sequence_labeling.labelwise_token_replacement_augmentor import \
    LabelwiseTokenReplacementAugmentor
from daaja.augmentors.sequence_labeling.utils import get_token2prob_in_label

tokens_list = [
    ["私", "は", "田中", "と", "いい", "ます"],
]
labels_list = [
    ["O", "O", "B-PER", "O", "O", "O"],
]

augmentor = LabelwiseTokenReplacementAugmentor(get_token2prob_in_label(tokens_list, labels_list), p=0.1)
target_tokens = ["君", "は", "吉田", "さん", "かい"]
target_labels = ["O", "O", "B-PER", "O", "O"]
print(augmentor.augment(target_tokens, target_labels))
# => (['私', 'は', '田中', 'さん', 'かい'], ['O', 'O', 'B-PER', 'O', 'O'])

Synonym replacement

LwTRと同様に文章中の各単語を二項分布${\rm Bin}(p)$に基づいて別の単語に置き換えます。 置き換え先の単語は置き換え元の単語の同義語から選ばれます。

from daaja.augmentors.sequence_labeling.synonym_replacement_augmentor import \
    SynonymReplacementAugmentor

augmentor = SynonymReplacementAugmentor(p=0.5)
target_tokens = ["君", "は", "吉田", "さん", "かい"]
target_labels = ["O", "O", "B-PER", "O", "O"]
print(augmentor.augment(target_tokens, target_labels)
# => (['雇い主', 'は', '田中', '君', 'かい'], ['O', 'O', 'B-PER', 'O', 'O'])

Mention replacement

文中の各固有表現を二項分布${\rm Bin}(p)$に基づいて、同じタイプの固有表現に置き換えます。 LwTRがラベル単位(B-LOCI-LOCなど)の置き換えだったのに対して、こちらはタイプ単位(LOCなど)で置き換えが発生します。 置き換わる単語はデータセット中の同じタイプの固有表現から選ばれます。

from daaja.augmentors.sequence_labeling.mention_replacement_augmentor import \
    MentionReplacementAugmentor
from daaja.augmentors.sequence_labeling.utils import get_entity_dict

tokens_list = [
    ["私", "は", "田中", "太郎", "です"],
]
labels_list = [
    ["O", "O", "B-PER", "I-PER", "O"],
]

entity_dict = get_entity_dict(tokens_list, labels_list)
augmentor = MentionReplacementAugmentor(entity_dict, p=1)
target_tokens = ["君", "は", "吉田", "さん", "かい"]
target_labels = ["O", "O", "B-PER", "O", "O"]
print(augmentor.augment(target_tokens, target_labels))
# => (['君', 'は', '田中', '太郎', 'さん', 'かい'], ['O', 'O', 'B-PER', 'I-PER', 'O', 'O'])

Shuffle within segments

文章を固有表現のタイプで区切り、各区切りについてシャッフルするか二項分布${\rm Bin}(p)$に基づいて決定します。

from daaja.augmentors.sequence_labeling.shuffle_within_segments_augmentor import \
    ShuffleWithinSegmentsAugmentor

augmentor = ShuffleWithinSegmentsAugmentor(p=1)
target_tokens = ["君", "が", "東京", "出身", "の", "田中", "君", "かい"]
target_labels = ["O", "O", "B-LOC", "O", "O", "B-PER", "O", "O"]

print(augmentor.augment(target_tokens, target_labels))
# => (['が', '君', '東京', '出身', 'の', '田中', '君', 'かい'], ['O', 'O', 'B-LOC', 'O', 'O', 'B-PER', 'O', 'O'])

SimpleDataAugmentationforNER

SimpleDataAugmentationforNERはそれぞれの手法のハイパーパラメータと生成する文章数を指定、今まで紹介した4つの手法を一度に実行するクラスです。

from daaja.methods.ner_sda.simple_data_augmentation_for_ner import \
    SimpleDataAugmentationforNER

tokens_list = [
    ["私", "は", "田中", "と", "いい", "ます"],
    ["筑波", "大学", "に", "所属", "して", "ます"],
]
labels_list = [
    ["O", "O", "B-PER", "O", "O", "O"],
    ["B-ORG", "I-ORG", "O", "O", "O", "O"],
]
augmentor = SimpleDataAugmentationforNER(tokens_list=tokens_list, labels_list=labels_list, p_power=1, p_lwtr=1, p_mr=1, p_sis=1, p_sr=1, num_aug=4)
tokens = ["吉田", "さん", "は", "株式", "会社", "A", "に", "出張", "予定", "だ"]
labels = ["B-PER", "O", "O", "B-ORG", "I-ORG", "I-ORG", "O", "O", "O", "O"]
augmented_tokens_list, augmented_labels_list = augmentor.augments(tokens, labels)
print(augmented_tokens_list)
# => [
# ['田中', 'さん', 'ます', '筑波', '大学', '大学', '所属', 'は', 'ます', 'に'],
# ['吉田', 'さん', 'は', 'ストック', '企業', 'A', 'に', '出張', '心積り', 'だ'],
# ['田中', 'さん', 'は', '筑波', '大学', 'に', '出張', '予定', 'だ'],
# ['吉田', 'さん', 'は', '会社', 'A', '株式', '出張', 'に', '予定', 'だ'],
# ['吉田', 'さん', 'は', '株式', '会社', 'A', 'に', '出張', '予定', 'だ']]
print(augmented_labels_list)
# => [
# ['B-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O', 'O', 'O', 'O'],
# ['B-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O', 'O', 'O', 'O'],
# ['B-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'O', 'O', 'O', 'O'],
# ['B-PER', 'O', 'O', 'I-ORG', 'I-ORG', 'B-ORG', 'O', 'O', 'O', 'O'],
# ['B-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O', 'O', 'O', 'O']]

おわりに

今回は日本語向けのData Augmentationライブラリdaajaを紹介しました。 手元でEDAについて軽く実験したところ、性能の向上がみられたので、SDAの実験も終わり次第別記事で公開したいと思います。

参考

ライブラリと実験の実装時に参考にさせていただきました。ありがとうございます。

クラウドソーシングのためのDawid-Skeneモデルと確率的プログラミングPyroによるベイズ化

自分の研究とはあまり関係はないのですが、最近趣味で確率的プログラミングを触っています。 これが結構面白いので試しにDawid-SkeneモデルをPyroで実装してみました。

はじめに

まずクラウドソーシングにおけるラベル集約について説明します。

教師あり学習では教師データを元に学習を行います。この教師データは人手によるアノテーションによって作成されています。

しかし、専門家を雇ってアノテーション作業を頼むのは非常に高いコストがかかります。

そこで、複数人のボランティアや非専門家(以下、ワーカー)にアノテーションを頼むというのがクラウドソーシングです。

一見良さそうに見えるアイデアですが、数をこなすためにデタラメにアノテーションを行う質の悪いワーカーが出てくるのが世の中の常です。

これを解消するために一つのデータに対して複数のワーカによるアノテーションを頼み、ラベルの集約を試みます。

f:id:kajyuuen:20200720110800p:plain

Dawid-Skeneモデルについて

ラベルを集約を考えたときまず思いつくのが多数決です。 しかし、多数決では質の悪いワーカーと質のいいワーカが同じだけの発言権(重み)を持ってしまいます。

かといって、真のラベルは手に入らないため教師あり学習でワーカーの重み付けも出来ません。

そこで考えられたのがDawid-Skeneモデル*1です。これは次のようなグラフィカルモデルによって記述できます。

f:id:kajyuuen:20200719061619p:plain

これはワーカーjがデータnに付けたラベル l^{j}_{n}から真のラベルである h_nを
推測するためのモデルです。

ここでの C^{(j)}EMアルゴリズムによって予測された真のラベルとワーカーjが予測したラベルからなる混同行列になります。

Bayesian Dawid-Skeneモデルについて

PaunらはDawid-Skeneモデルのベイズ化を行いました*2。以下がそのアルゴリズムになります。

f:id:kajyuuen:20200719064115p:plain

Dirはディレクレ分布、Catはカテゴリカル分布のことです。

Kはクラス数、Iはワーカーjがデータnに付けたラベルの数を意味しています。

比較実験

データセットには麻酔医5人が患者45人に対して全身麻酔への安全度を4段階で評価したものを利用しました。これはDawidらの論文中にて公開されています。

Bayesian Dawid-Skeneモデルの学習にはMCMCを利用しました。

真のクラスの割合

f:id:kajyuuen:20200719064707p:plain

左がベイズ、右がEMアルゴリズムによる学習です。 Bayesian Dawid-Skeneモデルはデータの生成結果もプロットしています。

見た感じかなり似たような所に収束していますね。

各ワーカの混同行列

各ワーカーの混同行列をヒートマップにしたものがこちらです。

f:id:kajyuuen:20200719064752p:plain

両方のモデルにおいて混合行列の対角成分に強い確率が割り当てられています。

おわりに

今回はDawid-SkeneモデルとBayesian Dawid-Skeneモデルについて実装を行ってみました。

EMアルゴリズムに比べて、MCMCによる学習はPyroに乗っかっている分かなり楽に実装出来る印象です。

しかし、Pyroの売りはPytorchを利用した確率的変分推論(Stochastic Variational Inference; SVI)にあると思います。なので次は大きめのデータセットEMアルゴリズムによる点推定とSVIでのベイズ学習について比較したいです。

実装

詳しいハイパーパラメータや実装については下記を参照してください。

ノートブック

github.com

EMアルゴリズムによるDawid-skeneの実装

https://github.com/kajyuuen/Dawid-skene

*1:Alexander Philip Dawid and Allan M Skene. Maximum likelihood estimation of observererror-rates using the em algorithm.Journal of the Royal Statistical Society: Series C(Applied Statistics), Vol. 28, No. 1, pp. 20–28, 1979.

*2:Silviu Paun, Bob Carpenter, Jon Chamberlain, Dirk Hovy, Udo Kruschwitz, and MassimoPoesio. Comparing bayesian models of annotation.Transactions of the Association forComputational Linguistics, Vol. 6, pp. 571–585, 2018.

フルアノテーションコーパスを利用出来ない状況下での固有表現抽出の論文について発表した

概要

NLP/CV SoTA Survey Challenge #3にて,フルアノテーションコーパスを利用しない固有表現抽出の論文を三本まとめて発表しました.

speakerdeck.com

コメント

非固有表現タグを学習に用いない固有表現抽出モデル: Better Modeling of Incomplete Annotations for Named Entity Recognitionを読んで実装しました

前回に引き続き,部分的アノテーションコーパスが使える固有表現抽出手法の紹介と実装です.

概要

Better Modeling of Incomplete Annotations for Named Entity Recognitionが読んだ論文になります.著者実装はこちら

この論文では部分的アノテーションコーパスに対して,学習を行えるモデルの提案を行っています.

タグ欠損における問題

固有表現抽出のタスクにおいて,ラベルが欠損している教師データを用いて学習を行う手法はいくつか提案されていますが,今までの研究でのラベル欠損の仮定には2つ問題があると主張しています.

問題1

アノテータは基本的に固有表現の一部にだけアノテーションを行うことはない. 例えば「田中太郎」という人名に対して「田中」にだけアノテーションされることはなく,必ず「田中太郎」としてアノテーションされると主張しています.

つまり下図のA.1のようにラベルが欠損することは無いということです.

問題2

アノテータは固有表現にアノテーションするようにだけ指示されていることが多く,Oタグ(固有表現で無いことを示すタグ)を能動的に付けないということ. つまり著者はOタグは学習に使うことが出来ないと考えています.

f:id:kajyuuen:20190818163937p:plain Better Modeling of Incomplete Annotations for Named Entity Recognitionより引用

これらの問題を踏まえた上で著者はA.3のようにラベルが欠損すると仮定し,このような教師データに対するアプローチを示しています.

ちなみに後の性能評価でも分かりますがA.3のような欠損をしている場合,前回紹介したFuzzy-LSTM-CRFではOタグを学習時に考慮出来なくなるため性能がとても悪くなります.

手法

一般的なCRFの損失関数は以下のように定義されます.

f:id:kajyuuen:20190818183543p:plain

ここで欠損が無いラベル列の集合を\boldsymbol{D},欠損があるラベル列を \boldsymbol{y}^{(i)} _ p と定義し,\boldsymbol{y}^{(i)} _ pを何らかの方法で欠損を埋めたラベル列の集合をC(\boldsymbol{y}^{(i)} _ p)とします.

すると先程のCRFの損失関数は以下のように書き換えることが出来ます.

f:id:kajyuuen:20190818182021p:plain

この確率分布qを求める方法として著者はHardとSoftの2種類の手法を提案しています. Hardでは最も通る可能性が高いラベル列に対してのみ確率を割り当て,Softでは取りうる全てのラベル列に対して確率を計算し割り当てを行います.

Hardの具体的な動きについては以下の通りです.

  1. 教師データをk分割する
  2. k分割した教師データの欠損ラベルにOタグを割り当て,分割したデータ毎にモデルを学習する
  3. k-1番目の教師データを学習したモデルを用いて,k番目の教師データのラベルを予測し付与する.この際最初から付与されていたラベルは必ず通るようにビタビアルゴリズムを適用する
  4. ラベリングしたデータを元にk個のモデルを再び同じように学習する
  5. 3と4を複数回繰り返した後,ラベル付けが終わった教師データを全て用いて新しくモデルを学習する

ちなみに定義した損失関数においてq=1とすると,Fuzzy-LSTM-CRFの損失関数と一致します.

実装

著者が論文中で提案したHard methodと比較対象であるSimple(LSTM-CRF), Fuzzy-LSTM-CRFを以下のリポジトリにて実装しました.

github.com

(リポジトリ名と内容に違いが出てきたのでどうにかしたいですね)

評価

データセットにはCoNLL-2003を用い,論文と同じように固有表現の五割をランダムに,全てのOタグを欠損させた状態で学習を行いました. ハイパーパラメータも論文に沿ってエポック数30回, k分割した教師データに対する学習とラベル付与(3と4の部分)は10回繰り返しました.

Hard method

           precision    recall  f1-score   support

      PER    0.84393   0.90291   0.87242      1617
      LOC    0.82902   0.86331   0.84581      1668
     MISC    0.78840   0.65812   0.71739       702
      ORG    0.76605   0.74714   0.75648      1661

micro avg    0.81139   0.81498   0.81318      5648
macro avg    0.80972   0.81498   0.81120      5648

Simple

欠損しているタグをOタグと仮定し,LSTM-CRFにて学習したモデルです.論文中で提案されています.

           precision    recall  f1-score   support

      LOC    0.93319   0.78717   0.85398      1668
      PER    0.98507   0.16327   0.28011      1617
      ORG    0.94824   0.48525   0.64198      1661
     MISC    0.89098   0.33761   0.48967       702

micro avg    0.93873   0.46388   0.62093      5648
macro avg    0.94722   0.46388   0.58205      5648

Fuzzy-LSTM-CRF

論文中ではLSTM-M-CRFとして紹介されています.

           precision    recall  f1-score   support

      ORG    0.22550   0.86033   0.35734      1661
     MISC    0.05517   0.82336   0.10341       702
      PER    0.87629   0.94187   0.90790      1617
      LOC    0.06072   0.91547   0.11389      1668

micro avg    0.11572   0.89536   0.20496      5648
macro avg    0.34199   0.89536   0.41150      5648

他の手法に比べてHard methodが圧倒的に性能が高いことが実験から分かりました.

感想

論文の提案手法,学習にとても時間がかかります. 最小のデータ分割数であるk=2でも合計エポック数は2(k)x30(epoch)x10=600回ほどで,これに制約付きビタビによるラベル付け等の時間も加わるので大体一日半くらい学習にはかかりました.Softだと多分更に学習時間が増えるはずです. ただ,Oタグ無しかつ5割の固有表現が欠損していてもF値が81%を超えるというのは凄いと思いました.(加えてHardは殆どLSTM-CRFを使い回せるので実装が意外と楽です.)

また論文やブログではCoNLL-2003のデータセットに対して固有表現をランダムに欠損を生じさせましたが,これが大きく性能を左右させるのでモデルのパラメータ等を揃えて実験を行っても性能を再現させるのが難しいような気がします.

部分的アノテーションが利用可能な固有表現抽出モデル Fuzzy-LSTM-CRFの実装

概要

固有表現タスクにおいて重要な役割を持つCRF(条件付き確率場)ですが,CRFでは全ての単語に対してラベルが付けられている必要があるため,アノテーションコストが高くなる傾向があります.そこで,今回は部分的アノテーションコーパスを利用可能な固有表現抽出モデルFuzzy-LSTM-CRFを実装し,その性能について実験してみました.

CRFとFuzzy CRF

まず通常のCRFとFuzzy CRF(Partial CRF)の違いについて説明します.

通常のCRFでは,系列\boldsymbol{X}=(x_1, \cdots, x_n)に対応するラベル列\boldsymbol{y}=(y_1, \cdots, y_n)の確率p(\boldsymbol{y} | \boldsymbol{x})が最大になるように学習を行います.

この確率は系列\boldsymbol{X}に対応するラベル列\boldsymbol{y}のスコアをs(\boldsymbol{X},\boldsymbol{y})と定義すると以下のように定義されます.

f:id:kajyuuen:20190714204241p:plain

損失関数は上式から対数を取った

f:id:kajyuuen:20190714204618p:plain

となり,これが最大になるように学習を行うのが通常のCRFです.

しかし,この損失関数ではラベル列\boldsymbol{y}=(y_1, \cdots, y_n)に少しでも欠損があると学習を行うことが出来ません.そこでFuzzy CRFでは欠損しているラベルに対して取りうる全ての経路\boldsymbol{Y_{possible}}について計算をすることで学習を行います.

f:id:kajyuuen:20190714204721p:plain

以上のように損失関数を定義することでラベル列が不完全であっても学習を行うことが可能になります.ちなみにラベル列が完全のときはCRFと同じ損失関数を持ちます.

下の論文にて解説,定義されています.

BiLSTM-CRF

Word embeddingsとCharacters embeddingsをBi-LSTMに投入します.その出力を更にCRFに投入し最適なラベル列を探索することで固有表現抽出を行うモデルです.

f:id:kajyuuen:20190714220608p:plain

Neural Architectures for Named Entity Recognitionより引用

今回はこのモデルをベースとし,欠損したデータを学習出来るようにCRFの代わりにFuzzy CRFを適用させました.

実装

PyTorchにてFuzzy-LSTM-CRFとLSTM-CRFの実装を行っています.

github.com

評価

データセット

今回はCoNLL2003の英語コーパスから各タグについて50%づつラベルを欠損させて学習を行いました.欠損させた結果は以下の通りです.

TYPE:O,             before 169578,      after 84790
TYPE:ORG,           before 6321,        after 3161
TYPE:MISC,          before 3438,        after 1720
TYPE:PER,           before 6600,        after 3301
TYPE:LOC,           before 7140,        after 3571

結果

  • Word embedding dim: 100
  • Character embedding dim: 25
  • エポック数: 50

以上のハイパーパラメータにて学習を行いました.

           precision    recall  f1-score   support

      PER    0.93029   0.88312   0.90609      1617
      LOC    0.89381   0.88309   0.88842      1668
      ORG    0.84442   0.81036   0.82704      1661
     MISC    0.68154   0.80484   0.73808       702

micro avg    0.85837   0.85198   0.85516      5648
macro avg    0.86335   0.85198   0.85674      5648

欠損ラベルなしでLSTM-CRFを学習した場合は以下の通りになりました.

           precision    recall  f1-score   support

      PER    0.95997   0.94929   0.95460      1617
      LOC    0.90504   0.93705   0.92077      1668
      ORG    0.89223   0.85731   0.87442      1661
     MISC    0.80056   0.81197   0.80622       702

micro avg    0.90380   0.90156   0.90268      5648
macro avg    0.90401   0.90156   0.90259      5648

フルアノテーションと比べても,そこまで抽出性能が劣化していないことがわかります.

参考

実装の際には以下の論文とリポジトリを参考にさせていただきました.