Edited at

協調フィルタリングを用いたPythonのラーメン推薦システム


全体の流れ


  1. 推薦システムの概要

  2. 協調フィルタリングについて

  3. データの作成

  4. 使用した機械学習モデルをざっくり説明

  5. 機械学習モデル間の出力速度と精度の比較

  6. 考察とまとめ


1. 推薦システムの概要

今回作成する推薦システムでは協調フィルタリングを用いて、事前に収集したデータ傾向からユーザーに最適なラーメンを推薦させる。


推薦プロセス


  1. 選択肢となる合計31枚のラーメン画像(s_0 ~ s_30)の中から好きなラーメン画像を5枚選んでもらう

  2. 学習済みの機械学習モデルに選択結果を入れる

  3. 機械学習モデルによって推薦候補のラーメン画像(y=0~3)の中から最適なラーメンが推薦される。


ラーメンの詳細

選択肢ラーメン画像(s_0 ~ s_30)



推薦候補のラーメン画像(y=0~3)


2. 協調フィルタリングについて

協調フィルタリングをざっくり説明すると以下の通りである。


協調フィルタリングとは、コミュニティー全体を基にしたレコメンドシステムで、あなたの趣味嗜好や興味が、反映される形のレコメンドエンジンとなります。協調フィルタリングは、あなたの趣味嗜好と類似しているユーザーを探し出して、そのユーザー達の嗜好に基づいて、オススメを行います。


※ codexsa「機械学習を使って630万件のレビューに基づいたアニメのレコメンド機能を作ってみよう(機械学習 k近傍法 初心者向け)」より引用。

つまり今回のシステムでは、事前に収集したデータ内のラーメン選択傾向とユーザー自身のラーメン選択傾向との類似性を元に推薦を行う。


3. データの作成

今回のデータは所属する大学ゼミ内で協力してもらい、5枚の選択肢の組み合わせを説明変数、推薦ラーメン画像4枚のうち好きなラーメン画像のラベル(y=0~3)を目的変数として、データを作成した。


元データ収集

前回の記事で紹介したslackbotを応用して選択結果をslackに自動収集するシステムを作成し、元データを収集した。

※データは説明変数をダミー変数化し、目的変数はyの値でラベルを与えた。



収集した元データを整理した結果は以下の通りである。

- raw data -

(総ユーザーデータ数, 選択肢候補数)=(45, 32)

y=0 : フカクサ製麺食堂 (ユーザーデータ数: 25)

y=1 : ラーメンこんじき 深草店 (ユーザーデータ数: 15)

y=2 : ラー麺 陽はまた昇る (ユーザーデータ数: 4)

y=3 : 健龍園 (ユーザーデータ数: 1)


データの拡張と整形

元データのままではデータ数が偏っているので、データから各推薦ラベル(y=0~3)と選択傾向を抽出してデータの拡張と整形を行う。


選択傾向の抽出

データから各推薦ラベル(y=0~3)の選択傾向を抽出すると以下のようになった。

y=0

['s_15', 's_20', 's_3', 's_5', 's_6', 's_7', 's_8', 's_14', 's_16', 's_17', 's_0', 's_21', 's_22', 's_28', 's_25', 's_10', 's_29', 's_18']

y=1

['s_22', 's_5', 's_6', 's_8', 's_9', 's_11', 's_17', 's_20', 's_15', 's_0', 's_26', 's_24', 's_28', 's_27', 's_2', 's_13']

y=2

['s_20', 's_4', 's_5', 's_8', 's_14', 's_30', 's_16', 's_19', 's_15', 's_23', 's_28', 's_27', 's_22']

y=3

['s_4', 's_30', 's_16', 's_23', 's_14', 's_1', 's_12']


データの拡張

抽出した選択傾向を元に各推薦ラベル(y=0~3)との組み合わせデータを拡張して同数にする。

拡張方法としては、「選択傾向の抽出データからランダムで5個選ぶ」という作業を任意の回数(拡張したいデータ数)の分だけ繰り返せば良い。

import numpy as np

import pandas as pd
from matplotlib import pyplot as plt
from PIL import Image
import io
%matplotlib inline
#カラム作成
columns_l=[]
for i in range(0, 31):
column="s_"+str(i)
columns_l.append(column)
columns_l.append("y")

#各拡張データ数の指定
data_num=100

#DataFrame作成
df=pd.DataFrame(np.zeros([4*data_num, 32]), columns=columns_l, index=range(4*data_num))
idx=0
N=0
for lis in col_list:
for i in range(data_num):
col=np.random.choice(lis, 5,replace=False)
df.loc[N][col]=1.0
df.loc[N]["y"]=idx
N+=1
idx+=1
#csvファイルとして保存
df.to_csv("augmented_data.csv")

