PerlではじめるテキストマイニングB!

■前置き

みなさんこんにちは。ダウンロードたけし(寅年)です。来年は年男なので今からお正月が待ち遠しい35歳2児の父です。

ここ数年、web広告業界ではコンテキスト解析とかユーザの行動分析とか、いわゆるデータマイニング/テキストマイニング系の話題が花盛りです。

自分もそんな業界に属しているんですが、ふと気がつくと日本語のテキストマイニング系モジュールを量産してしまっているので、ここらでいくつか紹介してみたいと思います。

今回はインターネットからブログなどのコンテンツを取得して、それを意味解析してクラスタリングする、といったようなことを題材にモジュールの紹介をしてみます。

■HTML::Featureで本文抽出

まずは分析する対象のデータを持ってくるところからなんですが、そこの所は割愛してコンテンツを持ってきたところから話をします。

持ってきたHTMLデータにはヘッダやらフッタやらサイドメニューやら、おおよそ本文とは関係ない部分がたっぷり含まれていますよね。

これらのノイズをどうにかして除去したい。逆に言うと本文だけを効率よく抽出したい、と考えるわけです。

そこでHTML::Featureというモジュールをつくりました。

事前の定義とか一切なしで、おおよそいい感じで本文部分を推測して抽出してくれます。

使い方はこう。

use HTML::Feature;
my $f = HTML::Feature->new(
    engines => [
        'HTML::Feature::Engine::LDRFullFeed',
        'HTML::Feature::Engine::GoogleADSection',
        'HTML::Feature::Engine::TagStructure',
    ]
);
print $f->parse($url)->text;

engines のところにごちゃごちゃ書いてますが、これは抽出ロジックのエンジン達です。ここに複数の抽出ロジックを並べておくと、うまく抽出できるまで上から順に試していきます。

エンジンは以下の3つを用意しています。

  • LDRFullFeed
    • WedataのLDRFullFeedデータを使ってXPathで本文箇所をピックアップします。なのでマッチするURLであればとても正確。
  • GoogleADSection
    • Google ADSenceを導入している場合はタグで囲まれてる部分を正規表現で抜きます。なのでこれもタグがあれば正確。
  • TagStructure
    • HTMLタグの構成を解析して各DOMノードをスコアリングし重要そうな場所を推測。明確に本文があれば結構正確だけど、リンクリストや記事がないようなサイトは苦手。

さて、いまここで「特定サイトのコンテンツだけは、手動で定義してでも正確に本文抽出したい」というような要望があったとします。その場合には独自のエンジンを記述することも可能です。

独自エンジンをengines の先頭に配置すれば、「 独自エンジン > LDRFullFeed > GoogleADSection > Tagstructure 」の優先順位で処理が進みます。

ちなみにenginesに何も指定しなければTagStructureがデフォルトエンジンとして動きます。

まずはこんな感じで本文と思わしきところだけを適当に引っこ抜いておきます。楽チンですね!

 参考)

  http://d.hatena.ne.jp/download_takeshi/20090728/1248813497

■特徴語の抽出 (Lingua::JA::TFIDF, Lingua::JA::OkapiBM25)

さて本文だけをスッポリ抽出できたら、次は本文の中からさらに「特徴的な単語」を抽出していきます。

情報検索の世界では特徴語抽出は「いろはのいの字」的なものです。それに関しては古くから「TF/IDF」というアルゴリズムが王道中の王道とされてきました。

ところが、いざ真面目に取り組もうとすると、仕込みというか事前準備がそれなりに必要となってしまい、「気軽に試してみる」というにはちょいと面倒な処理でした。

そこで「精度はちょっとテキトーでもいいから手軽にやってみたいよー」というラテン系な人のためにLingua::JA::TFIDFというモジュールを作りました。

なんの前準備もいらないので手軽です。内部的にかなり大胆な(テキトーな)計算をしてるんですが、その割にまあまあの精度がでます。

use Lingua::JA::TFIDF;

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

# 特徴語とスコアのハッシュをスコアの高い順に上位10件表示
print Dumper $result->list(10);

