ぺーぺーSEのテックブログ

備忘録用メモサイト

RSSを購読する

Pythonで機械学習 レコメンド編

代表的なレコメンドアルゴリズムと Python での実装をまとめる。
スクラッチではなく、できるだけライブラリ( scikit-surprise )を利用する。

レコメンドアルゴリズム

  • ポピュラリティ
    • 所謂人気ランキング
  • コンテンツベース(内容ベース)フィルタリング
    • アイテム間の類似度に基づいたレコメンド
      • アイテムの特徴ベクトルで類似度( Cos 類似度など)ソートしてレコメンドする方法
      • 例:野球のバットを買った人には野球のボールをおすすめしよう
    • 参考
  • 協調フィルタリング
    • ユーザの利用履歴を扱う
    • トランザクションデータ、ユーザ・アイテム行列
  • 上記のハイブリッド

ここでは 協調フィルタリング を扱う。

参考:推薦システムのアルゴリズム

レコメンドアルゴリズムでよく発生するネガティブが出来事は以下。

  • 同じようなアイテムばかりレコメンドされる
  • 人気のアイテム、長期間掲載しているアイテムばかりレコメンドされる
  • 数年に一度しか購入しない物に対するレコメンド結果がずっと表示される
  • ユーザー行動履歴が十分に蓄積されていないと精度がでない
  • データの管理コストがでかい
  • レコメンドの計算コストがでかい

協調フィルタリング

協調フィルタリングには主に以下の種類がある。

  • メモリベース(近傍ベース)
    • ユーザ・商品行列をそのまま利用、モデルを作成しない
    • 以下の手法がある
      • ユーザベース
      • アイテムベース
    • スパース性(データサイズに対して意味のある情報が少ない、 0 が多い行列)の高いデータには適用し辛い
  • モデルベース
    • ユーザ・商品行列をモデル構築に利用
    • 以下のような手法がある
      • 特異値分解(SVD)
      • 行列因子分解
      • 確率的(ナイーブベイズ、ベイジアンネットワークなど)
      • 分類、回帰
  • 上記のハイブリッド

狭義の協調フィルタリングは、近傍ベースを指す。

メモリベース協調フィルタリング

メモリベース協調フィルタリングのコンセプトは、「類似度」。
過去の利用履歴から似たもの同士を明らかにし、この類似度を使ってオススメ商品を推定する。
類似度といった場合、ユーザ同士の類似度と商品同士の類似度の 2 つが考えられ、前者を使う場合をユーザベース、後者をアイテムベースと言って区別する。

  • ユーザベース
    • ユーザーの行動履歴からユーザー間の類似度を計算し、おすすめするアイテムを決める手法
      • Aさんに何かおすすめしたい、Aさんは商品a、商品b、商品cを買っている。
      • 同じような行動履歴のBさんは商品a、商品b、商品c、商品dを買っている、ということはAさんには商品dをおすすめできる
  • アイテムベース
    • ユーザーの行動履歴からアイテム間の類似度を計算し、類似するアイテムをおすすめする手法
      • 商品Aは商品Bと一緒に購入されることが多いから、商品Aの購入者に商品Bをおすすめしよう

ユーザベース

ユーザベース協調フィルタリングのロジックは、いわゆるkNN回帰という機械学習手法を利用する。
kNN(k-Nearest-Neighbor、k近傍法 )を簡単に説明すると、推定する対象に最も特徴が似ているk個の観測値を参考にし、値を推定しようというもの。
アルゴリズムの概要は以下の通り。

  1. あるユーザと他のユーザの 類似度 を計算
  2. レコメンドしたい対象ユーザ(ユーザ A )と類似度の高い k 名のユーザを選ぶ
  3. 他のユーザ利用したアイテムのうち、ユーザAがまだ利用していないアイテムの集合を抽出
  4. それらのアイテム群のうち、おすすめ度が高いアイテムのリストを返却
    • この選定の際に、類似度が高いユーザが利用したアイテムほど重みが高くなるようにする

アイテムベース

ユーザベースはユーザ同士の類似度を考えたのに対して、アイテムベースはアイテム同士の類似度を考える。
それ以外の考え方はユーザベースに同じ。

用いられる類似度

類似度には以下のようなものがある。

  • ユークリッド距離( Euclidean distance )
    • 平方ユークリッド距離( Squared Euclidean distance )
    • 2 点間の普通の距離
  • コサイン類似度( Cosine Similarity )
  • ピアソンの積率相関係数( Pearson correlation coefficient )
    • ユーザの評価をそのユーザの評価全体の平均を用いて正規化する
    • データが正規化されていないような状況でユークリッド距離よりも良い結果を得られることが多いとされる

ユーザベースでは、 ピアソンの積率相関係数 を用いる。
そうすることによって以下のように評価の傾向が似ている 2 ユーザ間で高い相関を得られる。

  • ユーザ A :「ラーメンはまずまずで 3 点だがカレーとチャーハンはイマイチだから 1.5 点だな……。」
  • ユーザ B :「ラーメンはうまくて 5 点だがカレーとチャーハンはふつうで 3.5 点だな……。」

また、アイテムベースでは コサイン類似度 がよく用いられる。

実装

scikit-surprise ベースで実装する。

環境設定

環境設定は以下。

1
2
3
4
5
$ python -V
Python 3.6.1
$ pip -V
pip 9.0.1
$ pip install jupyter scikit-learn matplotlib scipy numpy scikit-surprise

データセット

データセットには SUSHI Preference Data Sets を使用する。

