Wikipediaを活用した表記ゆれへの対応

自然言語処理のタスクにおいて、表記ゆれの問題が常につきまといます。単純なパターンであれば単純なルールで対処が可能です。例えばアルファベットの大文字・小文字の混在であれば全て小文字に変換すれば良いし、半角文字と全角文字の混在であれば全て全角文字に変換すれば良いでしょう。

しかし、略語はどうでしょうか。例えばPCはおそらくパソコンのことですね。ただ、かしこまった文書だとパーソナルコンピュータと表記されているかもしれません。こうなってくると、単純なルールでの対処はもう難しいでしょう。

そこで、単語の正規化(名寄せ)が必要です。単語の正規化の話は、Sansanの発表資料に良くまとまっています。大きく分けて以下の方法が考えられます。

  • 単語マスタを用意しておき、入力単語に対してレーベンシュタイン距離(編集距離)を計算して最も距離が小さい単語を採用する

    • 方法としては単純で扱いやすい
    • マスタの用意が必要
    • PCパソコンのような略語に対応できない
  • word2vecなどの単語埋め込みを用いて、入力単語に対して類似語を抽出する

    • こちらの記事が実験の例
    • モデルの学習が必要
    • 目的に応じたコーパスの用意が必要
  • Encoder-Decoderを用いて、入力単語に対する正規化後の単語を生成する

    • こちらにcookpadの実験の例
    • モデルの学習が必要
    • 正解データとして正規化前後の単語のペアの準備が必要

個人的な印象としては、マスタ+レーベンシュタイン距離はOCR結果の補正とかに良さそうです(プリッツの認識結果がフ・リッツとかになってしまうパターン)。ただ単語の正規化としては、前述の略語への対応の難しさから厳しいかなという感じです。単語埋め込みの方法は、単語の正規化を行いたいドメインが限られている場合は良さそうです。具体的には、とにかく化粧品の表記ゆれが解消できれば良いんだ!みたいなパターンです。ただ、正規化を行わなくて良い単語もあるはずで、そのような単語も何らかの変換がされてしまうことが問題です。Encoder-Decoderでは入力の単語をそのまま出力することも可能なので、最も良さそうです。ただ、学習データ作成のコストが非常に高く、そこの壁を解消するための解決策がもう1つ必要でしょうか。

今回は、マスタやコーパス、正解データの新規作成を行うことなく、また機械学習も行わず表記ゆれへの対応を行う1つの案を紹介します。具体的には、エンティティリンキングの結果を単語の正規化結果として利用します。

Wikipediaを活用した表記ゆれへの対応方法

以下のエンティティリンキングまでの考えは、以下の資料をがっつり参考にしています。

www.slideshare.net

エンティティ辞書

まず、エンティティ辞書という概念を導入します。

例えば、以下のWikipediaのiPhoneのページにおいて、リンクが張られているアップルという単語には、Wikipedia中のアップル(企業)のページを指しています。ここで、リンクが張られている単語アップルエンティティ名、リンク先のページのタイトルアップル(企業)エンティティと呼びます。

f:id:pompom168:20190809152746p:plain

wikipediaの記事に出現するエンティティ名とエンティティを抽出して記録することで、エンティティ名とエンティティの対応表を作成することができます。これをエンティティ辞書と呼びます。以下の図は、エンティティ辞書の具体例です。

f:id:pompom168:20190809153155p:plain
https://www.slideshare.net/ikuyamada/pythonwikipedia-120034699 より引用

エンティティ名をキー、エンティティをバリューと考えると、任意の単語をキーとして辞書に問い合わせることでエンティティを獲得可能です。よって、エンティティを正規化後の単語とした単語の正規化が可能です。文書に対して、辞書に含まれるエンティティ名を抜き出してエンティティを抽出するタスクをエンティティリンキングと呼ぶらしいですが、つまりはエンティティリンキングを行って単語の正規化を行っていることになります。

エンティティ辞書には2つの有用な指標が存在します。それはリンク確率とコモンネスです。

リンク確率

各エンティティ名に対して定義される値です。エンティティ名がリンクとしてWikipediaのページに表れる確率を表します。

例えば、Wikipediaの全ページ中にアップルが1000回出現し、うち300回リンクとして出現したらエンティティ名アップルのリンク確率は0.3となります。

コモンネス

エンティティ名とエンティティの組み合わせに対して定義される値です。エンティティ名が特定のエンティティを指し示す確率を表します。

例えば、Wikipediaの全ページ中にアップルが1000回出現し、うち200回アップル(企業)のエンティティを指していたら、エンティティ名アップルとエンティティアップル(企業名)のコモンネスは0.2となります。

wikipedia2vecを用いたエンティティ辞書の構築

