協調フィルタリング
「推薦アルゴリズムの基本中の基本、協調フィルタリングから始めよう。」
田中VPoEがホワイトボードにユーザー×アイテムの行列を描く。
「Amazonの『この商品を買った人はこちらも』を見たことがあるだろう?あの仕組みだ。まずはMovieLensデータで手を動かしてみろ。」
協調フィルタリングの基本原理
協調フィルタリング(Collaborative Filtering, CF)は「似た行動をするユーザーは似た嗜好を持つ」という仮定に基づく推薦手法である。アイテムの中身(属性)を一切使わず、ユーザーの行動データのみで推薦を行う。
User-Based CF vs Item-Based CF
User-Based CF:
「あなたと似た人が買ったものを推薦」
1. ユーザー間の類似度を計算
2. 似たユーザーのアイテム評価を集約
3. 未評価アイテムのスコアを予測
Item-Based CF:
「あなたが買ったものに似たものを推薦」
1. アイテム間の類似度を計算
2. ユーザーが高評価したアイテムの類似アイテムを集約
3. 未評価アイテムのスコアを予測
ユーザー×アイテム行列
映画A 映画B 映画C 映画D 映画E
User1: 5 3 ? 1 ?
User2: 4 ? ? 1 ?
User3: 1 1 ? 5 4
User4: ? ? 5 4 ?
User5: ? 3 4 ? 5
? = 未評価 → ここを予測するのがCFの目的
類似度計算
コサイン類似度
import numpy as np
from scipy.spatial.distance import cosine
def cosine_similarity(vec_a, vec_b):
"""コサイン類似度を計算(共通評価のあるアイテムのみ使用)"""
# 共通に評価したアイテムのインデックスを取得
common = np.where((vec_a > 0) & (vec_b > 0))[0]
if len(common) == 0:
return 0.0
a = vec_a[common]
b = vec_b[common]
dot_product = np.dot(a, b)
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
# 例: User1とUser2の類似度
user1 = np.array([5, 3, 0, 1, 0]) # 0=未評価
user2 = np.array([4, 0, 0, 1, 0])
sim = cosine_similarity(user1, user2)
print(f"User1-User2 コサイン類似度: {sim:.4f}")
ピアソン相関係数
from scipy.stats import pearsonr
def adjusted_cosine_similarity(vec_a, vec_b):
"""調整済みコサイン類似度(平均値を引いて正規化)"""
common = np.where((vec_a > 0) & (vec_b > 0))[0]
if len(common) < 2:
return 0.0
a = vec_a[common]
b = vec_b[common]
# 各ユーザーの平均評価を引く
a_centered = a - np.mean(a)
b_centered = b - np.mean(b)
dot_product = np.dot(a_centered, b_centered)
norm_a = np.linalg.norm(a_centered)
norm_b = np.linalg.norm(b_centered)
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
類似度の比較
| 類似度 | 特徴 | 適用場面 |
|---|---|---|
| コサイン類似度 | 方向の類似性、スケール不変 | 暗黙的フィードバック |
| ピアソン相関 | 平均からの偏差の相関 | 明示的評価(甘い/辛い評価の補正) |
| ジャッカード係数 | 集合の重なり具合 | バイナリデータ(購入/非購入) |
User-Based CFの実装
import pandas as pd
from surprise import Dataset, Reader, KNNBasic
from surprise.model_selection import cross_validate
# MovieLensデータの読み込み
# データ形式: user_id, item_id, rating, timestamp
reader = Reader(rating_scale=(1, 5))
# サンプルデータで実装
ratings_data = pd.DataFrame({
'user_id': [1,1,1,1,2,2,2,3,3,3,3,4,4,4,5,5,5,5],
'item_id': [1,2,4,5,1,3,4,1,2,4,5,2,3,5,1,3,4,5],
'rating': [5,3,1,2,4,2,1,1,1,5,4,4,5,3,2,4,4,5],
})
dataset = Dataset.load_from_df(
ratings_data[['user_id', 'item_id', 'rating']], reader
)
# User-Based CF(コサイン類似度、近傍数k=2)
algo_user = KNNBasic(
k=2,
sim_options={'name': 'cosine', 'user_based': True}
)
# 交差検証
results = cross_validate(algo_user, dataset, measures=['RMSE', 'MAE'], cv=3)
print(f"User-Based CF - RMSE: {results['test_rmse'].mean():.4f}")
Item-Based CFの実装
# Item-Based CF(調整済みコサイン類似度)
algo_item = KNNBasic(
k=2,
sim_options={'name': 'cosine', 'user_based': False}
)
results = cross_validate(algo_item, dataset, measures=['RMSE', 'MAE'], cv=3)
print(f"Item-Based CF - RMSE: {results['test_rmse'].mean():.4f}")
User-Based vs Item-Based の使い分け
| 観点 | User-Based | Item-Based |
|---|---|---|
| 計算量 | ユーザー数に依存(大規模で重い) | アイテム数に依存 |
| 更新頻度 | ユーザー追加で再計算必要 | アイテム類似度は安定 |
| 適用場面 | ユーザー数 < アイテム数 | アイテム数 < ユーザー数(EC) |
| セレンディピティ | 高い(似た人の意外な嗜好) | 低い(類似アイテム中心) |
| 推薦理由 | 「あなたと似た人が…」 | 「この商品に似た…」 |
ECサイトではアイテム間の類似度が安定しているため、Item-Based CFが主流である。Amazonの「この商品を買った人はこちらも」はItem-Based CFがベースとなっている。
スクラッチ実装
Surpriseライブラリに頼らず、自力で実装してみよう。
import numpy as np
class SimpleItemBasedCF:
"""Item-Based 協調フィルタリングのスクラッチ実装"""
def __init__(self, k=10):
self.k = k
self.item_sim_matrix = None
self.user_item_matrix = None
def fit(self, user_item_matrix):
"""アイテム間類似度行列を計算"""
self.user_item_matrix = user_item_matrix.copy()
n_items = user_item_matrix.shape[1]
self.item_sim_matrix = np.zeros((n_items, n_items))
# 各アイテムペアの類似度を計算
for i in range(n_items):
for j in range(i + 1, n_items):
# 両方を評価したユーザーのみ使用
mask = (user_item_matrix[:, i] > 0) & (user_item_matrix[:, j] > 0)
if mask.sum() < 2:
continue
vec_i = user_item_matrix[mask, i]
vec_j = user_item_matrix[mask, j]
# 調整済みコサイン類似度
vec_i_c = vec_i - vec_i.mean()
vec_j_c = vec_j - vec_j.mean()
denom = np.linalg.norm(vec_i_c) * np.linalg.norm(vec_j_c)
if denom > 0:
sim = np.dot(vec_i_c, vec_j_c) / denom
self.item_sim_matrix[i, j] = sim
self.item_sim_matrix[j, i] = sim
def predict(self, user_idx, item_idx):
"""ユーザーuserのアイテムitemに対する評価を予測"""
# ユーザーが評価済みのアイテムを取得
rated_items = np.where(self.user_item_matrix[user_idx] > 0)[0]
if len(rated_items) == 0:
return 0.0
# 対象アイテムとの類似度でソートしてtop-kを取得
sims = self.item_sim_matrix[item_idx, rated_items]
top_k_idx = np.argsort(sims)[::-1][:self.k]
top_sims = sims[top_k_idx]
top_ratings = self.user_item_matrix[user_idx, rated_items[top_k_idx]]
# 加重平均
if np.abs(top_sims).sum() == 0:
return 0.0
return np.dot(top_sims, top_ratings) / np.abs(top_sims).sum()
def recommend(self, user_idx, n_items=10):
"""ユーザーへのTop-N推薦"""
unrated = np.where(self.user_item_matrix[user_idx] == 0)[0]
scores = [(item, self.predict(user_idx, item)) for item in unrated]
scores.sort(key=lambda x: x[1], reverse=True)
return scores[:n_items]
# 使用例
matrix = np.array([
[5, 3, 0, 1, 0],
[4, 0, 0, 1, 0],
[1, 1, 0, 5, 4],
[0, 4, 5, 0, 3],
[2, 0, 4, 4, 5],
])
cf = SimpleItemBasedCF(k=2)
cf.fit(matrix)
# User 0([5,3,?,1,?])への推薦
recs = cf.recommend(0, n_items=2)
for item_idx, score in recs:
print(f" Item {item_idx}: predicted score = {score:.2f}")
まとめ
| 項目 | ポイント |
|---|---|
| CFの原理 | 行動データのみで推薦、アイテム属性不要 |
| User-Based | 似たユーザー基準、セレンディピティ高い |
| Item-Based | 似たアイテム基準、安定性が高くEC向き |
| 類似度 | コサイン(暗黙的)、ピアソン(明示的) |
| 課題 | Cold Start、スパース性、スケーラビリティ |
チェックリスト
- User-BasedとItem-Based CFの違いを説明できる
- コサイン類似度とピアソン相関の使い分けを理解した
- Surpriseライブラリで協調フィルタリングを実装できる
- ECサイトでItem-Based CFが主流である理由を説明できる
- CFの予測スコア計算の仕組みを理解した
次のステップへ
協調フィルタリングの基礎を理解したところで、次はスパースなデータに強いMatrix Factorizationについて学ぼう。
推定読了時間: 30分