Gigazinize のなかみ
書いた人: noriaki 2007,09月07日(金) 18:00
先日のエントリでご紹介したGigazinizeですが,そのなかみの戦略や,利用しているWebサービスなどをご紹介します.
Gigazinizeがやってることの概観
まずは,Gigazinizeの内部でやっていることを順を追って説明します.
- 入力されたテキストをYahoo! 日本語形態素解析 Webサービスに丸投げ
- 返ってきた結果のうち,出現頻度Top10の単語をYahoo! ウェブ検索 Webサービスにそれぞれ投げる
- それぞれの検索結果ヒット数を利用して疑似IDFを求める
- (2)の出現頻度TFと掛け合わせてTF・IDFを求める
- TF・IDFのTop5の単語を半角空白でつないでFlickr APIで画像検索
- 画像がヒットするまで問合せの単語数を減らして(5)を繰り返す
列挙してみるとほぼ全てを外部のWebサービスに任せてます.以下では, TF・IDFの計算と,各WebサービスをRails(Ruby)から利用する方法について少し詳しく書いてみます.
単語のTF・IDFを計算する
TF・IDFは,文章中の特徴的な単語(重要とみなされる単語)を抽出するためのアルゴリズム
で,とても単純に言い表せばその文章に何が書いてあるかを表す単語を抽出するために単語に重み付けする方法です.
その名のとおり,TF(単語の出現頻度)とIDF(文書の逆頻度)を組み合わせてテキストから特徴語を抽出しています.
このとき,TFは数えるだけなので簡単なのですが,IDFに関しては基となる文書集合が必要になります.そこで,たつをさんのエントリを参考にして,Yahoo! ウェブ検索サービスを擬似的な文書集合に見立て,Web全体の文書数を192億と仮定して計算しました.
単語のスコア = 単語の出現頻度 * Math.log(Web全体の文書数 / Yahoo!ウェブ検索でのヒット数.to_f)
Railsから各Webサービスを利用する
Yahoo!JAPAN Web Services
Yahoo! Webサービスの各種APIをRails(Ruby)から利用するためには,net-yjwsライブラリが非常に便利でした.
このnet-yjwsはYahoo!デベロッパーネットワークで公開されているWebAPIをRubyから簡単に扱うことを可能にするライブラリで,今回利用した形態素解析WebサービスやWeb検索サービス以外にも,画像検索や,Yahoo! オークション,Yahoo! 地図情報などのAPIを利用することができます.
今回私が書いたソースコード(後述)では,送信する文字列長の問題から形態素解析サービスへの接続メソッドをPOSTで行うようにしたり,得られるデータを効率的に扱うために出力をいじったりしてますが,基本的にインスタンスを作ってAppIDと問合せを設定して実行するだけと簡単に扱うことができます.
使い方は例えばこんな感じです.(事前に上記ページからダウンロード&インストール)
yjws_test.rb
$KCODE = 'u' # Yahoo!から返ってくる値の文字コードはUTF-8
require 'net/yjws'
yjws = Net::YJWS::WebSearch.new
yjws.appid = 'gigazine_method_apply_image' # 自身のAppIDに置き換えてください
yjws.query = 'rails yahoo web services'
results = yjws.execute
results.each{|r| puts "#{r.title}: #{r.url}"}
実行結果
のほほん徒然 - Haml を実際に Rails で使うチュートリアル: http://d.hatena.ne.jp/uchiuchiyama/20070228/haml_tutorial
第1回 vol. 1 Yahoo! Mail Web Services等 : 秋元@今週の注目サービス : 記事 : MASHUPEDIA - マッシュペディア - : Web API x Mashup: http://www.mashupedia.jp/docs/view/5
日本語で読めるAjax関連情報のリンク集 【▲→川俣晶の縁側→ソフトウェア→技術雑記】: http://mag.autumn.org/Content.modf?id=20050928172048&mf_d=1
のほほん徒然: http://d.hatena.ne.jp/uchiuchiyama/20070228
(..省略..)
Flickr Web Services
Flickr ServicesをRails(Ruby)から利用するために,今回はFlickr.rbを利用しました.このライブラリで初期値として使われているAPIkeyがエラーで使えず苦労しましたが,Flickrへの接続インスタンスの生成メソッドをオーバーライドすることで解決しました.
具体的には,検索時と,検索してきた画像のURLやタイトルの取得時に別のAPIkeyが使われていたため, RuntimeError: Invalid API Key (Key has expired)が発生していました.
この問題を解決しつつFlickr.rbを試すには以下のようにします. また,Gigazinizeでは画像のライセンスを再利用可能なものに限定するために,検索時のオプションを追加しています(後述ソースコード参照).
インストール
Flickr.rbはrubygemsパッケージが用意されていますので,以下のようにしてインストールします.
$ sudo gem install flickr --include-dependencies
または,Flickr.rbのページからライブラリファイルをダウンロードしてきてload_pathの通ったところに置けばOKです.
flickr_test.rb
$KCODE = 'u' # 画像のタイトルなどにマルチバイト文字を含むため
require 'rubygems' # rubygems経由でインストールした場合
require 'flickr'
# Flickr.initializeメソッドをオーバーライド
# api_keyは自身のAPIkeyに置き換えてください
class Flickr
def initialize(api_key='4c3c6d6ff60a36824c9d3d8962cadc9e', email=nil, password=nil)
@api_key = api_key
@host = 'http://flickr.com'
@api = '/services/rest'
login(email, password) if email and password
end
end
flickr = Flickr.new
# titleやdescription, tagなどに'flower'が含まれる画像を検索
images = flickr.photos(:text => 'flower')
images.each{|image| puts "#{image.title}: #{image.url}"}
実行結果
Kensington Palace: http://flickr.com/photos/austinevan/1341721206
roses: http://flickr.com/photos/austrianpsycho/1340832029
The Tiniest Wildflower: http://flickr.com/photos/D L Ennis/1341731308
動物園一片小花海: http://flickr.com/photos/旅店/1340831151
(..省略..)
まとめ
Web上にあふれているWebAPIやWebサービスを利用すると,簡単におもしろいことができるようになってきました.こういうのをMashUpというのですが,一時期流行っていた「地図+なにか」だけではなく,自然言語処理的なWebサービスとかいろいろ生まれてくるといいなと思った残暑厳しい一日でした.
あと,最近ヤフーの画像検索サービスで,写真共有サイト「Flickr」の写真を検索できるようになったらしいので,Flickr.rbを利用しなくてもGigazinizeと同様のサービスができるかもしれませんね.
そんなGigazinizeに触発されてMashUpサービスを作っちゃったよ,という方はぜひコメントやトラックバックなどで教えてくださいね. もうすぐ締め切りのMash up Award 3rdには間に合わないかもしれませんが,きっと4thも開催されることでしょうし.
参考ページ
- tf-idf - Wikipedia
- http://ja.wikipedia.org/wiki/Tf-idf
- [を] 形態素解析と検索APIとTF-IDFでキーワード抽出
- http://chalow.net/2005-10-12-1.html
- RAA - net-yjws
- http://raa.ruby-lang.org/project/net-yjws/
- Flickr.rb
- http://redgreenblu.com/flickr/
ソースコード
gigazine_methods.rb
require 'net/yjws'
require 'flickr'
Net::HTTP.version_1_2
module GigazineMethods
class Image < Flickr
attr_accessor :text
def initialize(api_key='4c3c6d6ff60a36824c9d3d8962cadc9e', text=nil)
@text = text
@api_key = api_key
super(api_key)
end
def search(text=nil)
queries = prepare(text)
options = {'license' => '1,2,3,4,5,6', 'group_id' => '37996572902@N01'}
phts = []
begin
begin
options['text'] = queries.join(' ')
phts = photos(options)
break
rescue => e
end
queries.pop
end while queries.size != 0
[phts[rand(phts.size)], queries]
end
private
def prepare(text)
case text
when String
parse(text)
when Array
text
when nil
if @text.nil?
["kyoto", Time.now.strftime("%B")]
else
parse(@text)
end
else
text.to_a
end
end
def parse(text)
appid = 'gigazine_method_apply_image'
n = 19200000000
yjws_pos = Net::YJWS::MAService.new
yjws_pos.appid = appid
yjws_pos.sentence = text
yjws_pos.results = 'uniq'
yjws_pos.uniq_response = 'surface'
yjws_pos.filter = 9
yjws_search = Net::YJWS::WebSearch.new
yjws_search.appid = appid
yjws_search.results = 1
yjws_search.similar_ok = 1
results = yjws_pos.execute[:uniq_result].word_list.sort_by{|e| -e.count}[0, 10].map do |result|
yjws_search.query = result.surface
[result.count * Math.log(n / yjws_search.execute.total_results_available.to_f), result.surface]
end
results.sort_by{|e| -e[0]}[0, 5].map{|e| e[1]}
end
end
end
class Flickr
def initialize(api_key='4c3c6d6ff60a36824c9d3d8962cadc9e', email=nil, password=nil)
@api_key = api_key
@host = 'http://flickr.com'
@api = '/services/rest'
login(email, password) if email and password
end
end
class Flickr::Photo
def url(size='Medium')
if size=='Medium'
owner.photos_url + @id
else
sizes(size)['url']
end
end
def source_url(size='Medium')
@source_url ||= sizes(size)['source']
end
end
class Net::YJWS::MAService
def execute
uri = BASE_URI.dup
uri.query = to_query
uniq_result = nil
Net::HTTP.start(uri.host) do |http|
response = http.post(uri.path, uri.query)
src = response.body
xml = REXML::Document.new(src)
REXML::XPath.match(xml, "/ResultSet/uniq_result").each {|r|
uniq_result = Result.new
uniq_result.total_count = REXML::XPath.first(r, "total_count").text.to_i
uniq_result.word_list = []
REXML::XPath.match(r, "word_list/word").each {|w|
word = Word.new
if surface = REXML::XPath.first(w, "surface")
word.surface = surface.text
end
if count = REXML::XPath.first(w, "count")
word.count = count.text.to_i
end
uniq_result.word_list << word
}
} # uniq_result
end
{ :uniq_result=> uniq_result }
end
end