Matrix Factorization
「協調フィルタリングはスパースなデータに弱い。NetShop社の50万商品のうち、一人のユーザーが買うのはせいぜい数十品だ。」
田中VPoEがスカスカのユーザー×アイテム行列を示す。
「Matrix Factorizationはこのスパース問題を解決する。Netflix Prizeで優勝チームが使った手法だ。」
Matrix Factorizationの基本原理
Matrix Factorization(MF)は、ユーザー×アイテムの評価行列を低ランク行列の積に分解する手法である。
直感的な理解
元の行列 R (m×n) ≈ P (m×k) × Q^T (k×n)
R: ユーザー×アイテム行列(スパース)
P: ユーザー潜在因子行列(各ユーザーをk次元ベクトルで表現)
Q: アイテム潜在因子行列(各アイテムをk次元ベクトルで表現)
k: 潜在因子の数(ハイパーパラメータ、通常10〜200)
直感:
潜在因子 = 「アクション好き度」「ロマンス好き度」「コメディ好き度」...
ユーザーベクトル = [0.8, 0.2, 0.5, ...] ← アクション好き
アイテムベクトル = [0.9, 0.1, 0.3, ...] ← アクション映画
予測スコア = 内積 = 0.8*0.9 + 0.2*0.1 + 0.5*0.3 = 0.89
なぜMFが有効か
協調フィルタリングの問題点:
- スパース性: 99%以上が未評価 → 類似度計算が不安定
- スケーラビリティ: 全ペアの類似度計算はO(n^2)
MFの利点:
- 低次元ベクトルに圧縮 → スパース性を解消
- 潜在因子で類似性を暗黙的に捉える
- 予測はベクトルの内積 → 高速
- 未評価アイテムの予測が自然に可能
SVD(Singular Value Decomposition)
基本SVD
import numpy as np
from scipy.sparse.linalg import svds
# ユーザー×アイテム行列(0=未評価)
R = 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],
], dtype=float)
# 欠損値を平均で埋める(基本的なアプローチ)
user_means = np.true_divide(R.sum(1), (R != 0).sum(1))
R_filled = R.copy()
for i in range(R.shape[0]):
R_filled[i, R[i] == 0] = user_means[i]
# SVD分解(k=2次元)
U, sigma, Vt = svds(R_filled, k=2)
sigma_diag = np.diag(sigma)
# 予測行列の復元
R_pred = np.dot(np.dot(U, sigma_diag), Vt)
print("元の行列:")
print(R)
print("\n予測行列:")
print(np.round(R_pred, 1))
Funk SVD(SGDベース)
Netflix Prizeで有名になった手法。欠損値を無視して観測データのみで学習する。
class FunkSVD:
"""SGDベースのMatrix Factorization"""
def __init__(self, n_factors=10, lr=0.005, reg=0.02, n_epochs=20):
self.n_factors = n_factors
self.lr = lr
self.reg = reg
self.n_epochs = n_epochs
def fit(self, ratings):
"""
ratings: [(user_id, item_id, rating), ...]
"""
users = set(r[0] for r in ratings)
items = set(r[1] for r in ratings)
self.user_map = {u: i for i, u in enumerate(users)}
self.item_map = {it: i for i, it in enumerate(items)}
n_users = len(users)
n_items = len(items)
# ランダム初期化
self.P = np.random.normal(0, 0.1, (n_users, self.n_factors))
self.Q = np.random.normal(0, 0.1, (n_items, self.n_factors))
self.bu = np.zeros(n_users) # ユーザーバイアス
self.bi = np.zeros(n_items) # アイテムバイアス
self.mu = np.mean([r[2] for r in ratings]) # 全体平均
# SGDで学習
for epoch in range(self.n_epochs):
np.random.shuffle(ratings)
total_loss = 0
for user, item, rating in ratings:
u = self.user_map[user]
i = self.item_map[item]
# 予測
pred = self.mu + self.bu[u] + self.bi[i] + np.dot(self.P[u], self.Q[i])
# 誤差
error = rating - pred
total_loss += error ** 2
# パラメータ更新
self.bu[u] += self.lr * (error - self.reg * self.bu[u])
self.bi[i] += self.lr * (error - self.reg * self.bi[i])
P_u_old = self.P[u].copy()
self.P[u] += self.lr * (error * self.Q[i] - self.reg * self.P[u])
self.Q[i] += self.lr * (error * P_u_old - self.reg * self.Q[i])
rmse = np.sqrt(total_loss / len(ratings))
if (epoch + 1) % 5 == 0:
print(f"Epoch {epoch+1}: RMSE = {rmse:.4f}")
def predict(self, user, item):
u = self.user_map.get(user)
i = self.item_map.get(item)
if u is None or i is None:
return self.mu
return self.mu + self.bu[u] + self.bi[i] + np.dot(self.P[u], self.Q[i])
ALS(Alternating Least Squares)
ALSは暗黙的フィードバックデータに特に適したMF手法である。
暗黙的フィードバックの扱い
明示的フィードバック(星評価):
- 値そのものが嗜好の強さを表す
- 欠損値 = 未評価(嗜好不明)
暗黙的フィードバック(購買/閲覧):
- 行動あり = 嗜好あり(ただし確信度は様々)
- 行動なし ≠ 嗜好なし(知らなかっただけかも)
- 値の大きさ = 確信度(購入5回 > 購入1回)
import implicit
from scipy.sparse import csr_matrix
# 暗黙的フィードバックデータ(購入回数)
# user_id, item_id, purchase_count
data = np.array([
[0, 0, 5], [0, 1, 3], [0, 3, 1],
[1, 0, 4], [1, 3, 1],
[2, 0, 1], [2, 1, 1], [2, 3, 5], [2, 4, 4],
[3, 1, 4], [3, 2, 5], [3, 4, 3],
[4, 0, 2], [4, 2, 4], [4, 3, 4], [4, 4, 5],
])
# スパース行列に変換(item×user形式がimplicitの規約)
n_users, n_items = 5, 5
user_item = csr_matrix(
(data[:, 2], (data[:, 0], data[:, 1])),
shape=(n_users, n_items)
)
# ALSモデル
model = implicit.als.AlternatingLeastSquares(
factors=10,
regularization=0.1,
iterations=20,
)
# 学習(item×user形式で渡す)
model.fit(user_item.T)
# User 0への推薦
user_id = 0
item_ids, scores = model.recommend(
user_id,
user_item[user_id],
N=3,
filter_already_liked_items=True
)
print(f"User {user_id}への推薦:")
for item_id, score in zip(item_ids, scores):
print(f" Item {item_id}: score = {score:.4f}")
ALSの利点
| 特徴 | SGD | ALS |
|---|---|---|
| 並列化 | 困難 | 容易(P固定でQ更新、Q固定でP更新) |
| 暗黙的データ | 追加の工夫が必要 | 自然に扱える |
| 収束速度 | 遅い | 速い(大規模データ向き) |
| 実装 | シンプル | やや複雑 |
| 代表的ライブラリ | Surprise | implicit |
MFの拡張
バイアス項の導入
予測 = μ + bu + bi + P_u・Q_i
μ: 全体平均(例: 3.5)
bu: ユーザーバイアス(甘い評価者は+、辛い評価者は-)
bi: アイテムバイアス(人気作品は+、不人気作品は-)
P_u・Q_i: 潜在因子の内積(パーソナライズ部分)
時間的ダイナミクス
ユーザーの嗜好は時間とともに変化する:
- 最近の行動に高い重みを付与
- 時間減衰関数: weight = exp(-λ * (t_now - t_action))
- 季節性の考慮(冬にコートが売れる)
まとめ
| 項目 | ポイント |
|---|---|
| MFの原理 | ユーザー×アイテム行列を低ランク分解 |
| SVD | 明示的評価向け、SGDで学習 |
| ALS | 暗黙的フィードバック向け、並列化容易 |
| バイアス | 全体平均+ユーザー/アイテムバイアスで精度向上 |
| 適用場面 | スパースなデータ、大規模データセット |
チェックリスト
- Matrix Factorizationの直感的な意味を説明できる
- SVDとALSの違いと使い分けを理解した
- 明示的/暗黙的フィードバックの扱いの違いを理解した
- Funk SVDのSGD学習の仕組みを説明できる
- implicitライブラリでALSモデルを実装できる
次のステップへ
Matrix Factorizationを理解したところで、次はアイテムの属性情報を活用するコンテンツベース推薦を学ぼう。
推定読了時間: 30分