はてなブックマーク記事のレコメンドシステムを作成 PythonによるはてなAPIの活用とRによるモデルベースレコメンド
私は情報収集にはてなブックマークを多用しており、暇な時は結構な割合ではてなブックマークで記事を探してます。しかし、はてなブックマークは最新の記事を探すのは便利ですが、過去の記事を探すにはいまいち使えません。個人的には多少過去の記事でも自分が興味を持っている分野に関しては、レコメンドして欲しいと感じてます。
ありがたいことにはてなはAPIを公開しており、はてなブックマークの情報を比較的簡単に取得できます。そこでこのAPIを利用して自分に合った記事を見つけるようなレコメンド機能をRとPythonで作成してみたいと思います。
利用するデータは、はてなAPIを使って収集します。具体的には、はてなブックマークフィードを利用して自分のブックマークしているURLを取得し、そのURLをブックマークしているユーザをエントリー情報取得APIを用いて抽出し、そのユーザのブックマークしているURLを収集します。このuser⇒ブックマークURL⇒user⇒ブックマークURLという手順を繰り返せば大量にURLを収集することができますが、今回の目的は自分に合いそうな記事を見つけることなので、探索範囲は自分と同じ記事をブックマークしているユーザに限ります。
なるべく省エネでいきたいので、Pythonを用いて記事を収集し、Rを用いてデータ加工と記事のスコアリングを行います。システム構築という面ではPythonのみで作った方が良いかもしれないですが、データ加工、モデル構築に関してはRの方が個人的に慣れているので。
まず何をするにしてもデータが必要ということで、Pythonを用いてURLを収集します。データ収集に利用したのは以下のコードです。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys,re,time import csv import urllib2 import json import feedparser reload(sys) sys.setdefaultencoding("utf-8") def main(): # Web情報取得の準備 opener = urllib2.build_opener() user = "Overlap" # はてなuser_idを指定 # はてなブックマークのfeed情報の取得 url_list = [] id = 0 for i in range(0,200,20): feed_url = "http://b.hatena.ne.jp/" + user + "/rss?of=" + str(i) # はてなAPIに渡すクエリの作成 try: response = opener.open(feed_url) # urlオープン except: continue content = response.read() # feed情報の取得 feed = feedparser.parse(content) # feedパーサを用いてfeedを解析 # entriesがない場合break if feed["entries"] == []: break # urlリストの作成 for e in feed["entries"]: try: url_list.append([id,e["link"],user,e["hatena_bookmarkcount"],re.sub("[,\"]","",e["title"])]) # url_listの作成(titleのカンマとダブルクォーテーションを置換) id += 1 except: pass time.sleep(0.05) # アクセス速度の制御 # 対象urlをブックマークしているユーザの抽出 user_list = [] for i, url in enumerate(url_list): response = opener.open("http://b.hatena.ne.jp/entry/jsonlite/" + url[1]) # はてなAPIによるブックマーク情報の取得 content = response.read() tmp = json.loads(content) # jsonの解析 # userリストの作成 for b in tmp["bookmarks"]: user_list.append([url[0],b["user"]]) time.sleep(0.05) # アクセス速度の制御 # 自分と同じurlをブックマークしている数を集計 count_user = {} for i, (id,uname) in enumerate(user_list): if count_user.has_key(uname): count_user[uname] += 1 else: count_user[uname] = 1 # ブックマーク数上位のユーザのブックマークurl情報を取得 for uname, count in sorted(count_user.items(), key=lambda x:x[1],reverse=True): print uname, count if uname == user: continue # 自分のidは除く # 直近200件のブックマークurlを取得 for i in range(0,200,20): try: feed_url = "http://b.hatena.ne.jp/" + uname + "/rss?of=" + str(i) # feed取得用クエリ except: continue response = opener.open(feed_url) # feed情報の取得 content = response.read() feed = feedparser.parse(content) # feed情報の解析 if feed["entries"] == []: break for e in feed["entries"]: if [e["link"],uname] in [ [tmp[1],tmp[2]] for tmp in url_list]: continue # 過去に取得した情報は除く try: url_list.append([id,e["link"],uname,e["hatena_bookmarkcount"],re.sub("[,\"]","",e["title"])]) id += 1 except: pass time.sleep(0.05) # アクセス速度の制御 if count < 10: break # 同じブックマーク数が10より少ない場合break print len(url_list) # ファイルの出力 ofname = "url_list.csv" fout = open(ofname,"w") writer = csv.writer(fout,delimiter=",") writer.writerow(["id","url","user","count","title"]) for t in url_list: writer.writerow(t) fout.close() if __name__ == "__main__": main()
あまり使い回す予定はないのでmainの下にべた書きしてるし、エラー処理とか適当です。はてなフィードはfeedparserを用いてパースしています。feedparserに関しては以下のページが分かりやすいかと思います。
http://python.g.hatena.ne.jp/muscovyduck/20081221/p1
また、はてなブックマークエントリ情報取得APIはJSON形式で取得されるので、jsonライブラリを利用して解析してます。json形式の読み込みに関しては以下のページが参考になると思います。
http://tmlife.net/programming/python/python-json-module.html
あとAPIを高速で叩くのはサーバ負荷的に良くないと思うので、0.05秒程度の間を置くようにしてます。
上記のコードでは、自分と同じブックマーク数が10以上のユーザのブックマークURLを200件まで取得しています。この辺のパラメータは精度やデータ量との兼ね合いで変更した方がいいかもしれません。
URL情報を取得できたので、次はRを用いて上記情報からモデルを作成しレコメンドURLを抽出します。
Rでは、データをurl×userの行列に整形し、自分がブックマークしたurlを元にスコアリングを行います。データの整形にはreshape2を用いて、pivotテーブルを作成することにします。スコアリングとしては、単純な協調フィルタリングでは疎なデータの場合レコメンドできる記事が限られるので、モデルベースのフィルタリングを行うことにします。
アルゴリズムとしてはランダムフォレストを利用します。選択の理由としては、パラメータチューニングなしでもそこそこの精度が出る点と変数の貢献度(今回の場合は似ているユーザ)がわかるためです。ちなみにWindowsの場合、文字コードエラーが起こる場合がありますが、その場合はurl_list.csvをエディタなどでshift-jisに変換すれば良いでしょう。
以下が利用したコードです。
library(reshape2) library(randomForest) user <- "Overlap" # 自分のはてなid # データの読み込み url_list <- read.csv("url_list.csv",head=T,sep=",") tmp <- unique(url_list[,c(2,5)]) # urlとtitleを保存 tmp[,2] <- substr(tmp[,2],1,50) # タイトルの文字数を50字までに # ピボットテーブルによるデータ整形 dat <- dcast(melt(url_list,id.vars=c("url","user"),measure.vars="id"),url~user,length) url <- dat[,1] # urlの保存 dat <- dat[,-1] # urlの除外 target <- dat[,colnames(dat) == user] # user行の保存 dat <- dat[,colnames(dat) != user] # user行の除外 users <- colnames(dat) # user名の保存 colnames(dat) <- paste(rep("user",ncol(dat)),1:ncol(dat),sep="") # 列名の変更 dat <- data.frame(dat,target) # targetの追加 # randomForestを利用したモデル構築 dat.rf <- randomForest(factor(target)~.,data=dat) pred.rf <- predict(dat.rf,dat,type="prob")[,2] # モデルの適用 rank <- data.frame(url,dat$target,pred.rf) rank <- merge(rank,tmp,by="url",all.x=T) # titleの追加 rank <- rank[order(rank$pred.rf,decreasing=T),] rank <- rank[rank$dat.target==0,c(4,1,3)] # 非ブックマークのみに限定 rownames(rank) <- 1:nrow(rank) write.csv(rank,"rank.csv") # データの出力 # 貢献度の確認 sim_user <- data.frame(users,varImpPlot(dat.rf)) head(sim_user[order(sim_user$MeanDecreaseGini,decreasing=T),],20)
出力結果(上位10件)
"","title","url","pred.rf" "1","第19回 ロジスティック回帰の学習:機械学習 はじめよう|gihyo.jp … 技術評論社","http://gihyo.jp/dev/serial/01/machine-learning/0019",0.214 "2","SEも知っておきたいデータサイエンス - 行動予測を活用したCRMシステムの活用法と要件:ITpro","http://itpro.nikkeibp.co.jp/article/COLUMN/20130507/475061/",0.2 "3","「データ分析とソフトウエアの会社になります」?ジェフ・イメルト氏・米ゼネラル・エレクトリック(GE)","http://itpro.nikkeibp.co.jp/article/COLUMN/20130604/482084/",0.182 "4","NTT、「稼げる」研究所へ新組織 人工知能でビッグデータ技術を収益化 :日本経済新聞","http://www.nikkei.com/article/DGXZZO55804220T00C13A6000000/",0.174 "5","RとPythonによるデータ解析入門","http://www.slideshare.net/gepuro/rpython",0.148 "6","機械学習チュートリアル@Jubatus Casual Talks","http://www.slideshare.net/unnonouno/jubatus-casual-talks",0.148 "7","「ゲームとTwitterとFacebookしかしないなんてもったいない」、Gunosy開発チーム根掘","http://gigazine.net/news/20130417-gunosy/",0.144 "8","R統計解析入門: 統計解析 テクニカルデータプレゼンテーション 梶山 喜一郎","http://monge.tec.fukuoka-u.ac.jp/R_analysis/0r_analysis.html#cross_table",0.144 "9","情報学研究データリポジトリ ニコニコ動画コメント等データ","http://www.nii.ac.jp/cscenter/idr/nico/nico.html",0.142 "10","データサイエンティストを目指すに当たって、ぜひ揃えておきたいテキストたちを挙げてみる - 道玄坂で働","http://tjo.hatenablog.com/entry/2013/05/07/191000",0.138
見事にデータマイニング・ビッグデータ系の記事ばっかりですね。私の興味・関心を反映していて、個人的にはかなり満足できる結果になってます。はてなAPIはタグ情報なども取得できるので、タグ情報やタイトルのテキスト解析情報なども用いればより精度向上が見込めると思います。また、見たけどブックマークしなかった記事が多数含まれているんですが、閲覧履歴がないとここに対応するのは難しいですね。趣味レベルの試みですから現状ではこれで十分かと思います。
ちなみに貢献度が高かったユーザは以下の通りでした。
users MeanDecreaseGini user95 TohgorohMatsui 3.635295 user37 irisu22001 3.068073 user115 yokkuns 2.462939 user48 Keiku 1.886593 user61 moa108 1.737433 user5 asa6008885 1.734965 user76 s-feng 1.731526 user116 yoshia_e 1.723635 user102 TYK 1.573865 user34 ilford400 1.537937
貢献度が高いことは必ずしも類似度が高いというわけではないですが、自分と興味が近いユーザ候補がわかるのもこの分析の面白い点ではないでしょうか。
はてなブックマークは自由に使えるデータとして非常に面白いものだと思うので、今後も何か面白いことができないか考えていきたいですね。