メニュー 閉じる

K近傍法で映画のレコメンデーションエンジンを作ろう!

今回はUdemyで提供されている、「実践データサイエンス&機械学習 with Python」という、
教材を使ってKNN(K近傍法)を使った映画のレコメンデーションエンジンを実装してみました。

講師の吾妻さんの教材はこの教材以外にも興味深いものが用意されています。
この講座もおすすめですし、他の講座も興味あるものがあるかもしれません。ぜひ見てみてくださいね。

データサイエンスの心得としてはまずこの記事が参考になるかと思います。

データサイエンスが人間中心であるべき理由とおすすめの講座

 

pandasへデータの読み込みと映画の人気度の計算

早速ですが実際に「実践データサイエンス&機械学習 with Python」を受講されている方は、
動画を見ながら実装してみたことだと思います。

しかし、実際問題ただJupyterで動かしてみるだけでも意味がないわけではないのですが、
コードが意図していることまでは十分に考える時間がありませんよね。

今回はまずざっくりと「もしもう一度自分で実装してみるならどう考えるか」ということを念頭に、
講師の吾妻さんのコードを追いかけてみたいと思いました。

まずは、pandasの読み込みとデータの読み込みです。

import pandas as pd

r_cols = ['user_id', 'movie_id', 'rating']
ratings = pd.read_csv('/Users/linda/DataScience-Python3/ml-100k/u.data', sep='\t', names=r_cols, useclos-range(3))
ratings.head()

データの保存場所(/Users/linda/DataScience-Python3/ml-100k/u.dataとなっている部分)は、
それぞれ皆さんの保存した場所のパスを指定してくださいね。
ここではpandasというパッケージを使います。

user_id movie_id rating
0 0 50 5
1 0 172 5
2 0 133 1
3 196 242 3
4 186 302 3

データのcolumnは左からuser_id、movie_id、ratingとなっています。
読み込みの際に名前をつけていますのでわかりやすいですね。
「usecols=range(3)」で左から使うcolumnの列数を指定します。
.head()は頭の5行を表示するものです。

次にmovie_IDごとにユーザーからの評価点の合計と平均値を計算します。
ここではnumpyというパッケージを使います。

import numpy as np

