使用 Surprise 函式庫進行專業評估

到目前為止,我們手動實作了推薦系統。但手動實作有幾個問題:

  • 難以進行標準化的評估
  • 缺乏交叉驗證
  • 沒有內建的超參數最佳化

Surprise 是一個專門為推薦系統設計的 Python 函式庫,它內建了多種協同過濾演算法與評估工具。

安裝 Surprise

pip install scikit-surprise

Surprise 的核心概念

Surprise 的 API 與 Scikit-Learn 非常相似:

  • Dataset:載入評分資料
  • Reader:定義評分範圍
  • Trainset / Testset:訓練集與測試集
  • 演算法:SVD、NMF、KNNBasic、KNNWithMeans 等
  • Cross Validation:交叉驗證

載入資料到 Surprise

from surprise import Dataset, Reader
from surprise.model_selection import train_test_split
import pandas as pd

# 載入 MovieLens 資料
ratings = pd.read_csv('https://files.grouplens.org/datasets/movielens/ml-latest-small/ratings.csv')

# 定義 Surprise Reader(評分範圍 0.5-5.0)
reader = Reader(rating_scale=(0.5, 5.0))

# 載入資料
data = Dataset.load_from_df(
    ratings[['userId', 'movieId', 'rating']],
    reader
)

# 分割訓練/測試集
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)

print(f"訓練集: {trainset.n_users} 位使用者, {trainset.n_items} 部電影, {trainset.n_ratings} 筆評分")
print(f"測試集: {len(testset)} 筆評分")

訓練 SVD 模型

SVD (奇異值分解) 是 Netflix Prize 冠軍使用的核心演算法,也是目前最流行的協同過濾方法。

from surprise import SVD
from surprise import accuracy

# 建立 SVD 模型
svd_model = SVD(
    n_factors=100,    # 隱含特徵數量
    n_epochs=20,      # 訓練迭代次數
    lr_all=0.005,     # 學習率
    reg_all=0.02      # 正則化強度
)

# 訓練
svd_model.fit(trainset)

# 預測測試集
predictions = svd_model.test(testset)

# 評估
rmse = accuracy.rmse(predictions)
mae = accuracy.mae(predictions)

print(f"SVD 模型 RMSE: {rmse:.4f}")
print(f"SVD 模型 MAE: {mae:.4f}")

SVD 的直覺理解

SVD 的核心思想是:使用者和物品之間存在一些隱含的「因子」。

例如,電影的隱含因子可能是:

  • 因子 1:動作場面 vs 文戲
  • 因子 2:嚴肅 vs 輕鬆
  • 因子 3:現代 vs 古典
  • 因子 4:特效 vs 劇情

SVD 會自動從評分資料中「學習」出這些因子,以及每個使用者/電影對每個因子的偏好程度。

比較多種演算法

from surprise import SVD, NMF, KNNBasic, KNNWithMeans
from surprise.model_selection import cross_validate

# 定義要比較的演算法
algorithms = [
    ('SVD', SVD()),
    ('NMF', NMF()),
    ('KNN Basic', KNNBasic()),
    ('KNN With Means', KNNWithMeans()),
]

# 進行交叉驗證
results = []
for name, algo in algorithms:
    cv_results = cross_validate(
        algo, data,
        measures=['RMSE', 'MAE'],
        cv=5,          # 5 折交叉驗證
        verbose=False
    )
    
    avg_rmse = cv_results['test_rmse'].mean()
    avg_mae = cv_results['test_mae'].mean()
    fit_time = cv_results['fit_time'].mean()
    
    results.append({
        '演算法': name,
        'RMSE': round(avg_rmse, 4),
        'MAE': round(avg_mae, 4),
        '訓練時間(s)': round(fit_time, 2)
    })
    
    print(f"{name:20s}  RMSE: {avg_rmse:.4f}  MAE: {avg_mae:.4f}  時間: {fit_time:.2f}s")

