[推奨システム]CF/MFに基づく推奨システムモデリング実践


  • データソース:MovieLens Dataset
  • n/a.ターゲット


    コラボレーションフィルタリングベース、マトリクス分解ベースの推奨モデルの理解と実装
    訓練モデルの結果を説明し,結果を評価した.

    データの読み込み

    import pandas as pd
    
    rating_url = 'https://raw.githubusercontent.com/yoonkt200/python-data-analysis/master/data/ml-1m/ratings.dat'
    rating_df = pd.io.parsers.read_csv(rating_url, names=['user_id', 'movie_id', 'rating', 'time'], delimiter='::', engine ='python')
    rating_df.head()
    
    movie_url = 'https://raw.githubusercontent.com/yoonkt200/python-data-analysis/master/data/ml-1m/movies.dat'
    movie_df = pd.io.parsers.read_csv(movie_url, names=['movie_id', 'title', 'genre'], delimiter='::', engine ='python', encoding='ISO-8859-1')
    movie_df.head()

    各データフレームには、次の情報が含まれます.

  • rating_df
    user id:映画を見るユーザid
    映画id:映画id
    評価:映画に対するユーザーの評価
    time:ユーザーが映画を見る時間

  • movie_df
    映画id:映画id
    映画タイトル
    タイプ:ムービータイプ
  • EDA

  • プレイヤー数と映画数を知る
  • print("고유 아이디 수:", len(rating_df["user_id"].unique()))
    print("영화의 개수:", len(rating_df["movie_id"].unique()))
    >>> 고유 아이디 수: 6040
    >>> 영화의 개수: 3706
  • 映画点数分布可視化
  • import seaborn as sns
    sns.countplot(rating_df["rating"])
    object_cnt = rating_df["rating"].value_counts()
    for x,y,z in zip(object_cnt.index, object_cnt.values, object_cnt.values/object_cnt.sum()*100):
        plt.annotate(f"{y}\n({round(z,2)}%)", xy=(x-1,y+70), textcoords="data", ha="center")

    4点の最多は348971点、1点の最小は56174点だった.

    Rating Matrix


  • Rating Matrixは、ユーザを行、ItemをColumn、ValueをRatingとする行列である.

  • Ratingのタイプには、Explicit FeedbackとImplicit Feedbackがあります.Explicit Feedbackとは、映画点数、評論点数、賛など、ユーザーが直接評価の良し悪しを明確にするデータであり、Implicit Feedbackとは、ユーザーが閲覧、視聴、購入、コメントなどの単純なマトリクスや使用するデータを指す.

  • 推奨モデルとしてRating Matrixを用いた方法としては,コラボレーションフィルタリング(CF)とマトリックス分解(MF)がある.
  • Colaborative Filtering Modeling


    1つの方法は、お客様の好みと興味の表現に基づいて、好みと興味の中から似たようなパターンを持つお客様を見つけることです.趣味が合うお客様には、まだ購入していない商品は、クロス推薦や分類されたお客様の好みや生活形態に応じて、関連商品を推薦する形式のサービスを提供することができます.

    User-based CF / Item-based CF


    ユーザーベースの広告:似たような好みの顧客評価の点数に基づいて、どんな商品を推薦するかを予測します.一般的な方法
    Item-based CFに基づく:商品と商品の類似度を基準として推奨する.新しく登場した商品には適用しにくい欠点がある.

    CFに基づくKNNモデル学習

    from surprise import Dataset, Reader
    from surprise.model_selection import train_test_split
    
    # Reader, Dataset 오브젝트로 학습용 데이터셋 생성 및 분리
    reader = Reader(rating_scale=(1,5)) 	#1~5점 사이의 rating점수가 있다는 것을 알려줌
    data = Dataset.load_from_df(rating_df[["user_id", "movie_id", "rating"]], reader) 	
    trainset, testset = train_test_split(data, test_size=0.25)
    
    # KNNBasic 모델 학습
    from surprise import KNNBasic
    # k: 주변 샘플을 몇 개까지 참조할 것인지, cosine: 가장 일반적인 유사도 계산 방식
    algo = KNNBasic(k=40, min_k=1, sim_options={"user_based":True, "name":"cosine"})        
    algo.fit(trainset)
    predictions = algo.test(testset)
    
    # 모델 평가
    from surprise import accuracy
    acc = accuracy.rmse(predictions)
    acc
    RMSE: 0.9591
    0.9591293463391027

    テスト結果の確認

    prediction[:5]
    [Prediction(uid=2691, iid=912, r_ui=5.0, est=5, details={'was_impossible': False}),
     Prediction(uid=3648, iid=2166, r_ui=2.0, est=2.9331766731573703, details={'was_impossible': False}),
     Prediction(uid=4279, iid=1974, r_ui=4.0, est=3.7635448815576646, details={'was_impossible': False}),
     Prediction(uid=5767, iid=2288, r_ui=5.0, est=3.7394892080225373, details={'was_impossible': False}),
     Prediction(uid=5767, iid=2144, r_ui=4.0, est=4.01071767527927, details={'was_impossible': False})]
    uid:ユーザID,iid:映画ID,r ui:実績スコア,est:予測スコア

    Matrix Factorization Modeling


    MFベースSVDモデルの学習

    from surprise import SVD
    
    param_list = [10, 50, 100, 150, 200]
    rmse_list_by_factors = []
    ttime_list_by_factors = []
    
    # n_factor depth에 따른 RMSE 확인
    for n in param_list:
       train_start = time.time()
       algo = SVD(n_factors=n)
       algo.fit(trainset)
       train_end = time.time()
       print("training time of model: %.2f seconds" % (train_end-train_start))
       print(f"RMSE of test dataset in SVD model, n_factors={n}")
    
       predictions = algo.test(testset)
       rmse_list_by_factors.append(accuracy.mse(predictions))
       ttime_list_by_factors.append(train_end-train_start)
       print("-"*20)
    print("searching n_factors is finish.")

    n factorでのRMSE可視化

    # plt의 plot 함수로 결과 시각화
    import matplotlib.pyplot as plt
    plt.plot(param_list, rmse_list_by_factors)
    plt.title("RMSE by n_factors of SVD", fontsize=14)
    plt.xticks(param_list)
    plt.xlabel("n_factors", fontsize=12)
    plt.ylabel("RMSE", fontsize=12)
    for x, y in zip(param_list, rmse_list_by_factors):
       plt.annotate(f'{round(y,4)}', xy=(x+15,y), textcoords="data", ha="center")
    n_factor=50時RMSE最小、再増加時と適宜時RMSE上昇.

    最終RMSE評価

    algo = SVD(n_factors=50)          
    algo.fit(trainset)
    predictions = algo.test(testset)
    acc = accuracy.rmse(predictions)
    acc
    RMSE: 0.8737
    0.873735086939124

    評価時間の検討


    CF、MFベースの推奨システムの限界(仮定)

  • ユーザーの過去の好みが未来でも同じだと仮定
  • 時系列のプリファレンスではなく、推定または分解の点数
  • A点評価の選好はB点評価の選好と同じラインで学ぶ
  • テストデータの評価も同様に「ランダムな空きスペースを探す」方式で評価
  • したがって、テストデータを時間的に分離して、現在のお客様のニーズに合わせます.

    時間に基づいてSVDモデルを学習する

    rating_df['time'].quantile(q=0.8, interpolation='nearest') # 8:2로 나눌 수 있는 시간 기준탐색 --> 975768738
    
    train_df = rating_df[rating_df["time"]< 975768738][["user_id", "movie_id", "rating"]]
    test_df = rating_df[rating_df["time"]>= 975768738][["user_id", "movie_id", "rating"]]
    
    # 추출한 학습 데이터셋으로 SVD 모델 학습
    data = Dataset.load_from_df(train_df, reader=reader)
    train_data = data.build_full_trainset()
    
    algo = SVD(n_factors=50)
    algo.fit(train_data)
    
  • 標準時間より前のtrain dataでSVDモードを再学習
  • テストと評価


    推奨システムでの評価


    CF、MFベースの推奨システムは、通常MAPを使用する.
    推奨システムでは、各ユーザのPrecisionを計算し、すべての推奨ユーザに拡張し、平均指標を計算します.
    # 예측할 부분 (rating이 없는) 데이터만 추출
    test_data = train_data.build_anti_testset()
    
    # test 
    predictions = algo.test(testset)
    
    
    predictions = algo.test(test_data)
    
    # test 평가를 위해 시청하지 않은 영화의 예상 점수를 dictionary 형태로 추출
    estimated_unwatched_dict = {}
    
    for uid, iid, _, predicted_rating, _ in predictions:
      if uid in estimated_unwatched_dict:
        estimated_unwatched_dict[uid].append((iid, predicted_rating))
      else:
        estimated_unwatched_dict[uid] = [(iid, predicted_rating)]
    使用
  • build_anti_testset評価されていないデータをテストデータとして抽出できます.
  • テスト結果はmovie idと予測スコアがuser idをキーとしたestimated_unwatched_dictディックシャナに格納されている.
  • kパラメータによる推奨結果の評価と可視化

    def get_map_topk(k):
      user_metric = []
      for user in estimated_unwatched_dict:
        estimated_list = estimated_unwatched_dict[user].copy()
        estimated_list.sort(key=lambda tup: tup[1], reverse=True)
        try:
          top_k_prefer_list = [movie[0] for movie in estimated_list[:k]]
          actual_watch_list = user_watch_dict_list_test[user_watch_dict_list_test.index==user].values.tolist()[0]
          user_metric.append((user, top_k_prefer_list, actual_watch_list))
        except:
          pass
      
      # MAP: 유저들의 precision 평균으로 구함
      precision_list = []
      for user in user_metric:
        predictive_values = user[1]
        actual_values = set(user[2])
        tp = [pv for pv in predictive_values if pv in actual_values]
        precision = len(tp) / len(predictive_values)
        precision_list.append(precision)
      return sum(precision_list) / len(precision_list)
  • k本の映画推薦時の結果をMAPで評価する.
  • 各プレイヤーが推奨する映画はk個未満であるためtry-使用排除扉
  • k_param_list = range(1,30)
    map_list = []
    for k in k_param_list:    
      map_list.append(get_map_topk(k))
      
    plt.plot(k_param_list, map_list)
    plt.title('MAP by top k recommendation')
    plt.ylabel('MAP', fontsize=12)
    plt.xlabel('k', fontsize=12)
    plt.show()
  • kの増加に伴いMAPの減少が見られた.つまり、推薦される映画が多ければ多いほど、正確度は低くなります.
  • 推奨システムのMAPの大部分は0.1~0.2の間である.