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







