TFIDFを使ってwikipediaの各キーワードの特徴量を抽出
以前にk-means++をPerlで書いたのですが、実際に試すデータがなかったのでそのまま放置してました。せっかくなので大きなデータで試してみたいので、今回は下準備としてwikipediaの各キーワードに対し、その特徴を表すデータを抽出したいと思います。そして今回作ったデータを使って、k-meansや階層的クラスタリングなど他の手法をいずれ試してみる予定です。
今回は特徴量としてベタにTFIDFを使うこととします。TFIDFについては、下記のページが詳しいためそちらをご参照ください。
まずWikipediaのデータをダウンロードしてきます。以下のページから、「jawiki-latest-pages-articles.xml.bz2」をダウンロードしてください。
次にダウンロードしたXMLからテキストを抽出します。自前で解析してもいいのですが、ちょっと面倒なので既存の公開されているツールを使用します。ここではWP2TXTを使用します。また今回は必要ありませんが、テキスト以外にカテゴリの情報ですとか、リンク関係などの情報を使う場合はMediaWikiのソースに含まれるツール(maintenance/importDumper.php)などを使うとよいようです。
WP2TXTをダウンロードしたら、以下のようにしてWikipediaのXMLからテキストを抽出します。
% tar xvzf wp2txt-0.1.0.tar.gz % mkdir -p ~/data/wikipedia/txt (テキストデータ保存用) % wp2txt-0.1.0/bin/wp2txt ~/data/wikipedia/jawiki-latest-pages-articles.xml.bz2 -d -e utf8 -o ~/wikipedia/data/txt % ls ~/data/wikipedia/txt/ jawiki-latest-pages-articles.xml-001.txt jawiki-latest-pages-articles.xml-002.txt jawiki-latest-pages-articles.xml-003.txt jawiki-latest-pages-articles.xml-004.txt ...
よーし、これで各キーワードに対してその説明の文章を抽出できたので、この説明文の中から特に特徴的な語をTFIDFで抽出したいと思います。まずはTF(Term Frequency, 文章中の単勤の頻度)と、DF(Document Frequency, 各語がいくつの文書に出現したか)を数えます。
作成したコードは以下の通りです。先ほど作成したテキストファイルの各文をTet::MeCabを使って形態素解析して、TFとDFを数え、Tokyo CabinetのHashDBに保存します。以下のところカウントしているのは名詞だけですが、他の内容語(動詞、形容詞、形容動詞)を加えたかったり、名詞の中でもさらに品詞を絞りたい場合はちょろっと修正すればすぐにできます。
#!/usr/bin/perl # # count term frequency, document frequency # # Usage: # count_words.pl -d txtdir -o outdir # use strict; use warnings; use Getopt::Long; use Text::MeCab; use TokyoCabinet; use constant { DBM_TF => 'tf.hdb', DBM_DF => 'df.hdb', }; sub usage_exit { print <<'USAGE'; Usage: count_words.pl -t txtdir -o outdir -t, --textdir dir directory including wikipedia texts -o, --output outdir output directory USAGE exit 1; } sub get_words { my ($mecab, $words, $text) = @_; for (my $node = $mecab->parse($text); $node; $node = $node->next) { my @features = split /,/, $node->feature; #if ($features[0] =~ /^(名詞|動詞|形容詞|形容動詞)$/) { if ($features[0] eq '名詞') { $words->{$node->surface}++; } } } sub add_df { my ($dbm, $words) = @_; return if !$dbm || !$words; foreach my $word (keys %{ $words }) { $dbm->addint($word, 1); } } sub add_tf { my ($dbm, $title, $words) = @_; return if !$dbm || !$title || !$words; my @keys = sort { $words->{$b} <=> $words->{$a} } keys %{ $words }; my $val = join "\t", map { $_.":".$words->{$_} } @keys; $dbm->putasync($title, $val); } sub main { my ($opt_txt, $opt_out); GetOptions( 'textdir=s' => \$opt_txt, 'output=s' => \$opt_out, ); if (!$opt_txt || !$opt_out) { usage_exit(); } my $tfdb = TokyoCabinet::HDB->new(); my $dbpath = $opt_out.'/'.DBM_TF; if (!$tfdb->open($dbpath, $tfdb->OWRITER | $tfdb->OCREAT | $tfdb->OTRUNC)) { die 'cannot open TF dbm'; } my $dfdb = TokyoCabinet::HDB->new(); $dbpath = $opt_out.'/'.DBM_DF; if (!$dfdb->open($dbpath, $dfdb->OWRITER | $dfdb->OCREAT | $dfdb->OTRUNC)) { die 'cannot open DF dbm'; } my $mecab = Text::MeCab->new() or die 'mecab error'; opendir my $dh, $opt_txt or die "cannot open $opt_txt"; my @files = grep /^jawiki-latest-pages-articles\.xml-\d+\.txt$/, readdir($dh); closedir $dh; my $cnt = 1; foreach my $file (@files) { my $path = "$opt_txt/$file"; printf "(%d/%d) %s\n", $cnt++, scalar(@files), $path; open my $fh, $path or warn "cannot open $path"; my ($title, $body, %words); while (my $line = <$fh>) { chomp $line; if ($line =~ /^\[\[(.+)\]\]$/) { my $newtitle = $1; if ($title && $body) { my @sentences = split /。/, $body; foreach my $sentence (@sentences) { get_words($mecab, \%words, $sentence); } add_tf($tfdb, $title, \%words); add_df($dfdb, \%words); } $title = $newtitle; $body = ''; %words = (); } else { $body .= $line; } } } } main(); __END__
実際に実行してみます。TFは下のようなテキスト形式で保存されています。DFはバイナリ形式の整数です。
% mkdir -p ~/data/wikipedia/tfidf % ./count_words.pl -t ~/data/wikipedia/txt -o ~/data/wikipedia/tfidf (1/153) /home/mizuki/data/wikipedia/txt/jawiki-latest-pages-articles.xml-141.txt (2/153) /home/mizuki/data/wikipedia/txt/jawiki-latest-pages-articles.xml-061.txt (3/153) /home/mizuki/data/wikipedia/txt/jawiki-latest-pages-articles.xml-041.txt (4/153) /home/mizuki/data/wikipedia/txt/jawiki-latest-pages-articles.xml-134.txt ... % ls ~/data/wikipedia/tfidf df.hdb tf.hdb % tchmgr list tf.hdb | head 宝井誠明 年:3 シコ:2 型:2 映画:2 -:2 1992:2 月:2 春雄:1 フロム・ファーストプロダクション:1 2:1 1:1 東京:1 >出演:1 役:1 所属:1 明:1 日:1 出身:1 山本:1 11:1 1975:1 俳優:1 デビュー:1 宝井:1 特技:1 *:1 誠:1 都:1 テニス:1 野球:1 まさ:1 O:1 ドラマ:1 CM:1 サッカー:1 数々:1 血液:1 上原カエラ cm:4 AV:2 年:2 月:2 女優:1 ダンス:1 90:1 165:1 ら:1 日本:1 2:1 サイズ:1 W:1 57:1 2008:1 B:1 14:1 H:1 日:1 ホット:1 デビュー:1 スリー:1 特技:1 -:1 87:1 カエラ:1 身長:1 趣味:1 ヨガ:1 上 原:1 1988:1 8:1 37:1 Impression:1 First:1 リチャード・シモンズのスリムな料理SHOW 料理:5 番組:3 リチャード:3 ダイエット:2 放送:2 ジョーク:1 貴史:1 計算:1 >ヘルシー:1 主:1 SHOW:1 タレント:1 有名:1 ・・・:1 ギャグ:1 スリム:1 約束:1 ジョージ:1 オカマ:1 吉田:1 時:1 シモンズ:1 系:1 アメリカ:1 松尾:1 口調:1 エキスパート:1 進行:1 リチャード・シモンズ:1 中心:1 カロリー:1 他:1 CS:1 教祖:1 FOOD:1 チャンネル:1 ...
例えば、キーワード「宝井誠明」の説明文中では「年」「シコ」「型」などが多く現れています。ちょっとゴミが多いですかねー。まあその辺はIDFを使ったときに消えると信じて続けます。本当はこの段階で数字とか「ため」みたいなあまり意味のない語はフィルタリングした方がよさそうですが。
次に上で数え上げたTFとDFを使って、各キーワードの説明文中の語のTFIDFを計算します。そしてTFIDF値の高い上位50語を保存します。作成したコードは以下の通りです。
#!/usr/bin/perl # # TFIDF # # Usage: # tfidf.pl -t tfdb -d dfdb -o tfidfdb # use strict; use warnings; use Encode qw(encode decode); use Getopt::Long; use Text::MeCab; use TokyoCabinet; use constant { DBM_TFIDF => 'tfidf.hdb', MAX_WORD => 50, }; sub usage_exit { print <<'USAGE'; Usage: tfidf.pl -t tfdb -d dfdb -o tfidfdb -t, --tfdb dbm Term frequency(TF) dbm -d, --dfdb dbm Document frequency(DF) dbm -o, --output dbm TFIDF dbm USAGE exit 1; } sub calc_tfidf { my ($tf, $df, $total) = @_; my $idf = log($total / ($df + 1)); return $tf * $idf; } sub main { my ($opt_tf, $opt_df, $opt_tfidf); GetOptions( 'tfdb=s' => \$opt_tf, 'dfdb=s' => \$opt_df, 'output=s' => \$opt_tfidf, ); if (!$opt_tf || !$opt_df || !$opt_tfidf) { usage_exit(); } my $tfdb = TokyoCabinet::HDB->new(); if (!$tfdb->open($opt_tf, $tfdb->OREADER)) { die 'cannot open TF dbm'; } my $dfdb = TokyoCabinet::HDB->new(); if (!$dfdb->open($opt_df, $dfdb->OREADER)) { die 'cannot open DF dbm'; } my $tfidfdb = TokyoCabinet::HDB->new(); if (!$tfidfdb->open($opt_tfidf, $tfidfdb->OWRITER | $tfidfdb->OCREAT | $tfidfdb->OTRUNC)) { die 'cannot open output dbm'; } my $total = $tfdb->rnum(); $tfdb->iterinit(); while (my $title = $tfdb->iternext()) { my $tfval = $tfdb->get($title); next if !$tfval; my %tfidf; foreach my $item (split /\t/, $tfval) { my $itemdec = decode 'utf8', $item; my ($word, $tf) = split /:/, $itemdec; $word = encode 'utf8', $word; my $dfval = $dfdb->get($word); my $df = $dfval ? unpack 'n', $dfval : 0; $tfidf{$word} = calc_tfidf($tf, $df, $total); } my @words = sort { $tfidf{$b} <=> $tfidf{$a} } keys %tfidf; @words = @words[0 .. MAX_WORD-1] if scalar(@words) > MAX_WORD; next if !@words; my $tfidfval = join "\t", map { $_.":".int($tfidf{$_} * 100) } @words; #print "$title\t$tfidfval\n"; $tfidfdb->putasync($title, $tfidfval); } } main(); __END__
それでは実行して、各キーワードのTFIDFを求めてみます。
% ./tfidf.pl -t ~/data/wikipedia/tf.hdb -d ~/data/wikipedia/df.hdb -o ~/data/wikipedia/tfidf.hdb % tchmgr get ~/data/wikipedia/tfidf.hdb SMAP アーロン・ロジャース 指名:9952 QB:4464 パス:3605 ファーヴ:3340 ロジャース:3178 年:2976 グリーンベイ・パッカ ーズ:2665 こと:2581 パッカーズ:2367 全体:2216 カリフォルニア:1976 彼:1873 1:1683 先発:1531 シー ズン:1521 .:1474 %、:1412 2:1353 アレックス:1335 オークランド・レイダーズ:1332 ニューイングランド・ペイトリ オッツ:1332 アメリカンフットボール:1332 8:1199 ドラフト:1182 権:1156 位:1138 大学:1082 中:1080 インターセプ ト:1047 ヤード:1034 引退:1010 タッチダウン:967 NFL:954 時:950 トレード:929 成功:903 出身:901 戦:901 63:844 28:838 負傷:836 入団:817 会場:817 ヤーテージ:777 ミネソタ・バイキングス:777 がち:769 プレー:743 スミス:734 回:721 ワーナー:708 藤原雅長 年:5208 雅:1285 藤原:1271 近臣:1263 位:1138 従:1118 任:1110 長:957 元年:903 3:861 7:813 院:798 近衛:782 五位上民部権大輔:777 嘉:770 視:764 4:760 下:747 日:747 左:713 隆信:708 月:689 わら:638 叙:629 保:600 8:599 能:595 参議:572 守:556 蔵人:521 久安:515 五:507 少将:506 26:503 顕能:500 在任:460 2:451 ふじ:448 中将:448 19:445 処分:431 久:425 死去:424 任命:422 兼:416 教:406 後期:403 長男:401 この間:377 辞任:377 カイロの紫のバラ 映画:4659 セシリア:2333 トム:1738 カイロ:1250 ニュージャージー:1110 撮影:1069 ギル:1057 賞:1041 の:1026 紫:1021 バラ:971 生活:901 ジェフ・ダニエルズ:777 ラリタン・ディナー:777 バー トランドアイランド・アミューズメントパーク:777 サウス・アムボイ:777 館:690 ため:663 役:655 夢中:627 製作:593 モンク:585 アイエロ:583 ミア・ファロー:478 普通:467 監督:462 ハット:460 恋人:455 フレッド・アステア:434 of:433 彼女:430 films:428 Cairo:428 失業:422 作:412 TIME:409 英国:401 ALL:397 年代:395 夫:382 Rose:377 中:360 タイム:358 ロジャース:353 The:351 悲惨:350 ウェ イトレス:350 州:346 席:344 上映:334 ...
ゴミっぽい語がまだいくつか入っていますが、比較的特徴的な単語が上位にくるようになりました。
さて、これで各キーワードに対して、その特徴を表すベクトルが抽出できたことになりますので、次はこの特徴ベクトルを使ってまた遊びたいと思います。とりあえずはこの前のk-meansに入力を今回のDBMを使うようにして、なおかつC++で書き直して試してみますかねー。
そうそう、遊ぶにしてもできれば真面目にやりたいので、そろそろIIRをちゃんと読もうかな…。