Pythonでレコメンドシステムを作る(ユーザベース協調フィルタリング)
Pythonで協調フィルタリングを実装して、お寿司を推薦するシステムを作ってみます。
データセット
今回は寿司ネタの嗜好評価を集めたSUSHI Preference Data Setsを使います。
5000人が寿司ネタ100種類に対して5段階で評価(欠測値有り)したデータセットで、以下で公開されています。
まずはデータをロードします。
上記サイトのAll Data Setからsushi3-2016.zipをダウンロード・展開して、sushi3b.5000.10.scoreファイルをカレントディレクトリにコピーしておきます。
全体では5000行100列(5000人×寿司ネタ100種類)の構成となっており、各要素には評価値0〜4(値が大きいほど、好き)と、欠測値-1がセットされています。
import numpy as np scores = np.loadtxt('sushi3b.5000.10.score', delimiter=' ') print('scores.shape: {}'.format(scores.shape)) # scores.shape: (5000, 100) print('scores[0]: \n{}'.format(scores[0])) # scores[0]: # [-1. 0. -1. 4. 2. -1. -1. -1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. # -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. # -1. -1. -1. -1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. -1. -1. -1. -1. # -1. -1. -1. -1. 4. -1. 2. -1. -1. -1. -1. -1. -1. 0. -1. -1. -1. -1. # -1. -1. 0. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 2. -1. -1. # -1. -1. -1. -1. -1. -1. -1. -1. -1. -1.]
ユーザベースの協調フィルタリング
あるユーザの評価が高くなりそうなアイテムを推薦するために、協調フィルタリングと呼ばれる手法が使われます。 これは、他のユーザのアイテムAの評価(既知)から、おすすめしたいユーザのアイテムAの評価(未知)を推定する方法です。 アイテムそのものの内容やユーザの属性などを必要としないため、多種多様な商品を販売するECサイトなどで使われています。
協調フィルタリングには、ユーザベースとアイテムベースの2つの代表的な方法がありますが、今回はユーザベースの方法を実装してみます。
ユーザベースの協調フィルタリングでは、対象の(おすすめしたい)ユーザと、評価が類似するユーザを探して、そのユーザの評価値から計算します。
相関係数
ユーザベースの協調フィルタリングで、ユーザ同士の評価が類似している度合い(類似度)を計算するために、ピアソンの積率相関係数が使われます。
ユーザaとユーザbの相関係数は以下の式で求まります。
はユーザaのアイテムpに対する評価値で、はユーザaの評価値の平均です。
相関係数は-1〜+1の値となり、-1が負の相関、+1が正の相関があることを表します。
相関係数を求める実装を示します。
全ユーザのスコアをscores、予測したいユーザのインデックスをtarget_user_indexとして受け取り、ユーザのインデックスと類似度のタプルのリストを、類似度の高い順で返します。
類似度の計算にはnumpy.corrcoefを使っています。
また、共通のアイテムを3つ以上評価しているユーザのみを算出対象としています。
そうしないと、共通で評価しているアイテムが1つしかないのに、その1つの評価値が一致しただけで、類似度は1.0となってしまうためです。
0番目のユーザとその他のユーザで類似度を求めたところ、類似度が1.0となっているユーザが何人か発見できました。
186番目のユーザのスコアを見てみますと、4番目(イカ)、60番目(かんぴょう巻)、87番目(トビウオ)の値が完全に一致していることがわかります。
def get_correlation_coefficents(scores, target_user_index): similarities = [] target = scores[target_user_index] for i, score in enumerate(scores): # 共通の評価が少ない場合は除外 indices = np.where(((target + 1) * (score + 1)) != 0)[0] if len(indices) < 3 or i == target_user_index: continue similarity = np.corrcoef(target[indices], score[indices])[0, 1] if np.isnan(similarity): continue similarities.append((i, similarity)) return sorted(similarities, key=lambda s: s[1], reverse=True) target_user_index = 0 # 0番目のユーザ similarities = get_correlation_coefficents(scores, target_user_index) print('Similarities: {}'.format(similarities)) # Similarities: [(186, 1.0), (269, 1.0), (381, 1.0), ... print('scores[186]:\n{}'.format(scores[186])) # scores[186]: # [-1. -1. -1. 4. -1. -1. -1. 2. -1. 3. -1. -1. -1. -1. -1. -1. 4. -1. # -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 4. -1. # -1. -1. 4. -1. -1. -1. -1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. -1. # -1. -1. -1. -1. -1. -1. 2. -1. -1. -1. 2. -1. -1. -1. -1. -1. -1. -1. # -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 2. -1. -1. # -1. -1. -1. -1. -1. -1. -1. -1. -1. -1.]
予測
次に相関係数から、ユーザaのアイテムpに対する評価値を予測する式を示します。
Nはユーザ数ですが、全てのユーザと比較するのではなく、類似度の高いユーザからN人、あるいは、類似度が閾値以上のユーザN人というように絞り込みます。
実装では、全スコアscoresと先程の計算で得られた類似度similarities、予測したいユーザのインデックスtarget_user_indexとアイテムのインデックスtarget_item_indexをそれぞれ渡し、スコアを予測します。
類似度が0より大きく、かつ、類似度の高い上位5人のユーザの評価値のみを使って、計算します。
0番目のユーザの0番目のアイテム(エビ)の評価を予測すると、2.761となりました。
def predict(scores, similarities, target_user_index, target_item_index): target = scores[target_user_index] avg_target = np.mean(target[np.where(target >= 0)]) numerator = 0.0 denominator = 0.0 k = 0 for similarity in similarities: # 類似度の上位5人の評価値を使う if k > 5 or similarity[1] <= 0.0: break score = scores[similarity[0]] if score[target_item_index] >= 0: denominator += similarity[1] numerator += similarity[1] * (score[target_item_index] - np.mean(score[np.where(score >= 0)])) k += 1 return avg_target + (numerator / denominator) if denominator > 0 else -1 target_item_index = 0 # 3番目のアイテム(エビ) print('Predict score: {:.3f}'.format(predict(scores, similarities, target_user_index, target_item_index))) # Predict score: 2.761
ランキング
最後に予測される評価値が高い順にランキングを生成します。 実利用では、このランキングの順番でユーザにおすすめすることになります。
0番目のユーザには、92番目(キャビア)、61番目(ネギトロ巻き)、0番目(エビ)のアイテムを薦めると良さそうということがわかります。
この結果に対する考察は難しいところですが、ユーザ0は納豆巻(58番目のアイテム)の評価が高い(4)ので、ネギトロ巻きもなんとなく大丈夫そうです。
一方で、いくら(6番目)やとびっこ(42番目)などの魚卵系の寿司ネタに対する評価がない中、キャビアを薦めるのはギャンブルにも思えます。
def rank_items(scores, similarities, target_user_index): rankings = [] target = scores[target_user_index] # 寿司ネタ100種類の全てで評価値を予測 for i in range(100): # 既に評価済みの場合はスキップ if target[i] >= 0: continue rankings.append((i, predict(scores, similarities, target_user_index, i))) return sorted(rankings, key=lambda r: r[1], reverse=True) target_user_index = 0 # 0番目のユーザ rank = rank_items(scores, similarities, target_user_index) print('Ranking: {}'.format(rank)) # Ranking: [(92, 2.8500000000000001), (61, 2.7935924227841857), (0, 2.7609467700985615), ...
- 作者: Dietmar Jannach,Markus Zanker,Alexander Felfernig,Gerhard Friedrich,田中克己,角谷和俊
- 出版社/メーカー: 共立出版
- 発売日: 2012/06/22
- メディア: 単行本
- 購入: 1人 クリック: 15回
- この商品を含むブログ (1件) を見る