協調フィルタリング(Collaborative filtering)レコメンドエンジン

目次

1. 協調フィルタリングの概要
_1.1 協調フィルタリング(Collaborative Filtering)とは
_1.2 協調フィルタリングの長所・短所
2. 実験:
_2.1 環境設定
_2.2 データロード
_2.3 データ確認
_2.4 モデル作成
_2.5 レコメンドエンジンで距離計算
_2.6 入力したデータからレコメンドエンジン利用

1. 協調フィルタリングの概要

前回はTensorFlowでのレコメンダー【tensorflow-recommenders (TFRS)】を話しました。レコメンドエンジンは複数アルゴリズムがあります。今回の記事は協調フィルタリング(Collaborative filtering)を解説したいと思います。例えば、Google Playでのアプリのインストールの40%は、推奨事項によるものです。YouTubeの総再生時間の60%は、おすすめによるものです。レコメンドエンジンは非常に大切なことです。

1.1 協調フィルタリング(Collaborative Filtering)とは

協調フィルタリング(Collaborative Filtering、CF)は、多くのユーザの過去の行動履歴と嗜好情報を蓄積し、あるユーザの行動履歴から嗜好を推論するレコメンドエンジンです。

Items:システムが推奨するエンティティ。 Google Playストアの場合、Itemsはインストールするアプリです。

Query:システムが推奨を行うために使用する情報。 クエリは、次の組み合わせにすることができます。

Embedding:離散セット(この場合は、クエリのセット、または推奨するアイテムのセット)から埋め込みスペースと呼ばれるベクトル空間へのマッピングであり、次元削減した結果です。 通常、埋め込みスペースは低次元です。

Similarity Measures:類似性は、二つのユーザーやアイテムのペアを取得し、それらの類似性を測定する値を返す関数です。

距離と類似度の解説の詳細はこちらです。

 

1.2 協調フィルタリングの長所・短所

長所

・ドメイン知識は必要ありません。

・セレンディピティ:このモデルは、ユーザーが新しい興味を発見するのに役立ちます。

・優れた出発点:ある程度、システムは行列因数分解モデルをトレーニングするためにフィードバック行列のみを必要とします。

短所

・新しいアイテムを処理できません(コールドスタートの問題)。WALSおよびHeuristicsの手法が必要です。

・クエリ/アイテムのサイド機能(クエリまたはアイテムID以外の機能)を含めるのは難しいです。

 

2. 実験

環境:google colab

データセット:MovieLens 100K Dataset

ミネソタ大学のユーザが好きに映画の情報を眺めたり評価するデータセット

MovieLens 100K Dataset

1700本の映画で1000人のユーザーから100,000件の評価。 1998年リリース。

詳細:http://grouplens.org/datasets/movielens/

モデル: 協調フィルタリングのレコメンドエンジン

 

2.1 環境設定

ライブラリインストールとライブラリインポート

from __future__ import print_function

 

import numpy as np

import pandas as pd

import collections

from mpl_toolkits.mplot3d import Axes3D

from IPython import display

from matplotlib import pyplot as plt

import sklearn

import sklearn.manifold

import tensorflow.compat.v1 as tf

tf.disable_v2_behavior()

tf.logging.set_verbosity(tf.logging.ERROR)

 

# PandasDataFrameにいくつかの便利な関数を追加します。pd.options.display.max_rows = 10

pd.options.display.float_format = ‘{:.3f}’.format

def mask(df, key, function):

“””Returns a filtered dataframe, by applying function to key”””

return df[function(df[key])]

 

def flatten_cols(df):

df.columns = [‘ ‘.join(col).strip() for col in df.columns.values]

return df

 

pd.DataFrame.mask = mask

pd.DataFrame.flatten_cols = flatten_cols

 

# Altairをインストールし、そのcolabレンダラーをアクティブにします。

print(“Installing Altair…”)

!pip install git+git://github.com/altair-viz/altair.git

import altair as alt

alt.data_transformers.enable(‘default’, max_rows=None)

alt.renderers.enable(‘colab’)