wikipedia2vec自体は、skip-gramを拡張してWikipediaのデータから単語埋め込みを学習する方法論とその実装であるPythonのパッケージです。詳細は以前、以下の会社の技術ブログで紹介したのでよろしければご覧ください。

developers.microad.co.jp

パッケージとしてのwikipedia2vec(リポジトリ)に注目すると、実は単語埋め込みだけでなくエンティティ辞書構築の機能も存在します。

 

Wikipediaのダンプデータ

エンティティ辞書の構築のために、Wikipediaのダンプデータを使用します。以下のURL配下に存在します。

Index of /jawiki/latest/

こちらにデータの説明などがありますが、大体月に2回、最低でも月に1回は更新が行われているそうです(不定期)。ただ、少なくとも毎月更新されているので、エンティティ辞書の構築も毎月行うことでエンティティ辞書のメンテが可能です。

エンティティ辞書構築手順

手順は以下のとおりです。(ファイル名などは適宜変更ください)

最後のコマンドに--min-link-probオプションがありますが、これがリンク確率に関するところで、この値を大きくすればあまりリンクが張られていない単語がエンティティ名から除去されます。デフォルトだと0.2ですが、自分の感覚では0.1がちょうどよかったです。これはタスク依存です。

# ダンプデータの取得
$ wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2
 
# wikipedia2vecで扱える形に変換(jawiki.dbが出力)
$ wikipedia2vec build-dump-db jawiki-latest-pages-articles.xml.bz2 jawiki.db
 
# 辞書ファイルの作成(--min-link-probオプションでリンク確率のしきい値調整ができる)
$ wikipedia2vec build-dictionary jawiki.db jawiki_dic.pkl
$ wikipedia2vec build-mention-db jawiki.db jawiki_dic.pkl jawiki_mention.pkl --min-link-prob 0.1

使用法と表記ゆれへの対応例

以下のようにして使います。MentionDBインスタンスのqueryメソッドでエンティティを抽出できます。(この辺りは特にドキュメントとかはなかったので、ここらへんから読み解きました。何か間違いがあるかもしれません。)

In [1]: from wikipedia2vec.dictionary import Dictionary  
   ...: from wikipedia2vec.mention_db import MentionDB                                                   

In [2]: dic = Dictionary.load('jawiki_dic.pkl')                                                          

In [3]: db = MentionDB.load('jawiki_mention.pkl', dic)                                                   

In [4]: db.query('toyota')                                                                               
Out[4]: [<Mention toyota -> トヨタ自動車>]

# エンティティを抽出
In [6]: db.query('toyota')[0].entity.title                                                               
Out[6]: 'トヨタ自動車'

リストで返ってくることから分かるとおり、単語によっては複数のエンティティが抽出されます。例えばPCだと以下のように複数のエンティティが抽出されます。

この場合は、コモンネスが最も大きいエンティティ名とエンティティの組み合わせを抽出すれば良いはずです。なぜなら、コモンネスが最も大きいということは、そのエンティティ名に対してはそのエンティティが最も良く指し示されているからです。

In [8]: db.query('PC')                                                                                   
Out[8]: 
[<Mention pc -> 枢密院 (イギリス)>,
 <Mention pc -> パーソナルコンピュータ>,
 <Mention pc -> パソコンゲーム>,
 <Mention pc -> プレイヤーキャラクター>,
 <Mention pc -> プレストレスト・コンクリート>,
 <Mention pc -> Microsoft Windows>,
 <Mention pc -> パーセク>,
 <Mention pc -> PC>,
 <Mention pc -> PC/AT互換機>,
 <Mention pc -> プレストレスト・コンクリート橋>]

# コモンネスが最も大きいのはパーソナルコンピュータ
In [9]: for word_entity in db.query('PC'): 
   ...:     print(f'entity = {word_entity.entity.title}, コモンネス = {word_entity.commonness}') 
   ...:                                                                                                  
entity = 枢密院 (イギリス), コモンネス = 0.036575875486381325
entity = パーソナルコンピュータ, コモンネス = 0.5821011673151751
entity = パソコンゲーム, コモンネス = 0.014785992217898832
entity = プレイヤーキャラクター, コモンネス = 0.07937743190661478
entity = プレストレスト・コンクリート, コモンネス = 0.09961089494163425
entity = Microsoft Windows, コモンネス = 0.017120622568093387
entity = パーセク, コモンネス = 0.025680933852140077
entity = PC, コモンネス = 0.0840466926070039
entity = PC/AT互換機, コモンネス = 0.014007782101167316
entity = プレストレスト・コンクリート橋, コモンネス = 0.024124513618677044

よって、コモンネスが最大のエンティティを抽出する関数を作成しておきます。

