Scalaで集合知プログラミング その8


Scala de 集合知プログラミングを続けていきますよ(`・ω・´)

今回は前回検証&作成したモジュールを利用して、del.icio.usAPIを使って集合知的ゴニョゴニョをやったりしたいと思います。

del.icio.usのリンクを推薦するシステムを作る

それでは前回いじくりまわしたモジュールを利用して、del.icio.usのデータから推薦用のデータセットを創りだすというお仕事をしたいと思います。

なお、前回も簡単に触れたのですが、集合知プログラミング出版当時と現在では(多分)del.icio.usAPIの仕様が変わってしまっているらしく、本書のサンプルでは十分なデータ件数が取得できないのでまともな推薦ができないだろう…ということだけ前置きしておきます。

まあ、Scalaのお勉強がメインなので…今回はご勘弁(´・ω・`)

まずはPythonでデータセットの作成

前回いろいろと試してみたpydeliciousモジュールののget_popular, get_userposts, get_urlpostsの各メソッドを利用して、推薦用のデータセットを作成してみます

データセットの作成手順は次のような流れで行いますね(`・ω・´)

  1. ユーザ群を作成
    • get_popularを利用して現在人気のあるURLを取得
    • get_urlpostsを利用して取得したURLを投稿しているユーザ群を取得
  2. 各ユーザが投稿しているURLへの評価セットを作成
    • get_userpostsを利用してユーザが投稿しているURLを抽出します
    • 各ユーザが投稿しているURLに対して評点1を付与します
    • 他ユーザが投稿しているけども、該当ユーザが投稿していなければ評点0にします


まずは前段のユーザ群の抽出処理を書いてみますよ

from pydelicious import get_popular, get_userposts, get_urlposts

# ユーザ群を抽出しますよ
def initializeUserDict(tag, count=5):
  # ユーザ群格納用の変数を定義します
  user_dict = {}
  # 指定タグに応じた注目URLを抽出します
  for p1 in get_popular(tag=tag)[0:count]:
    # 抽出したURLごとに、該当URLを投稿しているユーザを取得します
    # なお、サンプルではhrefというキーですが
    # 現行APIではurlというキーを利用しているようなので変更します
    for p2 in get_urlposts(p1['url']):
      user=p2['user']
      # ユーザ群に追加します
      user_dict[user] = {}
  return user_dict

それではユーザ群からのURLへの評価付処理を書いてみますよ

# ユーザ群からURLへの評価を行います
def fillItems(user_dict):
  # 評価対象となる全URLを格納する変数を定義します
  all_items={}
  # 引数で渡されたユーザ群のユーザごとに投稿しているURLを取得します
  for user in user_dict:
    # ユーザ投稿URLの取得に失敗した場合は
    # 遅延しながら3回まで取得を挑戦します
    for i in range(3):
      try:
        posts=get_userposts(user)
        break
      except:
        print "Failed user "+user+", retrying"
        time.sleep(4)
    # 取得した投稿URLについて評点1を加えます
    for post in posts:
      # サンプルではhrefというキーですが
      # 現行のAPIではurlというキーを利用しているようなので変更します
      url = post['url']
      user_dict[user][url] = 1.0
      # 評価対象のURL群に加えます
      all_items[url]=1
  
  # 全ユーザについて、各ユーザが未評価のURLは0として得点を加えます
  for ratings in user_dict.values():
    for item in all_items:
      if item not in ratings:
        ratings[item] = 0.0

以上でデータセットの作成処理は完成です(`・ω・´)ただし、後半のfillItemsはグローバルの変数についてゴニョゴニョ処理するので若干気持ち悪いですね…まあ、そこら辺はScala版で治すとして、とりあえずdefliciousrec.pyとして保存しますね。

それでは実際に動かしてみますデス

