プログラマでありたい

おっさんになっても、プログラマでありつづけたい

RubyでYahoo! キーフレーズ抽出APIを使ってテキストマイニング

 Rubyでキーワード抽出およびTF-IDFでキーワードの重要度の重み付けが出来んかなと思ってGemを漁ってみました。残念ながら、日本語データに関しては、それらしいのは見つかりませんでした。キーワード抽出については、Mecabで形態素解析後に品詞同士のつながりのパターンを考えれば、割りと簡単に作れます。昔作ったことがあるけど、ソースはどっかに旅立たれていたので、改めて作った上でGitHubに登録しようかなぁと思います。またTF-IDFについては、何と言ってもIDFのデータをどうするかがポイントになります。そのあたりのデータ含めて、PerlのLingua::JA::TFIDFを参考に移植させて貰おうかなぁと考えています。

 一方で、てっとり早く上記の2つを実現する方法もあります。Yahooのキーフレーズ抽出APIを利用すると、キーワード抽出と重要度のスコアリングを同時にやってくれます。ただし、24時間あたり5万リクエストと1リクエストあたりのサイズや、レスポンスが上位20件までしか返さないという制約があります。
 それで充分という場合は、さくっと利用するとよいでしょう。下記は、簡単なサンプルです。Yahooのニュースを抜き出して、重要なキーワードを抜き出すといったイメージです。定期的に流していると、ニュースのトレンドの推移などが解ってくるのではないでしょうか?

require 'open-uri'
require 'rexml/document'
require 'nokogiri'

APPLICATION_ID = ENV["YAHOO_API_KEY"]
BASE_URL   = 'http://jlp.yahooapis.jp/KeyphraseService/V1/extract'

$word_list = Hash::new

def request(text)
  app_id = APPLICATION_ID
  params = "?appid=#{app_id}&output=xml"
  url = "#{BASE_URL}#{params}"+"&sentence="+URI.encode("#{text}")
  response = open(url)
  doc = REXML::Document.new(response).elements['ResultSet/']
  doc.elements.each('Result') {|element|
    text = element.elements["Keyphrase"][0]
    score = element.elements["Score"][0] #キーワードの重要度は、とりあえず切り捨て
    $word_list["#{text}"] = $word_list["#{text}"].nil? ? 1 : $word_list["#{text}"]+1
  }
end

def get_urls(page_url)
  urls = Array.new()
  uri = URI.parse(page_url)
  doc = Nokogiri::HTML(open(page_url))
  doc.xpath("//*[@id='main']//ul[@class='list']//a").each do |anchor|
    url = anchor[:href]
    url = uri.merge(url) if not uri =~ /^http/
    urls << url
  end
  return urls
end

def get_headline_text(page_url)
  text = ""
  doc = Nokogiri::HTML(open(page_url))
  if page_url.to_s.match(/dailynews/)
    text = doc.xpath("//*[@id='detailHeadline']").text
  else
    text = doc.xpath("//*[@id='main']//p[@class='hbody']").text
  end
  return text.gsub(/\n/,"")
end

def get(page_url)
  urls = get_urls(page_url)
  urls.each {|url|
    text = get_headline_text(url)
    #p text
    request(text)
  }
end

page_url = 'http://news.yahoo.co.jp/list/?c=economy'
get(page_url)
$word_list.each{ |key,value|
  p "#{key}=#{value}"
}


 サクッと作っただけなので、悪しからず。



See Also:
これはセンスが良い。Lingua::JA::TFIDF - プログラマになりたい
特徴語抽出のあれこれ - プログラマになりたい
キーワード抽出モジュール Lingua::JA::Summarize - プログラマになりたい


参照:
Lingua::JA::TFIDF - search.cpan.org
テキスト解析:キーフレーズ抽出API - Yahoo!デベロッパーネットワーク

HTML::ExtractContentを使って本文抽出

 先日、ブログの本文抽出をしたいと思ってちょっと調べてみました。rubyベースの実装のExtractContent.rbが良さげと思い色々試してみました。実際、良かったです。 ただ私の方が、あまりrubyに詳しくないことと、既にあるPerlのプログラムに組み込みたい制約があるので、別のものを探しました。それで見つけたのが、HTML::ExtractContent。その名もずばりの物がありました。よくよく見てみると、ExtractContent.rbを元に作っているようですね。

ACKNOWLEDGEMENT