In [23]: import numpy as np 
    ...: def extract_entity(db, word): 
    ...:     try: 
    ...:         entities = db.query(word) 
    ...:         if len(entities) == 1: 
    ...:             entity = entities[0].entity.title 
    ...:         else: 
    ...:             max_idx = np.argmax([entity.commonness for entity in entities]) 
    ...:             entity = entities[max_idx].entity.title 
    ...:     except KeyError: 
    ...:             return None 
    ...:     return entity 

以下、何パターンかエンティティ抽出例を示します。

In [20]: extract_entity(db, 'じゃがいも')                                                                
Out[20]: 'ジャガイモ'

In [21]: extract_entity(db, '馬鈴薯')                                                                    
Out[21]: 'ジャガイモ'

# 略語
In [24]: extract_entity(db, 'パワハラ')                                                                  
Out[24]: 'パワーハラスメント'

# ドラマの略語
In [25]: extract_entity(db, '逃げ恥')                                                                    
Out[25]: '逃げるは恥だが役に立つ'

# しょうゆ => 醤油とかは出来ない、Wikipedia中でしょうゆと書かれることが無さそう
In [28]: extract_entity(db, 'しょうゆ')                                                                  

キーワード抽出器として使用する

抽出されたエンティティはWikipediaの各ページのタイトルであるので、文書中のキーワードとして扱うこともできるでしょう。

例えばSHIROBAKOの劇場版に関するページから、以下のような文書を抽出したとします。

劇場版『SHIROBAKO』(2020年春ロードショー予定)の特報映像&場面写真が初公開となった。この中には、主人公・宮森あおいや安原恵麻らおなじみメンバーに加え、新キャラクターの姿も捉えられている。劇場版『SHIROBAKO』場面写真【画像クリックでフォトギャラリーへ】本作は、P.A.WORKSと水島努監督がタッグを組み、2014年に放送されたTVアニメ『SHIROBAKO』の完全新作劇場版。宮森あおい、安原絵麻、坂木しずか、藤堂美沙、今井みどりら、アニメーション業界で働く女性5人を中心に、アニメの完成までを追う物語だ。VIDEO劇場版『SHIROBAKO』は、2020年春公開予定。「コミックマーケット96」では、企業ブース「ムービック(No.1222)」にて、初の前売り券の会場限定販売を実施。

そして分かち書きを行い名詞だけを抽出した結果、以下の単語リストが得られたとします。 (分かち書きは今回の主題では無いので省略します。またアルファベットは大文字から小文字に変換する正規化を行っています。)

words = ['劇場版', 'shirobako', '特報', '場面写真', '公開', '宮森', 'キャラクター', '姿', '劇場版', 'shirobako', 'ロー
ドショー', '予定', '特報映像', '場面写真', '公開', '主人公', '宮森あおい', '安原', '恵麻', 'ら', 'おなじみ', '
メンバー', 'キャラクター', '姿', '劇場版', 'shirobako', '場面写真', '画像', 'クリック', 'フォトギャラリー', '
本作', 'p.a.works', '水島努', '監督', 'タッグ', '放送', 'tvアニメ', 'shirobako', '完全', '新作', '劇場版', '宮
森あおい', '安原絵麻', '坂木しずか', '藤堂美沙', '今井みどり', 'ら', 'アニメーション', '業界', '働く女性', '5
人', '中心', 'アニメ', '完成', '物語', 'video', '劇場版', 'shirobako', '公開予定', 'コミックマーケット', '企業
', 'ブース', 'ムービック', '初', '前売り券', '会場', '限定販売', '実施']

先程のextract_entity関数を使用して、以下のようにentityを抽出します。

In [35]: word_entity = {} 
    ...: for word in words: 
    ...:     entity = extract_entity(db, word) 
    ...:     if entity: 
    ...:         word_entity.update({word:entity}) 
{
    'shirobako': 'SHIROBAKO', 
    'p.a.works': 'ピーエーワークス',
    '水島努': '水島努', 
    'アニメーション': 'アニメーション', 
    'アニメ': 'アニメ', 
    'コミックマーケット': 'コミックマーケット',
    'ムービック': 'ムービック'
}

割りと良い感じだと思います。作品名・制作会社名・監督名が全て抽出できていますね。

ただし、あくまでもWikipediaに存在するページのタイトルしか抽出できないことに注意が必要です。なので、一般的な単語には強いですが、特定のドメインのみで細かい単語の正規化やキーワード抽出はできません。

まとめ

  • Wikipediaを活用した表記ゆれへの対応(単語の正規化)方法を紹介した
    • 特別なマスタ・コーパス・正解データなどが必要無い
    • 機械学習の必要もない
    • 毎月更新することで最新の状態にメンテ可能
    • キーワード抽出器としても利用可能
  • ただしあくまでもWikipediaのページのタイトルとして存在する単語にしか正規化が出来ない