ML без Data‑Science‑диплома: как собрать работающую рекомендательную систему на Python

TL;DR — Один разработчик способен за несколько часов внедрить персональные рекомендации в продукт, повысив CTR/конверсию на 5‑10 % и не потратившись на большую инфраструктуру. В статье разбираем путь — от формулировки бизнес‑задачи и подготовки данных до деплоя FastAPI‑сервиса и анализа A/B‑теста.

Зачем бизнесу «простые» рекомендации

Цифры, которые убеждают

  • Retail: персонализированная витрина увеличивает средний чек на 6‑9 % (по данным McKinsey, 2022).
  • Медиа: Spotify заявлял о +10 % времени прослушивания после запуска Discover Weekly.
  • B2B‑SaaS: рекомендации статей в базе знаний сокращают нагрузку на поддержку на 15‑20 %.

Универсальная мысль — персонализация = время пользователя + деньги бизнеса. Чем раньше появляется MVP, тем быстрее продукт входит в цикл улучшений данных и модели.

Когда «самописный» ALS достаточен

  • Пользовательская база < 1–2 млн MAU: детальная сегментация ещё не требует real‑time features.
  • Трафик на рекомендациях < 200 RPS: хватает одного t3.medium или даже недорогой GPU.
  • SLA (latency) 100–200 мс: можно комфортно жить без Faiss‑/Redis‑кэша.

Если вы вышли за эти пределы — пора задуматься о feature store, ANN‑поиске и онлайн‑обучении. Но для большинства стартапов и нишевых сервисов базовый ALS закрывает 80 % потребностей.

Данные за 30 минут: сбор, очистка, хранение

Откуда взять события

Публичные датасеты

  • MovieLens 100k/1M — классика для кино.
  • RetailRocket — real e‑commerce логи.
  • Goodbooks‑10k — книги + отзывы.

Собственные логи

  • Минимальный JSON‑ивент: {timestamp, user_id, item_id, event_type}.
  • Логи можно грести из S3/ClickHouse/BigQuery — важно унифицировать схему.

Очистка за 15 строк кода

import pandas as pd from pathlib import Path SRC = Path('raw/events.csv') DST = Path('clean/events.parquet.gzip') cols = ['user_id', 'item_id', 'timestamp'] df = pd.read_csv(SRC, usecols=cols, parse_dates=['timestamp']) # убираем ботов и пользователей с <3 событиями counts_u = df.user_id.value_counts() counts_i = df.item_id.value_counts() mask = (df.user_id.isin(counts_u[counts_u >= 3].index) & df.item_id.isin(counts_i[counts_i >= 3].index)) (df[mask] .sort_values('timestamp') .to_parquet(DST, compression='gzip'))

Хранение и версионирование

  • Parquet + gzip: любой офлайн batch; сжатие ≈ 75 %, читается поблочно.
  • Arrow / Feather: эксперименты в Jupyter; моментальное чтение в память.
  • Delta Lake: ETL‑пайплайны; ACID‑гарантии и schema evolution.

Лайфхак

Сохраняйте сразу слепок mapping user_id → int, item_id → int. Это экономит время

ALS «в разрезе»: теория, практика, тюнинг

Суть матричной факторизации

Мы имеем разреженную матрицу R (пользователь × товар), где элемент r_ui = 1, если было взаимодействие. Задача — приближённо разложить R ≈ P·Qᵀ, где строки P — вектора пользователей, Q — товаров. ALS попеременно оптимизирует P при фиксированном Q и наоборот (Hence Alternating Least Squares).

Почему именно библиотека implicit

  • GPU/CPU kernel’ы на OpenMP/CUDA.
  • Поддержка weighted‑λ‑regularization (Hu et al., 2008).
  • Умеет рекомендовать n предметов за один вызов (batched).
