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


Scala de 集合知プログラミング第3章のクラスタリングを進めていきますよ(`・ω・´)

今回は前回作成したデータセットを基に実際にクラスタを作成していきますデス

階層的クラスタリング

「最も似ている2つのグループをまとめる」という行動を繰り返すことで、グループの階層を作っていく手法を階層的クラスタリングと呼ぶみたいです。

詳しい過程については本書で解りやすい図と共に解説されているのですが、ざっくりと手順を書くとこんな感じですね

  • 最も類似度の高い2つのアイテム(グループ)を選択して、新しくグループ化
  • 次に類似度の高い2つのアイテム(グループ)を選択して、新しくグループ化
  • (... 以下、繰り返し...)
  • 残った2つのアイテム(グループ)を選択して、一つにグループ化

最終的にひとまとめになる&各アイテムは階層構造に整理されるみたいです(´・ω・`)

すごくざっくりと書くと、階層構造はたとえばこんな感じですね

  • ルート
    • グループ1
      • アイテムA
      • アイテムB
    • アイテムC

このように作成される階層構造は、ルートを頂点としたピラミッド型で表現できて、こいつをデンドログラムというグラフで表現することができるそうです(´・ω・`)

デンドログラムの実例も本書を参照してもらうこと…として、デンドログラムで表現することで、各アイテムのグループ状況に加えて階層構造上でのアイテム間の距離(類似具合)なんかも判断することが出来るとか(´・ω・`)

これを「クラスタのタイトネスとして解釈できる」って言うらしいですが、クラスタのタイトネスってなんじゃらホイ?

…と、とりあえず今回は階層的クラスタリングなグループ化をやっていきますよ(´・ω・`)

結構量が多くなるので、ポイントポイントで細切れにしながらサンプルのPythonコードを写経しつつ、Scalaへの翻訳をしていきますデス(´・ω・`)

まずはブログデータの読み込み

まずはブログデータの読み込み処理からやってみマスよ

  • Pythonでブログデータを読み込むコード
# clusters.pyに記述します

# blogdata.txtを読み込む処理です
def readfile(filename):
  # データファイルを1行ずつ読み込みます
  lines = [line for line in file(filename)]
  # 最初の行はタイトルなので列のタイトルとして処理します
  colnames = lines[0].strip().split('\t')[1:]
  # データ行の処理を開始します
  rownames = []
  data = []
  # 1行ずつ読み込んで行(ブログ)名とデータを別々に保持します
  for line in lines[1:]:
    p = line.strip().split('\t')
    # 行名(1列目)を格納
    rownames.append(p[0])
    # 行データをリストとして格納
    data.append([float(x) for x in p[1:]])
  # 各種情報をまとめて返す
  return rownames, colnames, data
  • Scalaでファイル読み込み

それではScalaでの読み込み処理を書いてみますですよ(´・ω・`)

そこまで集計しなくてもよさそうなのでイミュータブル気味で書いてみましたよ

// Cluster.scalaとして保存してやります

// パッケージ宣言をします
package org.plasticscafe.collective.cluster                                     

// 必要なモジュールを読み込みます
import scala.io.Source

// オブジェクトとして定義します
object Cluster{
  // ファイル読み込み用の処理です
  def readfile(filename:String):(List[String],
    List[String], List[List[Int]]) = {

    // URL一覧のリストファイルを取得します
    val source = Source.fromFile(filename)
    val lines = source.getLines().toList
    source.close

    // ヘッダ行から単語一覧を取得します
    // 1つめの要素はBlog名用のヘッダなので捨てます
    val colnames = lines(0).trim.split("\t").tail.toList
 
  // 再帰で集計処理していきます 
    def counting(lines:List[String]):(List[String], List[List[Int]]) =
      lines match {
      case Nil => (Nil, List.empty[List[Int]])
      // 行が残っている場合は処理続行です
      case a :: other => {
        // 処理対象の行をパースしてブログ名と単語頻出度に分解します
        val (rownames, data) = counting(other)
        val line = a.trim.split("\t")
        // これまでの処理結果とマージして値を返します
        (line(0) :: rownames, line.tail.map(_.toInt).toList :: data)
      }
    }

    // 集計処理を行って単語ヘッダと結合したうえで値を返します
    val (rownames, data) = counting(lines.tail)
    (rownames, colnames, data)
  }
}

とりあえずこんな感じですかね(´・ω・`)

グループ間の類似度の計算

クラスタリングではアイテム(グループ)間の類似度を基にグループ分けを進めていくことになりますが、今回は単語頻出ベクトルを利用したピアソン相関係数で定義していきます

  • まずはPythonで類似度計算です
# clusters.pyに追記します

# 2つのアイテム間のピアソン相関係数を計算する処理です
from math import sqrt
# 求める類似度は完全一致が1、完全に一致しないが0です
def pearson(v1, v2):
  # 各値を合計
  sum1 = sum(v1)
  sum2 = sum(v2)

  # 各値の平方を合計
  sum1Sq = sum([pow(v, 2) for v in v1])
  sum2Sq = sum([pow(v, 2) for v in v2])

  # 各要素の積の合計
  pSum = sum([v1[i] * v2[i] for i in range(len(v1))])

  # ピアソンスコアを計算
  num = pSum - (sum1 * sum2 / len(v1))
  den = sqrt((sum1Sq - pow(sum1, 2) / len(v1)) * (sum2Sq - pow(sum2, 2) / len(v1)))
  # 分母が0になった場合は計算不能なので類似度0
  if den == 0:
    return 0 

  # 計算したピアソン系数的類似度を返します
  # 1から引くことで小さければ小さい(0に近い)ほど類似!とします
  return 1.0 - num /den

それではピアソン相関係数計算をScalaでやってみますかね…といっても以前やった内容を単語ベクトルに合わせてちょろっと改変するだけですな(´・ω・`)

// Cluster.scala内のClusterオブジェクトに追記します

// 計算用のモジュールをインポートします
  import Math.{sqrt, pow}
  // ピアソン相関係数を求めます
  def pearson(v1:List[Int], v2:List[Int]):Double = {

    // 前要素数を取得してやります
    val n = v1.size

    // 全ての共通嗜好値の合計を計算します
    val sum1 = v1.sum
    val sum2 = v2.sum

    // 平方の合計を計算してやります
    val sum1Sq = v1.map(x => pow(x, 2)).sum
    val sum2Sq = v2.map(x => pow(x, 2)).sum

    // 積の合計を計算します
    // Scalaっぽいかどうかはアレなんですが...
    val pSum =  (for (i <- Range(0, n)) yield {v1(i) * v2(i)}).sum

    // ピアソンスコアを計算します
    val num = pSum - (sum1 * sum2 / n)
    val den = sqrt(sum1Sq - pow(sum1, 2) / n ) * sqrt(sum2Sq - pow(sum2, 2) / n )
    // 分母が0になったら誤りということで0を返します
    if(den == 0)  return 0
     
    // 値を返します
    // ピアソン相関係数は1 ~ -1の範囲で推移するので
    // 1から引くことで小さければ小さいほど類似する...と定義します         
    1.0 - num / den                                             
  }                 

とりあえずできましたな(`・ω・´)

クラスタをクラスで定義

各アイテム(グループ)を表現するクラスタは以下の各属性をもつので、こいつをクラスを利用することで表現してやります(`・ω・´)

  • グループの場合は、要素となるアイテム(2つ)
  • グループの場合は、要素間の類似度
  • 単語頻出度ベクトル
    • グループの場合は要素となるアイテムのベクトルの平均

それではクラスを定義してみますよ

# clusters.pyに追記します

# 階層的なツリーを表現するクラスを定義シマス(´・ω・`)
class bicluster:
  # 階層的なツリー内の各ノードを表現します
  def __init__(self, vec, left=None, right=None, distance=0.0, id=None):
    # 該当クラスタの子クラスタ(統合元)
    self.left = left
    self.right = right
    # ベクトル情報(単語頻出情報)
    self.vec = vec
    # クラスタに一意につけられたID
    self.id = id
    # 子クラスタがある場合は、その類似度
    self.distance = distance

Clusterクラスを定義してやります(`・ω・´)

Pythonではclusterクラスの各種パラメータに問答無用でデフォルト値Noneを突っ込んでいますが...Scalaでそれはちょっと…ということでOption型でも使ってみますかね(´・ω・`)

// Option型を使って値が入らない可能性のあるパラメータを定義してやります
// それと外部からアクセスできるようにvalで定義します(´・ω・`)
class Cluster(
  val vec:List[Int],
  val left:Option[Cluster]=None,
  val right:Option[Cluster]=None,
  val distance:Double=0.0,
  val id:Option[Int]=None
)

