LESSON

協調フィルタリング

「推薦アルゴリズムの基本中の基本、協調フィルタリングから始めよう。」

田中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-BasedItem-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分