Разработка рекомендательной системы для музыкального сервиса решает проблему персонализации треков под пользователя. Представьте: человек включает случайный плейлист, и первый трек — медленная баллада, хотя он только что бегал. 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 рабочих дня. Для консультации свяжитесь с нами.