...これ、何も考えずにコンパニオンオブジェクト / クラスにしてしまったけども…問題はないのかしら?

ちなみに、ダメでもともと!(`・ω・´)的にコンパニオンオブジェクトに追記したapplyメソッドはこんな感じです

def apply(vec:List[Int], left:Option[Cluster]=None,
  right:Option[Cluster]=None, distance:Double=0.0, id:Option[Int]=None) = {
  // クラスを作成                                                     
  new Cluster(vec, left, right, distance, id)
}

(´ε`;)ウーン…、かなり強引に書いてしまったけども良いのかしら?しかもコンパニオンオブジェクトの使い方が間違っている気がする…困った(´・ω・`)

詳しい人がいればツッコミおながいします。

とりあえずココマデで

…と、ちょっと長くなってきた&この後も時間がかかりそうなので、一旦切りたいと思います(´・ω・`)

今回は階層的クラスタリングを実際に行うための各種準備のコードを書いてみました

次回は、これまで準備してきた各要素を使って実際にクラスタリングを行う処理を書いていきたいと思います

XhtmlParserでOut of Memoryになって泣きたくなった話

追記

どうやら環境によって起こったり起こらなかったり…な現象みたいです(´・ω・`)

現在のところ次のようなパターンで発生しております(いろいろ試して追記していきます)

発生する環境
発生しない環境
  • Ubuntu 2.7.7final (OpenJDK Client VM, Java 1.6.0_18)
    • Source.fromURL(url) のcloseなし
    • パース成功
  • MacOS 2.7.7final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22)
    • Source.fromURL(url) のcloseなし
    • パース成功
  • MacOS 2.8.1final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22)
    • パース失敗

JVMの環境な気がしてきた…他OS環境でも試してみよう…っと(´・ω・`)

元記事

scala.xml.parsing.XhtmlParserを使ってXML(RSSフィード)をパースするといつのまにかメモリを食いつぶしてしまう...という現象に遭遇したので自分メモ

上記現象が発生してはまった前のエントリーのコードを検証用にシンプルにしでみました(´・ω・`)

import scala.io.Source
import scala.xml.{XML, NodeSeq}
import scala.xml.parsing.XhtmlParser

def feedgetter(url:String){
  try{
    // RSSフィードを読み込みます
    val source= Source.fromURL(url) 
    // XhtmlParserを使用するとメモリを食いつぶしてしまう
    val feeds = XhtmlParser(source)
    val datas = feeds \\ "title"
    source.close
    println("success") 
  }catch{
    case e:Exception => println("parse error")
  }
}

いまのところここらへんのURLのフィードでこけてるっぽいので…
http://feeds.feedburner.com/Mashable
http://feeds.feedburner.com/Publishing20

こんな感じで実行してみるよ(´・ω・`)

scala> feedgetter("http://feeds.feedburner.com/Mashable")
// なんかメモリ領域を食いつぶされてしまいました(´;ω;`)
java.lang.OutOfMemoryError: Java heap space
	at scala.collection.Iterator$class.toStream(Iterator.scala:1011)
	at scala.io.Source.toStream(Source.scala:181)
	at scala.collection.Iterator$$anonfun$toStream$1.apply(Iterator.scala:1011)
	at scala.collection.Iterator$$anonfun$toStream$1.apply(Iterator.scala:1011)
	at scala.collection.immutable.Stream$Cons.tail(Stream.scala:565)
	at scala.collection.immutable.Stream$Cons.tail(Stream.scala:557)
	at scala.collection.LinearSeqLike$$anon$1.next(LinearSeqLike.scala:57)
	at scala.io.Source$Positioner.next(Source.scala:260)
	at scala.io.Source.next(Source.scala:243)
	at scala.io.Source.next(Source.scala:181)
	at scala.collection.Iterator$class.toStream(Iterator.scala:1011)
	at scala.io.Source.toStream(Source.scala:181)
	at scala.collection.Iterator...
scala> 

ちなみに上記以外のURLで実行すると無問題...ってどういうこと...orz

// パース成功の場合は当然問題なく(´・ω・`)
scala> v
success

// 失敗したとしてもメモリ食いつぶすような事はしないです
scala> feedgetter("http://journals.aol.com/thecoolerblog/AOLNewsCooler/rss.xm"))
parse error
XML.loadStringを使用した場合

どうしようもなくなったので、代替手段として下記のようにXML.loadStringを使ってRSSフィードを読み込むようにしてやります

def feedgetter(url:String){
  try{
    val source= Source.fromURL(url) 
    // XhtmlParserの代わりにXML.loadStringを使用します
    val feeds = XML.loadString(source.getLines.mkString)
    val datas = feeds \\ "title"
    source.close
    println("success") 
  }catch{
    case e:Exception => println("parse error")
  }
}

実行結果はこんな感じです

// ちゃんとパースで来てるっぽいし(´・ω・`)
scala> feedgetter("http://feeds.feedburner.com/Mashable")
success

いちおう対象のフィードもさらっと眺めてみたけども…いまいち原因がわからないです(´・ω・`)腰据えてやらないとだめかも…

まとめ

…ということで今回は回避できたのだけども、これってなんかのバグになってるのかしら?

一応ここらへんとも関係がありそうな予感...とはっておいて、後は詳しい人にお任せします(´・ω・`)

http://scala-programming-language.1934581.n4.nabble.com/OutOfMemoryError-when-using-XMLEventReader-td2341263.html

とりあえずXhtmlParserを使った際にOut of Memoryになって泣きたくなった場合は、XML.loadStringを代わりに使うと幸せになれそうです(´・ω・`)

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


Scala de 集合知プログラミングも第3章に入っていきたいと思います(`・ω・´)

第3章では複数のアイテムをグループ化するクラスタリングの手法についてやっていきたいと思いますデス

クラスタリングを行うことで大規模データを扱いやすいように分類して可視化したり、分析したりすることが出来るようになるみたいですね(`・ω・´)

本書ではこのクラスタリングという手法について、データセットの作成・2種類のクラスタリングアルゴリズムの学習・生成したグループの可視化の3つを取り上げていくみたいです

教師あり学習 VS 教師なし学習

今回取り扱うクラスタリング教師なし学習と言われるみたいです

教師なし学習とはコンピュータが判断するための見本となる模範解答を入力せずに、データセットのみを基にして予想や分析を行う学習アルゴリズムの種類を指すようです

また、教師なし学習クラスタリングの他にも非負値行列因子分解や自己組織化マップなどがあるようです(´・ω・`)

なお、模範解答を適時入力することでデータに関する学習を行っていく教師あり学習にはニューラルネットワーク・決定木・サポートベクトルマシン・ベイジアンフィルタ等々…があるみたいです

これらの教師あり学習については本書の中盤以降で取り扱っていくようです(´・ω・`)

単語ベクトル

それではクラスタリングを行っていきましょう(`・ω・´)まずはクラスタリング用のデータを作成しますデス

ブロガーを分類する

今回クラスタリングの対象とするのは120件ほどの人気ブログのデータセットとし、各ブログのフィードに特定の単語が現れた回数を基にクラスタリングしてみます

このようなクラスタリングを行うことで同じような話題を取り上げているブログのグループを探し出すことができるようになるみたいです

ようするに、特定の単語を座標軸とみなしてその出現数を大きさ(スカラー)としたベクトル空間を作成して、これをもとにクラスタリングを行う…って解釈でいいですかね?

まあ、実際のところはコードを眺めながら考えていきますよ(`・ω・´)

pythonでフィード中の単語を数える

今回クラスタリングに利用する単語は全ブログ中で10%以上50%以下の頻度で出現するものとしてやります。

このようにある程度の単語を間引くことでニッチ過ぎる単語や、一般的すぎる単語を分析の対象から外してノイズを減らすことが出来るわけでね

なお、PythonでブログのRSSフィードを解析するためにはfeedperserモジュールを利用してやります(`・ω・´)

それではfeedparserモジュールを利用して単語の出現頻度をカウントアップしたデータセットを作成してやります

