豪鬼メモ

一瞬千撃

Wikipediaの共起語を使ってシソーラス検索をしよう

前回、Wikipediaの記事を解析して、単語の共起語のデータベースを作った。今回は、共起語データベースを解析して類語を推定する。すなわち、共起語データベースをシソーラスとみなして、関連語の検索を行う。


結論から言うと、そこそこそれっぽい関連語検索機能を実装することができた。以下のデモサイトで遊んでみてほしい。

例えば、日本語のデモサイトに「所沢」と入力されたい。「Related Words」の欄には、関連語として、「狭山」「飯能」「川越」「入間」「新座」などが出る。それらはいずれも共起語の傾向が「所沢」と似通っている。文中の「所沢」とそれらを置き換えてもそれほど違和感が生じないこともあるだろう。同様に、英語のデモサイトに「unix」などと入力されたい。「posix」「linux」「bsd」「macos」などが出る。それらも共起語の傾向が「unix」と似通っているので、しれっと置き換えても気づかれないこともあるだろう。意味的に近いかどうかは全く考慮していないにもかかわらず、共起語を見るだけで関連語を抽出することができるというこどだ。

入力した語の共起語は「Features」の欄に示される。その語とよく一緒に使われる語ということだ。共起語が関連語であることも多いが、共起しなくても関連語であることはあり得る。この微妙な違いが今回の要点である。


共起とは、複数の単語やフレーズが同じ場所に現れることである。ここでは、単語にのみ絞って話を進める。前回の記事では、各々の単語に着目して、同じ文もしくは前後20語以内に現れやすい単語を共起語とみなしてデータベースを作った。今回は共起語のデータを使って類語を推定する。

共起語と類語は異なる概念である。「toilet」の共起語は「flush」「wash」「urine」「sanitation」とかになるだろうが、それらは元の単語と共通した意味を持つものではなく、言い換えとして使えるものではない。一方、今回見つけたい類語とは、「toilet」に対する「bathroom」や「lavatory」などの、用法が似ていて言い換えとして使えそうな語である。類語は、共起することもあるだろうが、必ずしも共起しやすいとは限らない。「bathroom」を「toilet」の婉曲表現として使うのならば、「toilet」と共起させては意味がない。Wikipedia上では「bathroom」と「toilet」は普通に共起するけども。

共起語の傾向が似ている複数の語は類語であるとみなせる。「toilet」と「bathroom」はともに「flush」「wash」「unine」「sanitation」と共起すると考えられる。そうでなければ言い換えになり得ない。したがって、全ての2つの単語の組み合わせについて共起語の傾向の類似性を調べることができれば、それなりの類語辞書が作れることになる。もちろん、全ての単語の組み合わせを調べるのは現実的ではないので、何らかの簡便法を模索することになる。

共起語の傾向が似ているものを探すのであれば、主要な共起語が重複している語のペアのみを検査すればよいだろう。ならば、まず思い浮かぶのは、共起語の共起語を辿って検査対象とすることだ。「toilet」の共起語に「flush」があり、「flush」の共起語に「bathroom」があるならば、その目論見は成功する。


共起する可能性がある語を全て検査するのは時間がかかりすぎるので、適当にスコアリングと足切りをして、それっぽいものだけを調べることが必要になる。それには以下のようなアルゴリズムを用いる。実際のコードはここに置いておく。

  • 各語に関して、予め主要共起語とそのスコアを算定して、DBMのデータベースファイルを作っておく。
    • キーは語の正規化した文字列。
    • 値はタブ区切り(TSV)として、リストのデータを保持する。。
    • 値の最初の要素として、共起元の語のIDFを記録する。
    • 値の残りの要素として、主要共起語とそのスコアのリストを記録する。
    • 共起語のスコアは、共起確率にその共起語のIDFを掛けたもの。
    • この共起語とそのスコアを検索語の特徴量のベクトルとみなす。
  • 検索語が入力されたら、そのフレーズをトークンに分割する。
    • 各々のトークンの共起語をデータベースから取得し、スコアを足し合わせる。
      • その際、各々のトークンの自身のIDF値と、共起語のスコアを掛け算する。
    • トークンの合成された共起語リストの中から上位32語に関して、さらにその共起語を取得する。
      • 1段目の共起語のスコアと、2段目の共起語のスコアを掛けた値を2段目のスコアにする。
  • 1段目の共起語の上位16語と、2段目の共起語の上位128語を関連語の候補とみなす。
  • 関連語のそれぞれに対して、共起語データベースを調べ、特徴量を算定する。
    • 検索語と同じく、共起語の各々の共起確率にIDF値を掛けてスコアを算出する。
  • 検索語の特徴量と、関連語候補の各々の特徴量の間の類似度を計算する。
    • 検索語の共起語の空間に関連語候補の共起語をマッピングして、双方のベクトルのコサイン距離を測る。
  • 類似度が高い順にソートして結果を出力する。

