手軽にTF/IDFを計算するモジュール

情報検索の分野でよく使われるアルゴリズムで「TF/IDF」というものがあります。

ドキュメントの中から「特徴語」を抽出する、といったような用途でよく使われています。

TF/IDFアルゴリズムのくわしい解説はこことかここを見てください。

今回はこのTF/IDFの計算を「簡単」に実現するためのperlモジュールをCPANに上げましたので、ご紹介します。なまえはLingua::JA::TFIDFといいます。

Lingua::JA::TFIDF - TF/IDF calculator based on MeCab.
http://search.cpan.org/~miki/Lingua-JA-TFIDF

TF/IDF実装の困りどころ

TF/IDFの実装を試みた方であればわかると思うのですが、実際にやろうとすると、TF(Term Frequency)の計算はなんら難しくありませんが、IDF(Inverse Document Frequency)の計算がネックとなることがしばしばあったりします。

というのも、IDF(というかDF)については、「ある程度のボリュームもったドキュメントセット」を必要とするためです。

なので独自に大量のドキュメントを用意するか、wikipediaのデータ使ったりするか、もしくは検索エンジンコーパスに見立ててDFの数(検索したヒット数)を問い合わせる、といった類いの方法で実装することが多いかと思います。

独自に大量ドキュメントセットを用意するのはだいぶ面倒だし、1単語ずつ検索エンジン様に問い合わせるという戦略はHTTP的に高コストになってしまいます。

もっと手軽にさっくりとTF/IDFの計算ができないものか。。。

そこでLingua::JA::TFIDFの登場です!

仕組み

Lingua::JA::TFIDFは先述の後者側の方法(1単語ずつ検索エンジンにヒット数を問い合わせる方法)を「事前に済ませておいたよ」というモジュール、というかデータセットだと思っていただければわかりやすいです。

もっと平たく言うと、

 1. MeCabIPA辞書の中から、いくつかの品詞を条件に単語をピックアップ(約25万語)
 2. それらを事前にYahoo Web APIに対して問い合わせ、個々のヒット数を調査しておく
 3. それらの結果データをシリアライズして保存
 4. このデータを使って任意の文書のTF/IDFを計算する

といったモジュールになります。

上記の1〜3の処理で生成したデータファイルを同梱してあるので、
ユーザはなにも気にせず、TF/IDFの命令を実行するだけでOKというわけです。

ベタな戦法ですが、必要な人にとってはきっと「涙がでるほど嬉しい!」はずです。

ちなみに全面的にMeCab様に依存しているものなので、当然MeCabがインストールされていることが条件になります。

実例

使い方はこんな感じです。簡単です。

use Lingua::JA::TFIDF;
use Data::Dumper;

my $text = q(
我々は一人の英雄を失った。これは敗北を意味するのか?否!始まりなのだ!地球連邦に比べ我がジオンの国力は30分の1以下である。にも関わらず今日まで戦い抜いてこられたのは何故か!諸君!我がジオンの戦争目的が正しいからだ!一握りのエリートが宇宙にまで膨れ上がった地球連邦を支配して50余年、宇宙に住む我々が自由を要求して、何度連邦に踏みにじられたかを思い起こすがいい。ジオン公国の掲げる、人類一人一人の自由のための戦いを、神が見捨てる訳は無い。
私の弟、諸君らが愛してくれたガルマ・ザビは死んだ、何故だ!
戦いはやや落着いた。諸君らはこの戦争を対岸の火と見過ごしているのではないのか?しかし、それは重大な過ちである。地球連邦は聖なる唯一の地球を汚して生き残ろうとしている。我々はその愚かしさを地球連邦のエリート共に教えねばならんのだ。
ガルマは、諸君らの甘い考えを目覚めさせるために、死んだ!戦いはこれからである。
我々の軍備はますます復興しつつある。地球連邦軍とてこのままではあるまい。
諸君の父も兄も、連邦の無思慮な抵抗の前に死んでいったのだ。この悲しみも怒りも忘れてはならない!それをガルマは死を以って我々に示してくれたのだ!我々は今、この怒りを結集し、連邦軍に叩きつけて初め真の勝利を得ることが出来る。この勝利こそ、戦死者全てへの最大の慰めとなる。
国民よ立て!悲しみを怒りに変えて、立てよ国民!ジオンは諸君等の力を欲しているのだ。
ジーク・ジオン!!
);

