Scalaで集合知プログラミング その12
Scala de 集合知プログラミング第3章のクラスタリングを進めていきますよ(`・ω・´)
今回は前回作成したデータセットを基に実際にクラスタを作成していきますデス
階層的クラスタリング
「最も似ている2つのグループをまとめる」という行動を繰り返すことで、グループの階層を作っていく手法を階層的クラスタリングと呼ぶみたいです。
詳しい過程については本書で解りやすい図と共に解説されているのですが、ざっくりと手順を書くとこんな感じですね
- 最も類似度の高い2つのアイテム(グループ)を選択して、新しくグループ化
- 次に類似度の高い2つのアイテム(グループ)を選択して、新しくグループ化
- (... 以下、繰り返し...)
- 残った2つのアイテム(グループ)を選択して、一つにグループ化
最終的にひとまとめになる&各アイテムは階層構造に整理されるみたいです(´・ω・`)
すごくざっくりと書くと、階層構造はたとえばこんな感じですね
- ルート
- グループ1
- アイテムA
- アイテムB
- アイテムC
- グループ1
このように作成される階層構造は、ルートを頂点としたピラミッド型で表現できて、こいつをデンドログラムというグラフで表現することができるそうです(´・ω・`)
デンドログラムの実例も本書を参照してもらうこと…として、デンドログラムで表現することで、各アイテムのグループ状況に加えて階層構造上でのアイテム間の距離(類似具合)なんかも判断することが出来るとか(´・ω・`)
これを「クラスタのタイトネスとして解釈できる」って言うらしいですが、クラスタのタイトネスってなんじゃらホイ?
…と、とりあえず今回は階層的クラスタリングなグループ化をやっていきますよ(´・ω・`)
結構量が多くなるので、ポイントポイントで細切れにしながらサンプルの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
いちおう対象のフィードもさらっと眺めてみたけども…いまいち原因がわからないです(´・ω・`)腰据えてやらないとだめかも…
まとめ
…ということで今回は回避できたのだけども、これってなんかのバグになってるのかしら?
一応ここらへんとも関係がありそうな予感...とはっておいて、後は詳しい人にお任せします(´・ω・`)
とりあえず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 <省略>
ふう、これで漸くクラスタリングの話に進めますね(´・ω・`)
Scalaで集合知プログラミング その10
Scala de 集合知プログラミングの第2章を進めますよ(`・ω・´)
今回は前回までにやってきた推薦手法を実際に提供されている大規模データに適用させてみますYO!
MovieLensのデータセットを使う
ミネソタ大学のGroupLens Projectによって、作られた映画評価のデータセットであるMovieLensのデータを使ってみます。
ちなみにGroupLens Projectは協調フィルタリングに関する研究プロジェクトで、その映画版フィルタリングとしてのMovieLensが開発されているみたいです。
今回は公開されている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を利用して音楽推薦システムを構築する
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の類似度 ...)
こちらも。日本語でダラダラ書いてみると、各評点に類似度をかけた値の合計を類似度の合計でわった(正規化した)値、みたいな表現になりますかね(´・ω・`)
とりあえずコードを書いてみて理解していきましょー
- Pythonで推薦度を計算
# 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.usのAPIを使って集合知的ゴニョゴニョをやったりしたいと思います。
del.icio.usのリンクを推薦するシステムを作る
それでは前回いじくりまわしたモジュールを利用して、del.icio.usのデータから推薦用のデータセットを創りだすというお仕事をしたいと思います。
なお、前回も簡単に触れたのですが、集合知プログラミング出版当時と現在では(多分)del.icio.usのAPIの仕様が変わってしまっているらしく、本書のサンプルでは十分なデータ件数が取得できないのでまともな推薦ができないだろう…ということだけ前置きしておきます。
まあ、Scalaのお勉強がメインなので…今回はご勘弁(´・ω・`)
まずはPythonでデータセットの作成
前回いろいろと試してみたpydeliciousモジュールののget_popular, get_userposts, get_urlpostsの各メソッドを利用して、推薦用のデータセットを作成してみます
データセットの作成手順は次のような流れで行いますね(`・ω・´)
- ユーザ群を作成
- get_popularを利用して現在人気のあるURLを取得
- get_urlpostsを利用して取得したURLを投稿しているユーザ群を取得
- 各ユーザが投稿している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.usのAPIを使ったアイテムの推薦を試してみました。
なお、以前実装したScala側のgetRecommendにバグが有ったために、最初のほうで推薦度の計算結果が出力されなくて焦ったのは秘密です(`・ω・´)バグはシレッと修正してあります。
次回はアイテムベースのフィルタリングをやっていきたいと思いますデス
Scalaで集合知プログラミング その7
Scala de 集合知プログラミングの第2章の続きをやっていきますよ(`・ω・´)
今回はdel.icio.usのAPIを使って集合知的ゴニョゴニョをしていきますよ
del.icio.usのリンクを推薦するシステムを作る
前回までにやったアイテムの推薦、という内容を実際のサービスを利用したものとして発展させていきたいと思います。
今回サービスとして利用するのはオンラインブックマークのdel.icio.usデス
...と、本が出版されてからサービス名が地味に変わっている気がするのですが、まあ(゚ε゚)キニシナイ!!方向で
del.icio.usのAPI
とりあえず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://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 }
オブジェクト化
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,), (...
おお!できました(`・ω・´)