具体例を示そう。「バラク・オバマ」という検索語を入力したとしよう。そうすると、「バラク」と「オバマ」がトークンとして得られる。その各々に対して、共起語データベースを調べる。「バラク」の共起語としては「オバマ」「合衆国」「民主党」「大統領」などとそれらのスコアが得られる。「オバマ」の共起語としては「バラク」「民主党」「政権」「大統領」などとそれらのスコアが得られる。得られた共起語を合成して、検索語全体の特徴量を生成する。その際、「バラク」の共起語のスコアには「バラク」のIDF値を掛け、「オバマ」の共起語のスコアには「オバマ」のIDF値をかける。IDF値を重み付けにする理由は、頻度が低い語ほど意味のある情報を持っていると考えられるからだ。例えば「the beatles」では「the」の共起語より「beatles」の共起語の方が重要だ。そして、合成したスコアが上位32語の共起語に関しては、さらにその共起語を取得し、同様にスコアリングを行う。「民主党」の共起語として「ヒラリー」「上院」などが得られ、「大統領」の共起語として「トランプ」「クリントン」などが得られる。

直接の共起語の中から上位16語を取り出し、関節の共起語の中から上位128語と取り出して、関連語の候補とする。そして、それぞれの関連語候補に対してベクトル空間モデルで類似度を計算する。「バラク・オバマ」の合成した特徴量は「バラク: 99.80」「オバマ: 99.38」「民主党: 67.10」「政権: 59.65」などである。「ヒラリー」の特徴量は「ヒラリー: 50.00」「ダフ: 49.99」「スワンク: 49.99」「クリントン: 47.27」などである。双方を比べるに当たって、検索語である「バラク・オバマ」の共起語と同じ成分だけを「ヒラリー」の共起語から取り出して値を入れ、そうでない成分の値は0にする。そして双方のベクトルの「なす角」のコサインを類似度とみなす。

ここで、重要なハックがある。予め共起語のスコアのデータベースを作る際に、共起確率の値を0.05くらいを最大値としてキャップすることである。そうしないと、異常に共起しやすい語が全体の精度を悪化させてしまうからだ。例えば、「オバマ」には90%の確率で「バラク」が共起するのだが、それをキャップしないと「バラク」の共起語の類似度が異常に高くなってしまう。チンギス・ハンの子孫の「チャガタイ」の子孫に「バラク」なる人物がいるのだが、その影響で「バラク」の共起語として「チャガタイ」が得られる。「オバマ」の支配的な共起語が「バラク」で、「バラク」の支配的な共起語が「チャガタイ」なので、そのままだと「オバマ」の類義語の1位は「チャガタイ」になってしまう。それを防ぐためには、支配的な共起語をなくせばよい。共起率を重み付けに使いつつも、支配的な意見をなくし、より「民主的」に意思決定を行う。そのためのキャップである。

ところで、「オバマ」の共起語として「オバマ」はカウントされていない。よって、そのままでは、「ヒラリー」の共起語に「オバマ」が現れたとしても、その関連性を汲み取ることができない。それはもったいないので、「オバマ」の共起語として「オバマ」自身を共起率0.05で加えることにする。本来なら自身の共起率は1.0になるはずだが、上述した支配的な共起語の悪影響があるので、同じく0.05でキャップするというわけだ。

