辞書とルールで固有表現抽出器を作れるライブラリfunerを公開しました
概要
こんにちは@kajyuuenです。
辞書とルールによる固有表現抽出(Named Entity Recognition; NER)を実現するライブラリfuner
を作りました。
辞書とルールによって抽出した固有表現をこんな感じで確認できます。
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で報告していただけると幸いです。
日本語文書分類・固有表現抽出タスクに対する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手法の詳細に関しては下記の記事をご覧ください。
文書分類
実験に用いたコードはこちらです。
データセット
今回、文書分類タスクに用いたデータセットは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によって一つの文から何個データを生成するかといったパラメータを決める必要があります。 今回の実験では論文の著者が推奨するパラメータをそのまま採用しています。
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$に対してパラメータ探索を行い、最も良いモデルを採用しました。
各トークンと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手法についての解説とその使い方について紹介します。
また、このライブラリはPyPIに公開しているのでpip install daaja
でインストールが可能です。
はじめに
Data Augmentationとは
Data Augmentationとは元のデータから新しいデータを生成し、データ数を増やす手法です。 日本語ではデータ拡張という名前で知られています。 ラベル付きデータを擬似的に増やすことによって、アノテーションコストを必要とせずにモデルの汎化性能や精度の向上が期待できます。
対応している手法
現在daaja
は、次の2つの論文で紹介されているData Augmentation手法を実装しています。
- EDA: Easy Data Augmentation Techniques for Boosting Performance on Text Classification Tasks
- An Analysis of Simple Data Augmentation for Named Entity Recognition
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-LOC
やI-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で実装してみました。
はじめに
まずクラウドソーシングにおけるラベル集約について説明します。
教師あり学習では教師データを元に学習を行います。この教師データは人手によるアノテーションによって作成されています。
しかし、専門家を雇ってアノテーション作業を頼むのは非常に高いコストがかかります。
そこで、複数人のボランティアや非専門家(以下、ワーカー)にアノテーションを頼むというのがクラウドソーシングです。
一見良さそうに見えるアイデアですが、数をこなすためにデタラメにアノテーションを行う質の悪いワーカーが出てくるのが世の中の常です。
これを解消するために一つのデータに対して複数のワーカによるアノテーションを頼み、ラベルの集約を試みます。
Dawid-Skeneモデルについて
ラベルを集約を考えたときまず思いつくのが多数決です。 しかし、多数決では質の悪いワーカーと質のいいワーカが同じだけの発言権(重み)を持ってしまいます。
かといって、真のラベルは手に入らないため教師あり学習でワーカーの重み付けも出来ません。
そこで考えられたのがDawid-Skeneモデル*1です。これは次のようなグラフィカルモデルによって記述できます。
これはワーカーがデータ
に付けたラベル
から真のラベルである
を
推測するためのモデルです。
ここでのはEMアルゴリズムによって予測された真のラベルとワーカー
が予測したラベルからなる混同行列になります。
Bayesian Dawid-Skeneモデルについて
PaunらはDawid-Skeneモデルのベイズ化を行いました*2。以下がそのアルゴリズムになります。
Dirはディレクレ分布、Catはカテゴリカル分布のことです。
はクラス数、
はワーカー
がデータ
に付けたラベルの数を意味しています。
比較実験
データセットには麻酔医5人が患者45人に対して全身麻酔への安全度を4段階で評価したものを利用しました。これはDawidらの論文中にて公開されています。
Bayesian Dawid-Skeneモデルの学習にはMCMCを利用しました。
真のクラスの割合
左がベイズ、右がEMアルゴリズムによる学習です。 Bayesian Dawid-Skeneモデルはデータの生成結果もプロットしています。
見た感じかなり似たような所に収束していますね。
各ワーカの混同行列
各ワーカーの混同行列をヒートマップにしたものがこちらです。
両方のモデルにおいて混合行列の対角成分に強い確率が割り当てられています。
おわりに
今回はDawid-SkeneモデルとBayesian Dawid-Skeneモデルについて実装を行ってみました。
EMアルゴリズムに比べて、MCMCによる学習はPyroに乗っかっている分かなり楽に実装出来る印象です。
しかし、Pyroの売りはPytorchを利用した確率的変分推論(Stochastic Variational Inference; SVI)にあると思います。なので次は大きめのデータセットでEMアルゴリズムによる点推定とSVIでのベイズ学習について比較したいです。
実装
詳しいハイパーパラメータや実装については下記を参照してください。
ノートブック
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にて,フルアノテーションコーパスを利用しない固有表現抽出の論文を三本まとめて発表しました.
コメント
完全なアノテーションってなんだよというツッコミがあって確かにそうだなと思ってしまった。素直にフルアノテーションと書いておくかZihan+ 2019を紹介しておくかすればよかったなぁ…
— kajyuuen (@kajyuuen) September 20, 2019
辞書+生コーパスNERはこれからより流行る気がするので続きをまた何処かで発表したい…
— kajyuuen (@kajyuuen) September 18, 2019
非固有表現タグを学習に用いない固有表現抽出モデル: 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タグは学習に使うことが出来ないと考えています.
Better Modeling of Incomplete Annotations for Named Entity Recognitionより引用
これらの問題を踏まえた上で著者はA.3のようにラベルが欠損すると仮定し,このような教師データに対するアプローチを示しています.
ちなみに後の性能評価でも分かりますがA.3のような欠損をしている場合,前回紹介したFuzzy-LSTM-CRFではOタグを学習時に考慮出来なくなるため性能がとても悪くなります.
手法
一般的なCRFの損失関数は以下のように定義されます.
ここで欠損が無いラベル列の集合を,欠損があるラベル列を
と定義し,
を何らかの方法で欠損を埋めたラベル列の集合を
とします.
すると先程のCRFの損失関数は以下のように書き換えることが出来ます.

