Pythonでレコメンドシステムを作る(アイテムベース協調フィルタリング)

今回もお寿司データセットを使って、推薦システムを作ります。

www.kamishima.net

前回と同様、上記サイトのAll Data Setからsushi3-2016.zipをダウンロード・展開して、sushi3b.5000.10.scoreファイルをpythonファイルと同じディレクトリにコピーしておきます。

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.]

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

前回は、類似したユーザの評価値を基に評価値を予測するユーザベース協調フィルタリングを実装しました。

今回は、協調フィルタリングのもう一つの代表的な手法であるアイテムベースの協調フィルタリングを実装します。
アイテムベース協調フィルタリングは、ユーザではなく、評価が類似しているアイテムを探し、そのアイテムの評価値から未知のアイテムの評価値を予測する方法です。 ユーザベースと比較すると、一般的にアイテム数はユーザ数よりも少なく、増減の頻度も低いため、類似度の行列をバッチ的に前もって作成しておくことでオンライン環境でも現実的な時間で推薦アイテムを計算できるという利点があります。 このため、Amazon等の大規模なECサイトでも用いられています。

コサイン類似度

アイテム間の類似度を比較する方法として、コサイン類似度が使われます。

アイテムaとbのコサイン類似度は以下の式に表されます。 r_{u,a}はアイテムaに対するユーザuの評価値、\overline{r_u}はuの評価値の平均です。 なお、平均値で引いて-1(負の相関)〜+1(正の相関)となるように調整しています(通常のコサイン類似度はこの調整を行いません)。

類似度の計算処理を示します。

最初にmean_adjustmentで行毎に平均値で引いて補正します。 (平均値による減算処理を一括して行ってしまいます。)

後は素直にコサイン類似度を計算しています。 分子は内積、分母はL2ノルムの積のため、それぞれnumpy.dotnumpy.linarg.normで簡単に計算できます。

0番目のアイテム(エビ)と類似したアイテムを見てみますと、カニ(22番目)、甘エビ(9番目)、ボタンエビ(41番目)が並んでおり、納得できそうな結果になっています。 (この他にも、マグロと中トロと鉄火巻、イカとタコ、アジとイワシなどが類似度の高い組み合わせとなっていました。)

def mean_adjustment(scores):
    """
    行単位で平均値を引いて補正した行列を返す(NaNは0とする)
    """
    scores_nan = np.copy(scores)
    scores_nan[scores_nan < 0] = np.nan
    
    adjusted_scores = np.ndarray(shape=scores_nan.shape)
    for i, score in enumerate(scores_nan):
        adjusted_scores[i] = score - np.nanmean(score)
    
    return np.nan_to_num(adjusted_scores)


def get_cos_similarities(scores, target_item_index):
    """
    アイテム間のコサイン類似度を計算し、(アイテムのインデックス, 類似度)のタプルリストを類似度順にして返す
    """
    similarities = []
    # ユーザごとに平均値で補正し、アイテムを行(ユーザを列)に変換する
    items = mean_adjustment(scores).transpose()
    target_item = items[target_item_index]
    
    for i, item in enumerate(items):
        if i == target_item_index:
            continue
            
        similarity = np.dot(target_item, item.T) / (np.linalg.norm(target_item) * np.linalg.norm(item))
        similarities.append((i, similarity))
        
    return sorted(similarities, key=lambda s: s[1], reverse=True)
    
    
target_item_index = 0 # 0番目のアイテム(エビ)
    
similarities = get_cos_similarities(scores, target_item_index)

print('Similarities: {}'.format(similarities))
# Similarities: [(22, 0.074712607710488529), (9, 0.067591790210210778), (41, 0.061560317333338818), ...

評価値の予測

予測は以下の式で計算します。 iは評価したアイテムです。
なお、全てのアイテムから計算するのではなく、類似度の高い上位k個などというように絞ります。

続いてPythonでの実装です。 ここでは類似度の上位3個、かつ、類似度が0以上のアイテムのみを使って計算しています。

上で得られた類似度を使って、0番目のユーザのエビの評価値について予測すると、予測値は4となりました。 0番目のユーザが評価しており、かつ、エビと正の相関関係にあるのが、1つ(3番目のアイテム=イカ)しかなく、その評価値がそのまま使われる結果となりました。

def predict(scores, similarities, target_user_index):
    """
    評価の予測値を返す
    """
    numerator = 0.0
    denominator = 0.0
    k = 0
    
    target = scores[target_user_index]
    
    for similarity in similarities:
        # 類似度の高い順から5つのアイテムの評価値を使う
        if (k >= 5 or similarity[1] <= 0):
            break
        if (target[similarity[0]] < 0):
            continue
            
        numerator += similarity[1] * target[similarity[0]]
        denominator += similarity[1]
        k += 1
    
    return numerator / denominator if denominator != 0.0 else -1
        
    
target_user_index = 0 # 0番目のユーザ

print('Predict score: {}'.format(predict(scores, similarities, target_user_index)))
# Predict score: 4.0

ランキング

最後に全ての寿司ネタ100種類について評価値を予測し、ランキングにします。

0番目のユーザについては、0番目(エビ)、8番目(トロ)、9番目(甘エビ)、31番目(カツオ)、41番目(ボタンエビ)、53番目(とろサーモン)の評価が4と予測されており、これらをおすすめするのが良さそうです。

def rank_items(scores, target_user_index):
    """
    (アイテムのインデックス, 予測した評価値)のタプルリストを評価値降順で返す
    """
    ranking = []
    similarities_list = []
    
    # 寿司ネタ100種類について評価値を予測
    for i in range(100):
        if scores[target_user_index][i] >= 0:
            continue
        
        similarities =  get_cos_similarities(scores, i)
        
        predict_score = predict(scores, similarities, target_user_index)
        
        ranking.append((i, predict_score))
        
    return sorted(ranking, key=lambda r: r[1], reverse=True)


target_user_index = 0 # 0番目のユーザ

print('Ranking: {}'.format(rank_items(scores, target_user_index)))
# Ranking: [(0, 4.0), (8, 4.0), (9, 4.0), (31, 4.0), (41, 4.0), (53, 4.0), (5, 3.5194984228119655), 

情報推薦システム入門 -理論と実践-

情報推薦システム入門 -理論と実践-