Fire Engine

消防士→ITエンジニア→研究者

Rubyで文章間の類似度を計算するモジュールを作ってみた(TF-IDFとCos類似度による推定)

 最近、自然言語処理に興味を持ち始めました。今回は、二つの文章(テキストファイル)の類似度を計算するモジュールを作ってみました。いずれは、これを発展させていって、機械学習とかも組み込んで、Webサイトをユーザの嗜好に応じて推薦してくれるシステムとか作りたいなーって思っています。

今回の目次は以下のような感じです。

目次

なにをやるか

 ニュース記事を3つ取ってきて、その記事同士の関連がどれだけ強いのかを数値化します。比較するニュース記事は以下の三つで、それぞれ「A」「B」「C」とします。

A:自動運転、事故時の法的責任は?テスラの死亡事故問題から考える

B:グーグルが「完全自動運転」にこだわるわけ

C:【ソフトバンク】ノーバン投球の永野「一番中途半端な感じ」オチなしパフォーマンスを反省

タイトルだけ見てもわかるようにAとBは人工知能、自動運転関連の記事であり、Cは野球の記事です。AとBが関連が強く、AとCまたはBとCが関連が弱いような結果がでるものができればよしとします。

つくったもの

 今回書いたコードは下記のリンクから見れます。

github.com

 まだまだリファクタリングの余地ありまくりですが、今回はまず動くことを最優先したので、追い追い改良していきます。

 結果をさきに見てもらいます。(モジュールを使うためには事前にRuby、natto、Mecabのインストールが必要です。)カレントディレクトリに比較したい記事を入れたテキストファイルを置き、コマンドラインで叩くだけです。そうです、この部分は、かなりアナログです。ニュース記事をテキストファイルにコピペしています。

 ゆくゆくは以前の記事に書いたようなスクレイピングを使って、対象のテキストもプログラムから取ってくるようにするつもりです。

hirotsuru.hatenablog.com

コマンドラインで実行すると、値が返ってきます。後ほどまた説明しますが、この値は1に近ければ近いほど類似度が高く、0に近ければ近いほど類似度が低いことを示しています。

AとB 0.618120128908814

AとC 0.0010326528355030487

BとC 0.011337073768295269

結果を見てみると、類似性の傾向としては、望むような結果が出ており、一定の信頼度があるようです。記事の選定の時点でかなり類似性の高いものと、全く関連がないものをチョイスしているため、結果が極端ですが。

採用したアルゴリズム

 アルゴリズムと言えるほど、たいそうなものではないですが、処理の流れとしては、以下の通り。

  1. それぞれの記事からMeCabで単語だけ切り出して記事を単語リストに変換

  2. TF-IDFによる特徴語の抽出と特徴ベクトルの生成

  3. 特徴ベクトル同士のCos類似度を求める。

技術メモ

 「文章の類似性ってどう評価するんだろう?」という疑問が浮かび、いろいろ勉強しながら今回のモジュールを作りました。その作るに至るまでに学んだことを時系列順に説明したいと思います。

文章の類似度計算にはCos類似度

 まず、文章の類似度計算にはCos類似度というのがよく使われていることを知りました。Cos類似度は次の式で定義されています。

f:id:hirotsuru314:20160711104327p:plain

 ここで、aおよびbはベクトルであり、類似性の評価対象のデータです。Cos類似度では、ベクトル空間モデルにおいて、ベクトル同士の成す角の大きさでデータの類似性を評価します。つまり、角度(θ)が小さい場合、ベクトル同士が重なり合うようになるため、ベクトル同士が類似しているということになります。また、角度(θ)がゼロのとき、cosθは1になるため、cosθが1に近いほど、データの類似度が高いということになります。

 ということは、文章をベクトルで表す必要があるということ?そういうことです。

文章をベクトル化する

 文章をベクトル化する方法で、bag of words (BoW)モデルというのがあります。

Bag-of-words model - Wikipedia, the free encyclopedia

 これは、その文章にある単語が何回出現するかをカウントしていく方法です。あるニュース記事で、「プログラミング」という言葉が何回も出てくれば、この記事はIT関連かな?っていうのが予測できそうだと思います。例えば、これを広辞苑に乗っている24万語でカウントしたとします。すると、[5, 1, 0, 3,・・・ ]という具合に出現回数の配列ができます。これが紛れもなくベクトルです。この場合、文書は 24 万次元のベクトル(または空間上の座標)として表されます。

 ということは、文章を単語に分ける必要がありそうです。

文章を単語分割する

 形態素解析という難しそうな言葉がありますが、これは、簡単に言うと文章を品詞単位に分解することです。通常、英語では単語と単語の間をスペースで区切るので単語分割は容易ですが、日本語は助詞などを続けて書くのが通常ですので、話はそんなに単純ではありません。

 ここで、有効なのがオープンソースの日本語の形態素解析エンジンである『Mecab』です。

MeCab: Yet Another Part-of-Speech and Morphological Analyzer

 今回はrubyからMecabを利用するためにnattoというgemを使用しました。nattoは、形態素解析エンジンのMeCabrubyから使うためのインタフェースの役割をします。

 MecabとNattoの導入に関しては以下のリンクが参考になります。

5分でMacにMecabをインストールする方法 | Brainvalley 人工知能と脳科学のアーカイブサイト。