データセット作成用のコードは次のようにコマンドライン上から実行するタイプのものデス(`・ω・´)

# generatefeedvector.pyとして作成

# 必要なモジュールをインポートします
import feedparser
import re 

# 指定されたURLのRSSフィードから
# タイトルと単語の頻度の辞書を返します
def getwordcounts(url):
  # フィードをパースします
  d = feedparser.parse(url)
  # 単語の出現頻度を格納する変数を初期化します
  wc = {}
  
  # RSSフィード内のすべてのエントリについて処理します
  for e in d.entries:
    # フィード内容のテキストはsummaryまたはdescriptionという
    # 属性になるのでどちらでも取得できるように処理シマス 
    if 'summary' in e: summary = e.summary
    else: summary = e.description
    
    # 下記のgetwordsメソッドを利用して
    # フィード内容を単語に分解します
    words = getwords(e.title + ' ' + summary)
    # 取得した単語群をカウントアップしていきます
    for word in words:
      wc.setdefault(word, 0)
      wc[word] += 1
  # ブログタイトルと単語の出現頻度を返します
  return d.feed.title, wc

# フィードの内容から単語の出現頻度を返します
def getwords(html):
  # HTMLタグを取り除きます
  txt = re.compile(r'<[^>]+>').sub('', html)
  # 非アルファベット文字で分割して単語化します
  words = re.compile(r'[^A-Z^a-z]+').split(txt)
  # 小文字に変換して単語群を返します
  return [word.lower() for word in words if word != '']
  
### メインの処理です

# カウントアップ用の変数を初期化します
apccount = {}
wordcounts = {}

# 分析の対象となるブログのURLリストを
# http://kiwitobes.com/clusters/feedlist.txtからダウンロードして
# data/feedlist.txtとして保存します

# URLリストをリスト化して読み込みます
feedlist=[line for line in file('data/feedlist.txt')]
for feedurl in feedlist:
  try:
    # 各ブログごとに単語の出現頻度をカウントして
    # ブログ別に格納します
    title, wc = getwordcounts(feedurl)
    wordcounts[title]=wc
    # 各ブログに出現する単語を全単語リストに合算集計します
    for word, count in wc.items():
      apcount.setdefault(word, 0)
      if count > 1:
        apcount[word] += 1
    print 'Success to parse feed %s' % feedurl
  except:
    print 'Failed to parse feed %s' % feedurl

# 単語の全ブログ出現頻度で頻出度が高すぎる物、低すぎる物を除外します
wordlist = []
for w, bc in apcount.items():
  frac = float(bc) / len(feedlist)
  # 正確には出現頻度が指定した範囲内のものを
  # データとして格納します
  if frac > 0.1 and frac <0.5:
    wordlist.append(w)

# カウント結果をテキストファイルに書き出します
out = file('blogdata.txt', 'w')
# ヘッダー行を出力します
out.write('Blog')
for word in wordlist:
  out.write('\t%s' % word)
out.write('\n')
# ブログ毎に一行ずつ出力します
for blog, wc in wordcounts.item():
  out.write(blog)
  # 登録条件(指定出現頻度)を満たした単語のみ記録していきます
  for word in wordlist:
    if word in wc:
      out.write('\t%d' % wc[word])
    else:
      out.write('\t0')
   out.write('\n')

なんかUnicodeEncodeErrorがでてしまったので、こちらを参考に修正しております
naoeの日記


それでは実行してみますよ(`・ω・´)

% python generatefeedvector.py
Success to parse feed http://feeds.feedburner.com/37signals/beMH
<省略>

とりあえずそれっぽいデータファイルが作成されましたな(´・ω・`)

% head blogdata.txt
Blog	kids	music	want	wrong	service	needed	friend	tech	saying	lots	union	begin	choice	address	working	following	years	didn	i
Scalaでフィード中の単語を数える

それではScalaに翻訳してみましょうかね(´・ω・`)

ちなみに今回からはイミュータブル縛りは挫折しますデス…集計処理系でミュータブル使えないのはつらいの...orz

それではやってみます(`・ω・´)

// GenerateFeedVector.scalaに記述します

// パッケージとして定義します
package org.plasticscafe.collective.cluster

// 必要なモジュールを読み込みます
import scala.io.Source
import scala.xml.{XML, NodeSeq}
//import scala.xml.parsing.XhtmlParser
// 効率化のためにイミュータブル使いまくり
import scala.collection.mutable

// 単語ベクトル空間データセットを作成するオブジェクトです
object GenerateFeedVector {
  // URLごとにブログタイトルと単語出現頻度を計算します
  def getwordcounts(url:String):(String, Map[String, Int]) = {
    // RSSフィードを読み込みます
    val source = Source.fromURL(url) 
    // XhtmlParserを使用するとメモリを食いつぶしてしまう
    // val feeds = XhtmlParser(source)
    val feeds = XML.loadString(source.getLines.mkString)
    source.close

    // タイトルを読み込みます
    val title = (feeds \\ "title" )(0).text
    
    // RSSフィード内のコンテンツを読み込みます
    val summary = feeds \\ "summary"
    val descriptions = if(0 < summary.size){
       summary } else { feeds \\ "description" }


    // テキスト内の単語分割処理です
    def getwords(html:String):List[String] = {
      "<[^>]+>".r.replaceAllIn(html, "").split("[^A-Z^a-z]+").
        map(_.toLowerCase).toList
    }
    
    // 取得したRSSフィードごとに単語頻出度をカウントアップします
    val wc = mutable.Map.empty[String, Int]
    for (description <- descriptions){
      for(word <- getwords(title + ' ' + description.text)){
        if(!wc.isDefinedAt(word)) wc(word) = 0
        wc(word) += 1
      } 
    }
    // タプルの形式で値を返します 
    (title, wc.toMap) 
  }
  
  // RSSフィードの読み込み集計処理です
  def parseFeed(datafile:String="data/feedlist.txt"):(Int, 
    Map[String, Int], Map[String, Map[String, Int]]) = {
    
    // URL一覧のリストファイルを取得します
    val source = Source.fromFile(datafile)
    val feedlist = source.getLines().toList
    source.close
    
    // 集計用のミュータブルな変数を初期化します
    val apcount = mutable.Map.empty[String, Int]
    val wordcounts = mutable.Map.empty[String, Map[String, Int]]
     
    // URLごとに取得処理をします
    for(feedurl <- feedlist){
      try{
        // 単語頻出度を取得します
        var (title, wc) = getwordcounts(feedurl)
        // 集計を行います
        wordcounts(title) = wc
        // 全ブログの単語頻出度を記録します
        for(w <- wc){
          if(!apcount.isDefinedAt(w._1)) apcount(w._1) = 0
          if(1 < w._2) apcount(w._1) += 1
        }
        println("Success to parse feed " + feedurl)
      }catch{
        case e:Exception => println("Failed to parse feed " + feedurl)
      }
    }
    // 値をセットで返します
    return (feedlist.size, apcount.toMap, wordcounts.toMap)
  }
  
  // 高頻出単語と、低頻出単語をノイズとして除去します
  def filterData(apcount:Map[String, Int], feednum:Int):List[String] = {
    apcount.filter(x => 0.1 < x._2.toDouble / feednum && x._2.toDouble / feednum < 0.5).
      map(x => x._1).toList    
  }
  
  // 集計したデータをファイルに書き込みます
  def writeData(wordlist:List[String],
    wordcounts:Map[String, Map[String, Int]], datafile:String = "blogdata.txt"){
    // ファイルの書き込み用モジュールをインポートします 
    import java.io.PrintWriter
    // ヘッダーを書き込みます
    val out = new PrintWriter(datafile)
    out.print("Blog")
    wordlist.map(x => out.print("\t" + x))
    out.print("\n")
    // データ内容を書き出します
    wordcounts.map{
      case(title, wc) => {
        out.print(title)
        for(word <- wordlist){
          if(wc.contains(word)) out.print("\t" + wc(word))
          else out.print("\t0")
        }
        out.print("\n")    
      }
    }
    out.close()
  }
  // メイン処理として各メソッドを呼び出します 
  def main(args: Array[String]){    
    val (feedsize, apcount, wordcounts) = parseFeed("data/feedlist.txt") 
    
    val wordlist = filterData(apcount, feedsize)
    writeData(wordlist, wordcounts)
  }
}

うん、なんとかできましたな…途中で変なところに引っかかってお昼休み中に終わらなかったので、結局帰宅後に検証し直すはめに(´;ω;`)あ、引っかかった所は別エントリーにまとめます

…とりあえず実行してみますよ

% scala org.plasticscafe.collective.cluster.GenerateFeedVector
Success to parse feed http://feeds.feedburner.com/37signals/beMH
Success to parse feed http://feeds.feedburner.com/blogspot/bRuz
<省略>

なんとかデータソースもできたみたいです(´・ω・`)

% head blogdata.txt
Blog	looks	nbsp	used	e	down	side	please	interesting	read	number	able	behind	ways	drive	network	maybe	find	business
<省略>

ふう、これで漸くクラスタリングの話に進めますね(´・ω・`)

まとめ

今回はクラスタリングのためのデータセットを作成しました。途中でXhtml Parser関連の変な挙動に引っかかってしまったのですが、とりあえず回避できたのでドン( ゚д゚)マイで