print(“Done installing Altair.”)

 

# スプレッドシートをインストールし、認証モジュールをインポートします。

USER_RATINGS = False

!pip install –upgrade -q gspread

from google.colab import auth

import gspread

from oauth2client.client import GoogleCredentials

 

2.2 データロード

MovieLensのデータセットを読み込みます。 users、ratings、moviesのデータフレームを作成します。

# Download MovieLens データセット

from urllib.request import urlretrieve

import zipfile

 

urlretrieve(“http://files.grouplens.org/datasets/movielens/ml-100k.zip”, “movielens.zip”)

zip_ref = zipfile.ZipFile(‘movielens.zip’, “r”)

zip_ref.extractall()

print(“Done. Dataset contains:”)

print(zip_ref.read(‘ml-100k/u.info’))

 

# users データフレーム

users_cols = [‘user_id’, ‘age’, ‘sex’, ‘occupation’, ‘zip_code’]

users = pd.read_csv(

‘ml-100k/u.user’, sep=’|’, names=users_cols, encoding=’latin-1′)

 

#  ratingsデータフレーム作成

ratings_cols = [‘user_id’, ‘movie_id’, ‘rating’, ‘unix_timestamp’]

ratings = pd.read_csv(

‘ml-100k/u.data’, sep=’\t’, names=ratings_cols, encoding=’latin-1′)

 

#  movies データフレーム作成

genre_cols = [

“genre_unknown”, “Action”, “Adventure”, “Animation”, “Children”, “Comedy”,

“Crime”, “Documentary”, “Drama”, “Fantasy”, “Film-Noir”, “Horror”,

“Musical”, “Mystery”, “Romance”, “Sci-Fi”, “Thriller”, “War”, “Western”

]

movies_cols = [

‘movie_id’, ‘title’, ‘release_date’, “video_release_date”, “imdb_url”

] + genre_cols

movies = pd.read_csv(

‘ml-100k/u.item’, sep=’|’, names=movies_cols, encoding=’latin-1′)

 

モデル用のデータ加工

・IDは1から始まるので、0から始まるようにシフトします。

・ジャンルが割り当てられている映画の数を計算します。

・all_genres:映画のすべてのアクティブなジャンル。

・genre:アクティブなジャンルからランダムにサンプリングされます。

# IDは1から始まる変更

users[“user_id”] = users[“user_id”].apply(lambda x: str(x-1))

movies[“movie_id”] = movies[“movie_id”].apply(lambda x: str(x-1))

movies[“year”] = movies[‘release_date’].apply(lambda x: str(x).split(‘-‘)[-1])

ratings[“movie_id”] = ratings[“movie_id”].apply(lambda x: str(x-1))

ratings[“user_id”] = ratings[“user_id”].apply(lambda x: str(x-1))

ratings[“rating”] = ratings[“rating”].apply(lambda x: float(x))

 

# ジャンルの件数

genre_occurences = movies[genre_cols].sum().to_dict()

 

# all_genresとgenreのカラムを作成

def mark_genres(movies, genres):

def get_random_genre(gs):

active = [genre for genre, g in zip(genres, gs) if g==1]

if len(active) == 0:

return ‘Other’

return np.random.choice(active)

def get_all_genres(gs):

active = [genre for genre, g in zip(genres, gs) if g==1]

if len(active) == 0:

return ‘Other’

return ‘-‘.join(active)

movies[‘genre’] = [

get_random_genre(gs) for gs in zip(*[movies[genre] for genre in genres])]

movies[‘all_genres’] = [

get_all_genres(gs) for gs in zip(*[movies[genre] for genre in genres])]

 

mark_genres(movies, genre_cols)

 

# movielensのデータフレーム

movielens = ratings.merge(movies, on=’movie_id’).merge(users, on=’user_id’)

 

学習とテストデータをかける関数

def split_dataframe(df, holdout_fraction=0.1):

test = df.sample(frac=holdout_fraction, replace=False)

train = df[~df.index.isin(test.index)]

return train, test

 

2.3 データ確認

Usersデータ確認

users.describe(include=[np.object])

user_id  sex       occupation         zip_code

count    943       943       943       943

unique   943       2          21         795

top        419       M         student  55414

freq       1          670       196       9

 

moviesデータ確認

movies.describe(include=[np.object])

movie_id           title       release_date      imdb_url            year      genre    all_genres

count    1682     1682     1681     1679     1682     1682     1682

unique   1682     1664     240       1660     72         19         216

top        1303     Ice Storm, The (1997)     01-Jan-1995      http://…            1996     Drama   Drama

freq       1          2          215       2          355       547       376

 

ratingsデータ確認

ratings.describe(include=[np.object])

user_id  movie_id

count    100000  100000

unique   943       1682

top        404       49

freq       737       583

 

2.4 モデル作成

映画の評価するユーザーが少ないです。効率的な表現のために、tf.SparseTensor を使用します。

def build_rating_sparse_tensor(ratings_df):

indices = ratings_df[[‘user_id’, ‘movie_id’]].values

values = ratings_df[‘rating’].values

return tf.SparseTensor(

indices=indices,

values=values,

dense_shape=[users.shape[0], movies.shape[0]])

 

Mean Squared Errorのモデル評価

def sparse_mean_square_error(sparse_ratings, user_embeddings, movie_embeddings):

predictions = tf.gather_nd(

tf.matmul(user_embeddings, movie_embeddings, transpose_b=True),

sparse_ratings.indices)

loss = tf.losses.mean_squared_error(sparse_ratings.values, predictions)

return loss

 

# CFModel helper class

class CFModel(object):

self._embedding_vars = embedding_vars

self._loss = loss

self._metrics = metrics

self._embeddings = {k: None for k in embedding_vars}

self._session = None

 

@property

def embeddings(self):

“””The embeddings dictionary.”””

return self._embeddings

 

def train(self, num_iterations=100, learning_rate=1.0, plot_results=True,

optimizer=tf.train.GradientDescentOptimizer):

with self._loss.graph.as_default():

opt = optimizer(learning_rate)

train_op = opt.minimize(self._loss)

local_init_op = tf.group(

tf.variables_initializer(opt.variables()),

tf.local_variables_initializer())

if self._session is None:

self._session = tf.Session()

with self._session.as_default():

self._session.run(tf.global_variables_initializer())

self._session.run(tf.tables_initializer())

tf.train.start_queue_runners()

 

with self._session.as_default():

local_init_op.run()

iterations = []

metrics = self._metrics or ({},)

metrics_vals = [collections.defaultdict(list) for _ in self._metrics]

 

# Train and append results.

for i in range(num_iterations + 1):

_, results = self._session.run((train_op, metrics))

if (i % 10 == 0) or i == num_iterations:

print(“\r iteration %d: ” % i + “, “.join(

[“%s=%f” % (k, v) for r in results for k, v in r.items()]),

end=”)

iterations.append(i)

for metric_val, result in zip(metrics_vals, results):

for k, v in result.items():

metric_val[k].append(v)

 

for k, v in self._embedding_vars.items():

self._embeddings[k] = v.eval()

 

if plot_results:

# Plot the metrics.

num_subplots = len(metrics)+1

fig = plt.figure()

fig.set_size_inches(num_subplots*10, 8)

for i, metric_vals in enumerate(metrics_vals):

ax = fig.add_subplot(1, num_subplots, i+1)

for k, v in metric_vals.items():

ax.plot(iterations, v, label=k)

ax.set_xlim([1, num_iterations])

ax.legend()

return results

 

 

# マトリックス因数分解モデルを構築し、学習します。

def build_model(ratings, embedding_dim=3, init_stddev=1.):

# Split the ratings DataFrame into train and test.

train_ratings, test_ratings = split_dataframe(ratings)

# SparseTensor representation of the train and test datasets.

A_train = build_rating_sparse_tensor(train_ratings)

A_test = build_rating_sparse_tensor(test_ratings)

# Initialize the embeddings using a normal distribution.

U = tf.Variable(tf.random_normal(

[A_train.dense_shape[0], embedding_dim], stddev=init_stddev))

V = tf.Variable(tf.random_normal(

[A_train.dense_shape[1], embedding_dim], stddev=init_stddev))

train_loss = sparse_mean_square_error(A_train, U, V)

test_loss = sparse_mean_square_error(A_test, U, V)

metrics = {

‘train_error’: train_loss,

‘test_error’: test_loss

}

embeddings = {

“user_id”: U,

“movie_id”: V

}

return CFModel(embeddings, train_loss, [metrics])

 

 

協調フィルタリングを学習します。

# Build the CF model and train it.

model = build_model(ratings, embedding_dim=30, init_stddev=0.5)

model.train(num_iterations=1000, learning_rate=10.)

 

2.5 レコメンドエンジンで距離計算

Aladdinを入力すると、一番似ている映画が表示します。

movie_neighbors(reg_model, “Aladdin”, COSINE)

cosine score      titles     genres

94         1.000    Aladdin (1992)    Animation-Children-Comedy-Musical

587       0.848    Beauty and the Beast (1991)       Animation-Children-Musical

70         0.830    Lion King, The (1994)     Animation-Children-Musical

81         0.816    Jurassic Park (1993)       Action-Adventure-Sci-Fi

417       0.749    Cinderella (1950)            Animation-Children-Musical

173       0.746    Raiders of the Lost Ark (1981)     Action-Adventure

 

2.6 入力したデータからレコメンドエンジン利用

Google driveを接続して、スプレッドシートにデータを入力します。

 

USER_RATINGS = True #@param {type:”boolean”}

 

# 実行してスプレッドシートを作成し、それを使用して評価を入力します。

if USER_RATINGS:

auth.authenticate_user()

gc = gspread.authorize(GoogleCredentials.get_application_default())

# Create the spreadsheet and print a link to it.

try:

sh = gc.open(‘MovieLens-test’)

except(gspread.SpreadsheetNotFound):

sh = gc.create(‘MovieLens-test’)

 

worksheet = sh.sheet1

titles = movies[‘title’].values

cell_list = worksheet.range(1, 1, len(titles), 1)

for cell, title in zip(cell_list, titles):

cell.value = title

worksheet.update_cells(cell_list)

print(“Link to the spreadsheet: ”

“https://docs.google.com/spreadsheets/d/{}/edit”.format(sh.id))

 

 

下記のようなデータを入力します。

入力したデータをロードします。

# スプレッドシートから評価をロードし、DataFrameを作成します。

if USER_RATINGS:

my_ratings = pd.DataFrame.from_records(worksheet.get_all_values()).reset_index()

my_ratings = my_ratings[my_ratings[1] != ”]

my_ratings = pd.DataFrame({

‘user_id’: “943”,

‘movie_id’: list(map(str, my_ratings[‘index’])),

‘rating’: list(map(float, my_ratings[1])),

})

# Remove previous ratings.

ratings = ratings[ratings.user_id != “943”]

# Add new ratings.

ratings = ratings.append(my_ratings, ignore_index=True)

# Add new user to the users DataFrame.

if users.shape[0] == 943:

users = users.append(users.iloc[942], ignore_index=True)

users[“user_id”][943] = “943”

print(“Added your %d ratings; you have great taste!” % len(my_ratings))

ratings[ratings.user_id==”943″].merge(movies[[‘movie_id’, ‘title’]])

 

Added your 15 ratings; you have great taste!

/usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:18: SettingWithCopyWarning:

A value is trying to be set on a copy of a slice from a DataFrame

 

入力データから推奨事項と最近傍を提供する

# Matrix Factorization model

DOT = ‘dot’

COSINE = ‘cosine’

def compute_scores(query_embedding, item_embeddings, measure=DOT):

u = query_embedding

V = item_embeddings

if measure == COSINE:

V = V / np.linalg.norm(V, axis=1, keepdims=True)

u = u / np.linalg.norm(u)

scores = u.dot(V.T)

return scores

 

def user_recommendations(model, measure=DOT, exclude_rated=False, k=6):

if USER_RATINGS:

scores = compute_scores(

model.embeddings[“user_id”][943], model.embeddings[“movie_id”], measure)

score_key = measure + ‘ score’

df = pd.DataFrame({

score_key: list(scores),

‘movie_id’: movies[‘movie_id’],

‘titles’: movies[‘title’],

‘genres’: movies[‘all_genres’],

})

if exclude_rated:

# remove movies that are already rated

rated_movies = ratings[ratings.user_id == “943”][“movie_id”].values

df = df[df.movie_id.apply(lambda movie_id: movie_id not in rated_movies)]

display.display(df.sort_values([score_key], ascending=False).head(k))

 

def movie_neighbors(model, title_substring, measure=DOT, k=6):

# Search for movie ids that match the given substring.

ids =  movies[movies[‘title’].str.contains(title_substring)].index.values

titles = movies.iloc[ids][‘title’].values

if len(titles) == 0:

raise ValueError(“Found no movies with title %s” % title_substring)

print(“Nearest neighbors of : %s.” % titles[0])

if len(titles) > 1:

print(“[Found more than one matching movie. Other candidates: {}]”.format(

“, “.join(titles[1:])))

movie_id = ids[0]

scores = compute_scores(

model.embeddings[“movie_id”][movie_id], model.embeddings[“movie_id”],

measure)

score_key = measure + ‘ score’

df = pd.DataFrame({

score_key: list(scores),

‘titles’: movies[‘title’],

‘genres’: movies[‘all_genres’]

})

display.display(df.sort_values([score_key], ascending=False).head(k))

 

 

自分のレビューから、下記の映画リストを推論(レコメンド)されました。

user_recommendations(model, measure=COSINE, k=5)

cosine score      movie_id           titles     genres

219       0.698    219       Mirror Has Two Faces, The (1996)           Comedy-Romance

739       0.672    739       Jane Eyre (1996) Drama-Romance

844       0.670    844       That Thing You Do! (1996)          Comedy

236       0.647    236       Jerry Maguire (1996)       Drama-Romance

292       0.645    292       Donnie Brasco (1997)     Crime-Drama

 

# Embedding Visualization code (run this cell)

 

def visualize_movie_embeddings(data, x, y):

nearest = alt.selection(

type=’single’, encodings=[‘x’, ‘y’], on=’mouseover’, nearest=True,

empty=’none’)

base = alt.Chart().mark_circle().encode(

x=x,

y=y,

color=alt.condition(genre_filter, “genre”, alt.value(“whitesmoke”)),

).properties(

width=600,

height=600,

selection=nearest)

text = alt.Chart().mark_text(align=’left’, dx=5, dy=-5).encode(

x=x,

y=y,

text=alt.condition(nearest, ‘title’, alt.value(”)))

return alt.hconcat(alt.layer(base, text), genre_chart, data=data)

 

def tsne_movie_embeddings(model):

“””Visualizes the movie embeddings, projected using t-SNE with Cosine measure.

Args:

model: A MFModel object.

“””

tsne = sklearn.manifold.TSNE(

n_components=2, perplexity=40, metric=’cosine’, early_exaggeration=10.0,

init=’pca’, verbose=True, n_iter=400)

 

print(‘Running t-SNE…’)

V_proj = tsne.fit_transform(model.embeddings[“movie_id”])

movies.loc[:,’x’] = V_proj[:, 0]

movies.loc[:,’y’] = V_proj[:, 1]

return visualize_movie_embeddings(movies, ‘x’, ‘y’)

 

 

次元削減した後のEmbeddingを可視化します。

各映画の距離を確認することができます。

tsne_movie_embeddings(model_lowinit)

 

担当者:HM

香川県高松市出身 データ分析にて、博士(理学)を取得後、自動車メーカー会社にてデータ分析に関わる。その後コンサルティングファームでデータ分析プロジェクトを歴任後独立 気が付けばデータ分析プロジェクトだけで50以上担当

理化学研究所にて研究員を拝命中 応用数理学会所属