け日記

SIerから転職したWebアプリエンジニアが最近のITのキャッチアップに四苦八苦するブログ

Python scikit-surpriseを使ってレコメンドする

scikit-surpriseというライブラリを使って、お寿司データセットのレコメンドを実装します。

scikit-surpriseとは

scikit-surpriseは、レコメンドシステムで必要となる類似度評価や予測アルゴリズムなどを提供するライブラリです。

類似のライブラリとしてcrabrecsysなどもありますが、scikit-surpriseは最近もアップデートされ続けられており、伸びしろのあるライブラリかと思います。

surprise.readthedocs.io

pypi.python.org

github.com

利用する前にインストールしておきます。

$ pip install -U scikit-surprise

scikit-surpriseで寿司ネタをレコメンドする

scikit-surpriseで寿司ネタをレコメンドするモデルを構築してみます。

基本的な手順は3段階です。 1. データセットをDatasetクラスに読み込む 2. 評価値を予測するアルゴリズムを学習させてモデルを得る 3. モデルを評価する

事前準備

今回もSUSHI Preference Data Setsを使いますので、All Data Setからsushi3-2016.zipをダウンロード・展開して、sushi3b.5000.10.scoreファイルを.pyファイルと同じディレクトリにコピーしておきます。

www.kamishima.net

sushi3b.5000.10.scoreファイルは、以下のような5000行100列となっており、行がユーザ、列がアイテム(寿司ネタ)に対応して評価値(-1は未評価を表す)を持っています。

-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
-1 -1 -1 -1 -1 -1 0 -1 1 -1 -1 -1 0 -1 -1 -1 -1 0 -1 -1 -1 1 2 -1 0 -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 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 3 4 -1 -1 -1 3 -1 -1 -1 -1 -1 -1 4 -1 4 -1 -1 -1 -1 -1 -1 -1 -1 -1 3 -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 -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 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
...

しかし、このフォーマットではDatasetへロードできません。 1行が1評価値となるように変換する必要があります。
そのため、ユーザID アイテムID 評価値 に変換したファイル(sushi3b.5000.10.score_converted)を予め作成しておきます。 ユーザIDは0000〜4999、アイテムIDは00〜99とします。

def convert(input_file_name):
    """
    'ユーザID アイテムID 評価値'のフォーマットへ変換してファイルに出力する
    """
    output_file_name = input_file_name + '_converted' # 変換後のファイル名
    output = ''
    
    with open(input_file_name, mode='r') as f:
        lines = f.readlines()
        
        user_id = 0
        for line in lines:
            words = line.strip().split(' ')
            for item_id, word in enumerate(words):
                score = int(word)
                if score != -1:
                    output += '{0:04d} {1:02d} {2:01d}\n'.format(user_id, item_id, score)
                    
            user_id += 1
    
    with open(output_file_name, mode='w') as f:
        f.write(output)
    
    return output_file_name


file_name = convert('./sushi3b.5000.10.score')

with open(output_file_name, mode='r') as f:
    print(f.read())
# 0000 01 0
# 0000 03 4
# 0000 04 2
# 0000 12 1
# 0000 44 1
# ...

Datasetへの読み込み

scikit-surpriseでは、最初に評価値の情報をDatasetクラスへロードする必要があります。

まずはReaderを作成します。
引数line_formatで各行がユーザID、アイテムID、評価値の順番で列を持つこと、sepで各列が' '(半角スペース)で区切られることを明示的に指定しています。

Dataset.load_from_fileメソッドで、先程変換したファイルとReaderインスタンスを渡すことで、Datasetが生成されます。
現時点の最新バージョン(1.0.3)では、ファイル(または同梱されているデータセット)からしかロードできません。 (numpyのndarrayやPandasのDataFrameなど、メモリ上で直接ロードできるようになると嬉しいのですが、もう一つ不便なところですね。)
10/9追記 1.0.4からpandas.DataFrameからロードできるload_from_dfも用意されました。

from surprise import Reader, Dataset

reader = Reader(line_format='user item rating', sep=' ')
dataset = Dataset.load_from_file(file_name, reader=reader)

SVDによる学習と予測

得られたDatasetを使ってモデルを学習させます。

予測アルゴリズムにはSVDクラスを使います。
scikit-surpriseの予測アルゴリズムは全てAlgoBaseを親クラスとしており、子クラスはtrain、test、predict、compute_similaritiesなどのメソッドを実装しています。 そのため、どの予測アルゴリズムでも、共通したインタフェースで扱えるようになっています。

Dataset.build_full_trainsetメソッドで全てのデータを学習データセットとしたTrainsetインスタンスを取得して、trainメソッドで学習します。

0番目のユーザの0番目のアイテム(エビ)の評価値をpredictメソッドで予測してみたところ、2.72の評価値が得られました。

from surprise import SVD

model = SVD()

# 全てのデータを使って学習
trainset = dataset.build_full_trainset()
model.train(trainset)

# ユーザ間の類似度を計算
similarities = model.compute_similarities()

print('similarities.shape: {}'.format(similarities.shape))
# similarities.shape: (5000, 5000)
print('Similarity(User 0000 and 0001: {:.3f})'.format(similarities[0,1]))
# Similarity(User 0000 and 0001: 0.500)

# 0番目のユーザの0番目のアイテム(エビ)の評価値を予測する
user_id = '{:04d}'.format(0)
item_id = '{:02d}'.format(0)

prediction = model.predict(uid=user_id, iid=item_id)

print('Predicted rating(User: {0}, Item: {1}): {2:.2f}'
        .format(prediction.uid, prediction.iid, prediction.est))
# Predicted rating(User: 0000, Item: 00): 2.72

評価

最後にモデルを評価します。

Datasetのsplitメソッドでデータセットをk分割して交差検証することができます。
scikit-learnのtrain_test_splitなどと異なり、あくまでDatset内での分割となります。

evaluateにモデル、データセット、そして指標のリスト(1.0.3時点では'RMSE'、'MAE'、'FCP'の3種類)を渡すことで、交差検証を行います。
結果は指標毎のディクショナリとして返ってきます。

from surprise import evaluate

# 学習データとテストデータを4分割
dataset.split(n_folds=4)

# 平方平均二乗誤差と平均絶対誤差の算出
result = evaluate(model, dataset, measures=['RMSE', 'MAE'])
# Evaluating RMSE, MAE of algorithm SVD.
#
# ------------
# Fold 1
# RMSE: 1.1653
# MAE:  0.9437
# ------------
# Fold 2
# RMSE: 1.1534
# MAE:  0.9362
# ------------
# Fold 3
# RMSE: 1.1474
# MAE:  0.9277
# ------------
# Fold 4
# RMSE: 1.1560
# MAE:  0.9349
# ------------
# ------------
# Mean RMSE: 1.1555
# Mean MAE : 0.9356
# ------------
# ------------

print('Mean RMSE: {:.3f}'.format(sum(result['rmse'])/len(result['rmse'])))
print('Mean MAE: {:.3f}'.format(sum(result['mae'])/len(result['mae'])))
# Mean RMSE: 1.156
# Mean MAE: 0.936

おわりに

この他にも類似度の計算やグリッドサーチによるパラメータの最適化などの豊富な機能が提供されています。
機会があればまた紹介してきます。