#データの詳細
print("augmented data: "+str(df.shape))
train_name=[
'フカクサ製麺食堂',
'ラーメンこんじき 深草店',
'ラー麺 陽はまた昇る',
'健龍園'
]
for i in range(4):
print("y="+str(i)+" : "+train_name[i]+str(df[df["y"]==i].shape))

結果は以下のようになる。

augmented data: (200, 32)

y=0 : フカクサ製麺食堂(50, 32)

y=1 : ラーメンこんじき 深草店(50, 32)

y=2 : ラー麺 陽はまた昇る(50, 32)

y=3 : 健龍園(50, 32)

よってy毎にユーザーデータ数が50ずつに拡張された。


4. 使用した機械学習モデルをざっくり説明

ここから機械学習のモデルを作成していく。今回使用したモデルは以下の5つである。


DecisionTreeClassifier

二分岐するツリー構造から結果を予測する手法。





非常に高速。データのスケールを考慮する必要がない。可視化が可能で説明しやすい。

※「Pythonではじめる機械学習」より引用


RandomForestClassifier

複数のDecisionTreeClassifierを用意して多数決で結果を予測する手法。





ほとんどの場合単一の決定木よりも高速で、頑健で、協力。データのスケールを考慮する必要がない。高次元の疎なデータには適さない。

※「Pythonではじめる機械学習」より引用


LogisticRegression

直線で変数領域を分割して結果を予測する手法。





最初に試してみるべきアルゴリズム。非常に大きいデータセットに適する。非常に高次元のデータに適する。

※「Pythonではじめる機械学習」より引用


SVC(Support Vector Classifier)

曲線で変数領域を分割して結果を予測する手法。





同じような意味を持つ特徴量からなる中規模データセットに対しては強力。データのスケールを調整する必要がある。パラメータに敏感。

※「Pythonではじめる機械学習」より引用


KNeighborsClassifier

与えられたデータに近いデータラベルから多数決で結果を予測する手法。





小さいデータに関しては良いベースラインとなる。説明が容易。

※「Pythonではじめる機械学習」より引用

※各画像: Introduction to Machine Learning with Pythonより引用


5. 機械学習モデル間の出力速度と精度の比較

ここからは推薦システムに導入する最適なモデルを選択するために、各モデルの出力速度と精度を比較していく。

手順としては…

1. sklearnで機械学習モデルを実装し、各出力速度と精度を求める。

2. データ傾向を反映させるためにsklearnを使用して求めた結果にベイズ推定を加えて再度出力速度と精度を求める。


5-1. sklearnで機械学習モデルを実装

まずはデータを読み込む。

※augmented_data.csvと同様の手順でtuning.csv(data_num=50), evaluation.csv(data_num=250)を作成しておく。

import numpy as np

import pandas as pd
from matplotlib import pyplot as plt
from PIL import Image
import io
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_curve, auc, accuracy_score
from sklearn.metrics import confusion_matrix
from sklearn.tree import export_graphviz
import pydotplus
import graphviz
from IPython.display import Image
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
%matplotlib inline

#データ読み込み
#訓練用データ
data=pd.read_csv("augmented_data.csv", index_col=0)
#パラメータチューニング用データ(*今回は使用を省略)
tuning=pd.read_csv("tuning.csv", index_col=0)
#精度評価用データ
evaluation=pd.read_csv("evaluation.csv", index_col=0)

#説明変数と目的変数を分離させる。
train_x=data.drop("y", axis=1)
train_y=data["y"]
tune_x=tuning.drop("y", axis=1)
tune_y=tuning["y"]
eva_x=evaluation.drop("y", axis=1)
eva_y=evaluation["y"]


DecisionTreeClassifier実装

%%time

#モデルの作成
clf = DecisionTreeClassifier(random_state=0)
clf = clf.fit(train_x, train_y)
#モデルによる予測
pred = clf.predict(eva_x)

出力結果

CPU times: user 5.42 ms, sys: 2.25 ms, total: 7.66 ms

Wall time: 7.66 ms

#予測精度の評価

accuracy_score(pred, eva_y)

出力結果

0.961

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, pred), annot=True, fmt="d", cmap="Greens")
plt.title("classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Predicted label", fontsize=15)
plt.show()

出力結果



※True label: 正解ラベル, Predicted label: 予測ラベル

→図の(左上から右下にかけての)対角線上の数値が大きい・色が濃いほど高精度と言える。

またDecisionTreeClassifierでは各特徴量(説明変数)の重要度や、用いたツリー構造を可視化できる。

#各特徴量(説明変数)の重要度

plt.figure(figsize=(15, 5))
plt.bar(train_x.columns, clf.feature_importances_)
plt.title("feature_importances", fontsize=20)
plt.show()

出力結果

