機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

Snorkelのラベリング関数について(実装編)

はじめに



前回は前処理(候補の抽出)の流れを確認した。
Snorkelの前処理について(実装編)① - 機械学習・自然言語処理の勉強メモ
前処理が完了すると、関係の候補となるペアが出来る。
f:id:kento1109:20180115150612p:plain
※「配偶者」の候補となる人物名のペア

ただし、抽出した候補はエンティティが「人物名」のチャンクを抽出しただけ。なので、
f:id:kento1109:20180115151606p:plain
のように文章中の人物名のペアでも「配偶者」の関係ではない場合が存在する(なので、この時点では配偶者の関係をもつ「候補」と言われている)。
では、どのようにして「配偶者」の関係を持つ候補を見分ければよいか。
例えば以下の文章を見てみる。
f:id:kento1109:20180115151954p:plain
候補は、

Spouse('Rachel Hattingh' 'Graham Marshall')

だが、これは「配偶者」の関係を持つ候補と言えるだろうか。殆どの人は「YES」と答えるだろう。そして、その理由に「her husband」があるからと答える。
では、次の文章はどうだろう。
f:id:kento1109:20180115152802p:plain
候補は、

Spouse('Barack Obama' 'Michelle Obama')

である。
人物名を知らない人でも多くは、「last nameが同じだから配偶者と言える」と答えるだろう。(もちろん、他人同士でlast nameが同じ可能性もあるが、配偶者と考える方が妥当だろう)
おそらく人手で配偶者の関係をアノテーションする場合でも、似たような判断基準を用いるはずだ。このように候補が関係であるかどうかはいくつかのルールに基づいて判断される。Snorkelでは、このルールをラベリング関数として人手で定義し、その結果に基づいて判断を行う。
例えば、候補同士のlast nameが同じかどうかを判断する関数はSnorkelでは以下のように定義する。

def LF_same_last_name(c):
'''
Label as positive if both
'''
    p1_last_name = last_name(c.person1.get_span())
    p2_last_name = last_name(c.person2.get_span())
    if p1_last_name and p2_last_name and p1_last_name == p2_last_name:
        if c.person1.get_span() != c.person2.get_span():
            return 1
    return 0

※書き方は後で確認する。

これは人手で決めた「ルールベース」に基づいて作成するラベリング関数であるが、以下のように外部リソースを利用しても良い。(Distant Supervision

def known_spouse(x):
    pair = (x.person1_id, x.person2_id)
    return 1 if pair in KB else 0

f:id:kento1109:20180115161533p:plain
※ここまでの画像は全て下記より引用
snorkel/Snorkel-Workshop-FINAL.pdf at master · HazyResearch/snorkel · GitHub

戻り値について



まず、ラベリング関数の戻り値であるが、これは「1, 0, -1」のいずれかの値である必要がある。
戻り値の基準としては、

  • 1・・定義した規則に該当する場合、関係ありと判断する
  • -1・・定義した規則に該当する場合、関係なしと判断する
  • 0・・根拠がないので、判断を控える

通常、ラベリング関数は「1, 0」もしくは「-1, 0」の戻り値を組み合わせる。上記のLF_same_last_name関数の場合、「人物名のlast_nameが同じなら1、異なる場合は0」を返す。(人物名のlast_nameが異なるからといって、関係ないとは言えないので-1としない。

評価指標について



定義したラベリング関数はどのように評価すればよいか。例えば以下の2つの関数を定義する。

  1. 「人物名の周りにmarryという単語があれば1、なければ0」を返す。
  2. 「人物名のlast nameが同じであれば1、同じでなければ0」を返す。

1.の関数の条件に該当する候補はたくさん見つかるかもしれないが、この条件だけで配偶者かどうかは判断できないので精度が低い。
2.の関数の条件に該当する候補は少ないかもしれないが、この条件に合致する候補は配偶者である可能性が高いので精度は高い。
精度と網羅している割合の両方を評価するため、下記の評価指標を用いる。

  • Accuracy:そのラベリング関数が関係あり(なし)を正しく判断した候補の割合
  • Coverage:ラベリング関数によってラベリング(1, -1)された候補の割合

上記の場合、1.の方がCoverageは高い。しかし、2.の方がAccuracyは高い。
※正確なAccuracyを求めるためには真のラベルが必要
理想的なラベリング関数は、Accuracy・Coverageがともに高いことであるが、これらはトレード・オフの関係にあることが多い。

また、他のラベリング関数の値も使用した以下の指標も評価に用いられる。

  • Overlap:ラベリング(1, -1)&他の関数もラベリング(1, -1)の割合
  • Conflict:ラベリング(1, -1)&他の関数もラベリング(1, -1)&それらのラベルが異なる割合

下記3つの指標は常に、Coverage ≧ Overlap ≧ Conflictとなる。

コードを読んでいく



今回のチュートリアルは以下にある。
github.com

Pattern Matching Labeling Functions

キーワードや正規表現を使って関数を作る。
あるキーワードが出現すると、その候補は関係あり(なし)の可能性が高い」という前提に基づきラベリング関数を定義する。キーワードの選定などは、経験や知識に基づく場合もある(我々はhusband, wifeなどの周辺に出現する人物名同士は配偶者である可能性が高いことを知識として知っている)が、厳密には、キーワードと関係の条件付き確率に基づき定義することが出来る。
例えば、真の正解ラベルの情報から
P(candidate=0|keyword='boyfriend')=0.9
P(candidate=0|keyword='girlfriend')=0.8
※candidate=0は関係なし
という条件付き確率が得られる場合、この情報に基づき「周辺にboyfriendやgirlfriendが出現する人物名同士は配偶者でない」と判断する下記のラベリング関数を定義することが出来る。

friend= {'boyfriend', 'girlfriend'}
def LF_friend_terms_between(c):
    return -1 if len(friend.intersection(get_between_tokens(c))) > 0 else 0

Labeling Function Factories
Labeling Function Factoriesを使って、より簡単にラベリング関数を定義する。
1)Term Matching Factory
「人物名の間にhusband, wifeなどが出現する候補は配偶者であるとする」という判定をしたい場合、MatchTermsメソッドを使用してラベリング関数を下記のように記述できる。

marriage  = {'husband', 'wife'}

# we'll initialize our LFG and test its coverage on training candidates
LF_marriage = MatchTerms(name='marriage', terms=marriage, label=1, search='between').lf()

定義した後、この関数のCoverageが下記の方法で確認できる。

labeled = coverage(session, LF_marriage, split=0)
# Coverage: 7.66% (1702/22218)

尚、SentenceNgramViewerで実際の候補を確認できる。

SentenceNgramViewer(labeled, session, n_per_page=1)

f:id:kento1109:20180116105718p:plain:w1000
また、候補の真のラベルを持っている場合、ラベリング関数による抽出精度も測定可能。

tp, fp, tn, fn = error_analysis(session, LF_marriage, split=1, gold=L_gold_dev)

# now let's view what this LF labeled
SentenceNgramViewer(fp, session, n_per_page=1)

========================================
LF Score
========================================
Pos. class accuracy: 1.0
Neg. class accuracy: 0.0
Precision            0.356
Recall               1.0
F1                   0.525
----------------------------------------
TP: 63 | FP: 114 | TN: 0 | FN: 0
========================================

定義したラベリング関数では、精度が35.6%といまいちであることが分かる。
もう一つMatchTermsでのラベリング関数の作成例を確認する。

other_relationship  = {'boyfriend', 'girlfriend'}

LF_other_relationship = MatchTerms(name='other_relationship', terms=other_relationship, 
                                   label=-1, search='left', window=1).lf()

2)Regular Expression Factory
キーワードでなく正規表現を使いたい場合、MatchRegexメソッドを使用する。
例えば、単語「ex-husband, ex-wifeは関係なし」と定義したい場合、下記のように記述する。