最近ではTF/IDFのかわりに「BM25」というアルゴリズムも使われるようです。TF/IDFの改良版みたいなものですね。

こっちについてはつい数日前にモジュール化してみました。

Lingua::JA::OkapiBM25です。

使い方はLingua::JA::TFIDFとほぼ同じなのでコードは割愛しますが、試してみるとTF/IDFよりもややバランスがいいかな、といった結果が得られました。

 参考)

  http://d.hatena.ne.jp/download_takeshi/20081031/1225463411

  http://d.hatena.ne.jp/download_takeshi/20091206/1260130230

■Lingua::JA::Categorizeで文書分類してみる

いよいよ大詰めです。

ドキュメントから特徴語を抽出できるようになったら、あとは大量にデータをさばいていきましょう!

先ほどのLingua::JA::TFIDFなどでのアウトプットとして { URL => { 特徴語 => スコア, 特徴語 => スコア, ... }, ... } なデータが大量に蓄積できました。

これらのデータを使ってなにか面白いことができそうな気がしますよね!?

それではLingua::JA::Categorizeを使ってベイジアンによる文書分類器でもつくってみましょう。

Lingua::JA::Categorize は「お好みのカテゴリ構成の分類器をスピーディーに作るため」に書いたモジュールというかフレームワークのようなものです。

作り方は超簡単。モジュールをインストールして、適当なカテゴリ一覧をYAMLで書いて、あとはgenerate()と唱えてください。

トイレに行ってコーヒーでも飲んでいる間に分類器が出来上がってるはずです!

use Lingua::JA::Categorize;
my $c = Lingua::JA::Categorize->new;
$c->generate($category_config);
$c->save('save_file_name');

分類器を使うときはこう書きます。

use Lingua::JA::Categorize;
my $c = Lingua::JA::Categorize->new;
my $result = $c->categorize($text);
print Dumper $result->score; # 分類結果(1位から3位)

らくらくですね♪

 (参考)

  http://d.hatena.ne.jp/download_takeshi/20081124/1227539934

■クラスタリングをガンガンこなす(Text::Bayon)

さきほどの文書分類器は事前にカテゴリを定義して、それに即して自動的に文書を分類するものでした。

ですが実際にカテゴリリストを自分で定義しようとすると「世の中にはどんな分野のテーマがどれくらいあるかなんてわからないよ~、適切なカテゴリなんて決められないよ~」という事態にすぐに陥るわけです。

そこでちょっと視点をかえて、K-means法などを使って与えられたデータを自然なカタチにクラスタリング処理して、その上で各クラスタの重心にあつまってるデータを目視して、実際のカテゴリを検討する、みたいなことをしたくなるはずです。

そこでText::Bayonの登場です! (昨日書いたばかりのできたてホヤホヤだよ!)

Bayon」というのは非常に優秀な軽量クラスタリングツールでして、Mixiのfujisawaさんという方が開発&公開されています。

Text::Bayon はこの便利なBayonをperlからシームレスに利用するためのハンドリングモジュールです。これを使えばperlから透過的にBayonを使えるようになります。

use strict;
use Text::Bayon;

my $bayon = Text::Bayon->new;

# 架空のデータ生成関数
# { ドキュメントID => { 単語 => スコア, ... }, ... } なデータ構造を生成
my $input = _gene_data(); 

# Bayonに渡す任意のオプション
my %options = (
    number => 10,
    point  => 1,
    idf    => 1,
);

# クラスタリング!
my $output = $bayon->clustering($input_data, \%options);

print Dumper $output;
# 結果データはこんな構造
# { クラスタ => [ ドキュメントID, ドキュメントID, ... ], ... }

スムーズですね!そもそも便利なBayonですが、もっと便利に使えるようになりました。

■まとめ

こんな感じで、他にもいろいろなモジュールを使ったり、作ったりしながら、毎日毎日、飽きもせずデータマイニング的なことをしております。

今度どこかで「オレもPerlでマイニングしてるゼ」という人たちで集まってみたいものですね!

明日は id:ktatさんです。お楽しみに~。