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 пользователь взаимодействовал, независимо от рейтинга.

Ключевые компоненты

  1. Biases (( \mu, b_u, b_i )): Учитывают базовые предпочтения
  2. Explicit factors (( p_u, q_i )): Латентные представления как в SVD
  3. 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

  1. Нет learning rate: Аналитическое решение на каждом шаге
  2. Параллелизуемо: Каждый user/item решается независимо
  3. Эффективно для implicit: Правильно обрабатывает негативные примеры
  4. Масштабируемо: Работает на очень больших датасетах

Гиперпараметры

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:

  1. Начните с SVD - быстрый baseline
  2. Попробуйте SVD++ если:
    • Нужна максимальная точность
    • История взаимодействий информативна
    • Есть время на обучение

Для implicit feedback:

  1. Начните с EASE - лучший baseline
  2. Попробуйте 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++

ALS

  • Название: Collaborative Filtering for Implicit Feedback Datasets
  • Авторы: Yifan Hu, Yehuda Koren, Chris Volinsky
  • Конференция: ICDM 2008
  • Ссылка: https://ieeexplore.ieee.org/document/4781121
AI на дровах 🪵
Привет! Меня зовут Семён, я работаю в сфере ML и аналитики данных и пишу в блог nerdit.ru статьи о своем опыте и том, что может пригодиться начинающим в начале их пути изучения больших данных.

Подписаться на новости Nerd IT

Не пропустите последние выпуски. Зарегистрируйтесь сейчас, чтобы получить полный доступ к статьям.
jamie@example.com
Подписаться