LESSON

演習:推薦アルゴリズムを実装・比較しよう

「理論は十分だ。MovieLensデータを使って、実際に手を動かしてアルゴリズムを比較してみろ。」

田中VPoEがKaggle Notebookの環境を指す。

「協調フィルタリング、Matrix Factorization、コンテンツベースの3手法を実装し、どれがNetShop社に最適か判断してくれ。」

ミッション概要

MovieLensデータセットを用いて、複数の推薦アルゴリズムを実装・比較する演習である。各手法の特徴を実データで体感し、NetShop社への適用可能性を評価する。


Mission 1: データの準備とEDA(20分)

MovieLens 100K データセットをロードし、基本的なEDAを行え。

タスク:

  1. データの基本統計量を確認する(ユーザー数、アイテム数、評価数、スパース率)
  2. 評価分布をヒストグラムで可視化する
  3. ユーザーごとの評価件数の分布を確認する
  4. アイテムごとの被評価件数の分布を確認する(ロングテール構造)
解答例
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from surprise import Dataset

# MovieLens 100Kデータの読み込み
data = Dataset.load_builtin('ml-100k')
raw_ratings = data.raw_ratings  # (user, item, rating, timestamp)

df = pd.DataFrame(raw_ratings, columns=['user_id', 'item_id', 'rating', 'timestamp'])
df['rating'] = df['rating'].astype(float)

# 基本統計量
n_users = df['user_id'].nunique()
n_items = df['item_id'].nunique()
n_ratings = len(df)
sparsity = 1 - n_ratings / (n_users * n_items)

print(f"ユーザー数: {n_users}")
print(f"アイテム数: {n_items}")
print(f"評価数: {n_ratings}")
print(f"スパース率: {sparsity:.4f} ({sparsity*100:.1f}%)")
# ユーザー数: 943, アイテム数: 1682, 評価数: 100000, スパース率: 93.7%

# 評価分布
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

df['rating'].hist(bins=5, ax=axes[0])
axes[0].set_title('評価値の分布')
axes[0].set_xlabel('Rating')

df.groupby('user_id').size().hist(bins=50, ax=axes[1])
axes[1].set_title('ユーザーごとの評価件数')
axes[1].set_xlabel('Number of ratings')

df.groupby('item_id').size().hist(bins=50, ax=axes[2])
axes[2].set_title('アイテムごとの被評価件数(ロングテール)')
axes[2].set_xlabel('Number of ratings')

plt.tight_layout()
plt.show()

Mission 2: 協調フィルタリングの実装(20分)

User-Based CFとItem-Based CFを実装し、性能を比較せよ。

タスク:

  1. Surpriseライブラリを使い、User-Based CF(コサイン類似度、k=20)を実装する
  2. Item-Based CF(調整済みコサイン類似度、k=20)を実装する
  3. 5-Fold交差検証でRMSEとMAEを比較する
  4. 近傍数kを{5, 10, 20, 40, 80}で変化させ、最適なkを見つける
解答例
from surprise import KNNBasic, KNNWithMeans
from surprise.model_selection import cross_validate

reader = Reader(rating_scale=(1, 5))
dataset = Dataset.load_from_df(df[['user_id', 'item_id', 'rating']], reader)

# User-Based CF
user_cf = KNNWithMeans(k=20, sim_options={
    'name': 'cosine', 'user_based': True
})
user_results = cross_validate(user_cf, dataset, measures=['RMSE', 'MAE'], cv=5)

# Item-Based CF
item_cf = KNNWithMeans(k=20, sim_options={
    'name': 'pearson_baseline', 'user_based': False
})
item_results = cross_validate(item_cf, dataset, measures=['RMSE', 'MAE'], cv=5)

print(f"User-Based CF - RMSE: {user_results['test_rmse'].mean():.4f}, "
      f"MAE: {user_results['test_mae'].mean():.4f}")
print(f"Item-Based CF - RMSE: {item_results['test_rmse'].mean():.4f}, "
      f"MAE: {item_results['test_mae'].mean():.4f}")

# k の最適化
k_values = [5, 10, 20, 40, 80]
results_by_k = []

for k in k_values:
    algo = KNNWithMeans(k=k, sim_options={
        'name': 'pearson_baseline', 'user_based': False
    })
    cv = cross_validate(algo, dataset, measures=['RMSE'], cv=5, verbose=False)
    results_by_k.append({
        'k': k,
        'RMSE': cv['test_rmse'].mean()
    })

results_df = pd.DataFrame(results_by_k)
print(results_df)
# 通常k=20〜40あたりが最適

Mission 3: Matrix Factorizationの実装(20分)

SVDとNMFを実装し、協調フィルタリングと比較せよ。

タスク:

  1. Surprise SVD(factors=50, epochs=20)を実装する
  2. NMF(factors=50)を実装する
  3. 潜在因子数を{10, 30, 50, 100, 200}で変化させ、最適値を見つける
  4. 全手法の比較表を作成し、最も性能の良い手法を特定する
解答例
from surprise import SVD, NMF

# SVD
svd = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02)
svd_results = cross_validate(svd, dataset, measures=['RMSE', 'MAE'], cv=5)

