🎓 レベル:標準 | 重要度:A(必須)
📎 関連:第8章 レコメンデーション 目次 | 前提:クラスタリングによるセグメンテーション(k-means)
要点(BLUF)
- 協調フィルタリングは、アイテムの中身(属性)を一切見ず、ユーザー × アイテムの評価/購買の履歴だけから「あなたに似た人が好んだもの」を薦める手法です。「協調(collaborative)」とは、みんなの行動を協調的に使うという意味。系統は2つ——近傍法(クラスタリングによるセグメンテーション(k-means) のコサイン類似度で「似たアイテム/似たユーザー」を測り、その評価を加重平均)と、行列分解(評価行列 を潜在因子に分解)です。
- 推薦問題の正体は評価行列の欠測の穴埋めです。真の潜在因子 から生成した評価行列(ユーザー50・アイテム20、、各セルを確率 で観測)で、近傍法と行列分解がともに全体平均で埋めるベースライン(テストRMSE )を上回ります——アイテムベースCF ( 改善)、行列分解 ( 改善)。行列分解が最良で、データを生んだ の構造を直接取り戻すからです。
- あるユーザーへの Top-3 推薦(未観測アイテムの予測上位)は、予測評価 に対し真の評価が と、好むアイテムをきちんと当てます。ただし協調フィルタリングは履歴ゼロの新規ユーザー/新規アイテムには無力(コールドスタート)で、ここから属性を使う コンテンツベース・ハイブリッド推薦(コールドスタート対策) へつながります。
1. 協調フィルタリングとは:行動の履歴だけで薦める
レコメンドの出発点は、巨大な評価行列です。行がユーザー、列がアイテム、セルが「ユーザー がアイテム に付けた評価(あるいは買った/見たという事実)」。現実にはこの表の99% 以上が空白——誰もが全アイテムのごく一部しか触れていないからです。推薦とは、この空白のセルを予測し、各ユーザーについて「まだ出会っていないが高く評価しそうな」アイテムを上位提示することに他なりません。
協調フィルタリングの発想は、アイテムの中身をまったく見ないことにあります。映画のジャンルも、商品のスペックも使わない。使うのは評価行列の数字だけです。それでも推薦できるのは、「人々の好みには共通のパターンがある」から——「この映画を高く評価した人は、あの映画も高く評価しがち」「あなたと評価が似ているあの人が絶賛したものは、あなたも気に入る公算が大きい」。みんなの行動を協調的に寄せ集めて穴を埋める。これが名前の由来です。
実装の二系統を押さえます。近傍法(memory-based)は、評価行列からアイテム間(またはユーザー間)の類似度を測り、似たものの評価を加重平均して欠測を予測します。類似度には クラスタリングによるセグメンテーション(k-means) で使ったコサイン類似度をそのまま使います。もう一方の行列分解(model-based)は、評価行列 を低次元の潜在因子の積 で近似します。各ユーザーを 次元のベクトル(好みの傾向)、各アイテムを 次元のベクトル(特徴の傾向)で表し、その内積で評価を再構成する——「ジャンル」を人が与えるのではなく、データから潜在的な軸を発見させるのです。
flowchart LR R["評価行列 R(大半が欠測)"] --> N["近傍法:アイテム/ユーザー間のコサイン類似度"] R --> M["行列分解:R ≒ U Vᵀ(潜在因子K次元)"] N --> P1["欠測セルを類似度の加重平均で予測"] M --> P2["欠測セルを内積 pᵤ・qᵢ で予測"] P1 --> REC["予測上位を推薦(Top-N)"] P2 --> REC
2. コサイン類似度・近傍予測・行列分解(数式)
まず評価行列 ( ユーザー、 アイテム)を考えます。観測されたセルの集合を とし、 なら が分かっています。
近傍法では、まずアイテムどうしのコサイン類似度を測ります。アイテム の評価ベクトル (全ユーザーにわたる列)と の について
です(実装では平均中心化してから測ります=後述)。アイテムベースの予測は、ユーザー が実際に評価した他アイテム の評価を、アイテム との類似度で加重平均して作ります。アイテム平均 を基準に取り、
ここで はユーザー が評価済みのアイテム集合。「 に似たアイテムを が高く評価していれば、 は も高く評価するだろう」を式にしたものです。
行列分解は発想が違います。評価行列を、ユーザー因子 とアイテム因子 の積で近似します( と同じ役割を学習対象として と書きます)。予測は内積 。これを観測セルでの二乗誤差+L2 正則化を最小化して当てます。
潜在次元 が「いくつの軸で好みを説明するか」、 が過学習を抑える正則化の強さです。実務では全体平均 (さらにユーザ/アイテムのバイアス)を引いた残差を分解し、予測を とするのが標準で、本ノートのコードも を引いてから分解します( 個の潜在軸を「平均からのズレ」に専念させるため)。最適化は**確率的勾配降下法(SGD)**で、観測セルを1つずつ見て、誤差 を使い
と更新します( は学習率)。「予測が低すぎたら、 と の因子を同じ向きに少し伸ばす」——内積を上げる方向への素直な調整です。
3. 評価行列の穴を埋める(コード)
真の潜在因子 から評価行列を合成します。ユーザー50・アイテム20、 の 、 の 、真の評価 。各セルを確率 で観測(学習)、残りはテストとして隠します。(a) アイテムベースCF(観測のみで平均中心化 → アイテム間コサイン → テストセルを類似度加重平均で予測)、(b) 行列分解(潜在 、rng.normal(0,0.1) 初期化・SGD・学習率 ・正則化 )を実装し、いずれも全体平均で埋めるベースラインとテストRMSEで比べます。sklearn は使わず、類似度は自前の cosine、行列分解は numpy の SGD です。
import numpy as np
import pandas as pd
# ユーザー50 × アイテム20 の評価行列を、真の潜在因子 K=2 から生成する。
# R* = 3 + U V^T を 1..5 にclip。各セルを確率0.4で観測(残りはテスト)。
rng = np.random.default_rng(1)
n_users, n_items, K_true = 50, 20, 2
U_true = rng.normal(0, 1, (n_users, K_true))
V_true = rng.normal(0, 1, (n_items, K_true))
R_star = np.clip(3.0 + U_true @ V_true.T, 1.0, 5.0)
observed = rng.random((n_users, n_items)) < 0.4 # True=観測(学習), False=テスト
n_cell = n_users * n_items
n_obs = int(observed.sum())
print(f"評価行列: {n_users}ユーザー × {n_items}アイテム = {n_cell}セル")
print(f"観測(学習)セル: {n_obs}({n_obs/n_cell:.1%}), テストセル: {n_cell - n_obs}")
R_obs = np.where(observed, R_star, np.nan) # 未観測は NaN
mu_global = float(np.nanmean(R_obs))
test = ~observed
print(f"観測の全体平均 mu = {mu_global:.3f}")
# テストRMSE を測る関数(テストセルのみで真の R* と比較)
def rmse(pred):
return float(np.sqrt(np.mean((pred[test] - R_star[test]) ** 2)))
# === ベースライン:テストセルを全体平均で予測 ===
rmse_base = rmse(np.full_like(R_star, mu_global))
print(f"\n[ベースライン] 全体平均で予測 : テストRMSE = {rmse_base:.3f}")
# === (a) アイテムベースCF ===
# アイテム平均(観測のみ)で各列を中心化、未観測は0で埋める
item_mean = np.nanmean(R_obs, axis=0)
C = np.where(observed, R_obs - item_mean, 0.0)
# アイテム間コサイン類似度(自前。列ベクトルを正規化して内積)
def cosine_cols(M):
norm = np.sqrt((M ** 2).sum(axis=0))
norm[norm == 0] = 1e-9
Mn = M / norm
return Mn.T @ Mn
S = cosine_cols(C)
np.fill_diagonal(S, 0.0) # 自分自身は使わない
# テストセル(u,i)を、ユーザーuが観測した他アイテムjの中心化評価の類似度加重平均で予測
pred_cf = np.full_like(R_star, np.nan)
for u in range(n_users):
obs_j = np.where(observed[u])[0]
for i in np.where(test[u])[0]:
w = S[i, obs_j]
denom = np.abs(w).sum()
if denom < 1e-9:
pred_cf[u, i] = item_mean[i]
else:
pred_cf[u, i] = item_mean[i] + (w * C[u, obs_j]).sum() / denom
pred_cf = np.clip(pred_cf, 1.0, 5.0)
rmse_cf = rmse(pred_cf)
print(f"[近傍法] アイテムベースCF : テストRMSE = {rmse_cf:.3f}")
# === (b) 行列分解(SGD)===
# 残差 (r - mu) を K=2 で分解。予測 = mu + p_u . q_i
K, lr, reg, n_epoch = 2, 0.01, 0.05, 200
P = rng.normal(0, 0.1, (n_users, K))
Q = rng.normal(0, 0.1, (n_items, K))
obs_idx = np.argwhere(observed)
for epoch in range(n_epoch):
rng.shuffle(obs_idx)
for u, i in obs_idx:
err = (R_star[u, i] - mu_global) - P[u] @ Q[i]
pu = P[u].copy()
P[u] += lr * (err * Q[i] - reg * pu)
Q[i] += lr * (err * pu - reg * Q[i])
pred_mf = np.clip(mu_global + P @ Q.T, 1.0, 5.0)
rmse_mf = rmse(pred_mf)
print(f"[行列分解] 潜在K=2・SGD : テストRMSE = {rmse_mf:.3f}")
print(f"\nベースライン比の改善: CF {(1-rmse_cf/rmse_base):.1%} 改善 / 行列分解 {(1-rmse_mf/rmse_base):.1%} 改善")
# === (c) あるユーザーへの Top-3 推薦(行列分解の予測で未観測アイテム上位)===
target = 0
unseen = np.where(test[target])[0]
order = np.argsort(-pred_mf[target, unseen])[:3]
top3 = unseen[order]
rec = pd.DataFrame({
"アイテムID": top3,
"予測評価(MF)": pred_mf[target, top3],
"真の評価R*": R_star[target, top3],
}, index=[f"第{r+1}位" for r in range(3)])
print(f"\n=== ユーザー{target} への Top-3 推薦(未観測アイテムの予測上位)===")
print(rec.to_string(formatters={"予測評価(MF)": "{:.2f}".format, "真の評価R*": "{:.2f}".format}))
出力:
評価行列: 50ユーザー × 20アイテム = 1000セル
観測(学習)セル: 410(41.0%), テストセル: 590
観測の全体平均 mu = 3.004
[ベースライン] 全体平均で予測 : テストRMSE = 0.905
[近傍法] アイテムベースCF : テストRMSE = 0.582
[行列分解] 潜在K=2・SGD : テストRMSE = 0.465
ベースライン比の改善: CF 35.6% 改善 / 行列分解 48.5% 改善
=== ユーザー0 への Top-3 推薦(未観測アイテムの予測上位)===
アイテムID 予測評価(MF) 真の評価R*
第1位 4 3.53 3.51
第2位 0 3.44 3.48
第3位 12 3.26 3.06
出力の意味:評価行列は セルで、そのうち セル()だけを学習に使い、残り セルはテストとして隠しました。ベースラインは「テストセルを全部、観測の全体平均 で予測」する素朴な戦略で、テストRMSEは 。これを基準に、二つの協調フィルタリングがどれだけ改善するかを見ます。
アイテムベースCFは ——ベースラインから の改善です。「ユーザー が評価済みのアイテムのうち、いま予測したい に似たものの評価を重く見る」だけで、全体平均より大きく当たるようになりました。類似度は自前のコサイン(平均中心化した列ベクトルの内積)で、アイテムの中身は一切使っていません。
行列分解はさらに良く —— の改善です。これが最良なのは、データがまさに の潜在因子から生成されたから。SGD で各ユーザー・各アイテムの2次元因子を学習し、 で穴を埋めると、隠れていた2軸構造を取り戻して予測が締まります。近傍法が「似たものの平均」という局所的な使い方なのに対し、行列分解は行列全体を貫く低次元構造をモデル化するぶん、強い。
最後の Top-3 推薦が推薦の実物です。ユーザー0がまだ評価していないアイテムについて行列分解の予測値を出し、上位3つを並べました。予測評価 に対し、真の評価(本来は見えない正解)は ——予測の高い順に、本当に好むアイテムが並んでいます。「この人にはアイテム4・0・12を薦めよ」という出力が、履歴の数字だけから得られました。
3手法の誤差と、行列分解の予測の素性を1枚にまとめます(このブロックはデータ生成から各手法までを内部で再実行し、上と同じ値を描きます)。
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
# データ生成 → ベースライン/CF/行列分解 をこのブロック内で再実行し、
# (左) 3手法のテストRMSE比較、(右) 行列分解の予測 vs 実測(テストセル)を描く。
rng = np.random.default_rng(1)
n_users, n_items = 50, 20
U_true = rng.normal(0, 1, (n_users, 2))
V_true = rng.normal(0, 1, (n_items, 2))
R_star = np.clip(3.0 + U_true @ V_true.T, 1.0, 5.0)
observed = rng.random((n_users, n_items)) < 0.4
test = ~observed
R_obs = np.where(observed, R_star, np.nan)
mu_global = float(np.nanmean(R_obs))
rmse = lambda pred: float(np.sqrt(np.mean((pred[test] - R_star[test]) ** 2)))
rmse_base = rmse(np.full_like(R_star, mu_global))
item_mean = np.nanmean(R_obs, axis=0)
C = np.where(observed, R_obs - item_mean, 0.0)
norm = np.sqrt((C ** 2).sum(axis=0)); norm[norm == 0] = 1e-9
S = (C / norm).T @ (C / norm); np.fill_diagonal(S, 0.0)
pred_cf = np.full_like(R_star, np.nan)
for u in range(n_users):
obs_j = np.where(observed[u])[0]
for i in np.where(test[u])[0]:
w = S[i, obs_j]; denom = np.abs(w).sum()
pred_cf[u, i] = item_mean[i] + ((w * C[u, obs_j]).sum() / denom if denom > 1e-9 else 0.0)
pred_cf = np.clip(pred_cf, 1.0, 5.0)
rmse_cf = rmse(pred_cf)
P = rng.normal(0, 0.1, (n_users, 2)); Q = rng.normal(0, 0.1, (n_items, 2))
obs_idx = np.argwhere(observed)
for _ in range(200):
rng.shuffle(obs_idx)
for u, i in obs_idx:
err = (R_star[u, i] - mu_global) - P[u] @ Q[i]
pu = P[u].copy()
P[u] += 0.01 * (err * Q[i] - 0.05 * pu)
Q[i] += 0.01 * (err * pu - 0.05 * Q[i])
pred_mf = np.clip(mu_global + P @ Q.T, 1.0, 5.0)
rmse_mf = rmse(pred_mf)
fig, ax = plt.subplots(1, 2, figsize=(11.5, 4.4))
names = ["ベースライン\n(全体平均)", "アイテムベース\nCF(近傍法)", "行列分解\n(K=2)"]
vals = [rmse_base, rmse_cf, rmse_mf]
bars = ax[0].bar(names, vals, color=["C7", "C0", "C3"])
for b, v in zip(bars, vals):
ax[0].text(b.get_x() + b.get_width() / 2, v + 0.01, f"{v:.3f}", ha="center", fontsize=10)
ax[0].set_ylabel("テストRMSE(小さいほど良い)")
ax[0].set_title("3手法のテスト誤差:協調フィルタはベースラインを上回る")
ax[0].set_ylim(0, 1.0)
ax[1].scatter(R_star[test], pred_mf[test], s=14, alpha=0.4, color="C3", edgecolor="none")
ax[1].plot([1, 5], [1, 5], "k--", lw=1, label="完全一致の線")
ax[1].set_xlabel("真の評価 R*(テストセル)")
ax[1].set_ylabel("行列分解の予測評価")
ax[1].set_title(f"行列分解の予測 vs 実測(テストRMSE={rmse_mf:.3f})")
ax[1].legend(loc="upper left", fontsize=9)
ax[1].grid(alpha=0.3)
fig.tight_layout()
plt.show()
左の棒グラフは3手法のテストRMSE()で、協調フィルタリングがベースラインを明確に下回る(=当たる)こと、行列分解が最良であることが一目で読めます。右は行列分解の予測 vs 実測の散布図で、点群が点線(完全一致の線)の周りに帯状に集まっています。低評価も高評価もそれなりに追えており、 付近で予測がやや上振れ・ 付近でやや下振れする(中央に引っ張られる)正則化の効果も見て取れます。これが「欠測の穴を、履歴の数字だけで埋める」ということの中身です。
⚠️ よくある誤解
- コールドスタートには無力:協調フィルタリングは履歴のないユーザー/アイテムを推薦できません。新規ユーザーは評価行列に行がほぼ空で「似た人」も「評価済みアイテム」も無く、新規アイテムは列が空で誰の予測にも現れない——内積も加重平均も計算できないのです。本ノートの行列分解も、 に学習データのあるユーザー/アイテムしか因子を持ちません。ここを補うのが、アイテムの属性から薦める コンテンツベース・ハイブリッド推薦(コールドスタート対策) です。「最初の推薦」は協調では作れない、と覚えてください。
- 人気バイアスとフィルターバブル:協調フィルタリングは「みんなが評価したもの」に強く引かれます。人気アイテムは多くのユーザーと共起するので類似度・予測が高く出やすく、人気のものがますます薦められて人気になる正のフィードバックが生まれます(人気バイアス)。個人レベルでも、過去の嗜好に近いものばかり提示され、視野が狭まるフィルターバブルが起きます。多様性(diversity)やセレンディピティ(意外な良い出会い)を別途設計しないと、推薦は既存の好みと売れ筋を増幅するだけになりがちです。
- 現実の評価行列は本ノートよりはるかに疎:ここでは も観測しましたが、実サービスの評価行列は観測 未満がふつうです。スパースになるほど、共起するユーザー/アイテムが減って類似度が不安定になり、近傍法は崩れやすくなります(だから低次元構造をモデル化する行列分解が好まれます)。本ノートの好成績は「観測が比較的多い・ノイズが小さい・真に低ランク」という恵まれた合成設定ゆえで、実データでは前処理と正則化により神経を使います。
- 暗黙的フィードバックは「好き」とは限らない:本ノートは の明示評価(explicit feedback)を扱いましたが、実務で豊富なのは購買・クリック・視聴時間などの暗黙的フィードバック(implicit feedback)です。これは「買った=好き」とは限らず(贈り物・失敗購入もある)、何より「買わなかった=嫌い」が言えない(ただ知らなかっただけかも)。欠測を全部「負例」とは扱えないので、明示評価と同じ二乗誤差では測れず、信頼度重み付けや専用の損失が要ります。
- 推薦の理論体系は機械学習の領域:行列分解の最適化(ALS・暗黙的フィードバックの WALS)、評価指標(Precision@K・Recall@K・NDCG・カバレッジ)、深層推薦(ニューラル協調フィルタリング・系列推薦)などは機械学習テキストの守備範囲です。本ノートは重複させず、協調フィルタリングを**マーケの「一人ひとりへの提示」**にどう使うかに絞っています。
関連ノート
- コンテンツベース・ハイブリッド推薦(コールドスタート対策)(同章。協調が無力なコールドスタートを、アイテムの属性で補う対の手法。両者を で混ぜるハイブリッドへ)
- 第8章 レコメンデーション 目次
- クラスタリングによるセグメンテーション(k-means)(コサイン類似度・教師なしで「似た者」を測る発想。近傍法はその応用で、セグメントが「集団への推薦」なら協調フィルタは「個人への推薦」)
- 行列分解・レコメンドの一般理論(ALS・暗黙的フィードバック・NDCG など評価指標・深層推薦)は機械学習テキスト、薦めて反応を見て薦め直すオンライン最適化(多腕バンディット)は 08-03(予定)で扱います
- マーケティング・サイエンス 全体目次