from implicit.evaluation import precision_at_k, mean_average_precision_at_k # 80/20 split по времени train_mask = df.timestamp < df.timestamp.quantile(0.8) train, test = df[train_mask], df[~train_mask] train_mat = make_sparse(train) test_mat = make_sparse(test) model = AlternatingLeastSquares(factors=128, regularization=0.1, iterations=30, random_state=42) model.fit(train_mat * 40) # trick to emphasise confidence print("Precision@10:", precision_at_k(model, train_mat.T, test_mat.T, K=10)) print("MAP@10:", mean_average_precision_at_k(model, train_mat.T, test_mat.T, K=10))

Гиперпараметры «на глазомер»

  • factors 32–64: увеличиваем до 256, пока прирост Precision@10 > 0.5 pp.
  • regularization 0.05–0.1: чем больше данных, тем сильнее L2‑регуляризация.
  • iterations 15–30: 2‑3 эпохи до сходимости, остальное — fine‑tune.

Практический совет

Следите за Overfitting — MAP на валидации падает? Урежьте factors или увеличьте

От ноутбука к продакшену: FastAPI + Docker

REST‑ендпоинт в 7 строк

@app.get("/api/v1/recommend/") async def recommend(uid: int, k: int = 10): if uid not in user_ids: raise HTTPException(status_code=404) recs, _ = model.recommend(user_ids[uid], train_mat.tocsr(), N=k) return {"user_id": uid, "items": [int(inv_items[i]) for i in recs]}

Контейнеризация

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt --no-cache-dir COPY . . CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
# сборка и запуск $ docker build -t reco-mvp . $ docker run -d -p 80:8000 --name reco reco-mvp

Кэширование и batch‑запросы

  • Redis — для горячих пользователей (топ‑10 % генерации трафика).
  • gRPC или /batch роут — сокращаем overhead.

Эксперимент, метрики, мониторинг

Дизайн A/B‑теста

  • Метрика: CTR карточки товара
  • Ожидаемый uplift: 5 %
  • Alpha / Beta: 0.05 / 0.2
  • Витрина: 50 / 50 (Control / Treatment)

Калькулятор (optimizely, gstats) даёт n = 15 000 уникальных пользователей на ветку.

Анализ результатов

from statsmodels.stats.proportion import proportions_ztest ctr_a = 0.048 # control ctr_b = 0.051 # treatment n = 15000 stat, p = proportions_ztest(count=[ctr_a*n, ctr_b*n], nobs=[n, n]) print(f"z={stat:.2f}, p={p:.4f}")

Получили p = 0.018 < 0.05 – эффект статистически значим.

Production‑мониторинг

  • Prometheus + Grafana: latency, RPS, error‑rate.
  • DataDog / Sentry: трейсинг исключений.
  • Drift‑метрики: раз в сутки проверка распределений векторов пользователей.

Скалирование и стоимость владения

  • < 5 млн рекомендаций в сутки: t3.medium (2 vCPU, 4 GB) + 30 GB EBS; OpEx ≈ 40 $ / мес
  • 5–50 млн: g4dn.xlarge (NVIDIA T4) + Redis cache; OpEx ≈ 200 $ / мес
  • > 50 млн: GPU‑кластер + Faiss ANN + real‑time features; OpEx ≈ 1000 $ / мес

CapEx ≈ 0 $, если остаётесь в облаке. Главное — мониторить рост данных и вовремя переключаться на GPU или шардирование.

Границы применимости и roadmap 2.0

  1. Холодный старт: для новых юзеров ALS бессилен. Лечится hybrid: ALS + контент‑based TF‑IDF.
  2. Не учитываются временные паттерны: сезонность, тренды. Roadmap: seq2seq / Transformer‑рекомендации.
  3. Негативный фидбек: лайков нет, дизлайки не учтены. Roadmap: implicit + explicit, weighted loss.
  4. Эксплейнабельность: ALS — «чёрный ящик». Решение: модели LightFM с бинарными фичами.

Итоги

  • Скорость: MVP рекомендательной системы — вечером кодируем, утром заливаем A/B.
  • Бюджет: < 100 $ на инстанс и трафик в первые месяцы.
  • Масштабируемость: от ноутбука → GPU кластеру без смены библиотеки.

Полезные ссылки

Начать дискуссию