# NMF
nmf = NMF(n_factors=50, n_epochs=50)
nmf_results = cross_validate(nmf, dataset, measures=['RMSE', 'MAE'], cv=5)

print(f"SVD - RMSE: {svd_results['test_rmse'].mean():.4f}")
print(f"NMF - RMSE: {nmf_results['test_rmse'].mean():.4f}")

# 潜在因子数の最適化
factor_values = [10, 30, 50, 100, 200]
svd_by_factors = []

for f in factor_values:
    algo = SVD(n_factors=f, n_epochs=20)
    cv = cross_validate(algo, dataset, measures=['RMSE'], cv=5, verbose=False)
    svd_by_factors.append({
        'factors': f,
        'RMSE': cv['test_rmse'].mean()
    })

# 全手法比較
comparison = pd.DataFrame([
    {'手法': 'User-Based CF', 'RMSE': user_results['test_rmse'].mean()},
    {'手法': 'Item-Based CF', 'RMSE': item_results['test_rmse'].mean()},
    {'手法': 'SVD', 'RMSE': svd_results['test_rmse'].mean()},
    {'手法': 'NMF', 'RMSE': nmf_results['test_rmse'].mean()},
])
print("\n=== 全手法比較 ===")
print(comparison.sort_values('RMSE'))
# 通常SVDが最も良い性能を示す

Mission 4: Top-N推薦評価(20分)

RMSEだけでなく、ランキング指標で評価せよ。

タスク:

  1. 各ユーザーに対してTop-10推薦を生成する
  2. Precision@10、Recall@10、NDCG@10を計算する
  3. 手法間の比較を行う
  4. NetShop社に最も適した手法とその理由を述べる
解答例
from surprise.model_selection import train_test_split
from collections import defaultdict

def get_top_n(predictions, n=10):
    """予測結果からTop-N推薦を生成"""
    top_n = defaultdict(list)
    for uid, iid, true_r, est, _ in predictions:
        top_n[uid].append((iid, est))
    for uid, ratings in top_n.items():
        ratings.sort(key=lambda x: x[1], reverse=True)
        top_n[uid] = ratings[:n]
    return top_n

def precision_recall_at_k(predictions, k=10, threshold=4.0):
    """Precision@KとRecall@Kを計算"""
    user_est_true = defaultdict(list)
    for uid, _, true_r, est, _ in predictions:
        user_est_true[uid].append((est, true_r))

    precisions = {}
    recalls = {}

    for uid, user_ratings in user_est_true.items():
        user_ratings.sort(key=lambda x: x[0], reverse=True)
        top_k = user_ratings[:k]

        n_relevant = sum(1 for _, true_r in user_ratings if true_r >= threshold)
        n_rec_relevant = sum(1 for est, true_r in top_k if true_r >= threshold)

        precisions[uid] = n_rec_relevant / k if k > 0 else 0
        recalls[uid] = n_rec_relevant / n_relevant if n_relevant > 0 else 0

    return (
        sum(precisions.values()) / len(precisions),
        sum(recalls.values()) / len(recalls)
    )

# 各手法でTop-N評価
trainset, testset = train_test_split(dataset, test_size=0.2, random_state=42)

models = {
    'Item-Based CF': KNNWithMeans(k=20, sim_options={
        'name': 'pearson_baseline', 'user_based': False
    }),
    'SVD': SVD(n_factors=50, n_epochs=20),
    'NMF': NMF(n_factors=50),
}

for name, model in models.items():
    model.fit(trainset)
    predictions = model.test(testset)
    precision, recall = precision_recall_at_k(predictions, k=10, threshold=4.0)
    print(f"{name} - Precision@10: {precision:.4f}, Recall@10: {recall:.4f}")

# NetShop社への提案
# ECサイトでは暗黙的フィードバック(購買/閲覧)が主 → ALS推奨
# 新商品も多い → コンテンツベースとのハイブリッドが必要

Mission 5: 考察とNetShop社への提案(10分)

各手法の比較結果を踏まえ、NetShop社への提案をまとめよ。

タスク:

  1. 各手法の精度比較結果をまとめる
  2. 精度以外の観点(Cold Start対応、計算コスト、更新頻度)で比較する
  3. NetShop社に最も適した手法の組み合わせを提案する
  4. 具体的な実装ロードマップを策定する
解答例
NetShop社への提案:

手法比較(精度以外含む):
| 手法 | RMSE | Cold Start | 計算コスト | 更新性 |
|------|------|-----------|----------|--------|
| User-CF | 0.97 | 弱い | 高 | 遅い |
| Item-CF | 0.94 | 弱い | 中 | 安定 |
| SVD | 0.93 | 弱い | 低 | 要再学習 |
| コンテンツ | N/A | 強い | 低 | 即時 |

推奨: ハイブリッドアプローチ
  Phase 1: Item-CF + 人気ランキング(即効性)
  Phase 2: SVD/ALS + コンテンツベース(精度向上)
  Phase 3: セッションベース + リアルタイム化(体験向上)

達成度チェック

  • MovieLensデータのEDAを完了し、スパース率を確認した
  • User-Based/Item-Based CFを実装し比較した
  • SVDとNMFを実装し全手法の比較表を作成した
  • Top-N推薦のランキング指標で評価した
  • NetShop社への具体的な提案をまとめた

推定所要時間: 90分