次回は今回作成したデータを利用して階層的クラスタリングをやりたいと思います(´・ω・`)

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


Scala de 集合知プログラミングの第2章を進めますよ(`・ω・´)

今回は前回までにやってきた推薦手法を実際に提供されている大規模データに適用させてみますYO!

MovieLensのデータセットを使う

ミネソタ大学のGroupLens Projectによって、作られた映画評価のデータセットであるMovieLensのデータを使ってみます。

MovieLens Data Sets

ちなみにGroupLens Projectは協調フィルタリングに関する研究プロジェクトで、その映画版フィルタリングとしてのMovieLensが開発されているみたいです。

Movie Lens

今回は公開されているMovieLens用データセットを使って推薦を行ってみます(`・ω・´)

MovieLens DataSetsはいくつかのデータを持っているのですが、今回は評価値データであるu.dataと映画のデータであるu.itemの2つを利用します。

  • u.data
    • ユーザID, 映画ID, ユーザによる映画絵の評価、タイムスタンプで構成
196     242     3       881250949
186     302     3       891717742
22      377     1       878887116
  • u.item
  • 映画ID、映画名、リリース日…などのデータで構成されています
1|Toy Story (1995)|01-Jan-1995||http://us.imdb.com/M/title-exact?Toy%20Story%20(1995)|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0

MovieLensは(評価数が)100K, 1M, 10Mなどの複数規模のデータが公開されているのですが、今回は一番規模の小さい100K(映画数1682, ユーザ数943)を利用したいと思います。

…ということで、とりあえずファイルをダウンロードして適当な位置に保存してやりますよ

PythonでMovieLens

まずはPythonを使ってMovieLensのデータから、本書で作成した推薦処理に適用できる形のデータセットに変換する処理をサンプル写経として書いていきますよ(`・ω・´)

# recommendation.pyに追記します

# MovieLensのdata Setsを読み込みます
def loadMovieLens(path='data/ml-data'):
  # 映画のタイトルを取得する処理です
  movies = {}
  for line in open(path + '/u.item'):
    # u.item内のデータから映画IDと映画名のペアを作成していきます
    (id, title) = line.split('|')[0:2]
    movies[id] = title
  
  # 実際の評価値データを変換していきます
  prefs = {}
  for line in open(path + '/u.data'):
    #  評価値データからユーザ→アイテム形式の辞書データを生成シマス
    (user, movieid, rating, ts) = line.split('\t')
    prefs.setdefault(user, {})
    prefs[user][movies[movieid]] = float(rating)
  return prefs

作成するデータの構造を見てみると、ユーザベースの協調フィルタリングを実現するみたいですね。

それでは実行してみますよ