movieProperties = ratings.groupby('movie_id').agg({'rating': [np.size, np.mean]}
movieProperties.head()

movieProperties関数を作ります。.aggメソッドはデータ集計に用いる関数です。
ratings.groupby関数でmovie.idを指定します。これによって同じmovie.idは一つにまとめられます。
そして、np.sizeが評価点の集計値であり、np.meanが平均値を計算します。

 

データの正規化をどうするかは分析の肝になる?

次に、「データの正規化」を行います。
これは活用しやすい形にデータを整形することです。

具体的には評価点の集計値について以下のように計算します。

$$\frac{(x-np.min(x)) }{ (np.max(x)-np.min(x))}$$

これは、任意(x)の映画の評価点の集計値から最小値である評価点の集計値を引き、
最大値の集計値から最小値の集計値を引いたもので割るという処理をしています。

例えば「大乱闘スマッシュシスターズ」という映画があったとして評価の集計値が100でした。
ここから全く人気のない「寂しい歌」という映画で一人が1点をつけているだけだったとすれば、
分子は99となります。

評価点の最大値が500だったとして、分子は最大値から最小値を引いたものですから、
分母は499となります。

つまるところ、「大乱闘スマッシュシスターズ」は99÷499と計算されます。
0.1983…ということで一番評価点が集まった映画と比べるとだいたい2割程度の人気?と言えます。

movieNumRatings = pd.DataFrame(movieProperties['rating']['size])
movieNormalizedNumRatings = movieNumRatings.apply(lambda x: (x - np.min(x)) / (np.max(x) - np.min(x)))
movieNormalizedNumRatings.head()

一応、moviePropertiesの最大値と最小値を見ておきましょうか。

np.max(movieProperties)
np.min(movieProperties)
*np.maxについて
rating size 584.0 mean 5.0 dtype: float64

今回の正規化で行った計算はどのようなものだったのでしょうか?
データに対して、任意のxが全体のどのくらいの位置にあるかが分かりました。
正規化後の数字が1に近いほどたくさんの評点が集まった映画であり、
数字が0に近いほどあまり評点が集まらなかった映画であると言えます。

人気のある映画は沢山の人に見られて評価する人も多いでしょうから、
合計の評点数が大きくなります。確かに「人気がある」一つの指標と言えるかもしれませんね。

 

映画ジャンル情報の取得とディクショナリ格納

movieDict = {}
withopen(r'/Users/linda/DataScience-Python3/ml-100k/u.item',encoding="ISO-8859-1") as f:
    temp =''
for line in f:
        fields = line.rstrip('\n').split('|')
        movieID =int(fields[0])
        name = fields[1]
        genres = fields[5:25]
        genres =map(int, genres)
        movieDict[movieID] = (name, np.array(list(genres)), movieNormalizedNumRatings.loc[movieID].get('size'), movieProperties.loc[movieID].rating.get('mean'))
ここでも映画のジャンル情報ファイル(u.item)はご自身のディレクトリを指定してくださいね。
小生の環境(Python3.8.6)ではエンコーディングエラーが表示されてしまいました。

※データの取得自体には影響が無いので引き続き講義を進めることはできます.

また、元々はエンコーディングに関する記載がfor line in f:以下に記載されていましたが、
データファイルを読み込む際にエンコーディングを指定することで回避しています。(参考URL
エンコーディングエラーは上記のコードでは出ないと思います。

$ print(movieDict[1])
('Toy Story (1995)', array([0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 0.7735849056603774, 3.8783185840707963)

 

作品間の距離を定義してみよう!

ここまで、映画作品の人気度を定義し、そしてジャンルデータの取得を行いました。
次は人気度とジャンルの2つのデータから作品同士がどれだけ似ているかを計算します。

「人気度が高い映画は似ている」かもしれませんし、
「ジャンルが似ている映画は内容も似ている」かもしれないという仮説を立てるわけです。

確かに人気映画には共通点があるとしたら、とある映画Aという人気の映画が好きな人は、
その人がまだ見ていない人気の映画Bも好きである可能性が高いと言えます。

戦争映画が好きな人は、戦争映画が好きな可能性は確かに高いと言えます。

from scipy import spatial

def ComputeDistance(a, b):
genresA = a[1]
genresB = b[1]
genreDistance = spatial.distance.cosine(genresA, genresB)
popularityA = a[2]
popularityB = b[2]
popularityDistance = abs(popularityA - popularityB)
return genreDistance + popularityDistance

ComputeDistance(movieDict[2], movieDict[4])

ここではscipyからspatialをインポートします。
ジャンルの距離を図るためにspatialでコサイン距離を求めます。

このspatial.distance.sosineは2つの1次元配列同士の距離を計算しています。。
数学アレルギー患者に言わせると1次元配列というと「ドキッ」としてしまいますが、
これは「普通の配列」のことです。[1,2,3,4,5,6]という感じのものです。

あくまでこれは「距離」を計算しているので、
例えば「トイストーリー」と「ゴールデンアイ」はどう頑張っても似ていないですよね。
確かにデータ上もまったくジャンルが一致していません。

('Toy Story (1995)', array([0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 0.7735849056603774, 3.8783185840707963)
('GoldenEye (1995)', array([0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]), 0.22298456260720412, 3.2061068702290076)

このコサイン距離を計算すると、1になります。

def ComputeDistance(a, b):
genresA = a[1]
genresB = b[1]
genreDistance = spatial.distance.cosine(genresA, genresB)
return (genreDistance)

$ ComputeDistance(movieDict[1], movieDict[2])

1.0

ひとまず、これで映画の類似度を数字にすることができましたね。
普段エンジニアとして仕事をされている方ではなく、
いきなりデータサイエンスに興味がありこの記事を見つけてしまったという方にとってはかなり重い内容だと思いますが、
そういった方はぜひUdemyの講座の方を先に見てみてください。

ひととおり見てから自分でコードを理解して書いてみるという方法が1番負担なく、
そして勉強になると思います。

わからないコードがあればQiitaやStock Overflowなどで質問してみると、
やさしいエンジニアの先輩方が考えてくれる(教えてくれる)と思います。

どこまで理解できているか、環境等はどうなっている状態かもしっかり書いておくと、
OSやバージョンに固有のエラーについても検討して回答してくれるのでより正確な回答が期待できます。

続きは(やるかわからないけど)また次回!

 

環境とレコメンデーションエンジン作成で使えるデータ集

The Movies Dataset|Kaggle
https://www.kaggle.com/rounakbanik/the-movies-dataset

環境構築はAnacondaが比較的容易ですが、UNIXの理解がある方であれば、
venvなどローカルの仮想環境を作って作業用ディレクトリ内に設定ファイルをおいておき、
随時利用するときはその仮想環境を使うという形で運用しても良いかもしれませんね。

私は最近はAnacondaをアンインストールしてvenvを使っていますが、
やはり直感的にすぐ使えるようにするならAnacondaも便利だなと思います。

【さようなら】Anacondaを安全に削除しよう【アンインストール】