Hiromichi Kishi contributed towards development of this module as a partner of pair programming.

Implementation of this module is based on the Ruby module ExtractContent by Nakatani Shuyo.
AUTHOR

INA Lintaro
COPYRIGHT

Copyright (C) 2008 INA Lintaro / Hatena. All rights reserved.
Copyright of the original implementation

Copyright (c) 2007/2008 Nakatani Shuyo / Cybozu Labs Inc. All rights reserved.
LICENCE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

 で、実際に触ってみました。下記のソースで抽出できます。

use strict;
use warnings;
use HTML::ExtractContent;
use LWP::UserAgent;

my $url = shift @ARGV;
my $ua = LWP::UserAgent->new;
$ua->agent('Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)');
my $res = $ua->get($url);

my $extractor = HTML::ExtractContent->new;
$extractor->extract($res->decoded_content);
print $extractor->as_text,"\n";


ブログの本文抽出の結果

$ ./call.pl http://d.hatena.ne.jp/dkfj/20081128/1227832594
■[自然言語処理]ブログの本文抽出
必要に迫られて、ブログの本文抽出をしたいと思います。2年前くらいにも少し試みたことがあるのですが、ソース消失。。。
アプローチとして、2つの方法があると思います。
・各ブログサービス(livedoor、ameba、hatena等々)ごとの構造を解析して、それぞれ専用のモジュールを作る
長所としては、抽出の精度が高くなる。
短所としては、ブログの構成が変わる度にバージョンアップの必要がある。
作成するモジュールが多い。対応外のブログはお手上げ。
・RSSのdescription等を活用して本文部分を推定する、汎用モジュールを作る
長所としては、一つのモジュールのみ保守すれば良い。対象のブログサービスの構成が変わっても影響がない(はず)
短所としては、抽出精度の限界がある。おそらく90%くらいが限界か
世の中の事例を見ていると、2番目のアプローチが多いようです。有名どころをピックアップ
Ceekz Logs:ブログの本文抽出にチャレンジ
zuzara:ブログの記事本文を抽出するスクリプトをつくってみた
MOONGIFT:タイトル・本文抽出クローラー「Webstemmer」
nakatani @ cybozu labs:Webページの本文抽出
nakataniさんが、Rubyの抽出プログラムを公開してくださっているので、そちらから試そうかと思います。また、経過を報告したいと思います。
Permalink | コメント(0) | トラックバック(0) | 09:36

 素晴らしいです。逆に、HTML Extractとなっていますが、Yahooのトップページのようなものは苦手なようです。(そりゃそうですね)

ブログの本文抽出

 必要に迫られて、ブログの本文抽出をしたいと思います。2年前くらいにも少し試みたことがあるのですが、ソース消失。。。


アプローチとして、2つの方法があると思います。
・各ブログサービス(livedoor、ameba、hatena等々)ごとの構造を解析して、それぞれ専用のモジュールを作る
  長所としては、抽出の精度が高くなる。
  短所としては、ブログの構成が変わる度にバージョンアップの必要がある。
  作成するモジュールが多い。対応外のブログはお手上げ。
・RSSのdescription等を活用して本文部分を推定する、汎用モジュールを作る
  長所としては、一つのモジュールのみ保守すれば良い。対象のブログサービスの構成が変わっても影響がない(はず)
  短所としては、抽出精度の限界がある。おそらく90%くらいが限界か


 世の中の事例を見ていると、2番目のアプローチが多いようです。有名どころをピックアップ
Ceekz Logs:ブログの本文抽出にチャレンジ
zuzara:ブログの記事本文を抽出するスクリプトをつくってみた
MOONGIFT:タイトル・本文抽出クローラー「Webstemmer」
nakatani @ cybozu labs:Webページの本文抽出


 nakataniさんが、Rubyの抽出プログラムを公開してくださっているので、そちらから試そうかと思います。また、経過を報告したいと思います。


追記:
HTML::ExtractContentを使って本文抽出

改めて、集合知プログラミングが素晴らしい

プログラマーに最適なデータマイニングの教科書 『集合知プログラミング』
 簡潔に集合知プログラミングの魅力を伝えている素晴らしいエントリーです。序文で簡単な紹介。魅力1〜3で優れている点、その上でまとめ。最後に関連書籍を、コメント付きで紹介しているのが素晴らしい。書き方の参考にしたいです。


 私も幾つか書きかけで止まっているので、もう一度読み直して身につけようと思いました。