# パッケージをインポートします
>>> from recommendation import *
# データセットを変換しますよ(`・ω・´)
>>> prefs = loadMovieLens()
# うん、出来ました
>>> prefs['87']
{'Birdcage, The (1996)': 4.0, 'E.T. the Extra-Terrestrial (1982)': 3.0, 'Bananas (1971)': 5.0, 'Sting, The (1973)': 5.0, 'Bad Boys (1995)': 4.0, 'In the Line of Fire (1993)': 5.0, 'Star Trek: The Wrath of Khan (1982)': 5.0, 'Speechless (1994
<省略>

せっかくなので、このデータを使って実際に推薦を行って見ます(´・ω・`)

# 作成したデータを使って推薦処理を実行します
>>> getRecommendations(prefs, '87')[0:10]
[(5.0, 'They Made Me a Criminal (1939)'), (5.0, 'Star Kid (1997)'), (5.0, 'Santa with Muscles (1996)'), (5.0, 'Saint of Fort Washington, The (1993)'), (5.0, 'Marlene Dietrich: Shadow and Light (1996) '), (5.0, 'Great Day in Harlem, A (1994)'), (5.0, 'Entertaining Angels: The Dorothy Day Story (1996)'), (5.0, 'Boys, Les (1997)'), (4.8988444312892296, 'Legal Deceit (1997)'), (4.815019082242709, 'Letter From Death Row, A (1998)')]

うん、できました(`・ω・´)ついでにアイテムベースの推薦もやってみますよ

# アイテム間の類似度データセットを作成してやりますよ
>>> itemsim = calculateSimilarItems(prefs)
100 / 1664
200 / 1664
<省略>

# アイテムベースの推薦を行います
>>> getRecommendItems(prefs, itemsim, '87')[0:10]
[(5.0, "What's Eating Gilbert Grape (1993)"), (5.0, 'Temptress Moon (Feng Yue) (1996)'), (5.0, 'Street Fighter (1994)'), (5.0, 'Spice World (1997)'), (5.0, 'Sliding Doors (1998)'), (5.0, 'Shooting Fish (1997)'), (5.0, "Roseanna's Grave (For Roseanna) (1997)"), (5.0, 'Rendezvous in Paris (Rendez-vous de Paris, Les) (1995)'), (5.0, 'Reluctant Debutante, The (1958)'), (5.0, 'Police Story 4: Project S (Chao ji ji hua) (1993)')]

データ数が小さいからそこまでの差は出ませんが、アイテムベースのほうが若干計算が速いですね(´・ω・`)

Scala de MovieLens

それでは先のPythonコードをScalaコードに翻訳していきますよ(`・ω・´)

// Recommendation.scala 内のRecommendationオブジェクトに追記します

// ファイル読み込み用のパッケージをインポートします
import scala.io.Source
// MovieLens Data Setsから推薦用のデータセットを作成します
def loadMovieLens(path:String="data/ml-data"):Map[String, Map[String, Double] = {
  // movieLensのitemデータを読み込みます
  //   ダウンロードしたitemデータがぶっ壊れていたみたいなので
  //   若干編集して使いました…Pythonは問題なく行けたのは何故だ?
  val source_item = Source.fromFile(path + "/u.item.modify")
  val movies = source_item.getLines().map(x =>
       x.split('|').take(2) match { case Array(a, b) => (a -> b)}
    ).toMap
  // ファイル読み込みをクローズします
  source_item.close
    
  // 評価値データを読み込みリストとして展開します
  val source_data = Source.fromFile(path + "/u.data")
  val prefs = source_data.getLines().
    map(x => x.split("\t") match { case Array(a, b, c, d) =>
      (a, movies(b), c.toDouble)}).toList
  // 読み込みをクローズします
  source_data.close
  
  // transformPrefsを参考にして
  // 入れ子構造に再構築する再帰関数を用意します
  def convert(datas:List[(String, String, Double)], 
    items:Map[String, Map[String, Double]]
     = Map.empty[String, Map[String, Double]]):Map[String, Map[String, Double]] = {      
    // 処理用に展開したリストがなくなれば終了
    if(datas.isEmpty){
      items
    }else {
      //// 再帰処理を行います
      // 先頭だけ取り除いて処理続行です
      convert(datas.tail, 
        //// 取り出した先頭要素をリターン用のMapに組み込んでやります
        // リターン用のMapの第1要素にアイテムキーが存在していなければ追加シマス
        if(!items.isDefinedAt(datas.head._1)){
          items + (datas.head._1 -> Map(datas.head._2 -> datas.head._3))
        // リターン用のMapの第1要素にアイテムキーが存在していた場合は
        // 第二要素(内側のMap)に新しい値の組み合わせを追加します
        }else{
          // どの第1要素に追加するかをmapで判定し 
          items.map{case(item, values) => 
            // 追加すべき要素が見つかった場合はMapの要素を結合します
            if(item == datas.head._1){
              item -> (values ++ Map(datas.head._2 -> datas.head._3))
            // 該当しない要素であればそのままリターンします
            }else{ item -> values }}
        })
    }
  }
  // 再帰関数を利用して展開したリストを二重Mapに再編集してやります
  convert(prefs)
}

勢いでイミュータブルな実装にしてみたけども、ごちゃごちゃしているからミュータブル版も書いてみる(´・ω・`)多分こっちのほうが処理ははるかに速いです

def loadMovieLens(path:String="data/ml-data"):Map[String, Map[String, Double]] = {
  val source_item = Source.fromFile(path + "/u.item.modify")
  val movies = source_item.getLines().map(x =>
       x.split('|').take(2) match { case Array(a, b) => (a -> b)}
    ).toMap
  source_item.close
  import scala.collection.mutable
  val prefs = mutable.Map.empty[String, mutable.Map[String, Double]]
  
  val source_data = Source.fromFile(path + "/u.data")
  for(line <- source_data.getLines){
    var Array(a, b, c, d) = line.split("\t")
    if(!prefs.isDefinedAt(a)) prefs(a) = mutable.Map.empty[String, Double]
    prefs(a)(movies(b))  = c.toDouble
  }
  source_data.close
  
  // ファイル読み込みをクローズして
  // イミュータブルをミュータブルに変更して値をリターン
  prefs.map(x => (x._1, x._2.toMap)).toMap
}

ファイル読み込みについてはこちらを参照しました
ゆろよろ日記
遙か彼方の彼方まで


それでは実行してみますよ(´・ω・`)

scala> import org.plasticscafe.collective.recommend.Recommendation._
import org.plasticscafe.collective.recommend.Recommendation._

// データセットを作成してやります
scala> val prefs = loadMovieLens()
prefs: Map[String,Map[String,Double]] = Map((710,Map(Toy Story (1995) -> 4.0, Aladdin (1992) -> 3.0, Groundhog Day (1993) -> 3.0, Fugitive, The (1993) -> 4.0, Citizen Kane (1941) -> 5.0, Titanic (1997) -> 4.0, Cop Land (1997) -> 3.0, Air For
<省略>

// 確認してみますよ
scala> prefs("87")
res0: Map[String,Double] = Map((Truth About Cats & Dogs, The (1996),4.0), (E.T. the Extra-Terrestrial (1982),3.0), (Searching for Bobby Fischer (1993),4.0), (Th
<省略>

// 推薦してみます
scala> getRecommendations(prefs, "87").take(10)
res1: List[(Double, String)] = List((5.0,Star Kid (1997)), (4.89884443128923,Legal Deceit (1997)), (4.815019082242709,Letter From Death Row, A (1998)), (4.7321082983941425,Hearts and Minds (1996)), (4.696244466490867,Pather Panchali (1955)), (4.652397061026758,Lamerica (1994)), (4.538723693474813,Leading Man, The (1996)), (4.535081339106104,Mrs. Dalloway (1997)), (4.532337612572981,Innocents, The (1961)), (4.527998574747078,Casablanca (1942)))

//// ついでにアイテムベースの推薦もやってみますよ
// アイテム間類似度を計算してやります
scala> val itemsim = calculateSimilarItems(prefs)
itemsim: Map[String,List[(Double, String)]] = Map((Mrs. Dalloway (1997),List((1.0,Mediterraneo (1991)), (0.5,Pink Floyd - The Wall (1982)), (0.3333333333333333,

// アイテムを推薦してみます
scala> getRecommendationItems(prefs, itemsim, "87").take(10)
res5: List[(Double, String)] = List((5.000000000000001,When Night Is Falling (1995)), (5.000000000000001,Kull the Conqueror (1997)), (5.000000000000001,Country Life (1994)), (5.000000000000001,Golden Earrings (1947)), (5.000000000000001,Shiloh (1997)), (5.0,Dangerous Beauty (1998)), (5.0,Two or Three Things I Know About Her (1966)), (5.0,Dead Presidents (1995)), (5.0,Reluctant Debutante, The (1958)), (5.0,Mercury Rising (1998)))


おお、なんとかできましたな(´・ω・`)

ユーザベース VS アイテムベース

ユーザベースとアイテムベースはそれぞえ利点と欠点があるので適材適所で使いましょーとのことです。

たとえば次のようになりますね

  • ユーザベースの協調フィルタリング
    • メリット
      • 動作がシンプル
      • 動的に計算できるのでバックグラウンドの処理はいらない
    • デメリット
      • データ数が増えると動作が低速に
      • 疎なデータ(全アイテムに対して各ユーザが評価したアイテム数が少ない)の場合は精度が悪い
  • アイテムベースの協調フィルタリング
    • メリット
      • データ数が増えてもある程度高速に動く
      • 疎なデータでも精度が劣化しづらい
    • デメリット
      • バッチ処理などでアイテム類似度を定期的に計算する必要あり

以上の状況からユーザベースのフィルタリングは頻繁に変更が行われる小規模データセットをベースに推薦するのに向いていて、 逆にアイテムベースは大規模だけれども変更の頻度が少ないデータセットを利用するのに向いていると言えるみたいです。

エクササイズ

本書から読者への挑戦状的なアレです

今回はスルーしますが、いつかやりたいと思うので羅列だけしておきます(´・ω・`)

  • Tanimoto係数について調査、検討みる
  • del.icio.usにおけるタグの類似性を計算してアイテムの推薦を考えてみる
  • ユーザベースフィルタリングを、事前にユーザ間類似度を計算しておくことで効率化してみる
  • del.icio.usのデータを基にアイテムベースの協調フィルタリングをやってみる
  • Audioscrobbler(現last.fm)のAPIを利用して音楽推薦システムを構築する

まとめ

ユーザの嗜好データを基に類似度を駆使してアイテムの推薦を行なってきた第2章はこれで終わりです。

次回は第3章のグループを見つけ出す処理についてやっていきます…ってクラスタリング

ちなみに、ここまでやってきてみてイミュータブルな実装は集計処理系には向かないなぁ…とハゲしく痛感しているところ(´・ω・`)やり方が悪いだけかも知れないですが

まあ、Scala的折衷主義でイミュータブルとミュータブルを使い分ければいいのでしょうが…なんかいい方法はないものかなぁ?

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


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

今回は第2章の推薦からアイテムベースのフィルタリングについてやっていきますよ

アイテムベースのフィルタリング

前回までに行ってきたアイテムの推薦では、全ユーザについての各アイテムに対する評価を記録したデータセットを利用する、ユーザベースの協調フィルタリングというものでした。

ただし、ユーザベースの協調フィルタリングではデータ数が多くなるに従って複数のユーザが共通して利用しているアイテムが少なくなりすぎるため、ユーザ間の類似度を正確に出すことが難しくなってしまうみたいです。

なので、巨大なデータセットが対象にあるような場合は事前にアイテム間の類似を計算しておいて、そのアイテム間類似度を利用してユーザへの推薦を行うアイテムベースの協調フィルタリングが向いているとのこと(´・ω・`)

アイテムベースの協調フィルタリングは次のようなイメージで行われますね

  • 全アイテムについて、アイテム間の類似度を事前に計算
  • 推薦対象のユーザが高く評価しているアイテムを取得
  • 推薦対象ユーザが高評価をつけているアイテムに類似するアイテムを推薦

ちなみにアイテム間の類似度を事前に計算しておくことで、実際にリアルタイムで行われる推薦処理について負荷を下げることも出来ますデス。

まあ、具体的な作法については実際にコードを書きながら見ていきましょうかね(´・ω・`)

アイテム間の類似度のデータセットを作る

とりあえずアイテム間の類似度を計算したデータセットを作るところから始めて見ますデス

元々用意していたユーザベースのデータセットからアイテム間の類似度を計算してやりますYO!

まずは本書のサンプルを写経してPython的類似度計算をしてやりますよ。実際にはこれまで作成してきた各メソッドを上手く組み合わせていく感じですね(´・ω・`)

# recomendation.pyに追記します

# ユーザベースのデータセットから各アイテム間の類似度を計算して
# {Item:[(類似度, Item), (類似度, Item), ... ], ... }の形式に再編成します
def calculateSimilarItems(prefs, n=10):
  # 類似度格納用の変数を初期化します
  result = {}
  # ユーザ→アイテムの形式のデータを
  # アイテム→ユーザの形式に反転します
  itemPrefs = transformPrefs(prefs)
  
  # 処理の進捗状況表示用の変数を定義シマス
  c = 0
  # 各アイテムごとに類似度計算処理を行います
  for item in itemPrefs:
    # 処理状況を表示します(100アイテムごと)
    c += 1
    if c % 100 == 0:
      print "%d / %d" % (c, len(itemPrefs))
    # 各アイテムごとに類似度ランキングを計算します
    scores = topMatches(itemPrefs, item, n = n, similarity = sim_distance)
    result[item] = scores
  # 計算終了で値を返します
  return result

うん、こんな感じでできましたな(`・ω・´)とりあえず動かしてみますよ

# モジュールを読み込みます
>>> import recommendation
# アイテム間の類似度を計算シマスです
>>> itemsim = recommendation.calculateSimilarItems(recommendation.critics)
>>> itemsim
{'Lady in the Water': [(0.40000000000000002, 'You Me and Dupree'), (0.2857142857142857, 'The Night Listener'), (0.22222222222222221, 'Snakes on a Plane'), (0.22222222222222221
<省略>

無事計算できてますね(`・ω・´)

それでは本題のScalaコードへの翻訳を行ってみますよ

// Recommendation.scala内のRecommendationオブジェクトに追記します

// アイテム間類似度を計算してやります
def calculateSimilarItems(prefs:Map[String, Map[String, Double]],
  n:Int=10):Map[String, List[(Double, String)]] = {
  
  // ユーザ→アイテム形式を反転シマス
  val itemPrefs = transformPrefs(prefs)
  
  // 前アイテムについて類似度ランキングを計算してまとめます
  itemPrefs.map(item => item ._1 ->
   topMatches(itemPrefs, item._1, n=n, similarity=sim_distance))
}

うん、イミュータブルにするのが若干面倒なので進捗表示を省いてしまいましたが…こんな感じでいかがでしょう(´・ω・`)進捗表示版は文末におまけで付けておきますです

それでは動かしてみますよ

// インポートします
scala> import org.plasticscafe.collective.recommend.Recommendation._
import org.plasticscafe.collective.recommend.Recommendation._

// 類似度を計算しますよ
scala> val itemsim = calculateSimilarItems(critics)
itemsim: Map[String,List[(Double, String)]] = Map((Superman Returns,List((0.16666666666666666,Snakes on a Plane), (0.10256410256410256,The Night Listener), (0.0

おお、できました(`・ω・´)実際にこのような前処理を行う場合は定期的にバッチで回す感じですネ

推薦を行う

それでは作成したアイテム間類似度のデータエットを利用して推薦を行ってみますよ(`・ω・´)

推薦の詳細な手順は次のようになりますね

  • 特定のユーザが評価した全てのアイテムを取得
  • 取得アイテムに似ている他のアイテムを探索して類似度で重み付け
  • ユーザ評価と、類似度の重み付けから各アイテムの推薦度を計算

推薦度の計算は次のような感じですかね(´・ω・`)実際には以前計算したユーザベースの推薦度と殆ど変わらないです

ユーザAに対してのユーザAが未評価のアイテムaの推薦度

((アイテムaとbの類似度 × ユーザAのアイテムbへの評点) + (アイテムaとcの類似度 × ユーザAのアイテムcへの評点) + ... ) ÷ (アイテムaとbの類似度 +アイテムaとcの類似度 ...)

こちらも。日本語でダラダラ書いてみると、各評点に類似度をかけた値の合計を類似度の合計でわった(正規化した)値、みたいな表現になりますかね(´・ω・`)

とりあえずコードを書いてみて理解していきましょー

# recommendation.pyに追記

# アイテムベースで推薦度を計算
def getRecommendItems(prefs, itemMatch, user):
  # ユーザが評価したアイテムを取得
  userRatings = prefs[user]
  # 計算用の変数を初期化
  scores = {}
  totalSim = {}
  
  # ユーザが評価したアイテムをループ
  for (item, rating) in userRatings.items():
    # ユーザ評価アイテムの類似アイテムごとに処理
    for (similarity, item2) in itemMatch[item]:
      # このアイテムに対してユーザが既に評価していれば無視
      if item2 in userRatings:
        continue
      
      # 評点 * 類似度の合計を計算
      scores.setdefault(item2, 0)
      scores[item2] += similarity * rating
      
      # 全ての類似度を合計
      totalSim.setdefault(item2, 0)
      totalSim[item2] += similarity
  
  # ソレゾレ重み付けして合計したスコアを、類似度の合計で正規化    
  rankings = [(score/totalSim[item], item) for item, score in scores.items()]
      
  # 降順にソートして結果を返す
  rankings.sort()
  rankings.reverse()
  return rankings  

うん、できましたな(`・ω・´)それでは実行してみますよ

# モジュールをリロード
>>> reload(recommendation)
<module 'recommendation' from 'recommendation.pyc'>

# 先ほど計算したアイテム間類似度を利用して計算します
>>> recommendation.getRecommendItems(recommendation.critics, itemsim, 'Toby')
[(3.182634730538922, 'The Night Listener'), (2.5983318700614575, 'Just My Luck'), (2.4730878186968837, 'Lady in the Water')]

とりあえずPython側はOKのようです(´・ω・`)

  • Scalaで推薦度を計算

それでは本題のScalaへの翻訳を行っていきますよ(`・ω・´)

一度getRecommendで似た様なコードを書いているので、今回は最初からimmutableバーションにしてみます

// Recommendation.scala内のRecommendationオブジェクトに追記します

// 推薦度の計算式を定義します                                                        
def getRecommendationItems(prefs:Map[String, Map[String, Double]],
    itemMatch:Map[String, List[(Double, String)]],
    user:String):List[(Double, String)] = {
    
    // 推薦対象ユーザの評価状況を取得します
    val userRatings = prefs(user)

    // 推薦の対象となるアイテムを抽出して
    // 各アイテムごとの重みつきスコアを計算してやりますデス
    //計算結果は集計しやすいように (アイテム名、類似度 * 評価値、類似度)の形式にします
    val candidates = itemMatch.filter(item => !userRatings.contains(item._1)).
      flatMap(item =>
        item._2.filter(x => userRatings.contains(x._2)).
        map(x => (item._1, x._1 * userRatings(x._2), x._1)))

    // 各評価者ごとに同一アイテムの重みつきスコアが複数存在するので
    // パターンマッチ再帰処理を使って集計してやります
    // なお、あらかじめタプルの第一要素(アイテム名)でソートされているのが前提です
    def summary(in:List[(String, Double, Double)]):List[(String, Double, Double)] = in match {                                                                                
      // 空リストはスルーします
      case Nil => Nil
      // 同じキーを持つ要素が連続できたら集計して処理を継続します
      case a :: b :: other if a._1 == b._1 => summary((a._1, a._2 + b._2, a._3 + b._3) :: other)
      // 同じキーが連続しない場合はリストの第2要素以下で処理を継続します
      case a :: other => a :: summary(other)
    }
    
    // 重複ありの重みつきスコアをソートして集計します
    // 集計した結果を更にソートして戻りとしてリターンしますデス(`・ω・´)
    summary(candidates.toList.sort(_._1 > _._1)).
      map(item => (item._2 / item._3, item._1)).sort(_._1 > _._1)
  }

うん、できたようなので実行してみますよ(`・ω・´)

scala> getRecommendationItems(critics, itemsim, "Toby")
res0: List[(Double, String)] = List((3.182634730538922,The Night Listener), (2.5983318700614575,Just My Luck), (2.4730878186968837,Lady in the Water))

うんきちんと計算できていますね(`・ω・´)

まとめ

今回はユーザの嗜好データからアイテム間類似度を計算して新しいデータセットを作ることで、アイテムベースの協調フィルタリングを試してみました。

次回はGroupLensプロジェクトのMovieLensという実際の映画評価のデータセットを使って推薦処理を書いてみたいと思います(`・ω・´)

おまけ

アイテム類似度のデータセットを作るScala版処理に、無理やり進捗表示を付けてみました。

もっといい方法がありそうなので、教えてください詳しい人(´・ω・`)

def calculateSimilarItems(prefs:Map[String, Map[String, Double]],
  n:Int=10):Map[String, List[(Double, String)]] = {
  
  val itemPrefs = transformPrefs(prefs)
  
  // 再帰処理で変換を行います
  // 再帰で処理を行うようにデータセットMapをリストに変換します
  // ... ちょっと強引すぎるので他の方法があったら教えてください(´・ω・`)
  def create_itemsim(items:List[(String, Map[String, Double])],
    n:Int=0):Map[String, List[(Double, String)]] = items match {  
    // 値がなくなったら空のMapを返します
    case Nil => Map.empty[String, List[(Double, String)]]
    // 再帰で行う実際の処理です
    case a :: other => {
      // 100件ごとにメッセージを出力します
      if(n % 100 == 0) println(n + "/" + items.size)
      // 計算したアイテム類似度ランキングを計算済みのMapに結合していきます
      //    itemPrefを直接使えばtoMapしなくてもいいよな(´・ω・`)
      //    スコープ的に気持ち悪いけども
      create_itemsim(other, n +1) +
      (a._1 -> topMatches(items.toMap, a._1, n=n, similarity=sim_distance))
    }
  }
  // 再帰処理のためにデータセットMapをリストに変換します
  create_itemsim(itemPrefs.toList)
}

計算効率的にダメな気がする...

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にバグが有ったために、最初のほうで推薦度の計算結果が出力されなくて焦ったのは秘密です(`・ω・´)バグはシレッと修正してあります。

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

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


Scala de 集合知プログラミングの第2章の続きをやっていきますよ(`・ω・´)

今回はdel.icio.usAPIを使って集合知的ゴニョゴニョをしていきますよ

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

前回までにやったアイテムの推薦、という内容を実際のサービスを利用したものとして発展させていきたいと思います。

今回サービスとして利用するのはオンラインブックマークのdel.icio.usデス

delicious

...と、本が出版されてからサービス名が地味に変わっている気がするのですが、まあ(゚ε゚)キニシナイ!!方向で

del.icio.usAPI

とりあえずDeli.cio.usをPythonで利用しやすくするためにpydeliciousを導入してやります
pydelicious google code

導入はeasy installであっさりと

$sudo easy_install pydelicious

それでは使ってみますかね(`・ω・´)

>>> import pydelicious
# 注目されているprogrammingタグのついたブックマーク一覧を取得しました
>>> pydelicious.get_popular(tag='programming')
[{'extended': '', 'description': u'Using the Cython Compiler to write fast Python code', 'tags': u'python programming c compiler performance c++ optimization software presentation cython', 'url': u'http://www.behnel.de/cython200910/talk.html', 'user': u'remosu', 'dt': u'2009-10-30T13:32:03Z'}, {'extended': '', 'description': u'ontwik | Lectures, Screencasts and conferences for real web developers & designers', 'tags': u'screencast conferences webdev webdesign video resources programming education learning development', 'url': u'http://ontwik.com/', 'user': u'certainly', 'dt': u'2010-10-16T14:47:50Z'}, {'extended': '', 'description': u'Free ebook: Programming Windows Phone 7, by Charles Petzold', 'tags': u'free silverlight programming phone windows windowsphone7 microsoft development mobile pdf', 'url': u'http://blogs.msdn.com/b/microsoft_press/archive/2010/10/28/free-ebook-programming-windows-phone-7-by-charles-petzold.aspx', 'user': u'derek.lakin', 'dt': u'2010-10-28T16:43:38Z'}, {'extended': '', 'description': u'What Every Computer Scientist Should Know About Floating-Point Arithmetic', 'tags': u'programming math', 'url': u'http://docs.sun.com/source/806-3568/ncg_goldberg.html', 'user': u'agles', 'dt': u'2004-03-25T14:44:49Z'}, {'extended': '', 'description': u"Making Asynchronous Programming Easy - Somasegar's WebLog - Site Home - MSDN Blogs", 'tags': u'programming asynchronous c# vb.net', 'url': u'http://blogs.msdn.com/b/somasegar/archive/2010/10/28/making-asynchronous-programming-easy.aspx', 'user': u'andyparkhill', 'dt': u'2010-10-28T22:35:48Z'}, {'extended': '', 'description': u'Ksplice \xbb Hosting backdoors in hardware - System administration and software blog', 'tags': u'security kernel hardware linux hacking programming drivers unix', 'url': u'http://blog.ksplice.com/2010/10/hosting-backdoors-in-hardware/', 'user': u'handshandy', 'dt': u'2010-10-27T17:35:55Z'}, {'extended': '', 'description': u'IMPURE', 'tags': u'data information interaction interface programming software technology visualization tool', 'url': u'http://www.impure.com/', 'user': u'creoquecreo', 'dt': u'2010-10-26T10:02:32Z'}, {'extended': '', 'description': u'Phono - jQuery Phone Plugin', 'tags': u'jquery phone programming webdesign', 'url': u'http://www.phono.com/', 'user': u'socialpest', 'dt': u'2005-03-03T10:23:43Z'}]

出版後しばらく立っているのでAPI廃止されてたらどうしよう…と思ってたのですが、何とか使えるみたいですね(´・ω・`)

ただし、仕様変更はあったらしくどうやら取得件数制限がかかるようになってますね(´・ω・`)デフォルトだと15件までみたい…って、負荷を考えればそりゃそうだわな

ちなみに取得用urlにパラメータをcount=<件数>で指定すると最大100件までは取れるみたいですが…pydeliciousで今回使用するメソッドにはそういうオプションがない…だ..と...orz

うーん、大規模なデータが取得できないとなると余り分析に意味は出なそうなのですが…まあ、今回はコードを書くのが第一目的なのでとりあえずやってみマス

Scalaにはモジュールがない…よ..な、そりゃ

…と、Pythonでは本書の指示通りにAPIを使える見込みが立ったのですが、本特集の本題であるScalaでやるという目的を達成できるような、Scala de deliciousなモジュールは提供されていなさそう(´・ω・`)

いや、あるのかも知れないけど正直探すのが大変すぐるので、今回のサンプルの中で利用しているpydeliciousのメソッドを車輪の再発明してみますよ

  • get_popular
  • get_userposts
  • geturlposts
get_popularメソッド

まずは最新のpopularな投稿リストを取得するメソッドです

呼び出しはこんな感じですね

pydelicious.get_popular(tag='programming')

タグを引数として渡すことで、指定タグの付けられたもののみを取得します

pydelicious.get_popularで実際に取得できるデータはこんな感じですね

# 全部表示するとあれなので2件だけ表示します
>>> pydelicious.get_popular(tag='programming')[0:2]
# 出力データです
>>> pydelicious.get_popular(tag='programming')[0:2]
[{'extended': '', 'description': u'Using the Cython Compiler to write fast Python code', 'tags': u'python programming c compiler performance c++ optimization software presentation cython', 'url': u'http://www.behnel.de/cython200910/talk.html', 'user': u'remosu', 'dt': u'2009-10-30T13:32:03Z'}, {'extended': '', 'description': u'ontwik | Lectures, Screencasts and conferences for real web developers & designers', 'tags': u'screencast conferences webdev webdesign video resources programming education learning development', 'url': u'http://ontwik.com/', 'user': u'certainly', 'dt': u'2010-10-16T14:47:50Z'}]

上記の結果を見ると、戻りはこんな感じのキーで構成されたMapデータで返ってくれば良いみたいですね

  • extended
  • description
  • tags
  • url
  • user
  • dt

また、pydeliciousのコードを眺めてみると、内部的には以下のURLに接続してRSS情報を引っ張るような処理をしているみたいデス

http://delicious.com/rss/popular/<タグ名>

programmingタグがつくとたとえばこんな感じ
http://delicious.com/rss/popular/programming


そんなわけでURLからフィードを取得して、適当に要素を再構成するコードを書いてみました(´・ω・`)

import scala.io.Source
import scala.xml.{XML, NodeSeq}
import scala.xml.parsing.XhtmlParser

// 取得用のURLを定義します
val rss_url = "http://delicious.com/rss/"

// popularなフィードを取得するメソッドです
def get_popular(tag:String = ""):List[Map[String, String]] = {
  // URLから提供フィードを取得してXMLの"item"要素を取得します
  val source = Source.fromURL(rss_url + "popular/"+ tag)
  val feeds = XhtmlParser(source) \\ "item"
  // 取得した各フィードの内容を再構成します
  feeds.map(feed =>
    // extendedについては該当のものが見当たらないので
    // 適当に空白扱ってます(´・ω・`)まあ、今回は使わないので…
    // 残りの要素は多分これでいいはずデス
    Map("extended" -> (feed \\ "extended").text,
      "description" -> (feed \\ "title").text,
      "tags" -> (feed \\ "subject").text,
      "url" -> (feed \\ "link").text,
      "user" -> (feed \\ "creator").text,
      "dt" -> (feed \\ "date").text
    )
  ).toList
}

参考にしたのはこちら

get_userpostsメソッド

これは与えられたユーザのポストを取得するメソッドです

呼び出しはこんな感じですね

pydelicious.get_userposts(user='tsegaran')

ユーザ名を引数として渡してやります(`・ω・´)ちなみにサンプルの"tsegaran"は集合知プログラミングの筆者さんデス

pydelicious.get_userpostsで実際に取得できるデータはこんな感じですね

# 全部表示するとあれなので2件だけ表示します
>>> pydelicious.get_userposts(user='tsegaran')[0:2]
# 出力データです
[{'extended': '', 'description': u'How to Build a 8\xd78 RGB LED Matrix with PWM using an Arduino | Francis ...', 'tags': u'arduino shiftregister', 'url': u'http://francisshanahan.com/index.php/2009/how-to-build-a-8x8x3-led-matrix-with-pwm-using-an-arduino/', 'user': u'tsegaran', 'dt': u'2010-09-09T00:19:54Z'}, {'extended': '', 'description': u'shogun | A Large Scale Machine Learning Toolbox', 'tags': u'python svm machinelearning', 'url': u'http://www.shogun-toolbox.org/', 'user': u'tsegaran', 'dt': u'2010-07-15T23:09:37Z'}]

こちらのほうも戻りはこんな感じのキーで構成されてればいいみたいデス(´・ω・`)

  • extended
  • description
  • tags
  • url
  • user
  • dt

pydeliciousのコードからでは、以下のURLに接続してRSS情報を引っ張ってますね

http://delicious.com/rss/<ユーザ名>

tsegaranのフィードを引っ張る場合はこんな感じ
http://delicious.com/rss/popular/tsegaran

以上の情報からこちらの方もScala版を作成しますデス

// 取得用のURLを定義します
val rss_url = "http://delicious.com/rss/"

// userのフィードを取得するメソッドです
def get_userposts(user:String):List[Map[String, String]] = {
  // URLから提供フィードを取得してXMLの"item"要素を取得します
  val source = Source.fromURL(rss_url + user)
  val feeds = XhtmlParser(source) \\ "item"
  // 取得した各フィードの内容を再構成します
  feeds.map(feed =>
    Map("extended" -> (feed \\ "extended").text,
      "description" -> (feed \\ "title").text,
      "tags" -> (feed \\ "subject").text,
      "url" -> (feed \\ "link").text,
      "user" -> (feed \\ "creator").text,
      "dt" -> (feed \\ "date").text
    )
  ).toList
}
get_urlpostsメソッド

最後に与えられたURLに関する投稿を取得するget_urlpostsメソッドです

呼び出しはこんな感じですね

pydelicious.get_urlposts(url='http://google.co.jp/')

urlを引数として渡してやります

pydelicious.get_urlpostsで実際に取得できるデータはこんな感じですね

# 全部表示するとあれなので2件だけ表示します
>>> pydelicious.get_urlposts(url='http://google.co.jp/')[0:2]
# 出力データです
[{'extended': '', 'description': u'[from jayce_fun] Google', 'tags': u'google', 'url': u'http://www.google.co.jp/', 'user': u'jayce_fun', 'dt': u'2011-01-16T06:03:38Z'}, {'extended': '', 'description': u'[from largeleft] Google', 'tags': u'google', 'url': u'http://www.google.co.jp/', 'user': u'largeleft', 'dt': u'2011-01-14T13:56:33Z'}]

こちらのほうも戻りはこんな感じのキーで構成されてればいいみたいデス(´・ω・`)

  • extended
  • description
  • tags
  • url
  • user
  • dt

pydeliciousのコードからでは、以下のURLに接続してRSS情報を引っ張ってますね

http://delicious.com/rss/url/

http://google.co.jp/のフィードを引っ張る場合はこんな感じ
http://delicious.com/rss/url/ec16dada27c84a7c23fff9c27265355b

以上の情報からこちらの方もScala版を作成しますデス

import java.security.MessageDigest

// 取得用のURLを定義します
val rss_url = "http://delicious.com/rss/"

// urlのフィードを取得するメソッドです
def get_urlposts(url:String):List[Map[String, String]] = {
  // 引数として渡されたurlをハッシュ化します
  val hash_url = MessageDigest.getInstance("MD5").
    digest(url.getBytes()).map((n : Byte) => "%02x".format(n & 0xff)).mkString

  // URLから提供フィードを取得してXMLの"item"要素を取得します
  val source = Source.fromURL(rss_url + "url/"+ hash_url)
  val feeds = XhtmlParser(source) \\ "item"
  // 取得した各フィードの内容を再構成します
  feeds.map(feed =>
    Map("extended" -> (feed \\ "extended").text,
      "description" -> (feed \\ "title").text,
      "tags" -> (feed \\ "subject").text,
      "url" -> (feed \\ "link").text,
      "user" -> (feed \\ "creator").text,
      "dt" -> (feed \\ "date").text
    )
  ).toList
}

md5によるハッシュ化の参考はこちらデス
みずぴー日記

オブジェクト化

3つの各処理とも結構同じような処理をしているので共通化して、かつpydeliciousと同じように使用できるようにオブジェクトとしてまとめてしまいます

// パッケージ化します
package org.plasticscafe.collective.recommend

import scala.io.Source
import scala.xml.{XML, NodeSeq}
import scala.xml.parsing.XhtmlParser

object ScalaDelicious {
  // 取得用のURLを定義します
  val rss_url = "http://delicious.com/rss/"

  // popularなフィードを取得するメソッドです
  def get_popular(tag:String = ""):List[Map[String, String]] = {
    // urlからフィードを取得します
    get_feed(rss_url + "popular/"+ tag) 
  }
  
  // userのフィードを取得するメソッドです
  def get_userposts(user:String):List[Map[String, String]] = {
    get_feed(rss_url + user) 
  }
  
  // urlのフィードを取得するメソッドです
  def get_urlposts(url:String):List[Map[String, String]] = {
    // 引数として渡されたurlをハッシュ化します
    val hash_url = MessageDigest.getInstance("MD5").
      digest(url.getBytes()).map((n : Byte) => "%02x".format(n & 0xff)).mkString
      get_feed(rss_url + "url/"+ hash_url) 
  }
  
    // フィードを取得する共通処理です
  private def get_feed(url:String):List[Map[String, String]] = {
    // URLから提供フィードを取得してXMLの"item"要素を取得します
    val source = Source.fromURL(url)
    val feeds = XhtmlParser(source) \\ "item"
    // 取得した各フィードの内容を再構成します
    feeds.map(feed =>
      Map("extended" -> (feed \\ "extended").text,
        "description" -> (feed \\ "title").text,
        "tags" -> (feed \\ "subject").text,
        "url" -> (feed \\ "link").text,
        "user" -> (feed \\ "creator").text,
        "dt" -> (feed \\ "date").text
      )
    ).toList
  }
}

うん、なんとかまとまったみたいなので試しに動かしてみますよ(`・ω・´)

// インポートします
scala> import org.plasticscafe.collective.recommend.ScalaDelicious
import org.plasticscafe.collective.recommend.ScalaDelicious

// get_popularを使ってみます
scala> ScalaDelicious.get_popular("programming")                  
res2: List[Map[String,String]] = List(Map((extended,), (dt,2009-10-30T13:32:03Z), (url,http://www.behnel.de/cython200910/talk.html), (description,Using the Cython Compiler to write fast Python code), (tags,python programming c compiler performance c++ optimization software presentation cython), (user,remosu)), Map((extended,), (dt,2010-10-16T14:47:50Z), (url,http://ontwik.com/), (description,ontwik | Lectures, Screencasts and conferences for real web developers & designers), (tags,screencast conferences webdev webdesign video resources programming education learning development), (user,certainly)), Map((extended,), (dt,2010-10-28T16:43:38Z), (url,http://blogs.msdn.com/b/microsoft_press/archive/2010/10/28/free-ebook-programming-windows-phone-7-by-charles-petzold.aspx), (description,Free ...

// get_userpostsを使ってみます
scala> ScalaDelicious.get_userposts("tsegaran")                   
res3: List[Map[String,String]] = List(Map((extended,), (dt,2010-09-09T00:19:54Z), (url,http://francisshanahan.com/index.php/2009/how-to-build-a-8x8x3-led-matrix-with-pwm-using-an-arduino/), (description,How to Build a 8×8 RGB LED Matrix with PWM using an Arduino | Francis ...), (tags,arduino shiftregister), (user,tsegaran)), Map((extended,), (dt,2010-07-15T23:09:37Z), (url,http://www.shogun-toolbox.org/), (description,shogun | A Large Scale Machine Learning Toolbox), (tags,python svm machinelearning), (user,tsegaran)), Map((extended,), (dt,2010-07-15T23:08:55Z), (url,http://mypoyozo.com/#tour), (description,Poyozo | Make Life Make Sense), (tags,data personal visualization), (user,tsegaran)), Map((extended,), (dt,2010-07-08T21:43:23Z), (url,http://www.sagenb.org/), (description,Sign in -...

// get_urlpostsを使ってみます
scala> ScalaDelicious.get_urlposts("http://google.co.jp/")
res4: List[Map[String,String]] = List(Map((extended,), (dt,2011-01-16T06:03:38Z), (url,http://www.google.co.jp/), (description,[from jayce_fun] Google), (tags,google), (user,jayce_fun)), Map((extended,), (dt,2011-01-14T13:56:33Z), (url,http://www.google.co.jp/), (description,[from largeleft] Google), (tags,google), (user,largeleft)), Map((extended,), (dt,2011-01-14T02:51:19Z), (url,http://www.google.co.jp/), (description,[from ttake2000] Google), (tags,google), (user,ttake2000)), Map((extended,), (dt,2011-01-12T00:08:33Z), (url,http://google.co.jp/), (description,[from nakapond] Google), (tags,), (user,nakapond)), Map((extended,), (dt,2011-01-10T22:20:55Z), (url,http://www.google.co.jp/), (description,[from jospefois_2sc372] Google), (tags,), (user,jospefois_2sc372)), Map((extended,), (...

おお!できました(`・ω・´)

今回のまとめ

とりあえずScala用のpydelicious簡易版ができたので、次回はこいつを使ってdeli.cio.usのデータを操作して集合知的ゴニョゴニョをやりたいと思います(´・ω・`)