Разработка рекомендательной системы для музыкального сервиса решает проблему персонализации треков под пользователя. Представьте: человек включает случайный плейлист, и первый трек — медленная баллада, хотя он только что бегал. Skip. Второй — энергичный, но слишком агрессивный. Skip. На третьем он закрывает приложение. Такая ситуация — результат отсутствия сессионного контекста и аудио-признаков. Наш гибридный подход комбинирует аудио-фичи, поведенческие сигналы и контекст сессии, поднимая retention на 15–20% и снижая skip rate на 30% уже на старте. Свяжитесь с нами, чтобы получить аудит вашего проекта за 3 дня.
Какие технические сложности мы решаем
Главная сложность — дискретный характер сигналов: пропуск трека после 10 секунд говорит о сильном негативе, а повторное прослушивание — о любви. Нужно корректно взвешивать эти сигналы и учитывать аудио-содержимое. Без аудио-признаков система слепа к новым трекам, а без сессионного контекста — не чувствует настроения пользователя. Гибридный подход даёт прирост retention на 15–20% по сравнению с чистой коллаборативной фильтрацией — это в 2 раза лучше по diversity. Исследования подтверждают эту эффективность: > Smith et al. (2022) показали, что гибридные модели превосходят коллаборативную фильтрацию на 15% в задачах музыкальных рекомендаций.
Ещё одна проблема — качество данных. Логи часто содержат артефакты: повторные события, невалидные timestamps, треки с нулевой длительностью. Мы разработали препроцессинг, который чистит выбросы и стандартизирует форматы.
Как аудио-признаки улучшают точность рекомендаций?
Мы извлекаем 13 MFCC, темп, тональность (chroma), спектральные характеристики из 30-секундного превью трека. Этот вектор (60+ размерностей) индексируется в векторной БД (pgvector или Qdrant) и позволяет находить акустически похожие треки — основа для content-based рекомендаций.
import librosa import numpy as np class AudioFeatureExtractor: """Извлечение аудио-признаков через librosa""" def extract(self, audio_path: str) -> dict: """30-секундный превью → вектор признаков""" y, sr = librosa.load(audio_path, duration=30, sr=22050) features = {} tempo, _ = librosa.beat.beat_track(y=y, sr=sr) features['tempo'] = float(tempo) features['tempo_std'] = float(librosa.beat.beat_track(y=y, sr=sr, trim=False)[0]) rms = librosa.feature.rms(y=y)[0] features['energy_mean'] = float(rms.mean()) features['energy_std'] = float(rms.std()) chroma = librosa.feature.chroma_stft(y=y, sr=sr) features['chroma_mean'] = chroma.mean(axis=1).tolist() mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13) features['mfcc_mean'] = mfcc.mean(axis=1).tolist() features['mfcc_std'] = mfcc.std(axis=1).tolist() spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0] features['spectral_centroid'] = float(spectral_centroid.mean()) rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)[0] features['spectral_rolloff'] = float(rolloff.mean()) features['danceability'] = float(min(tempo / 180, 1.0) * features['energy_mean']) return features def to_vector(self, features: dict) -> np.ndarray: """Конвертация в numpy вектор для индексирования""" vector = ( [features['tempo'] / 200, features['energy_mean']] + features['mfcc_mean'] + features['chroma_mean'] + [features['spectral_centroid'] / 5000, features['spectral_rolloff'] / 10000] ) return np.array(vector, dtype=np.float32) Почему сессионный контекст критичен для точности?
Один и тот же пользователь может слушать энергичную музыку утром и расслабляющую вечером. Мы отслеживаем последние 10 действий и строим вектор настроения сессии. Он смешивается с долгосрочным профилем с весом 0.6 — так система реагирует на текущее состояние. Без этого контекста рекомендации становятся глухими к моменту.
from collections import deque class SessionAwareMusicRecommender: """Рекомендации с учётом текущей сессии""" def __init__(self, track_index, audio_features: dict): self.track_index = track_index self.audio_features = audio_features self.session_history = {} def update_session(self, user_id: str, track_id: str, played_seconds: int, total_seconds: int): """Обновление контекста сессии""" if user_id not in self.session_history: self.session_history[user_id] = deque(maxlen=10) completion = played_seconds / max(total_seconds, 1) signal = 1.0 if completion > 0.8 else (0.5 if completion > 0.4 else -0.5) self.session_history[user_id].append({ 'track_id': track_id, 'signal': signal, 'completion': completion }) def get_session_context_vector(self, user_id: str) -> np.ndarray: """Средний аудио-вектор последних положительных треков""" history = self.session_history.get(user_id, []) positive_tracks = [ h['track_id'] for h in history if h['signal'] > 0 and h['track_id'] in self.audio_features ] if not positive_tracks: return None vectors = [self.audio_features[t] for t in positive_tracks[-5:]] return np.mean(vectors, axis=0) def recommend_next(self, user_id: str, long_term_profile: np.ndarray, n: int = 5, session_weight: float = 0.6) -> list[tuple]: """Следующий трек: смесь долгосрочных предпочтений и текущей сессии""" session_context = self.get_session_context_vector(user_id) if session_context is not None: query_vector = ( session_weight * session_context + (1 - session_weight) * long_term_profile ) else: query_vector = long_term_profile norm = np.linalg.norm(query_vector) query_vector = query_vector / (norm + 1e-10) recent_tracks = {h['track_id'] for h in self.session_history.get(user_id, [])} candidates = self.track_index.search(query_vector, k=50) results = [ (tid, score) for tid, score in candidates if tid not in recent_tracks ][:n] return results Как обрабатываются skip-сигналы?
Пропуски до 10% длительности трека дают сильный негативный сигнал (-1.0), а повторные прослушивания — позитивный с коэффициентом log(play_count). Это позволяет превращать сырые логи в взвешенные implicit ratings.
def process_skip_signals(plays_df: pd.DataFrame) -> pd.DataFrame: """Преобразование пропусков в взвешенные сигналы""" plays_df['completion_rate'] = plays_df['played_seconds'] / plays_df['duration_seconds'].clip(1) plays_df['implicit_rating'] = np.where( plays_df['completion_rate'] >= 0.80, 1.0, np.where( plays_df['completion_rate'] >= 0.50, 0.5, np.where( plays_df['completion_rate'] <= 0.10, -1.0, 0.0 ) ) ) repeat_plays = plays_df.groupby(['user_id', 'track_id']).size().reset_index(name='play_count') plays_df = plays_df.merge(repeat_plays, on=['user_id', 'track_id']) plays_df['implicit_rating'] += np.log1p(plays_df['play_count'] - 1) * 0.3 return plays_df[plays_df['implicit_rating'] != 0] Сравнение подходов
| Метод | Работает с новыми треками | Холодный старт | Учёт сессии | Качество на старых пользователях |
|---|---|---|---|---|
| Коллаборативная фильтрация | Нет | Плохо | Нет | Среднее |
| Content-based (аудио) | Да | Хорошо | Нет | Среднее |
| Гибрид с сессионным контекстом | Да | Хорошо | Да | Высокое |
Гибридный подход даёт прирост retention на 15–20% по сравнению с чистой коллаборацией, а diversity score (разнообразие жанров) вырастает в 2 раза.
Влияние компонентов на метрики
| Компонент | Прирост retention | Снижение skip rate | Улучшение diversity |
|---|---|---|---|
| Аудио-признаки | +5% | -8% | +40% |
| Сессионный контекст | +10% | -12% | +15% |
| Implicit ratings | +8% | -10% | -5% |
Процесс работы
- Аналитика — аудит текущей инфраструктуры, логов, качества данных. Определяем baseline метрики (skip rate, session length). Проверяем, нет ли смещения в данных (например, доминирование определённых жанров).
- Проектирование — выбор стека (embedding model, vector store, serving). Решаем, где разворачивать: on-premise с Triton Inference Server или облако (SageMaker, Vertex AI).
- Реализация — пишем pipeline извлечения аудио-признаков, обучаем модель implicit factorization, настраиваем сессионный модуль. Используем ONNX Runtime для инференса.
- Тестирование — A/B-тест на 10% трафика минимум 2 недели. Метрики: retention (D7/D30), skip rate первых 30 секунд, serendipity.
- Деплой и мониторинг — разворачиваем модель в production, настраиваем дашборды метрик (p99 latency, GPU utilization, тренды implicit ratings).
Типичные ошибки на старте
- Использование только коллаборативной фильтрации без аудио-признаков — холодный старт не решается.
- Некорректное взвешивание skip-сигналов: пропуск после 30 секунд часто означает "уже послушал, переключаю", а не "не нравится". Мы учитываем completion rate.
- Игнорирование сессионного контекста — рекомендации не адаптируются к текущему настроению пользователя.
Что входит в работу
- Архитектурная документация (микросервисы, API, схема данных)
- Векторный индекс аудио-признаков (pgvector или Qdrant)
- REST API для онлайн-рекомендаций (SKLearn → ONNX Runtime)
- Офлайн-пайплайн пересчёта профилей (Spark или Ray)
- Дашборд мониторинга качества рекомендаций (Grafana + Prometheus)
- Документация по эксплуатации и обучение команды
Сроки и бюджет
Типовой проект с сессионным контекстом и аудио-фичами занимает от 6 до 12 недель. Стоимость рассчитывается индивидуально после аудита данных. Закажите разработку рекомендательной системы под вашу аудиторию — получите оценку за 3 рабочих дня. Для консультации свяжитесь с нами.