# インポートします
>>> from deliciousrec import *
>>> 
# ユーザ群の抽出をしてみますよ
>>> delusers=initializeUserDict('programming')
>>> delusers
{u'badmonkey0': {}, u'liyuan.cpp': {}, u'dtspayde': {}, u'Perle': {}, u'amin_hjz': {}, u'huitseeker': {}, u'vitukas': {}, u'felipekm': {}, u'malinky': {}, u'GrahamSayers': {}, 
<省略>

# ユーザによるURLへの評価値を取得します
>>> fillItems(delusers)
>>> delusers
{u'badmonkey0': {u'http://visualstudiofeeds.com/index.php?option=com_content&view=article&id=1509:Nullable%20DateTime%20and%20Ternary%20Operator%20in%20C#&catid<...省略>

とりあえず、こんな感じでデータセットを作成することが出来ました(`・ω・´)

Python de del.icio.usの推薦

それでは作成したdel.icio.usベースのデータセットを利用して推薦を行ってみますよ

まずはユーザ群の中からランダムに一人を取得してやります

>>> import random
>>> user=delusers.keys()[random.randint(0, len(delusers)-1)]
>>> user
u'lucide'

次に該当ユーザに類似する他ユーザのランキングをだしてみましょう(`・ω・´)

>>> import recommendation
>>> recommendation.topMatches(delusers, user)
[(0.37661218075611624, u'm_djamaluddin'), (0.086538649977499854, u'wolftype'), (0.086538649977499854, u'sakhamoori'), (0.086538649977499854, u'riccardomurri'), (0.086538649977499854, u'oliver.beckmann')]

類似度が計算できたので、今度はオススメURLランキングを計算してみますよ(`・ω・´)

>>> recommendation.getRecommendations(delusers, user)[0:10]
[(0.11526079757331825, u'http://blogs.msdn.com/b/somasegar/archive/2010/10/28/making-asynchronous-programming-easy.aspx'), (0.11526079757331825, u'http://blogs.msdn.com/b/microsoft_press/archive/2010/10/28/free-ebook-programming-windows-phone-7-by-charles-petzold.aspx'), (0.057630398786659126, u'https://www.wealthfront.com/'), (0.057630398786659126, u'https://www.nytimes.com/2011/01/16/opinion/16greene.html?partner=rss&emc=rss'), (0.057630398786659126, u'https://wiki.cis.jhu.edu/software/caworks'), (0.057630398786659126, u'https://wiki.cis.jhu.edu/projects'), (0.057630398786659126, u'https://wiki.cis.jhu.edu/clr/Log'), (0.057630398786659126, u'https://wiki.cis.jhu.edu/clr'), (0.057630398786659126, u'https://mail.google.com/mail/?shva=1#inbox'), (0.057630398786659126, u'https://jna.dev.java.net/')]

最後にデータセットのユーザとアイテムを入れ替えて、特定のURLによく似た内容(と思われる)サイトのURLを抽出してみますよ

# URLをランダムにひとつだけ抽出してやります(`・ω・´)
>>> url = recommendation.getRecommendations(delusers, user)[0][1]
>>> url
u'http://blogs.msdn.com/b/somasegar/archive/2010/10/28/making-asynchronous-programming-easy.aspx'

# 類似URLランキングを計算してやります 
>>> recommendation.topMatches(recommendation.transformPrefs(delusers),url)
[(0.31280043601230151, u'https://www.wealthfront.com/'), (0.31280043601230151, u'https://www.ibm.com/developerworks/aix/library/au-cleancode/'), (0.31280043601230151, u'http://zeptojs.com/'), (0.31280043601230151, u'http://yuml.me/'), (0.31280043601230151, u'http://www.youtube.com/user/siemens')]

うん、無事に出来ました(`・ω・´)

Scalaでデータセットの作成

それでは本番のScalaへの翻訳をやっていきたいと思いますデス(`・ω・´)基本的な処理の流れはPythonと同様にやっていきますYO!

まずはユーザ群の抽出処理ですネ、以降の処理はパッケージ化として新しく作成するDeliciousRecオブジェクトに記載していきますデス

// packege定義をシマス
package org.plasticscafe.collective.recommend

// del.icio.us接続用のAPIをインポートシマス
import ScalaDelicious.{get_popular, get_userposts, get_urlposts}

// Pythonの時と同様に利用するためにobjectとして定義します 
object DeliciousRec {
  
  // まずは注目ランキングからユーザ群を抽出します
  def initializeUserDict(tag:String="", count:Int=5):Map[String, Map[String, Double]] = {
    // 条件に適した注目一覧からユーザ群を抽出します
    // 途中でtoSetすることで重複を除去します
    // ... これでいいのかわからんけども(´・ω・`)
    get_popular(tag).take(count).flatMap(x =>
      get_urlposts(x("url")).map(y =>
        y("user") -> Map.empty[String, Double])).toSet.toMap
  }
}

ユーザ抽出処理ができたので、次に評価値の計算を行いますよ(`・ω・´)

Python版ではinitializeUserDictの計算結果を格納した変数を直接編集していましたが、あまり好みではないので普通の関数形式で行いますデス

まあ、メモリの節約とかそんな理由だとは思うのですが…好みということで(´・ω・`)

それでは実際に処理を記述していきます。

// DeliciousRecオブジェクト内に追記します

// sleep 用にパッケージを利用します
import Thread.sleep

// ユーザ→URLへの評価値(投稿していれば1、してなければ0)の計算を行います
def fillItems(users:Map[String, Map[String, Double]]):
  Map[String, Map[String, Double]] = {
  
  // 全てのURLを格納する処理を記述します
  // 変更するのでvarで定義してやります
  // ちなみに重複排除のためにSetで定義します
  var all_items = Set.empty[String]
  
  // 接続エラーがあった場合にリトライするための再帰処理です
  def connect_userposts(user:String, retry:Int = 0):Map[String, Double] = {
    try {
      // リトライ回数が3回を超えた場合は空のMapを返します
      retry match {
        case retry if(2 < retry) => Map.empty[String, Double]
        // 各ユーザごとのURL投稿状態を取得して
        // 投稿済みのものを評価1として格納します
        // 同時に全てのURLにも追加していきます
        case _ => {
          get_userposts(user).map(x => {
            all_items += x("url")
            x("url") -> 1.0
          }).toMap        
        }
      }  
    // エラーが発生したらリトライします
    } catch {
      case e:Exception => connect_userposts(user, retry + 1)
    }
  }

  // 各ユーザごとに投稿URLを取得して、URL投稿状態から評価値を取得します
  users.map(user => user._1 -> connect_userposts(user._1)).
    map(user => user._1 -> all_items.map(item =>
      if(user._2.contains(item)) item -> 1.0 else item -> 0.0).toMap)
} 

最後の評価値計算は、実際には次のように分解できますデス

  // ユーザの投稿済みのURLに評価値1をつけたデータを取得
  val prefs = users.map(user => user._1 -> connect_userposts(user._1)).
  
  // 全てのURLの中でユーザが未投稿のものに0を付けたデータを再構成
  prefs.map(user => user._1 -> all_items.map(item =>
      if(user._2.contains(item)) item -> 1.0 else item -> 0.0).toMap)

Pythonのときと同様に、一度計算した変数をミュータブル的に再編集したほうが計算効率は良さそうなのですが…まあ、好みということで(´・ω・`)

あと、前半で利用しているvar定義のall_itemsをイミュータブルにするには、たとえば次のようにAPIを叩く処理を2周する方法しか思いつかなかったので…涙をのんで諦めます(´;ω;`)

  // connect_userpostsと同様の処理を行います
  def connect_allurls(user:String, retry:Int = 0):Map[String, Double] = {
    try {
      retry match {
        case retry if(2 < retry) => Map.empty[String, Double]
        case _ => get_userposts(user).map(x => x("url").toSet
      }  
    // エラーが発生したらリトライします
    } catch {
      case e:Exception => connect_userposts(user, retry + 1)
    }
  }  
  val  all_items:Set[String] = users.map(user => user._1 -> get_allurls(user._1))
  
  ... <以下省略>

ネットワーク越しの処理なんて何度もやってられないですしね…でも、何か別な方法がありそうな気がする(´・ω・`)mapの処理結果をタプルでとって再編集するとか、とか?まあ、今後の課題ということで(`・ω・´)


ちなみに今回は使わなかったのですが、Scala2.8からbreakが使えるようになってるみたいなので c⌒っ゚д゚)っφ メモメモ...
Onion開発しつつ、PEGEXを開発する日記


それでは上記コードの動作テストをしてみますよ

// パッケージをインポートしますよ
scala> import org.plasticscafe.collective.recommend.DeliciousRec._
import org.plasticscafe.collective.recommend.DeliciousRec._

// ユーザ群を取得します
scala> val delusers = initializeUserDict("Programming")
delusers: Map[String,Map[String,Double]] = Map((onila,Map()), (motzel,Map()), (sakhamoori,Map()), (keifoo,Map()), (GrahamSayers,Map()), (liyuan.cpp,Map()), (pro
<省略>

// ユーザ群から評価値データを取得してやります
scala> val deldatas = fillItems(delusers)
deldatas: Map[String,Map[String,Double]] = Map((onila,Map(http://msdn.microsoft.com/en-us/library/aa175863(SQL.80).aspx -> 0.0, http://
<省略>

うん、それっぽくデータセットが作れているみたいなので推薦処理をやってみますよ(`・ω・´)

Scala de del.icio.usの推薦

まずはユーザ同士の類似度計算をしてみますよ

// とりあえずランダムにユーザを取得してやりますよ
scala> import scala.util.Random                                     
import scala.util.Random
scala> val r = Random                                               
r: scala.util.Random.type = scala.util.Random$@1f95673
scala> val user = (delusers.take(r.nextInt(delusers.size)).last)._1 
user: String = malinky
 
 // 推薦用のパッケージを読み込みます
scala> import org.plasticscafe.collective.recommend.Recommendation._
import org.plasticscafe.collective.recommend.Recommendation._

// 類似ユーザ一覧を計算しますよ
scala> topMatches(deldatas, user)                                   
res6: List[(Double, String)] = List((0.388984088127295,paulmkelly), (0.32109343125254997,currierjp), (0.253202774377805,mad9scientist), (0.18531211750305998,rtistic20009), (0.11742146062831499,rohit_aman))

(´ε`;)ウーン…若干Pythonのときと計算結果が違うような気がする…のはデータ数が少なくて偏りがあるからかしら?

まあ、そこらへんは想定内なので(... ということにしておいて)次に推薦度の計算をしてやりますよ(´・ω・`)

// 指定したユーザに推薦すべきURLを抽出します
scala> getRecommendations(deldatas, user)                           
res7: List[(Double, String)] = List((0.5708616093068347,http://stackoverflow.com/questions/194812/list-of-freely-available-programming-books), (0.5204495879786718,http://retinart.net/miscellaneous/grammar/), (0.4264420746485701,http://www.formstack.com/the-anatomy-of-a-perfect-landing-page), (0.3760300533204072,http://fontsinuse.com/), (0.3072285506543868,http://www.robotregime.com/), (0.28883906931652936,http://www.supernicestudio.com/rfp/), (0.23842704798836645,http://www.underconsideration.com/fpo/), (0.213221037324285,http://the99percent.com/tips/6975/Email-Etiquette-for-the-Super-Busy), (0.1880150266602036,http://thinkvitamin.com/code/starting-with-git-cheat-sheet/), (0.16280901599612216,http://www.smashingmagazine.com/2011/01/18/time-saving-and-educational-resources-for-web-design...

うん、できましたな(`・ω・´)

最後にデータセットのユーザとURLの位置を入れ替えて、オススメURLの類似サイトを挙げてやりますデス

// オススメサイト第一位のURLを抽出します
scala> val url = getRecommendations(deldatas, user).head._2
url: String = http://stackoverflow.com/questions/194812/list-of-freely-available-programming-books

// オススメサイトの類似サイトを提示してやります
scala> topMatches(transformPrefs(deldatas), url)
res8: List[(Double, String)] = List((0.7065706076682351,http://retinart.net/miscellaneous/grammar/), (0.5828173250240295,http://fontsinuse.com/), (0.5593035962878414,http://the99percent.com/tips/6975/Email-Etiquette-for-the-Super-Busy), (0.5076620752574295,http://brand-identity-essentials.com/100-principles), (0.4375894392055062,http://wp-snippets.com/))

うん、半ば無理やりだけども何とかなったな(´・ω・`)

まとめ

今回は実際に運用されているdel.icio.usAPIを使ったアイテムの推薦を試してみました。

なお、以前実装したScala側のgetRecommendにバグが有ったために、最初のほうで推薦度の計算結果が出力されなくて焦ったのは秘密です(`・ω・´)バグはシレッと修正してあります。

次回はアイテムベースのフィルタリングをやっていきたいと思いますデス