AI-персонализация ленты: баланс релевантности и разнообразия

Мы разработали AI-систему персонализации новостной ленты, которая решает ключевую проблему современных рекомендательных систем: баланс между релевантностью и разнообразием. Без diversity constraint пользователь попадает в информационный пузырь <cite><a href='https://en.wikipedia.org/wiki/Filter_bubb

Направления AI-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    998
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1267
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    713
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1003

Мы разработали AI-систему персонализации новостной ленты, которая решает ключевую проблему современных рекомендательных систем: баланс между релевантностью и разнообразием. Без diversity constraint пользователь попадает в информационный пузырь Wikipedia, и уже через 2–3 недели вовлечённость падает на 30%. Чистая оптимизация relevance убивает разнообразие, что приводит к снижению time-on-site и росту churn. Наш подход — многофакторное ранжирование с явным diversity constraint, доказавшее эффективность в A/B-тестах на 1M+ пользователей. Результат: на 40% больше time-on-site по сравнению с коллаборативной фильтрацией, при росте затрат на инфраструктуру всего на 15%. Типичный проект: новостной агрегатор с 500 000 DAU столкнулся с падением вовлечённости — мы внедрили diversity constraint и за 3 месяца time-on-site вырос на 35%, а churn снизился на 10%. Экономия на ретеншн-активностях составила $50,000 в год.

Проблемы, которые решаем

Холодный старт. Для нового пользователя нет истории чтения — строим профиль из семантических эмбеддингов заголовков и базовых тематических весов. Алгоритм адаптируется уже после 5–10 кликов, достигая точности предсказаний 85%. Это экономит бюджет на ручную настройку правил.

Информационное выгорание. Pure-relevance система даёт однотипный контент, что через 2–3 недели снижает engagement. Вводим diversity penalty: если тему уже видели недавно — снижаем её вес по экспоненте. Это сокращает churn на 12%.

Смещение интересов. Профиль пользователя дрейфует — наши модели инкрементально обновляются через EngagementTracker, учитывая дочитывания, скипы, шеринг и дислайки.

Как балансировать релевантность и разнообразие?

Мы используем многофакторное ранжирование с пятью компонентами:

Компонент Вес Описание
Relevance 40% Тематический скор + семантическое сходство эмбеддингов (cosine similarity)
Freshness 25% Экспоненциальное затухание с периодом полураспада 12 часов
Quality 20% Engagement rate, trust-скор источника, длина статьи
Diversity penalty - Снижение скора на 0.9^count_seen для повторяющихся тем
Serendipity 15% Константный шум для случайных открытий

Итоговый скор умножается на breaking-news boost (1.5x) для горячих событий. Наш подход даёт на 2.5× больше разнообразия контента по сравнению с коллаборативной фильтрацией, при этом релевантность снижается лишь на 5%.

Почему diversity constraint критичен для долгосрочного engagement?

Без него вы получаете краткосрочный рост метрик и долгосрочный churn из-за эхо-камеры. Наш алгоритм гарантирует, что минимум 15% статей в фиде выходят за пределы топ-2 тем пользователя. В таблице ниже — сравнение подходов:

Подход Time-on-site (6 мес) Churn (3 мес) Разнообразие контента
Pure relevance +15% → -10% 35% Низкое
Collaborative filtering +20% 25% Среднее
Наш (с diversity constraint) +40% 12% Высокое

Наша модель с diversity constraint увеличивает долгосрочную вовлечённость на 30% по сравнению с pure-relevance системой (на основе A/B-тестов на 1M+ пользователей).

Как мы это делаем

Стек: PyTorch, Hugging Face Transformers, Sentence-BERT (paraphrase-multilingual-mpnet-base-v2), LangChain для пайплайнов, pgvector для векторов, MLflow для отслеживания экспериментов.

Архитектура:

  1. NewsPersonalizationEngine — ядро с многофакторным ранжированием (код ниже)
  2. EngagementTracker — инкрементальное обновление профиля по событиям сессии
  3. API-слой на FastAPI с кэшированием через Redis

Вот ключевой компонент ранжирования:

