Gigazinize のなかみ

書いた人: noriaki 2007,09月07日(金) 18:00

先日のエントリでご紹介したGigazinizeですが,そのなかみの戦略や,利用しているWebサービスなどをご紹介します.

Gigazinizeがやってることの概観

まずは,Gigazinizeの内部でやっていることを順を追って説明します.

  1. 入力されたテキストをYahoo! 日本語形態素解析 Webサービスに丸投げ
  2. 返ってきた結果のうち,出現頻度Top10の単語をYahoo! ウェブ検索 Webサービスにそれぞれ投げる
  3. それぞれの検索結果ヒット数を利用して疑似IDFを求める
  4. (2)の出現頻度TFと掛け合わせてTF・IDFを求める
  5. TF・IDFのTop5の単語を半角空白でつないでFlickr APIで画像検索
  6. 画像がヒットするまで問合せの単語数を減らして(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-yjwsYahoo!デベロッパーネットワークで公開されている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_tutorial1回 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

このエントリをdel.icio.usにブックマークしているユーザ数このエントリをdel.icio.usに追加する
このエントリをはてなブックマークしているユーザ数このエントリをはてなブックマークに追加する
 | Tags ,

このエントリはアーカイブされています。
コメントする場合は、お手数ですが「このページのURL」を記載した上で、新しいエントリにお願いします。