集合知プログラミングが凄すぎる件について
リコメンドエンジンを作る 集合知プログラミング



集合知プログラミング
Toby Segaran
オライリージャパン
売り上げランキング: 190

大量データの処理方法 はてなさんの場合

KOF 2008 の発表資料
 大量データの処理の実際の話。特に気になるのが、ドキュメントのベクトル化。確かにベクトルの方向で類似文章を見つけることは出来ると思うのですが、id:naoyaさん自身が言う通り辞書の単語数×ドキュメント数の計算をしないといけません。大量データを処理する場合、現実にはなかなか難しいと思っていました。
 その辺りは、下記のようにさらっと流されています。転置インデックスを利用と書いているので、関連するドキュメントだけ抜き出して計算ということなんでしょうか。TRIEといえばMecabで有名ですが、色々な所で使われているんですね。

現実的な計算時間で計算するには
 ・行列がスパースであることを利用
   転置インデックスを利用する
 ・top K が取得できれば良い
   様々な手法で足切り


 どちらにしろ、このようにノウハウを惜しげもなく公開して貰えることはありがたいですね。

これはセンスが良い。Lingua::JA::TFIDF

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

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

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

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

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

 TF/IDFのネックは、製作者の指摘の通り「ある程度のボリュームもったドキュメントセット」というところ。これに対しての解は、wikipediaを使う、もしくは、検索エンジンを使うといったものです。が、確かに面倒くさい。
 これに対して、予めドキュメントセットを用意しましたというのが、Lingua::JA::TFIDFの凄いところ。この場合、未知語の対応どうするのという問題が残ります。これに対して、割り切って対応しています。ここがセンス良いなぁと思います。

じつは勘の鋭い人にはすぐにばれてしまいますが、

MeCabが辞書として持っていない「未知語」に対してはどうやってDFを計算しているのか?

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

 さらには、オプションで都度未知語のDFの数値を調べにいくオプションもあるようです。これをフィードバックする仕組みがあれば完璧ですね。


後で、遊んでみよう

キーワード抽出モジュール Lingua::JA::Summarize

 時間が空いたので、Lingua::JA::Summarizeも試してみました。0.07をCPAN経由でインストールしようとすると、途中でエラーが出ました。Class::Accessor::FastとClass::ErrorHandlerに依存するようなので、予めインストールしておきましょう。で、テストがどうしても通らないのですが、force installでとりあえず問題なしのようです。

以下、サンプルコード。CPANのサンプルに、utf8で使うために数行追加しているだけです。

#!/usr/bin/perl

# Functional style

use Lingua::JA::Summarize qw(:all);

# OO style
$s = Lingua::JA::Summarize->new({
      charset => 'utf8',
       mecab_charset => 'utf8',
     })
    ;

$s->analyze_file('news.txt');

@keywords = $s->keywords({ minwords => 3, maxwords => 5 });
print join(' ', @keywords) . "\n";

連続している名詞の評価がポイントということで、どう処理しているのかを見てみました。

if ($H =~ /^名詞/) {
    if ($H =~ /(非自立|代名詞)/) {
        $add_longword->();
        next;
    } elsif (! $longword->{text} && $H =~ /接尾/) {
        # ng
        next;
    }
}

まぁ、これしかないですね。これだと「まつのはこんぶ」などの言葉だと検出出来ないですね。まぁ、どうしろと言われたら、ちょっと困りますが。。。

$ mecab
まつのはこんぶ
まつの  名詞,固有名詞,人名,名,*,*,まつの,マツノ,マツノ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
こんぶ  名詞,一般,*,*,*,*,こんぶ,コンブ,コンブ


ウェイトの計算は、ここで解説しているようにMecabのコストを使っているようです。なるほどなぁと思いました。

sub _calc_weight {
    my $self = shift;
    foreach my $word (keys(%{$self->{stats}})) {
        my $target = $self->{stats}->{$word};
        my $cost = $target->{cost};
        $cost = $self->default_cost * DEFAULT_COST_FACTOR unless $cost;
        $target->{weight} =
            ($target->{count} - 0.5) * $cost / $self->{wordcount} / 6;
        if (length($word) == 1) {
            $target->{weight} *= $self->singlechar_factor;
        }
    }
}

シンプルなソースで、非常に勉強になります。