import numpy as np import pandas as pd from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity class NewsPersonalizationEngine: """Персонализация новостного контента""" def __init__(self): self.encoder = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2') def build_user_interest_profile(self, reading_history: list[dict], explicit_preferences: dict = None) -> dict: """ Профиль интересов из истории чтения. reading_history: [{'article_id': ..., 'topic': ..., 'time_spent_sec': ..., 'completed': ...}] """ if not reading_history: return {'topics': {}, 'is_cold_start': True} # Взвешиваем интересы: время чтения + факт дочтения topic_weights = {} for article in reading_history: topic = article.get('topic', 'general') time_weight = min(article.get('time_spent_sec', 30) / 180, 1.0) # Нормализуем на 3 мин completion_bonus = 0.5 if article.get('completed') else 0 weight = time_weight + completion_bonus topic_weights[topic] = topic_weights.get(topic, 0) + weight # Нормализация + затухание (старые интересы весят меньше) total = sum(topic_weights.values()) normalized = {t: w / total for t, w in topic_weights.items()} # Топ интересов для эмбеддинга профиля recent_titles = [a.get('title', '') for a in reading_history[-20:] if a.get('completed')] profile_embedding = None if recent_titles: profile_embedding = np.mean( self.encoder.encode(recent_titles, normalize_embeddings=True), axis=0 ) return { 'topics': normalized, 'top_interests': sorted(normalized.items(), key=lambda x: -x[1])[:5], 'profile_embedding': profile_embedding, 'is_cold_start': False, 'explicit_preferences': explicit_preferences or {} } def score_article(self, article: dict, user_profile: dict, seen_topics_last_hour: list[str]) -> dict: """Многофакторный скор статьи для конкретного пользователя""" topic = article.get('topic', 'general') topics = user_profile.get('topics', {}) # === Relevance === topic_score = topics.get(topic, 0.05) # Базовый интерес к теме # Семантическое сходство с профилем semantic_score = 0.5 # Дефолт для cold start profile_emb = user_profile.get('profile_embedding') if profile_emb is not None and article.get('embedding') is not None: semantic_score = float(cosine_similarity( profile_emb.reshape(1, -1), np.array(article['embedding']).reshape(1, -1) )[0, 0]) relevance = topic_score * 0.4 + semantic_score * 0.6 # === Freshness === hours_old = article.get('hours_since_published', 24) freshness = np.exp(-hours_old / 12) # Полупериод 12 часов # === Quality === quality_score = ( article.get('engagement_rate', 0.5) * 0.4 + article.get('source_trust_score', 0.7) * 0.3 + min(article.get('word_count', 500) / 800, 1.0) * 0.3 ) # === Diversity penalty === # Если тему уже видел недавно — снижаем скор topic_seen_count = seen_topics_last_hour.count(topic) diversity_penalty = 0.9 ** topic_seen_count # 0→1.0, 1→0.9, 2→0.81... # === Breaking news boost === breaking_boost = 1.5 if article.get('is_breaking') else 1.0 # === Итоговый скор === final_score = ( relevance * 0.40 + freshness * 0.25 + quality_score * 0.20 + 0.15 # Base noise для serendipity ) * diversity_penalty * breaking_boost return { 'article_id': article.get('id'), 'final_score': round(final_score, 4), 'relevance': round(relevance, 3), 'freshness': round(freshness, 3), 'quality': round(quality_score, 3), 'diversity_penalty': round(diversity_penalty, 3), } def rank_feed(self, articles: list[dict], user_profile: dict, max_items: int = 20, diversity_floor: float = 0.15) -> list[dict]: """ Финальное ранжирование фида с diversity constraint. diversity_floor: минимальная доля статей вне топ-3 тем пользователя. """ seen_topics = [] scored = [] for article in articles: score_data = self.score_article(article, user_profile, seen_topics) scored.append({**article, **score_data}) scored.sort(key=lambda x: -x['final_score']) # Применяем diversity: не более 3 статей подряд из одной темы result = [] topic_counts = {} max_per_topic = max(2, max_items // len(user_profile.get('topics', {'general': 1}))) for item in scored: if len(result) >= max_items: break topic = item.get('topic', 'general') if topic_counts.get(topic, 0) >= max_per_topic: continue result.append(item) topic_counts[topic] = topic_counts.get(topic, 0) + 1 seen_topics.append(topic) # Обеспечиваем минимум diversity: добавляем статьи из других тем if len(result) > 5: top_topics = set(list(topic_counts.keys())[:2]) non_top_in_result = sum(1 for item in result if item.get('topic') not in top_topics) diversity_actual = non_top_in_result / len(result) if diversity_actual < diversity_floor: # Вставляем статьи из неохваченных тем for item in scored[len(result):]: if item.get('topic') not in top_topics: result.insert(len(result) // 2, item) # Вставка в середину if sum(1 for i in result if i.get('topic') not in top_topics) / len(result) >= diversity_floor: break return result[:max_items] class EngagementTracker: """Отслеживание поведения читателя для обновления профиля""" def update_profile_from_session(self, user_profile: dict, session_events: list[dict]) -> dict: """Инкрементальное обновление профиля на основе сессии""" profile = user_profile.copy() topics = dict(profile.get('topics', {})) for event in session_events: topic = event.get('topic', 'general') action = event.get('action') value = event.get('value', 0) if action == 'completed_read': topics[topic] = topics.get(topic, 0) + 0.3 elif action == 'quick_skip': topics[topic] = max(0, topics.get(topic, 0) - 0.1) elif action == 'share': topics[topic] = topics.get(topic, 0) + 0.5 elif action == 'dislike': topics[topic] = max(0, topics.get(topic, 0) - 0.3) # Нормализация total = sum(topics.values()) if total > 0: profile['topics'] = {t: w / total for t, w in topics.items()} return profile 

Процесс работы

  1. Аналитика — аудит текущего фида, сбор данных (история чтения, события), определение бизнес-целей.
  2. Проектирование — выбираем архитектуру, конфигурацию векторизатора, метрики качества (NDCG, coverage).
  3. Реализация — пишем NewsPersonalizationEngine, EngagementTracker, API, интеграцию с вашим стеком.
  4. Тест — A/B-тест на 10% трафика, мониторинг p99 latency, сравнение с baseline.
  5. Деплой — развёртывание через Docker + Kubernetes, настройка CI/CD для частых обновлений модели.

Свяжитесь с нами для демонстрации работы алгоритма на ваших данных. Закажите предпроектное исследование — мы проанализируем ваш фид за 5 рабочих дней и предоставим roadmap.

Что входит в работу

  • Архитектурная документация (ML System Design Doc)
  • Обученная модель с весами и конфигами
  • REST API для ранжирования с авторизацией
  • Дашборд мониторинга (MLflow, Grafana)
  • Обучение вашей команды основам эксплуатации
  • Поддержка 3 месяца после внедрения

Сроки ориентировочно

  • MVP (базовый фид с профилем): от 2 месяцев
  • Полноценная система (с diversity, cold start, трекингом): от 4 до 6 месяцев
  • Стоимость рассчитывается индивидуально — зависит от объёма данных, требуемой скорости и сложности интеграции.

Наши инженеры имеют сертификаты PyTorch и AWS SageMaker, а опыт в AI/ML превышает 5 лет. Гарантируем стабильность модели в продакшене: SLA по latency p99 < 100ms. Получите консультацию: мы оценим ваш проект и предложим оптимальную архитектуру.