# 顯示比較表格
import pandas as pd
results_df = pd.DataFrame(results)
print("\n=== 演算法比較 ===")
print(results_df.to_string(index=False))

超參數最佳化(Grid Search)

from surprise.model_selection import GridSearchCV

# 定義參數網格
param_grid = {
    'n_factors': [50, 100, 150],
    'n_epochs': [20, 30],
    'lr_all': [0.005, 0.01],
    'reg_all': [0.02, 0.1]
}

# 執行 Grid Search
gs = GridSearchCV(
    SVD,
    param_grid,
    measures=['rmse', 'mae'],
    cv=3,
    n_jobs=-1  # 使用所有 CPU 核心
)

gs.fit(data)

# 最佳參數
print("最佳參數 (RMSE):")
print(gs.best_params['rmse'])
print(f"最佳 RMSE: {gs.best_score['rmse']:.4f}")

print("\n最佳參數 (MAE):")
print(gs.best_params['mae'])
print(f"最佳 MAE: {gs.best_score['mae']:.4f}")

為使用者產生推薦

# 使用最佳參數重新訓練
best_params = gs.best_params['rmse']
best_svd = SVD(**best_params)

# 在完整資料集上訓練
full_trainset = data.build_full_trainset()
best_svd.fit(full_trainset)

# 為特定使用者推薦未看過的電影
def predict_for_user(user_id, n_recommendations=10):
    """為使用者預測所有未評分電影的評分,並推薦最高的 N 部"""
    # 取得使用者已評分的電影
    user_ratings = ratings[ratings['userId'] == user_id]
    rated_movies = user_ratings['movieId'].tolist()
    
    # 所有電影
    all_movies = movies['movieId'].tolist()
    
    # 未評分的電影
    unrated_movies = [m for m in all_movies if m not in rated_movies]
    
    # 預測每部未評分電影的評分
    predictions = []
    for movie_id in unrated_movies:
        predicted_rating = best_svd.predict(user_id, movie_id).est
        predictions.append((movie_id, predicted_rating))
    
    # 排序取最高的 N 部
    predictions.sort(key=lambda x: x[1], reverse=True)
    top_predictions = predictions[:n_recommendations]
    
    results = []
    for movie_id, predicted_rating in top_predictions:
        movie_info = movies[movies['movieId'] == movie_id].iloc[0]
        results.append({
            'title': movie_info['title'],
            'genres': movie_info['genres'],
            'predicted_rating': round(predicted_rating, 2)
        })
    
    return results

# 測試
print("\n=== SVD 推薦結果 ===")
recommendations = predict_for_user(1, 10)
for i, rec in enumerate(recommendations, 1):
    print(f"{i}. {rec['title']:50s} 預測評分: {rec['predicted_rating']:.2f}  類型: {rec['genres']}")

使用 Vibe Coding 做 SVD 推薦

🔥 【SVD 推薦詠唱範例】 「請幫我使用 Surprise 函式庫建立電影推薦系統: 1. 從 MovieLens 資料集載入評分資料。 2. 比較 SVD、NMF、KNNWithMeans 三種演算法的 RMSE。 3. 對 SVD 進行超參數最佳化(n_factors、n_epochs、lr_all、reg_all)。 4. 用最佳模型為使用者 1 預測 10 部未看過電影的評分。 5. 輸出推薦列表與預測評分。 6. 計算 Precision@5 與 Recall@5。」

本日總結

在本章中,你學到了:

  1. Surprise 函式庫:專門為推薦系統設計的 Python 套件
  2. SVD 演算法:Netflix Prize 冠軍演算法,使用隱含因子
  3. 演算法比較:SVD、NMF、KNN 的效能對比
  4. Grid Search:自動尋找最佳超參數組合
  5. 產生推薦:用訓練好的模型為使用者預測評分

下一章,我們將學習如何解決推薦系統的冷啟動問題!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!