1回のクエリの処理で、直接の共起語を取り出すのに共起語データベースを1回検索し、その上位16語のそれぞれの共起語を取り出すのに16回検索し、そうして得られた関連語の上位128語を取り出すのに128回検索する。直接の共起語の上位16語も関連語候補に加えて同じ処理を行う。よって、少なくとも1+16+128+16 = 161回のDBM操作が必要となる。C++だったらHashDBMの操作のスループットは100万QPSとか余裕で出るが、Pythonから呼び出すとその半分もいかない。とはいえ、161回などDBMにとっては大したことはない数字だ。類似度の計算では、各々の関連語候補の特徴量128個を連想配列に変換した上で、その検索を128回行う。ということはハッシュ関数の計算だけでも(128+128)*(128+16) = 36864回行うことになる。現状のチューニングでは0.5秒以内に応答できるようになっているが、より高速化したい場合にはチューニングを変えるなりC++で実装するなりの工夫をすることになるだろう。


ここまで述べたアルゴリズムだと、再現率(recall)に限界がある。なぜなら、共起語データベースでは上位128語までしか記録していないので、それ以後に重要な類語があっても拾えないのだ。もしかして「オバマ」の類語として「カーター」を拾うべきかもしれないが、「大統領」「民主党」「上院」いずれの上位共起語にも「カーター」は含まれていないので、類似度の比較対象にすらならない。これを克服するためには共起語データベースの収録語数を増やすか、「カーター」の共起語に「大統領」等が含まれることを利用した転置索引を作る必要がある。しかし、いずれの方法でも、計算負荷がかなり上がってしまって、1台で処理するのには荷が重い。事前に類語データベースを作るようにして分散処理でそれを行うような工夫が求められる。

もう一つの問題は、頻出語の精度(precision)が悪いことだ。「walk」の類語として「step」「foot」「troop」「march」「stride」などを取得するのが本来の目的であるが、現状ではそのいずれも取得できない。「walk」の上位共起語は「away」「fame」「kilometres」「through」などだが、「歩く」という意味を推定できるものがあまりないのだ。IDFをもっと強烈にかけるとか、品詞によるフィルタを導入するとか、活用形を原形に正規化するとか、いくつかアイデアはあるが、この問題は一筋縄ではいかないだろう。正直、頻出語に関してはWordNetWiktionaryからデータを引っ張ってきた方が早い。

細かい問題はもっとある。日本語の分かち書きの問題の一例として、「東京大学」は「東京大学」と一語なのに「京都大学」は「京都大」と「学」になる問題とか、「アッシュール」が「アッ」と「シュール」に切られてしまう問題などがある。そもそも単語のbag-of-wordsのモデルだと細かく単語が切れすぎる場合にはうまくいかない。「宮本茂」は「宮本」と「茂」に分けられるが、宮本武蔵吉田茂のコンテキストを合成してもマリオやルイージは出てこない。これは英語でも同じで、「Mountain View」のように複数の頻出語を組み合わせて特殊な意味を持たせている場合には、単語単位ではうまく特徴をまとめられない。とはいえ、英単語辞書で類語を利用するなら日本語や複数単語への対応の優先度は低い。

Wikipediaならではの問題もある。特に日本語版でアダルト関係の記事がやたら充実していることから、「桃太郎」の類語がアダルト系に席巻されたりする。また、スポーツや音楽やアニメの記事もやたら充実しているので、「少女」の類語が「魔法使い」だったりと、一般常識からは少しずれた回答が得られることも多い。まあ、Wikipedia関係者の常識を反映しているとも言えるが。Wikipedia以外のテキストを入力すればこの問題は緩和できるが、いずれにせよコーパスのバイアスの問題は永遠のテーマである。


まとめ。Wikipediaから抽出した共起語を使って、そこそこ使える類語検索機能を実装することができた。Wikipediaを使っているだけあって、固有名詞に特に強いものとなっている。一方で、一般名詞や動詞や形容詞には弱い。この件に関しては追って検討したい。