Matrix Factorization Модели
Matrix Factorization - это фундаментальный подход в рекомендательных системах, который представляет пользователей и items в общем латентном пространстве. В этой главе мы рассмотрим три модели: SVD, SVD++ и ALS.
SVD - Singular Value Decomposition
Теория
SVD (Сингулярное разложение) - это классический метод matrix factorization для explicit feedback (рейтингов).
Truncated SVD
На практике используется Truncated SVD, который сохраняет только наибольших сингулярных значений.
Это обеспечивает:
- Уменьшение размерности
- Шумоподавление: отбрасываются малые сингулярные значения
- Обобщение: лучше работает на новых данных
Предсказание
Рейтинг предсказывается как скалярное произведение факторов.
Гиперпараметры
n_components (k)
- Описание: Количество латентных факторов
- Диапазон: 10 - 200
- По умолчанию: 20
- Влияние:
- Малые (< 10): Недообучение, слишком простая модель
- Средние (20-50): Хороший баланс для большинства датасетов
- Большие (> 100): Возможно переобучение, медленнее
Рекомендации:
- Начните с 20-50
- Увеличивайте для сложных датасетов
- Мониторьте качество на validation
n_iter
- Описание: Количество итераций для randomized SVD
- Диапазон: 5 - 20
- По умолчанию: 10
- Влияние: Больше итераций → точнее, но медленнее
random_state
- Описание: Random seed для воспроизводимости
- Рекомендация: Всегда устанавливайте для экспериментов
Примеры использования
Базовый пример
from recommender import SVDRecommender, load_movielens, InteractionDataset, Evaluator
# 1. Загрузка данных (explicit ratings)
df = load_movielens(size='100k')
# 2. Создание датасета (НЕ бинаризовать!)
dataset = InteractionDataset(
df,
implicit=False, # Важно: explicit feedback
min_user_interactions=10
)
train, test = dataset.split(test_size=0.2, strategy='random', seed=42)
# 3. Обучение SVD
model = SVDRecommender(
n_components=50, # 50 латентных факторов
n_iter=10,
random_state=42
)
model.fit(train.data)
# 4. Предсказание рейтингов
user_ids = [1, 1, 2, 2]
item_ids = [10, 50, 10, 100]
predicted_ratings = model.predict(user_ids, item_ids)
print("Предсказанные рейтинги:")
for u, i, r in zip(user_ids, item_ids, predicted_ratings):
print(f" User {u}, Item {i}: {r:.2f}")
# 5. Рекомендации
recommendations = model.recommend([1, 2, 3], k=10, exclude_seen=True)
for user_id, items in recommendations.items():
print(f"\nUser {user_id}:")
for item_id, score in items[:5]:
print(f" Item {item_id}: score = {score:.3f}")
# 6. Оценка (rating prediction)
evaluator = Evaluator(metrics=['rmse', 'mae'], k_values=[])
results = evaluator.evaluate(model, test, task='rating_prediction')
evaluator.print_results(results)
Подбор количества факторов
# Эксперимент с разным количеством факторов
factors_range = [10, 20, 50, 100, 150]
results = {}
for n_comp in factors_range:
print(f"\nТестирование {n_comp} факторов...")
model = SVDRecommender(n_components=n_comp, random_state=42)
model.fit(train.data)
# Оценка RMSE
user_ids = test.data['user_id'].values
item_ids = test.data['item_id'].values
true_ratings = test.data['rating'].values
pred_ratings = model.predict(user_ids, item_ids)
rmse = np.sqrt(np.mean((true_ratings - pred_ratings) ** 2))
results[n_comp] = rmse
print(f" RMSE: {rmse:.4f}")
# Лучшее количество факторов
best_k = min(results, key=results.get)
print(f"\n✅ Лучшее k: {best_k} (RMSE: {results[best_k]:.4f})")
Анализ объяснённой дисперсии
# Тренируем модель
model = SVDRecommender(n_components=100, random_state=42)
model.fit(train.data)
# Анализ сингулярных значений
singular_values = model.svd.singular_values_
explained_variance = model.svd.explained_variance_ratio_
import matplotlib.pyplot as plt
# График объяснённой дисперсии
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.plot(range(1, len(singular_values) + 1), singular_values, 'bo-')
plt.xlabel('Component')
plt.ylabel('Singular Value')
plt.title('Сингулярные значения')
plt.grid(True)
plt.subplot(1, 2, 2)
cumsum_var = np.cumsum(explained_variance)
plt.plot(range(1, len(cumsum_var) + 1), cumsum_var, 'ro-')
plt.xlabel('Number of Components')
plt.ylabel('Cumulative Explained Variance')
plt.title('Накопленная объяснённая дисперсия')
plt.grid(True)
plt.axhline(y=0.9, color='g', linestyle='--', label='90%')
plt.legend()
plt.tight_layout()
plt.savefig('svd_analysis.png')
plt.close()
# Сколько факторов нужно для 90% дисперсии?
n_for_90 = np.argmax(cumsum_var >= 0.9) + 1
print(f"Для 90% дисперсии нужно {n_for_90} факторов")
Производительность
Время обучения (Intel i7, 16GB RAM):
| Датасет | Факторы | Время |
|---|---|---|
| ML-100K | 20 | ~3 сек |
| ML-100K | 100 | ~5 сек |
| ML-1M | 50 | ~10 сек |
Качество (RMSE на ML-100K):
| Факторы | RMSE | MAE |
|---|---|---|
| 10 | 0.952 | 0.752 |
| 20 | 0.935 | 0.738 |
| 50 | 0.928 | 0.731 |
| 100 | 0.930 | 0.732 |
Когда использовать SVD
✅ Хорошо подходит:
- Explicit feedback (рейтинги 1-5)
- Быстрое обучение необходимо
- Baseline для explicit ratings
- Интерпретация через латентные факторы
- Относительно плотные матрицы
❌ Не подходит:
- Implicit feedback (используйте ALS или EASE)
- Очень разреженные матрицы
- Нужен учёт implicit feedback (используйте SVD++)
- Cold start (новые пользователи/items)
SVD++ - SVD с Implicit Feedback
Теория
SVD++ расширяет базовый SVD, добавляя учёт implicit feedback - информации о том, с какими items пользователь взаимодействовал, независимо от рейтинга.
Ключевые компоненты
- Biases (( \mu, b_u, b_i )): Учитывают базовые предпочтения
- Explicit factors (( p_u, q_i )): Латентные представления как в SVD
- Implicit factors (( y_j )): Дополнительная информация из истории взаимодействий
Обучение через SGD
SVD++ обучается стохастическим градиентным спуском (SGD).
Гиперпараметры
n_factors (k)
- Описание: Количество латентных факторов
- Диапазон: 10 - 100
- По умолчанию: 20
- Влияние: Как в SVD, но SVD++ может работать с меньшим k благодаря implicit feedback
n_epochs
- Описание: Количество эпох SGD
- Диапазон: 10 - 50
- По умолчанию: 20
- Влияние: Больше эпох → лучше сходимость, но риск переобучения
lr (learning rate)
- Описание: Скорость обучения
- Диапазон: 0.001 - 0.01
- По умолчанию: 0.005
- Влияние:
- Малый (< 0.001): Медленная сходимость
- Средний (0.005): Обычно оптимален
- Большой (> 0.01): Нестабильная сходимость
reg (regularization)
- Описание: Сила регуляризации (λ)
- Диапазон: 0.001 - 0.1
- По умолчанию: 0.02
- Влияние:
- Малая (< 0.01): Переобучение
- Средняя (0.02-0.05): Хороший баланс
- Большая (> 0.1): Недообучение
Примеры использования
Базовый пример
from recommender import SVDPlusPlusRecommender, load_movielens, InteractionDataset
# 1. Загрузка данных
df = load_movielens(size='100k')
# 2. Создание датасета (explicit feedback)
dataset = InteractionDataset(df, implicit=False, min_user_interactions=10)
train, test = dataset.split(test_size=0.2, strategy='random', seed=42)
# 3. Обучение SVD++
model = SVDPlusPlusRecommender(
n_factors=20,
n_epochs=20,
lr=0.005,
reg=0.02,
random_state=42
)
print("Обучение SVD++ (может занять несколько минут)...")
model.fit(train.data)
# 4. Предсказание рейтингов
user_ids = [1, 1, 2, 2]
item_ids = [10, 50, 10, 100]
predictions = model.predict(user_ids, item_ids)
print("\nПредсказания:")
for u, i, p in zip(user_ids, item_ids, predictions):
print(f" User {u}, Item {i}: {p:.2f}")
# 5. Оценка
from recommender import Evaluator
evaluator = Evaluator(metrics=['rmse', 'mae'])
results = evaluator.evaluate(model, test, task='rating_prediction')
evaluator.print_results(results)
Мониторинг обучения
# Добавим callback для мониторинга
class TrainingMonitor:
def __init__(self, model, test_data):
self.model = model
self.test_data = test_data
self.history = {'epoch': [], 'train_loss': [], 'test_rmse': []}
def on_epoch_end(self, epoch, train_loss):
# Оценка на test
test_preds = self.model.predict(
self.test_data['user_id'].values,
self.test_data['item_id'].values
)
test_rmse = np.sqrt(np.mean((self.test_data['rating'].values - test_preds) ** 2))
self.history['epoch'].append(epoch)
self.history['train_loss'].append(train_loss)
self.history['test_rmse'].append(test_rmse)
print(f"Epoch {epoch}: Train Loss = {train_loss:.4f}, Test RMSE = {test_rmse:.4f}")
def plot(self):
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.plot(self.history['epoch'], self.history['train_loss'])
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Training Loss')
ax1.set_title('Training Loss')
ax1.grid(True)
ax2.plot(self.history['epoch'], self.history['test_rmse'])
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Test RMSE')
ax2.set_title('Test RMSE')
ax2.grid(True)
plt.tight_layout()
plt.savefig('svdpp_training.png')
plt.close()
# Использование (требует модификации кода модели для поддержки callback)
# monitor = TrainingMonitor(model, test.data)
# model.fit(train.data, callback=monitor.on_epoch_end)
# monitor.plot()
Сравнение SVD и SVD++
from recommender import SVDRecommender, SVDPlusPlusRecommender, Evaluator
# Подготовка данных
df = load_movielens(size='100k')
dataset = InteractionDataset(df, implicit=False)
train, test = dataset.split(test_size=0.2, seed=42)
evaluator = Evaluator(metrics=['rmse', 'mae'])
# SVD
print("=" * 50)
print("Обучение SVD...")
svd = SVDRecommender(n_components=20, random_state=42)
svd.fit(train.data)
svd_results = evaluator.evaluate(svd, test, task='rating_prediction')
# SVD++
print("\n" + "=" * 50)
print("Обучение SVD++...")
svdpp = SVDPlusPlusRecommender(n_factors=20, n_epochs=20, random_state=42)
svdpp.fit(train.data)
svdpp_results = evaluator.evaluate(svdpp, test, task='rating_prediction')
# Сравнение
print("\n" + "=" * 50)
print("СРАВНЕНИЕ:")
print(f"{'Метрика':<10} {'SVD':<10} {'SVD++':<10} {'Улучшение':<10}")
print("-" * 50)
for metric in ['rmse', 'mae']:
svd_val = svd_results[metric]
svdpp_val = svdpp_results[metric]
improvement = ((svd_val - svdpp_val) / svd_val) * 100
print(f"{metric.upper():<10} {svd_val:<10.4f} {svdpp_val:<10.4f} {improvement:>9.2f}%")
Производительность
Время обучения (ML-100K, 20 факторов):
| Модель | Эпохи | Время |
|---|---|---|
| SVD | - | ~3 сек |
| SVD++ | 10 | ~2 мин |
| SVD++ | 20 | ~4 мин |
| SVD++ | 50 | ~10 мин |
Качество (RMSE на ML-100K):
| Модель | RMSE | MAE |
|---|---|---|
| SVD | 0.935 | 0.738 |
| SVD++ | 0.918 | 0.721 |
| Улучшение | 1.8% | 2.3% |
Когда использовать SVD++
✅ Хорошо подходит:
- Explicit ratings с implicit feedback
- Когда история взаимодействий информативна
- Нужна максимальная точность на explicit ratings
- Есть время на обучение
- Хотите учесть "что пользователь смотрел", а не только "какую оценку поставил"
❌ Не подходит:
- Pure implicit feedback (используйте ALS)
- Нужна скорость обучения (используйте SVD)
- Очень большие датасеты (медленное обучение)
- Real-time updates (SGD медленный для инкрементальных обновлений)
ALS - Alternating Least Squares
Теория
ALS (Alternating Least Squares) - это итеративный алгоритм matrix factorization, специально разработанный для implicit feedback.
Преимущества ALS
- Нет learning rate: Аналитическое решение на каждом шаге
- Параллелизуемо: Каждый user/item решается независимо
- Эффективно для implicit: Правильно обрабатывает негативные примеры
- Масштабируемо: Работает на очень больших датасетах
Гиперпараметры
n_factors (k)
- Описание: Количество латентных факторов
- Диапазон: 10 - 200
- По умолчанию: 20
- Влияние: Как в SVD, но ALS может работать с большими k
n_iterations
- Описание: Количество чередующихся итераций
- Диапазон: 5 - 30
- По умолчанию: 15
- Влияние: Больше итераций → лучше сходимость
- Рекомендация: 10-20 обычно достаточно
reg (λ)
- Описание: Сила регуляризации
- Диапазон: 0.001 - 0.1
- По умолчанию: 0.01
- Влияние:
- Малая (< 0.01): Переобучение
- Средняя (0.01-0.05): Хороший баланс
- Большая (> 0.1): Недообучение
alpha (α)
- Описание: Scaling для confidence
- Диапазон: 1.0 - 100.0
- По умолчанию: 40.0
- Влияние:
- Малый (< 10): Weak confidence, больше влияние негативов
- Средний (20-50): Обычно оптимален
- Большой (> 80): Strong confidence, игнорирует негативы
Совет: alpha зависит от масштаба данных. Если ( r_{ui} ) большие, используйте малый alpha.
Примеры использования
Базовый пример
from recommender import ALSRecommender, load_movielens, InteractionDataset, Evaluator
# 1. Загрузка данных
df = load_movielens(size='1m')
# 2. Создание датасета (IMPLICIT feedback)
dataset = InteractionDataset(
df,
implicit=True, # Важно: бинаризовать для ALS
min_user_interactions=5
)
train, test = dataset.split(test_size=0.2, strategy='random', seed=42)
# 3. Обучение ALS
model = ALSRecommender(
n_factors=50,
n_iterations=15,
reg=0.01,
alpha=40.0,
random_state=42
)
print("Обучение ALS...")
model.fit(train.data)
# 4. Рекомендации
recommendations = model.recommend([1, 2, 3], k=10, exclude_seen=True)
for user_id, items in recommendations.items():
print(f"\nUser {user_id}:")
for item_id, score in items[:5]:
print(f" Item {item_id}: score = {score:.3f}")
# 5. Оценка
evaluator = Evaluator(metrics=['precision', 'recall', 'ndcg'], k_values=[10, 20])
results = evaluator.evaluate(model, test, task='ranking', train_data=train)
evaluator.print_results(results)
Подбор alpha
# Эксперимент с разными alpha
alpha_values = [1, 10, 20, 40, 60, 80, 100]
results = {}
for alpha in alpha_values:
print(f"\nТестирование alpha={alpha}")
model = ALSRecommender(
n_factors=50,
n_iterations=15,
reg=0.01,
alpha=alpha,
random_state=42
)
model.fit(train.data)
eval_results = evaluator.evaluate(model, test, task='ranking', train_data=train)
ndcg = eval_results['ndcg@10']
results[alpha] = ndcg
print(f" NDCG@10: {ndcg:.4f}")
# Лучший alpha
best_alpha = max(results, key=results.get)
print(f"\n✅ Лучший alpha: {best_alpha} (NDCG@10: {results[best_alpha]:.4f})")
Анализ факторов
# Обучение модели
model = ALSRecommender(n_factors=50, n_iterations=15)
model.fit(train.data)
# Анализ user factors
user_factors = model.user_factors # shape: (n_users, n_factors)
print(f"User factors shape: {user_factors.shape}")
# Нахождение похожих пользователей
def find_similar_users(user_id, k=5):
if user_id not in model.user_mapping:
return []
user_idx = model.user_mapping[user_id]
user_vec = user_factors[user_idx]
# Cosine similarity
similarities = user_factors @ user_vec
norms = np.linalg.norm(user_factors, axis=1) * np.linalg.norm(user_vec)
similarities = similarities / (norms + 1e-8)
# Top-k (исключая самого пользователя)
top_indices = np.argsort(similarities)[::-1][1:k+1]
similar_users = []
for idx in top_indices:
similar_user_id = model.reverse_user_mapping[idx]
sim_score = similarities[idx]
similar_users.append((similar_user_id, sim_score))
return similar_users
# Пример
similar = find_similar_users(user_id=1, k=5)
print(f"\nПользователи похожие на User 1:")
for uid, sim in similar:
print(f" User {uid}: similarity = {sim:.3f}")
Сравнение с EASE
from recommender import EASERecommender, ALSRecommender, Evaluator
import time
# Подготовка
evaluator = Evaluator(metrics=['precision', 'recall', 'ndcg'], k_values=[10])
# EASE
print("Обучение EASE...")
start = time.time()
ease = EASERecommender(l2_reg=500.0)
ease.fit(train.data)
ease_time = time.time() - start
ease_results = evaluator.evaluate(ease, test, task='ranking', train_data=train)
# ALS
print("\nОбучение ALS...")
start = time.time()
als = ALSRecommender(n_factors=50, n_iterations=15, alpha=40.0)
als.fit(train.data)
als_time = time.time() - start
als_results = evaluator.evaluate(als, test, task='ranking', train_data=train)
# Сравнение
print("\n" + "=" * 60)
print("СРАВНЕНИЕ EASE vs ALS:")
print("=" * 60)
print(f"{'Метрика':<20} {'EASE':<15} {'ALS':<15}")
print("-" * 60)
for metric in ['precision@10', 'recall@10', 'ndcg@10']:
ease_val = ease_results[metric]
als_val = als_results[metric]
print(f"{metric:<20} {ease_val:<15.4f} {als_val:<15.4f}")
print(f"{'Время обучения':<20} {ease_time:<15.2f}s {als_time:<15.2f}s")
Производительность
Время обучения (ML-1M, 50 факторов):
| Итерации | Время |
|---|---|
| 5 | ~15 сек |
| 10 | ~30 сек |
| 15 | ~45 сек |
| 20 | ~60 сек |
Качество (NDCG@10 на ML-1M implicit):
| Модель | Факторы | NDCG@10 |
|---|---|---|
| ALS | 20 | 0.352 |
| ALS | 50 | 0.357 |
| ALS | 100 | 0.359 |
| EASE | - | 0.385 |
Когда использовать ALS
✅ Хорошо подходит:
- Implicit feedback (клики, просмотры, покупки)
- Большие датасеты (масштабируется хорошо)
- Нужна параллелизация
- Production системы (стабильный, без learning rate)
- Confidence weighting важна
❌ Не подходит:
- Explicit ratings (используйте SVD/SVD++)
- Нужна максимальная точность (EASE/LightGCN лучше)
- Очень маленькие датасеты (overhead ALS не оправдан)
- Нужна максимальная скорость (EASE быстрее)
Сравнение всех трёх моделей
Таблица сравнения
| Аспект | SVD | SVD++ | ALS |
|---|---|---|---|
| Тип данных | Explicit | Explicit + Implicit | Implicit |
| Скорость | ⚡⚡⚡ Быстро | ⚡ Медленно | ⚡⚡ Средне |
| Качество | ⭐⭐ Хорошо | ⭐⭐⭐ Отлично | ⭐⭐ Хорошо |
| Масштабируемость | ⭐⭐ Средняя | ⭐ Низкая | ⭐⭐⭐ Высокая |
| Гиперпараметры | 1-2 (простые) | 4 (сложные) | 4 (средние) |
| Параллелизуемость | ❌ Нет | ❌ Нет | ✅ Да |
| Обучение | Closed-form | SGD | Alternating LS |
Выбор модели
Для explicit ratings:
- Начните с SVD - быстрый baseline
- Попробуйте SVD++ если:
- Нужна максимальная точность
- История взаимодействий информативна
- Есть время на обучение
Для implicit feedback:
- Начните с EASE - лучший baseline
- Попробуйте ALS если:
- Датасет очень большой
- Нужна параллелизация
- Хотите confidence weighting
Практические советы
1. Выбор количества факторов
# Правило: начните с k = sqrt(min(n_users, n_items))
k_start = int(np.sqrt(min(dataset.n_users, dataset.n_items)))
print(f"Рекомендуемое k: {k_start}")
# Потом экспериментируйте: [k_start/2, k_start, k_start*2]
2. Нормализация данных
# Для SVD/SVD++: центрируйте рейтинги
from recommender.data import normalize_ratings
df_norm = normalize_ratings(df, method='standard')
# Теперь mean=0, std=1
3. Регуляризация
# Начните с reg = 0.01-0.02
# Увеличивайте если видите переобучение (train << val)
# Уменьшайте если видите недообучение (train high, val high)
4. Early stopping для SVD++
# Следите за validation RMSE
# Останавливайте если не улучшается 3-5 эпох подряд
Научные статьи
SVD
- Название: Matrix Factorization Techniques for Recommender Systems
- Авторы: Yehuda Koren, Robert Bell, Chris Volinsky
- Журнал: IEEE Computer, 2009
- Ссылка: https://ieeexplore.ieee.org/document/5197422
SVD++
- Название: Factorization meets the neighborhood: a multifaceted collaborative filtering model
- Авторы: Yehuda Koren
- Конференция: KDD 2008
- Ссылка: https://dl.acm.org/doi/10.1145/1401890.1401944
ALS
- Название: Collaborative Filtering for Implicit Feedback Datasets
- Авторы: Yifan Hu, Yehuda Koren, Chris Volinsky
- Конференция: ICDM 2008
- Ссылка: https://ieeexplore.ieee.org/document/4781121

