🎬

映画のレコメンドシステムを作成してみた

2021/01/04に公開

映画レビューのデータセットを使用した強調フィルタリングのレコメンドシステムを作成したのでまとめます。

詳細なコードは、GitHubGoogle Colabをご参照ください。

協調フィルタリングについてはこちらのサイトを参考にしています。
https://qiita.com/hik0107/items/96c483afd6fb2f077985

使用しているデータセットの概要

MovieLens というサイトの、一番容量の軽い MovieLens Latest Datasets を使用しています。

ml-latest-small.zip を解凍すると、いくつかのファイルが展開されますが、以下の2つの csv を使用します。

  1. 映画のタイトルとジャンルが記載された movies.csv。
  2. レビューのレートが記載された ratings.csv。なおレビューの値は0.5~5.0 となっています。

なお、2つのcsvファイルをDataFrameに変換しmovieid カラムでマージし、必要となるユーザー(正確にはデータセット上、ユーザーがマスキング処理されているのでユーザーID)、映画のタイトル、レビューのレートのみ抽出すると、下記のようなデータが作成されます。

これをカラムがユーザーID、インデックスが映画のタイトルとなるように整形します。

df = dataset.pivot_table(index='title', columns='userId', values='rating')
df.columns.name = None
df

整形の際、留意点が2点あります。

1点目は pandas の pivot ではなく、pivot_table を使用する点です。

pivot はデータの集約ができないため、重複が存在する場合、それらが全く同じ値であってもエラーになって集計できないためであります。

http://ynomura.dip.jp/archives/2019/03/pandaspivotpivo.html

2点目は、DataFrame.columns.name = None の処理です。

pivot_table でデータを整形すると、下記のようにマルチインデックスになってしまうので、上記の処理でマルチインデックスを解除しています。

http://ynomura.dip.jp/archives/2019/03/pandasmeltpivot.html

レコメンドについて

なお、参考サイトは辞書型データを使用しているので、レビューしていない映画のレビュー値は存在しないが、このブログのように DataFrame でデータを作成すると、レビューしていない映画のレビュー値がNaNとして認識されてしまいます。

# .notnull() を使って NaN がないデータを抽出している
movie1 = dataset[dataset[person1].notnull()][person1].index

そこで、類似値算定の際、レビュー値に NaN が入っている部分を無視する処理を追加しています。

また、レビュー値は0.5~5.0 と大きな差がないのでコサイン類似度は使っていません。

詳しくは下記サイトをご参照ください。

https://qiita.com/Qiitaman/items/fa393d93ce8e61a857b1

https://qiita.com/michi_wkwk/items/613034b1ec52b6be4720

類似度を算定する関数を作成します。

### 類似値の算定
def get_similairty(person1, person2, dataset):

    # レートがNaNになっているものを除いた、指定した人の見た映画リスト
    movie1 = dataset[dataset[person1].notnull()][person1].index
    movie2 = dataset[dataset[person2].notnull()][person2].index

    ## 各映画とも見た人の集合
    set_movie1 = set(movie1)
    set_movie2 = set(movie2)
    set_both = set_movie1.intersection(set_movie2)

    if len(set_both)==0: #共通でみた映画がない場合は類似度を0とする
        return 0

    # 同じ映画のレビュー点の差の2乗を計算
    # この数値が大きいほど「似ていない」と定義 
    distance = pow(dataset.loc[list(set_both)][person1] - dataset.loc[list(set_both)][person2], 2)
    distance = distance.sum()

    return 1/(1+np.sqrt(distance)) #各映画の似てなさの合計の逆比的な指標を返す

ユーザーID が 1 のユーザーと、50 のユーザーの類似値を算定してみると、以下のようになります。

# ユーザーは 1~610
get_similairty(1, 50, df)

出力
0.12002728245132872

この類似値を算定する関数を利用してレコメンド関数を作成します。

ここでもレビュー値が NaN のものは無視する処理を追加します。

### レコメンド関数
def get_recommend(person, top_N, dataset):

     #推薦度スコアを入れるための箱を作っておく
    totals = {}
    simSums = {}

    # 自分以外のユーザのリストを取得してFor文を回す
    # -> 各人との類似度、及び各人からの(まだ本人が見てない)映画の推薦スコアを計算するため
    list_others = list(dataset)
    list_others.remove(person)

    for other in list_others:

        # 本人以外が見た映画
        other_movie = dataset[dataset[other].notnull()][other].index
        set_other = set(other_movie) # 集合

        # 本人が見た映画
        self_movie = dataset[dataset[person].notnull()][person].index        
        set_self_movie = set(self_movie)  # 集合

        # 本人がまだ見たことが無い映画の集合を取得
        set_new_movie = set_other.difference(set_self_movie)

        # あるユーザと本人の類似度を計算(simは0~1の数字)
        sim = get_similairty(person, other, dataset)

        # (本人がまだ見たことがない)映画のリストでFor分を回す
        for movie in set_new_movie:

            # "類似度 x レビュー点数" を推薦度のスコアとして、全ユーザで積算する
            totals.setdefault(movie,0)
            totals[movie] += dataset[other][movie]*sim

            # またユーザの類似度の積算値をとっておき、これで上記のスコアを除する
            simSums.setdefault(movie,0)
            simSums[movie] += sim

    rankings = [(total/simSums[movie],movie) for movie, total in totals.items()]
    rankings.sort()
    rankings.reverse()

    return [i[1] for i in rankings][:top_N]

ユーザーID が 12 のユーザーに対し5つレコメンドする映画を出力させてみると、以下のようになります。

# ユーザーは 1~610
recommend_movie = get_recommend(12, 5, df)
pd.DataFrame(recommend_movie, columns=["RecommendMovie"])

以上になります、最後までお読みいただきありがとうございました。

参考情報

レコメンドの理論については、推薦システムのアルゴリズムというPDFが参考になりました。

Discussion