この確率分布を求める方法として著者はHardとSoftの2種類の手法を提案しています.
Hardでは最も通る可能性が高いラベル列に対してのみ確率を割り当て,Softでは取りうる全てのラベル列に対して確率を計算し割り当てを行います.
Hardの具体的な動きについては以下の通りです.
ちなみに定義した損失関数においてとすると,Fuzzy-LSTM-CRFの損失関数と一致します.
実装
著者が論文中で提案したHard methodと比較対象であるSimple(LSTM-CRF), Fuzzy-LSTM-CRFを以下のリポジトリにて実装しました.
(リポジトリ名と内容に違いが出てきたのでどうにかしたいですね)
評価
データセットには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では,系列に対応するラベル列
の確率
が最大になるように学習を行います.
この確率は系列に対応するラベル列
のスコアを
と定義すると以下のように定義されます.

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

となり,これが最大になるように学習を行うのが通常のCRFです.
しかし,この損失関数ではラベル列に少しでも欠損があると学習を行うことが出来ません.そこでFuzzy CRFでは欠損しているラベルに対して取りうる全ての経路
について計算をすることで学習を行います.

以上のように損失関数を定義することでラベル列が不完全であっても学習を行うことが可能になります.ちなみにラベル列が完全のときはCRFと同じ損失関数を持ちます.
下の論文にて解説,定義されています.
- Learning Named Entity Tagger using Domain-Specific Dictionary
- Training Conditional Random Fields Using Incomplete Annotations
BiLSTM-CRF
Word embeddingsとCharacters embeddingsをBi-LSTMに投入します.その出力を更にCRFに投入し最適なラベル列を探索することで固有表現抽出を行うモデルです.
Neural Architectures for Named Entity Recognitionより引用
今回はこのモデルをベースとし,欠損したデータを学習出来るようにCRFの代わりにFuzzy CRFを適用させました.
実装
PyTorchにてFuzzy-LSTM-CRFとLSTM-CRFの実装を行っています.
評価
データセット
今回は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
フルアノテーションと比べても,そこまで抽出性能が劣化していないことがわかります.
参考
実装の際には以下の論文とリポジトリを参考にさせていただきました.