Новий сайт з відеоуроками, де першим користувачам рекомендують випадкові ролики — відтік 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 проектів, значний досвід і сертифіковані інженери — ось чому нам довіряють.







