Розробка рекомендаційної системи на колаборативній фільтрації
Клієнт із e-commerce скаржиться: "У нас 200 тисяч товарів, але середній чек не зростає. Каталог гігантський, користувачі губляться". Проблема знайома — без персональних рекомендацій відвідувач іде без покупки, а якщо й купує, то лише один товар. Ми впровадили Collaborative Filtering інтернет-магазину з аудиторією 2 млн користувачів і збільшили cross-sell на 22% за перші два тижні після запуску. Це принесло додаткові 5 млн грн на місяць — конкретна економія для бізнесу.
Collaborative Filtering — найпоширеніший підхід для товарних рекомендацій: "користувачі, схожі на тебе, купували ось це". Не потрібні описи товарів — лише історія взаємодій. Працює для будь-яких доменів, але вимагає достатнього обсягу транзакцій (>50K) і погано справляється з холодним стартом. Докладніше про метод можна прочитати в Collaborative Filtering.
Чому колаборативна фільтрація потребує багато даних? — розробка рекомендаційної системи
Алгоритм будує приховані фактори на основі перетинів користувачів і товарів. При малій кількості транзакцій матриця user-item надто розріджена — фактори не сходяться, рекомендації стають зашумленими. Для стабільного результату потрібно не менше 50 тисяч транзакцій і 5 тисяч унікальних користувачів. Якщо даних менше, використовуємо content-based підхід або гібридні схеми.
Як колаборативна фільтрація вирішує проблему персоналізації?
ALS (Alternating Least Squares) — матрична факторизація, яка розкладає розріджену матрицю user-item на добуток двох щільних матриць прихованих факторів. Масштабується на мільйони рядків: для 1M користувачів × 100K товарів навчання на CPU (8 ядер) — 5-15 хвилин. Ключові гіперпараметри: factors=64-128, iterations=15-30, regularization=0.001-0.1.
Зважування подій: перегляд=1, додавання в кошик=3, покупка=5, повторна покупка=8. Це покращує якість — важливий не лише факт, а й вага дії.
import numpy as np
import scipy.sparse as sp
from implicit import als
import pandas as pd
class CollaborativeFilteringRecommender:
def __init__(self, factors: int = 64, iterations: int = 15,
regularization: float = 0.01):
self.model = als.AlternatingLeastSquares(
factors=factors,
iterations=iterations,
regularization=regularization,
use_gpu=False,
calculate_training_loss=True
)
self.user_map = {}
self.item_map = {}
self.reverse_item_map = {}
def fit(self, interactions_df: pd.DataFrame,
user_col: str = "user_id",
item_col: str = "item_id",
weight_col: str = "weight") -> None:
"""
interactions_df: user_id, item_id, weight (1=перегляд, 2=додавання в кошик, 5=покупка)
"""
unique_users = interactions_df[user_col].unique()
unique_items = interactions_df[item_col].unique()
self.user_map = {u: i for i, u in enumerate(unique_users)}
self.item_map = {it: i for i, it in enumerate(unique_items)}
self.reverse_item_map = {i: it for it, i in self.item_map.items()}
rows = interactions_df[item_col].map(self.item_map)
cols = interactions_df[user_col].map(self.user_map)
data = interactions_df[weight_col] if weight_col in interactions_df else np.ones(len(interactions_df))
self.matrix = sp.csr_matrix(
(data, (rows, cols)),
shape=(len(unique_items), len(unique_users))
)
self.model.fit(self.matrix)
def recommend(self, user_id, n: int = 10,
exclude_purchased: bool = True) -> list[tuple]:
"""Top-N рекомендацій для користувача"""
if user_id not in self.user_map:
return self._popular_items(n)
user_idx = self.user_map[user_id]
filter_items = None
if exclude_purchased:
user_items = self.matrix.T.getcol(user_idx)
filter_items = user_items.indices
item_indices, scores = self.model.recommend(
userid=user_idx,
user_items=self.matrix.T[user_idx],
N=n,
filter_already_liked_items=exclude_purchased
)
return [
(self.reverse_item_map[idx], float(score))
for idx, score in zip(item_indices, scores)
]
def similar_items(self, item_id, n: int = 10) -> list[tuple]:
"""Схожі товари (item2item)"""
if item_id not in self.item_map:
return []
item_idx = self.item_map[item_id]
similar_indices, scores = self.model.similar_items(item_idx, N=n + 1)
return [
(self.reverse_item_map[idx], float(score))
for idx, score in zip(similar_indices, scores)
if idx != item_idx
][:n]
def _popular_items(self, n: int) -> list[tuple]:
"""Fallback: популярні товари для нових користувачів"""
item_popularity = np.array(self.matrix.sum(axis=1)).flatten()
top_indices = np.argsort(item_popularity)[-n:][::-1]
return [
(self.reverse_item_map[idx], float(item_popularity[idx]))
for idx in top_indices
]
Конфігурація навчання ALS
Гіперпараметри ALS:
| Параметр | Рекомендовані значення | Вплив |
|---|---|---|
| factors | 64-256 | Якість факторів: більше — точніше, але зростає пам'ять |
| iterations | 15-30 | Збіжність: більше 30 рідко покращує |
| regularization | 0.001-0.1 | Регуляризація: вище — менше переобучення, але знижується точність |
| use_gpu | True/False | Прискорення на GPU: для матриць > 500K×100K |
BPR — коли порядок важливіший за точність
BPR (Bayesian Personalized Ranking) оптимізує не рейтинг, а порядок: для кожного користувача модель намагається розташувати куплені товари вище за некуплені. Це ефективніше поєднується з неявними даними — кліками, переглядами. На практиці BPR дає на 10-15% вищий Precision@10 порівняно з ALS на неявних даних.
class BPRRecommender:
"""
BPR оптимізує порядок, а не точність передбачення рейтингу.
Краще підходить, коли немає явних оцінок — тільки clicks/views/purchases.
"""
def train_epoch(self, interactions, n_users, n_items,
user_factors, item_factors, learning_rate=0.01, reg=0.01):
"""SGD крок навчання BPR"""
user_idx = np.random.randint(n_users)
user_items = interactions[user_idx].indices
if len(user_items) == 0:
return 0
pos_item_idx = np.random.choice(user_items)
neg_item_idx = np.random.randint(n_items)
while neg_item_idx in user_items:
neg_item_idx = np.random.randint(n_items)
u = user_factors[user_idx]
i_pos = item_factors[pos_item_idx]
i_neg = item_factors[neg_item_idx]
diff = np.dot(u, i_pos - i_neg)
sigmoid = 1 / (1 + np.exp(-diff))
loss = -np.log(sigmoid + 1e-10)
grad = (1 - sigmoid)
user_factors[user_idx] += learning_rate * (grad * (i_pos - i_neg) - reg * u)
item_factors[pos_item_idx] += learning_rate * (grad * u - reg * i_pos)
item_factors[neg_item_idx] += learning_rate * (-grad * u - reg * i_neg)
return loss
Коли використовувати ALS, а коли BPR?
ALS краще підходить для великих маркетплейсів із мільйонами транзакцій і явними оцінками (рейтинги) або зваженими неявними даними. BPR ефективніший для нішевих магазинів, де важлива персоналізація порядку показу товарів, особливо при неявних сигналах (кліки, перегляди). На практиці ми часто комбінуємо обидва підходи: ALS для базових рекомендацій, BPR для ранжування в топ-N.
Холодний старт: рішення
Холодний старт — головний ворог колаборативної фільтрації. Для нових товарів без історії використовуємо гібридний підхід: комбінуємо ALS з content-based ознаками (категорія, бренд, ціна). Для нових користувачів — fallback на популярні товари (метод _popular_items у коді вище) або сегментні рекомендації на основі демографії. Також застосовуємо item2item рекомендації на основі метаданих.
Як оцінити якість рекомендацій?
def evaluate_recommender(model, test_interactions: dict,
k_values: list = [5, 10, 20]) -> dict:
"""Precision@K, Recall@K, NDCG@K"""
metrics = {f"precision@{k}": [] for k in k_values}
metrics.update({f"recall@{k}": [] for k in k_values})
metrics.update({f"ndcg@{k}": [] for k in k_values})
for user_id, true_items in test_interactions.items():
if not true_items:
continue
recommendations = model.recommend(user_id, n=max(k_values))
rec_items = [item_id for item_id, _ in recommendations]
for k in k_values:
top_k = set(rec_items[:k])
true_set = set(true_items)
hits = len(top_k & true_set)
metrics[f"precision@{k}"].append(hits / k)
metrics[f"recall@{k}"].append(hits / len(true_set) if true_set else 0)
dcg = sum(
1 / np.log2(i + 2)
for i, item in enumerate(rec_items[:k])
if item in true_set
)
idcg = sum(1 / np.log2(i + 2) for i in range(min(k, len(true_set))))
metrics[f"ndcg@{k}"].append(dcg / idcg if idcg > 0 else 0)
return {k: np.mean(v) for k, v in metrics.items()}
Для типового інтернет-магазину ми отримуємо NDCG@10 в діапазоні 0.25-0.45. Це показник того, що перші 10 рекомендацій дійсно релевантні.
Порівняння ALS і BPR
| Параметр | ALS | BPR |
|---|---|---|
| Тип даних | Рейтинги, неявні (з вагами) | Тільки неявні (порядок) |
| Масштабованість | Мільйони рядків, 5-15 хв CPU | До 500K рядків, 30-60 хв |
| Холодний старт | Вимагає fallback | Краще узагальнення на нових юзерах |
| Метрика якості | NDCG@10 0.25-0.45 | Precision@10 трохи вища |
| Де застосовуємо | Великі маркетплейси | Нішеві магазини з історією |
Процес роботи: від аналітики до деплою
- Аналітика: збираємо логи подій, готуємо дані — чистка від ботів, нормалізація ваг, усунення дірявих даних.
- Проектування: вибираємо алгоритм (ALS/BPR), визначаємо гіперпараметри, pipeline для інференсу, налаштовуємо кешування.
- Реалізація: пишемо код, інтегруємо з вашою базою даних, підключаємо API.
- Тест: A/B-тест на 10% трафіку, заміряємо CR, AOV, Engagement. Оцінюємо метрики.
- Деплой: розгортаємо модель, підключаємо моніторинг через Prometheus/Grafana, налаштовуємо регулярне перенавчання.
Що входить у роботу (deliverables)
- Готова модель ALS/BPR з оптимізованими гіперпараметрами.
- API для real-time рекомендацій (REST/gRPC).
- Документація з експлуатації та перенавчання.
- Навчання команди: як інтерпретувати метрики, оновлювати ваги.
- Підтримка протягом місяця після запуску.
Наш досвід: понад 5 років у ML, 15+ успішних впроваджень в e-commerce, команда сертифікованих інженерів. Гарантуємо якість — результат підтверджений A/B тестами.
Строки орієнтовно
Від 2 до 6 тижнів залежно від обсягу даних і складності інтеграції. Вартість розраховується індивідуально — залежить від необхідної якості, кількості користувачів і SLA. Отримайте консультацію: ми оцінимо ваш проект і запропонуємо рішення під ключ. Замовте розробку рекомендаційної системи та збільште середній чек у вашому магазині.