my $calc = Lingua::JA::TFIDF->new;
my $result = $calc->tfidf($text);
print Dumper $result->list(10);

結果

$VAR1 = [
          {
            '連邦' => '51.5824158847192'
          },
          {
            '諸君' => '44.6736574508163'
          },
          {
            '地球' => '25.6811539727557'
          },
          {
            '怒り' => '18.1728130488376'
          },
          {
            '戦い' => '16.9994600697375'
          },
          {
            'ジオン' => '16.8849466710836'
          },
          {
            'エリート' => '14.5288604458417'
          },
          {
            '悲しみ' => '13.444211864191'
          },
          {
            '勝利' => '10.7808653109117'
          },
          {
            '戦争' => '10.6616811165072'
          }
        ];

うーん、いとも簡単に「眉無しギレン様」の顔が思い出されてきますね。

ちなみに$result->dumpとするとTFとDFとMeCabの品詞、未知語かどうか、などの情報が取得できます。

きっかけ

Lingua::JA::TFIDFを作ろうと思ったきっかけは、Lingua::JA::Summarizeのおかげです。

Lingua::JA::Summarizeはid:kazuhookuさんの作でして、IDFの計算を「MeCabの単語生起コストを使う」というとても秀逸なモジュールです。今までも何度かありがたく使わせてもらっていました。作りもスマートで、とても良くできたプログラムだと思います。

しかしながらブログのコメント欄にMeCab作者の工藤さんから以下のようなコメントがありました。

mecab 0.90 から CRF という方法を使っていて、単語正規コスト=~ idf と近時できるわけではありません。
mecab 0.81 や chasen が採用しているコストはそうやっても実用上は問題ないと思います。

okuさんも「がーん。」って書いてましたが、私も「がーん。」と思った訳であります。

「だったら力技でIDFかき集めておこうじゃないの」と思うに至り、Linuga::JA::TFIDFを作るに至ってしまった訳でありまする。

じつはちょっとヘボイ部分もあります

じつは勘の鋭い人にはすぐにばれてしまいますが、
MeCabが辞書として持っていない「未知語」に対してはどうやってDFを計算しているのか?


、、じ、じつはそれは、、「既知語」の平均値を使ってます。。。


だせー、平均値かよぉ www


と笑われてしまいそうですが、それが気になる人は以下のようなオプションをつけてオブジェクトを作ってください。

my $calc = Lingua::JA::TFIDF->new(fetch_df => 1);

こうしておけば未知語に遭遇した都度YahooAPIを叩いてDFの数値を調べに行くようになります。

でも実際に使ってみると、未知語が云々ってあんまり気にならないと思います。

思ったほど未知語にぶつかるケースは多くないというか、まぁ、どんな文書を解析するかによるけどね。

ちなみにギレンの演説をfetch_df付きで改めて解析するとこうなります。

$VAR1 = [
          {
            '連邦' => '51.5824158847192'
          },
          {
            '諸君' => '44.6736574508163'
          },
          {
            'ジオン' => '33.2342173052196'
          },
          {
            '地球' => '25.6811539727557'
          },
          {
            'ガルマ' => '20.9867127672851'
          },
          {
            '怒り' => '18.1728130488376'
          },
          {
            '戦い' => '16.9994600697375'
          },
          {
            'エリート' => '14.5288604458417'
          },
          {
            '悲しみ' => '13.444211864191'
          },
          {
            'ガルマ・ザビ' => '11.654489029292'
          }
        ];

「ガルマ」と「ガルマ・ザビ」がベスト10にランクインしましたぁ!

この差をどうとらえるべきか?ガルマってキーワードは、はたしてどれくらい重要なんでしょうか?

「ギレンの演説においてガルマは超重要でしょ!」という方はfetch_dfオプションを有効にして「正確なTF/IDF値」を計算してください。

「ガルマなんてそんな重要でもないやねー、しょせん脇役でしょ」という人はそのまま使ってください。当然その方が処理は速いので。


個人的には「ガルマ」は超重要人物だと思いますが、やっぱり処理がトロクなるのは嫌なので見殺しにしようと思います。

ギレンに「何故だ!」って聞かれたら?

そりゃもちろん、「坊やだからさ」って答えます。


シューッ!