#用いたツリー構造を可視化

train_name=[
"フカクサ製麺食堂",
"ラーメンこんじき 深草店",
"ラー麺 陽はまた昇る",
"健龍園"
]
export_graphviz(clf, out_file="tree.dot", feature_names=train_x.columns,
class_names=train_name, impurity=False, filled=True)

graph=pydotplus.graph_from_dot_file(path="tree.dot")
Image(graph.create_png())

出力結果


RandomForestClassifier実装

%%time

#モデルの作成
clf=RandomForestClassifier(n_estimators=1000, random_state=0)
clf = clf.fit(train_x, train_y)
#モデルによる予測
pred = clf.predict(eva_x)

出力結果

CPU times: user 1.28 s, sys: 28 ms, total: 1.31 s

Wall time: 1.34 s

#予測精度の評価

accuracy_score(pred, eva_y)

出力結果

0.983

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, pred), annot=True, fmt="d", cmap="Greens")
plt.title("classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Predicted label", fontsize=15)
plt.show()

出力結果

またRandomForestClassifierでもDecisionTreeClassifierと同様に各特徴量(説明変数)の重要度を可視化できる。

#各特徴量(説明変数)の重要度

plt.figure(figsize=(15, 5))
plt.bar(train_x.columns, clf.feature_importances_)
plt.title("feature_importances", fontsize=20)
plt.show()

出力結果


LogisticRegression実装

%%time

#モデルの作成
clf=LogisticRegression(solver="lbfgs", multi_class="auto", random_state=0)
clf = clf.fit(train_x, train_y)
#モデルによる予測
pred = clf.predict(eva_x)

出力結果

CPU times: user 63.5 ms, sys: 13.5 ms, total: 77 ms

Wall time: 78.2 ms

#予測精度の評価

accuracy_score(pred, eva_y)

出力結果

0.984

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, pred), annot=True, fmt="d", cmap="Greens")
plt.title("classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Predicted label", fontsize=15)
plt.show()

出力結果

またLogisticRegressionでは各説明変数毎の回帰係数の組み合わせを可視化できる。

#回帰係数の組み合わせを可視化

plt.figure(figsize=(10, 10))
for i in range(len(clf.coef_)):
plt.subplot(4, 1, i+1)
plt.bar(train_x.columns, clf.coef_[i])
plt.title("y={}_label_coefficients".format(i), fontsize=20)
plt.tight_layout()
plt.show()

出力結果


SVC実装

%%time

#モデルの作成
clf=SVC(C=1.0, gamma=1.0, random_state=0, probability=True)
clf = clf.fit(train_x, train_y)
#モデルによる予測
pred = clf.predict(eva_x)

出力結果

CPU times: user 88.8 ms, sys: 3.49 ms, total: 92.3 ms

Wall time: 92.3 ms

#予測精度の評価

accuracy_score(pred, eva_y)

出力結果

0.978

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, pred), annot=True, fmt="d", cmap="Greens")
plt.title("classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Predicted label", fontsize=15)
plt.show()

出力結果


KNeighborsClassifier実装

%%time

#モデルの作成
clf=KNeighborsClassifier()
clf = clf.fit(train_x, train_y)
#モデルによる予測
pred = clf.predict(eva_x)

出力結果

CPU times: user 26.1 ms, sys: 2.23 ms, total: 28.3 ms

Wall time: 28.8 ms

#予測精度の評価

accuracy_score(pred, eva_y)

出力結果

0.935

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, pred), annot=True, fmt="d", cmap="Greens")
plt.title("classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Predicted label", fontsize=15)
plt.show()

出力結果


5-2. 機械学習モデルの出力結果にベイズ推定を加えてデータ傾向を反映させる

「5-1. sklearnで機械学習モデルを実装」で求めた各精度の信頼度をさらに向上させるために、収集データの結果から各事象の事前確率を求めてベイズの定理に当てはめ、(ベイズ推定による)新たな予測を行う。


ベイズの定理

ベイズの定理の直感的な解釈としては、「数字の出る確率が"均等ではない"サイコロ」によって確率を求めていくイメージ。