exes_rgxs = {' ex[- ](husband|wife)'}

LF_exes = MatchRegex(name='exes', rgxs=exes_rgxs, label=-1, search='between').lf()

Labeling Function Factoriesで使用するオプションについて:
terms: 関数で使用する文字・正規表現
label: 判別結果(1 or -1)
search: 検索箇所('left'|'right'|'between'|'sentence')
window: 検索箇所を('left'|'right')の場合の範囲指定

Distant Supervision Labeling Functions

DBpediaなどの外部リソースを使用してラベリング関数を定義する。
例えば、DBpediaには以下のような配偶者データが構造化データとして登録されている。

from lib.dbpedia import known_spouses 

list(known_spouses)[0:5]
[('Eleanor Powell', 'Glenn Ford'),
 ('Andronikos Doukas', 'Maria of Bulgaria'),
 ('Marjorie Rambeau', 'Willard Mack'),
 ('Margo St. James', 'Paul Avery'),
 ('Joan of England', 'William II the Good')]

候補がDBpediaのknown_spousesに登録されている場合、関係ありとするラベリング関数は以下のように定義できる。

LF_distant_supervision = DistantSupervision("dbpedia", kb=known_spouses).lf()
labeled = coverage(session, LF_distant_supervision, split=1)
Custom Labeling Functions

その他、自作関数を用いて任意のラベリング関数を定義できる。例えば、人物名の出現箇所に距離がある場合、それらは関係がないと判定する関数は下記のように記述できる。

def LF_too_far_apart(c):
    """Person mentions occur at a distance > 50 words"""
    return -1 if len(list(get_between_tokens(c))) > 50 else 0
Composing Labeling Functions

ラベリング関数を組み合わせて新しいラベリング関数を定義することもできる。

def LF_marriage_and_too_far_apart(c):
    return 1 if LF_too_far_apart(c) != -1 and LF_marriage(c) == 1 else 0

LF_marriage_and_not_same_person = lambda c: LF_too_far_apart(c) != -1 and LF_marriage(c)

score(session, LF_marriage_and_too_far_apart, split=1, gold=L_gold_dev)
Applying Labeling Functions

作成したラベリング関数をリストにまとめて、それら全てを候補に適用する。

LFs = [
    LF_marriage, LF_other_relationship,
    LF_exes, LF_distant_supervision
]
Generating the Label Matrix

ラベル行列を生成する。

from snorkel.annotations import LabelAnnotator
labeler = LabelAnnotator(lfs=LFs)

np.random.seed(1701)

L_train = labeler.apply(split=0, lfs=LFs, parallelism=1)
print L_train.shape
>> (22218, 4)
L_dev = labeler.apply_existing(split=1, lfs=LFs, parallelism=1)
print L_dev.shape
>> (2799, 4)

ラベル行列を確認する。

L_train.lf_stats(session)

f:id:kento1109:20180116114843p:plain:w1000
正解ラベル付きデータでAccuracyを確認する。

L_dev.lf_stats(session, labels=L_gold_dev.toarray().ravel())

f:id:kento1109:20180116115110p:plain:w1000
実際のAccuracyやCoverageを確認して、ラベリング関数を追加したりして調整する。