Новый сайт с видеоуроками, где первым пользователям рекомендуют случайные ролики — отток 80% за первую неделю. Мы решаем эту проблему с помощью Content-Based Filtering — метода, который не требует данных о других пользователях и выдаёт релевантные рекомендации уже после 3–5 взаимодействий. Наш опыт — более 50 внедрённых рекомендательных систем для e-commerce, медиа и EdTech. Наши инженеры сертифицированы по MLOps и NLP, что гарантирует качество каждого этапа.
Как Content-Based Recommendation System решает холодный старт?
Минимальная история взаимодействий — главная проблема онбординга. Content-Based Filtering строит профиль пользователя исключительно на основе атрибутов объектов, с которыми он взаимодействовал. Метод использует семантические эмбеддинги текста, TF-IDF, категориальные и числовые признаки, объединяя их в единый мультимодальный профиль. Precision@10 на новых пользователях достигает 0.15–0.30, что в 10–30 раз выше, чем у популярного бейзлайна (0.01–0.03).
Архитектура Content-Based Recommendation System
Многомодальный контентный профиль: пошаговый подход — разработка рекомендательной системы
- Сбор данных: item_id, title, description, категории, теги, цена, рейтинг и другие числовые признаки.
- Векторизация текста: используем мультиязычный sentence-transformers (
paraphrase-multilingual-mpnet-base-v2), получаем 768-мерные эмбеддинги. - TF-IDF признаки: дополнительно извлекаем 5000 n-граммов (1-2), сжимаем до 50 компонент через SVD.
- Категориальные признаки: бинаризация через MultiLabelBinarizer.
- Числовые признаки: StandardScaler для price, rating, review_count, release_year.
- Взвешенная конкатенация: текст (вес 2), TF-IDF (0.5), категории (1.0), числа (0.3). Результат L2-нормализуется.
import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.preprocessing import MultiLabelBinarizer, StandardScaler from sklearn.metrics.pairwise import cosine_similarity import torch from sentence_transformers import SentenceTransformer class ContentBasedRecommender: def __init__(self): self.text_model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2') self.tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) self.mlb = MultiLabelBinarizer() self.scaler = StandardScaler() self.item_vectors = None self.item_ids = [] def build_item_profiles(self, items_df: pd.DataFrame) -> np.ndarray: """ items_df: item_id, title, description, category (list), tags (list), price, rating, release_year, ... """ feature_parts = [] # 1. Семантические эмбеддинги текста (описание + название) texts = ( items_df['title'].fillna('') + '. ' + items_df.get('description', pd.Series([''] * len(items_df))).fillna('') ).tolist() print(f"Encoding {len(texts)} items...") text_embeddings = self.text_model.encode( texts, batch_size=64, show_progress_bar=True, convert_to_numpy=True, normalize_embeddings=True ) feature_parts.append(text_embeddings) # 2. TF-IDF признаки из текста tfidf_features = self.tfidf.fit_transform(texts).toarray() # PCA для сжатия from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=50) tfidf_compact = svd.fit_transform(tfidf_features) feature_parts.append(tfidf_compact) # 3. Категориальные признаки if 'categories' in items_df.columns: cat_features = self.mlb.fit_transform( items_df['categories'].apply(lambda x: x if isinstance(x, list) else []) ) feature_parts.append(cat_features.astype(float)) # 4. Числовые признаки num_cols = ['price', 'rating', 'review_count', 'release_year'] available_num = [c for c in num_cols if c in items_df.columns] if available_num: num_features = self.scaler.fit_transform( items_df[available_num].fillna(items_df[available_num].median()) ) feature_parts.append(num_features) # Взвешенная конкатенация weights = [2.0, 0.5, 1.0, 0.3][:len(feature_parts)] weighted_parts = [p * w for p, w in zip(feature_parts, weights)] combined = np.hstack(weighted_parts) # L2 нормализация norms = np.linalg.norm(combined, axis=1, keepdims=True) self.item_vectors = combined / (norms + 1e-10) self.item_ids = items_df['item_id'].tolist() return self.item_vectors def build_user_profile(self, liked_items: list[str], weights: list[float] = None) -> np.ndarray: """Профиль пользователя как взвешенное среднее понравившихся товаров""" item_indices = [ self.item_ids.index(item_id) for item_id in liked_items if item_id in self.item_ids ] if not item_indices: return None liked_vectors = self.item_vectors[item_indices] if weights is not None and len(weights) == len(item_indices): w = np.array(weights[:len(item_indices)]).reshape(-1, 1) user_vector = np.average(liked_vectors, axis=0, weights=w.flatten()) else: # Последние взаимодействия важнее recency_weights = np.exp(np.linspace(-1, 0, len(item_indices))) user_vector = np.average(liked_vectors, axis=0, weights=recency_weights) norm = np.linalg.norm(user_vector) return user_vector / (norm + 1e-10) def recommend(self, user_profile: np.ndarray, exclude_items: list[str] = None, n: int = 10, diversity_penalty: float = 0.1) -> list[tuple]: """Рекомендации с MMR штрафом за похожесть""" if user_profile is None: return [] # Базовые скоры scores = cosine_similarity( user_profile.reshape(1, -1), self.item_vectors )[0] # Исключение уже просмотренных if exclude_items: for item_id in exclude_items: if item_id in self.item_ids: idx = self.item_ids.index(item_id) scores[idx] = -1 # MMR (Maximal Marginal Relevance) для разнообразия selected_indices = [] selected_embeddings = [] while len(selected_indices) < n: if not selected_embeddings: # Первый — самый релевантный best_idx = np.argmax(scores) else: # Следующие: релевантность минус штраф за схожесть с уже выбранными selected_matrix = np.vstack(selected_embeddings) similarity_to_selected = cosine_similarity( self.item_vectors, selected_matrix ).max(axis=1) adjusted_scores = scores - diversity_penalty * similarity_to_selected # Маскировка уже выбранных for idx in selected_indices: adjusted_scores[idx] = -1 best_idx = np.argmax(adjusted_scores) if scores[best_idx] < 0: break selected_indices.append(best_idx) selected_embeddings.append(self.item_vectors[best_idx]) return [ (self.item_ids[idx], float(scores[idx])) for idx in selected_indices ] def item_to_item(self, item_id: str, n: int = 10) -> list[tuple]: """Похожие товары для страницы товара""" if item_id not in self.item_ids: return [] item_idx = self.item_ids.index(item_id) item_vector = self.item_vectors[item_idx] scores = cosine_similarity( item_vector.reshape(1, -1), self.item_vectors )[0] scores[item_idx] = -1 # Исключаем сам товар top_indices = np.argsort(scores)[-n:][::-1] return [(self.item_ids[idx], float(scores[idx])) for idx in top_indices] Обновление профиля в реальном времени
class OnlineUserProfileUpdater: """Инкрементальное обновление профиля без перестройки""" def __init__(self, recommender: ContentBasedRecommender): self.rec = recommender self.user_profiles = {} self.user_history = {} def update_on_interaction(self, user_id: str, item_id: str, interaction_type: str): """Обновление профиля после взаимодействия""" weights = { 'view': 1.0, 'click': 1.5, 'add_to_cart': 3.0, 'purchase': 5.0, 'dislike': -2.0, 'skip': -0.5 } weight = weights.get(interaction_type, 1.0) if user_id not in self.user_history: self.user_history[user_id] = [] self.user_history[user_id].append({ 'item_id': item_id, 'weight': weight, 'interaction_type': interaction_type }) # Пересчёт профиля (последние 50 взаимодействий) history = self.user_history[user_id][-50:] liked = [h['item_id'] for h in history if h['weight'] > 0] liked_weights = [h['weight'] for h in history if h['weight'] > 0] if liked: self.user_profiles[user_id] = self.rec.build_user_profile( liked, liked_weights ) def get_recommendations(self, user_id: str, n: int = 10) -> list[tuple]: profile = self.user_profiles.get(user_id) if profile is None: return [] exclude = [h['item_id'] for h in self.user_history.get(user_id, [])] return self.rec.recommend(profile, exclude_items=exclude, n=n) Content-based с sentence-transformers даёт Precision@10 = 0.15–0.30 на новых пользователях (против 0.01–0.03 у популярных товаров). Строить профиль достаточно из 3–5 взаимодействий. Инференс: SentenceTransformer энкодинг 10K товаров — 5–10 минут на CPU, 30–60 секунд на GPU.
| Метрика | Content-Based (новые пользователи) | Popular Baseline | Улучшение |
|---|---|---|---|
| Precision@10 | 0.15–0.30 | 0.01–0.03 | в 10–30 раз |
| Recall@10 | 0.10–0.25 | 0.005–0.01 | в 10–25 раз |
| Coverage | 80%+ | 1–5% | в 10–30 раз |
Почему Content-Based Filtering выгоднее Collaborative Filtering для нового проекта?
Для платформ с малым числом пользователей или высокой ротацией контента Collaborative Filtering неэффективен из-за разреженности матрицы взаимодействий. Content-Based использует богатые метаданные и не страдает от холодного старта — выдача релевантна уже после первого клика. Например, в нишевом онлайн-кинотеатре с 5000 фильмов и 200 активными пользователями контентная фильтрация даёт coverage 80%+, тогда как collaborative filtering покрывает лишь 5% каталога. Подробнее о Collaborative Filtering. Средняя окупаемость системы — 6–12 месяцев за счёт снижения оттока.
| Характеристика | Content-Based | Collaborative Filtering |
|---|---|---|
| Зависимость от других пользователей | Нет | Да |
| Холодный старт | 3–5 действий | Требует много данных |
| Разнообразие (coverage) | 80%+ | 5–20% |
| Serendipity | Низкая | Высокая |
| Вычислительная сложность | O(N * F) | O(пользователи * товары) |
Что входит в работу
- Документация по архитектуре, входным данным и API endpoints.
- Исходный код пайплайна векторизации и рекомендательного модуля.
- Доступы к обученным model weights и векторным индексам.
- Обучение команды администрированию и обновлению модели.
- Поддержка в течение первого месяца после внедрения (решение инцидентов, оптимизация).
Ориентировочные сроки и стоимость
Срок реализации зависит от объёма данных и сложности интеграции. Типичный проект занимает от 4 до 8 недель. Для точной оценки пришлите описание вашей предметной области и объём каталога — мы рассчитаем стоимость и сроки за один рабочий день. Свяжитесь с нами — мы проанализируем ваши данные и подберём оптимальную архитектуру.
Получите консультацию: мы покажем, как контентная фильтрация повлияет на вашу воронку. Система существенно поднимает вовлечённость на этапе онбординга, когда у пользователя ещё нет истории. А с нашей реализацией вы получаете гарантию качества: мы уже прошли все типовые грабли — от выбора эмбеддингов до калибровки штрафа за разнообразие. Более 50 проектов, 7+ лет опыта и сертифицированные инженеры — вот почему нам доверяют.