【簡単】RubyとMecabを使うならnattoがおすすめ! | Brainvalley 人工知能と脳科学のアーカイブサイト。

Mecabをインストールすると、下のようにコマンドラインからすぐに利用できます。

$ mecab
今日の天気は晴れのち曇り
今日 名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
の 助詞,連体化,*,*,*,*,の,ノ,ノ
天気 名詞,一般,*,*,*,*,天気,テンキ,テンキ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
晴れ 名詞,一般,*,*,*,*,晴れ,ハレ,ハレ
のち 名詞,副詞可能,*,*,*,*,のち,ノチ,ノチ
曇り 動詞,自立,*,*,五段・ラ行,連用形,曇る,クモリ,クモリ
EOS

 MeCabには、標準的な辞書として「ipadic」というものが提供されており、これをもとに単語分割します。この単語分割の際にどの辞書を使うかはかなり重要です。その理由は、単語というものは常に新しいものが出てくるため、それに対応するために辞書も更新しないといけないからです。

 また、上の例を見てわかるように「の」や「は」などの助詞は文章の特徴を表さないので、今回は名詞のみをカウントするようにしました。

 ここで、一つ疑問が湧きました。単語をカウントするといっても、「今日」とか「人」のようにどんな文章にもよく出てくるものもあれば、「人工知能」や「プログラミング」のように、特定の分野の文章にしか登場しないような単語もある。これを同じようにカウントすると、うまく文章の特徴を表せないのではないか。

文書内の単語の重み付け: TF-IDFについて

 文書内の単語の重み付けにTF-IDFという手法を使います。TF-IDFでできることは、文書内での単語の重要度を評価して、文章の特徴語を抽出することです。TF-IDFについては、TFとIDFに分けて理解するとわかりやすいです。

tf-idf - Wikipedia

TF

 TFはTerm Frequencyの頭文字をとったもので「文章中にある単語が何回現れたか」のことです。

 計算式は、

  TF = 単語の登場回数 / 文章中の単語の総数

つまり、「ある文章中に、何回も出てくる言葉ほど重要」ということです。

IDF

 Inverse Document Frequencyの頭文字をとったもので、「文章群の中で、ある単語がいくつの文章に現れたか」のことです。

 計算式は、

  IDF = log(文章群中の文章の総数 /その単語が出てくる文章数)

 つまり、単語の珍しさのようなもので、「いろんな文章によく出てくる単語はそんなに重要じゃない」ということです。

 このTFとIDFをかけ合わせたものが、TF-IDFの値となり、ある文章のTF-IDF値の配列が、その文章の特徴ベクトルとなります。

 文章の特徴ベクトルまで求めることができれば、上述のcos類似度の式で、類似度を数値化することができるわけですが、あと一つ困難なポイントがあります。IDFのところで、「文章群」と書きましたが、この文章の集まりをどこから持ってくるかというのがかなり重要です。つまり、TF-IDF法では重要度を分析したい文書だけでなく,その他の関係ないたくさんの文書の集まりが必要となるわけです。

IDFに何を採用するか

 一つの方法として、評価対象のデータと同じ場所から文書を集める方法です。例えば、ニュース記事の類似度を評価したいのであれば、ニュースサイトから5000記事を取ってきて、文書群とし、IDFを算出するというようなやり方です。

 私は今回、Wikipediaから文章を取ってきました。Wikipediaは原則スクレイピングが禁止されているみたいですが、下記のリンクから、文章を全てダウンロードできるようになっています。

Index of /jawiki/

 今回はWikipediaのAbstractを抽出し、データ量が膨大すぎるので、そこからさらに文章数を絞ってDFの算出に使っています。

精度をあげるためにやりたいこと

形態素解析に用いる辞書の検討

 mecab-ipadic-NEologdというものがあることを知りました。 mecab-ipadic-NEologdは、多数のWeb上の言語資源から得た新語を追加することでカスタマイズした MeCab 用のシステム辞書みたいです。その他にも、Wikipediaはてなキーワードなどを導入したりして新語に対応した辞書を作っているみたいです。

GitHub - neologd/mecab-ipadic-neologd: Neologism dictionary based on the language resources on the Web for mecab-ipadic

IDF算出に用いる文書群の検討

 今回は、WikipediaのAbstractを使いましたが、ここの選択が結果に大きく影響を与えることは間違いないので、いろいろ試してみたいと思います。

単語の出現位置

 単語が文章中のどの位置に出現するかということも取り込めたら、精度が上がりそうな気がします。例えば、大事なことは、最後の方に出てくるとか。どんなアルゴリズムで実装できるかはわかりませんが。

今後やること

 まだまだ精度をあげるための工夫は必要ですが、文章の特徴ベクトルが求められるようになったということは、それなりに汎用性があると思います。未知の文章でも、今回の方法で特徴ベクトルを作れば、k-meansなどで実装した分類器にかけて、文章のカテゴライズなどもできるはずです。

   今後は、今回作ったものをベースに、より高度なアルゴリズムの導入とクローラーと併せてWebアプリ化をやっていきます。最終的には、ユーザに応じてどの記事を読んだら勉強になるかを優先順位をつけて推薦してくれるようなシステムを作りたいです。結局はGunosyとかがやっていることと同じですが、それを「学び」に特化した形でやりたいと思っています。