1
2
3
4
5
6
7
8
9
10
11
12
$ wget http://www.kamishima.net/asset/sushi3-2016.zip
$ unzip sushi3-2016.zip
$ rm sushi3-2016.zip
$ ls ./sushi3-2016/
README-en.txt
sushi3.idata
sushi3b.5000.10.order
README-ja.txt
sushi3.udata
sushi3b.5000.10.score
README-stat-ja.txt
sushi3a.5000.10.order

sushi3b.5000.10.score は 5000 行 100 列( 5000 人 × 寿司ネタ 100 種類)の構成となっており、各要素には評価値 0 〜 4 (値が大きいほど、好き)と、欠測値 -1 がセットされている。

データのロード

scikit-surprise でデータをロードするには、 Dataset クラスに対応した形式にする必要がある。
形式は 1行1評価値 で、以下のようにする。

1
ユーザID アイテムID 評価値

sushi3b.5000.10.score を上記の形式に変換・Dataset形式でロードするコードは以下。
なお、ユーザ ID は 0000 〜 4999 、アイテム ID は 00 〜 99 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def convert(input_file_name):
# 返還後のファイル名
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

# 「ユーザID アイテムID 評価値」形式にデータファイルを変換
output_file_name = convert('sushi3-2016/sushi3b.5000.10.score')

# with open(output_file_name, mode='r') as f:
# print(f.read())

データフォーマットを変換した後、以下のように Dataset 形式でデータをロードし、 trainset を作成する。

1
2
3
4
5
6
7
from surprise import Reader, Dataset

# ファイルからデータをロードし、Dataset形式に
# dataset.raw_ratings にデータが格納される
dataset = Dataset.load_from_file(output_file_name, reader=reader)
# dataset を計算し、trainset 形式に
trainset = dataset.build_full_trainset()

dataset には以下のようなフィールドがある。(面倒なので説明は一部のみ)

  • build_full_trainset
  • construct_testset
  • construct_trainset
  • folds
  • load_builtin : scikit-surprise が準備しているデータセットの読み込み
  • load_from_df : DataFrame 形式のデータを Dataset 形式へ変換するメソッド
  • load_from_file : ファイルからデータをロードし、 Dataset 形式へ変換するメソッド
  • load_from_folds : ファイルからデータをロードし、 Dataset 形式へ変換するメソッド(クロスバリデーションなどで利用)
  • n_folds
  • ratings_file
  • raw_folds
  • raw_ratings : ユーザID アイテムID 評価値 None 形式のデータ
  • read_ratings
  • reader
  • shuffle
  • split

trainset には以下のようなフィールドがある。(面倒なので説明は一部のみ)

  • all_items : n_items の range を返却するメソッド
  • all_ratings
  • all_users
  • build_anti_testset
  • build_testset
  • global_mean : 評価値の平均( _global_mean )を計算するメソッド
  • ir : ユーザ ID をインデクスとした各アイテムの評価値のタプル
  • knows_item
  • knows_user
  • n_items : アイテム数
  • n_ratings : 評価数(ユーザ数 x アイテム数、欠損は考えられていない)
  • n_users : ユーザ数
  • offset
  • rating_scale
  • to_inner_iid : trainset 内部で管理しているアイテム ID へ変換するメソッド
  • to_inner_uid
  • to_raw_iid
  • to_raw_uid
  • ur : アイテム ID をインデクスとした各ユーザの評価値のタプル

協調フィルタリング

scikit-surprise で実装できるアルゴリズムの一覧は ここ

scikit-surprise の各アルゴリズムのクラスは AlgoBase クラスを継承して作成されている。
このクラスには、 compute_similaritiesalgo.train(trainset) など全てのアルゴリズムで共通して利用される実装されている。
sim_options オプションを利用することで、類似度の計算方法( cosine 、 msd 、 pearson 、 pearson_baseline )やユーザベースかアイテムベース( user_based )などを指定することができる。

メモリベース協調フィルタリング

以下のような形で各アルゴリズムの学習を実行する。

1
2
3
4
5
6
7
8
from surprise import KNNBasic

sim_options = {
'name': 'pearson', # 類似度を計算する方法を指定( cosine,msd,pearson,pearson_baseline )
'user_based': True # False にするとアイテムベースに
}
algo = KNNBasic(k=5, min_k=1,sim_options=sim_options)
algo.train(trainset)

上記で、 k近傍法 の学習が完了する。
algo.compute_similarities() を実行するとユーザ間の類似度を計算してくれ、「ユーザ数 x ユーザ数」の 2 次元配列を返却する。
なお、 algo.train を実行すると内部で algo.compute_similarities() が実行されており、その後 algo.sim で「ユーザ数 x ユーザ数」の 2 次元配列でユーザ間の類似度を取得できる。

1
print(train.sim)

また、以下のように学習後のモデルを使用して評価の予測値を取得できる。

1
2
3
4
5
user_id = '{:04d}'.format(0)
item_id = '{:02d}'.format(92)

pred = algo.predict(uid=user_id, iid=item_id)
print('Predicted rating(User: {0}, Item: {1}): {2:.2f}'.format(pred.uid, pred.iid, pred.est))

スクラッチでの実装例は以下。

  • ユーザベース
    • http://ohke.hateblo.jp/entry/2017/09/22/230000
    • https://qiita.com/hik0107/items/96c483afd6fb2f077985
  • アイテムベース
    • http://ohke.hateblo.jp/entry/2017/09/29/230000
    • https://qiita.com/kotaroito/items/6acb58bb16b68a460af9

モデルベース協調フィルタリング

特異値分解( SVD )

1
2
3
4
from surprise import SVD

algo = SVD()
algo.train(trainset)

参考