ベイズの定理 (Bayes' theorem)

P(B)を事象Aが起きる前の事象Bが起きる確率(事前確率)、P(B|A)を事象Aが起きた後で事象Bが起きる確率(事後確率)とする。このときP(B|A)は下式で表される。これをベイズの定理と言う。




統計WEBより画像・文章引用


ベイズ推定の実装

実装にあたり、元データから算出各事前確率は以下の通りである。

上記の数値と各機械学習モデルを用いてベイズ推定を行う関数を作成する

def Bayesian_probability(clf, x):

#各事前確率
pre_pro=[
0.444,
0.222,
0.167,
0.167
]

#予測ラベル(yの値)を格納するリスト
predict_one=[]

for idx in range(len(clf.predict_proba(x))):
each_proba=[]
for i in range(4):
each_proba.append((clf.predict_proba(x)[idx]*pre_pro)[i]/sum(clf.predict_proba(x)[idx]*pre_pro))
predict_one.append(np.argmax(each_proba))
#予測ラベルのリストを返す
return predict_one


各機械学習モデルにベイズ推定を実装する

※ ツリー構造によって(確率1.0まで分岐させる)完全な予測を行わせているため、DecisionTreeClassifierに限り今回はベイズ推定を行わないこととした。


RandomForestClassifierへ実装

%%time

#モデルの作成
clf=RandomForestClassifier(n_estimators=1000, random_state=0)
clf = clf.fit(train_x, train_y)
#ベイズ推定による予測
b_pred=Bayesian_probability(clf, eva_x)

出力結果

CPU times: user 23min 35s, sys: 2.72 s, total: 23min 38s

Wall time: 23min 43s

#予測精度の評価

accuracy_score(b_pred, eva_y)

出力結果

0.987

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, b_pred), annot=True, fmt="d", cmap="Oranges")
plt.title("Bayesian classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Bayesian Predicted label", fontsize=15)
plt.show()

出力結果



※True label: 正解ラベル, Bayesian Predicted label: ベイズ推定による予測ラベル


LogisticRegressionへ実装

%%time

#モデルの作成
clf=LogisticRegression(solver="lbfgs", multi_class="auto", random_state=0)
clf = clf.fit(train_x, train_y)
#ベイズ推定による予測
b_pred=Bayesian_probability(clf, eva_x)

出力結果

CPU times: user 11.1 s, sys: 15 ms, total: 11.1 s

Wall time: 5.57 s

#予測精度の評価

accuracy_score(b_pred, eva_y)

出力結果

0.987

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, b_pred), annot=True, fmt="d", cmap="Oranges")
plt.title("Bayesian classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Bayesian Predicted label", fontsize=15)
plt.show()

出力結果


SVCへ実装

%%time

#モデルの作成
clf=SVC(C=1.0, gamma=1.0, random_state=0, probability=True)
clf = clf.fit(train_x, train_y)
#ベイズ推定による予測
b_pred=Bayesian_probability(clf, eva_x)

出力結果

CPU times: user 2min 30s, sys: 1.28 s, total: 2min 31s

Wall time: 2min 33s

#予測精度の評価

accuracy_score(b_pred, eva_y)

出力結果

0.981

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, b_pred), annot=True, fmt="d", cmap="Oranges")
plt.title("Bayesian classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Bayesian Predicted label", fontsize=15)
plt.show()

出力結果


KNeighborsClassifierへ実装

%%time

#モデルの作成
clf=KNeighborsClassifier()
clf = clf.fit(train_x, train_y)
#ベイズ推定による予測
b_pred=Bayesian_probability(clf, eva_x)

出力結果

CPU times: user 2min 27s, sys: 1.57 s, total: 2min 29s

Wall time: 2min 32s

#予測精度の評価

accuracy_score(b_pred, eva_y)

出力結果

0.902

#ヒートマップによる結果の可視化

sns.heatmap(confusion_matrix(eva_y, b_pred), annot=True, fmt="d", cmap="Oranges")
plt.title("Bayesian classification results", fontsize=18)
plt.ylabel("True label", fontsize=15)
plt.xlabel("Bayesian Predicted label", fontsize=15)
plt.show()

出力結果


6. 考察とまとめ

今回の実装ではダミー変数を用いたシンプルな変数による協調フィルタリングを行ったため、比較的精度の高い予測結果となった。

また単純なsklearnの機械学習モデルの結果にベイズ推定の関数を加えることで、(KNeighborsClassifierを除く)ほぼ全ての機械学習モデルにおいて予測精度が向上した。

比較した機械学習モデルの中で最も良い精度としては、ベイズ推定を付加したRandomForestClassifierとLogisticRegressionの0.987(98.7%)であった。しかしRandomForestClassifierでベイズ推定を行った際にかなりの計算時間を要したため、今回の推薦システムにはLogisticRegressionを採用することにする。


今後について

今後の予定としては…

1. 推薦システムをwebアプリとしてリリース(flaskで実装予定)

2. 推薦システムを発展させ、未知のラーメン画像が人気となるか否かを判断するシステムを実装する(Qiita投稿済み)

といったことを考えている。


開発環境

jupyter Notebook 5.7.4


参考URL

codexsa「機械学習を使って630万件のレビューに基づいたアニメのレコメンド機能を作ってみよう(機械学習 k近傍法 初心者向け)」

Pythonで作る株価予測SlackBot

Introduction to Machine Learning with Python

統計WEB: ベイズの定理