Статья о том, почему «лучшие хиты Queen» и «что-нибудь под пробежку» – это принципиально разные запросы, и что с этим делать ML-инженеру музыкального стриминга

Представьте: вы включаете умную колонку и говорите «поставь лучшие хиты Queen». Колонка думает секунду – и выдаёт вам подборку, в которой половина треков Queen куда-то вытеснена группами, которые вы ранее слушали. Допустим неплохими группами, но не Queen.

Именно такую проблему пришлось решать отделу ML Research музыкального сервиса Звук при разработке системы генерации плейлистов по текстовым запросам. И решение оказалось нетривиальным. Всем привет, у микрофона Федор Бузаев, DL инженер Звука, младший научный сотрудник НУЛ Lambda и аспирант ФКН ВШЭ.

Работа принята на EACL 2026 Workshop NLP4MusA. Авторы: Фёдор Бузаев, Ринат Муллахметов, Роман Богачёв, Илья Седунов, Олег Павлович, Камиль Мазитов, Дарья Пугачёва, Иван Сухарев (Zvuk, AIRI, НИУ ВШЭ, Иннополис).

Два разных мира в одной строке запроса

В прошлой части мы рассказали, как научили ГигаМикс понимать запросы вроде «что-нибудь тягучее и немного грустное для работы в воскресенье вечером» – построили семантический поиск, который ищет треки по смыслу, а не по ключевым словам. В финале Ринат обронил: следующий шаг – персонализация. Время выполнять обещание.

Когда пользователь пишет «музыка для утренней пробежки» – это один тип запроса. Персональный, субъективный. У одного человека пробежка под трэп, у другого – под симфонический метал. Рекомендательная система тут в своей стихии: знаешь историю прослушиваний – подбираешь идеально.

Но «музыка из Ла-Ла Ленда» – это совсем другое дело. Есть конкретный саундтрек, конкретные треки. Никакой персонализации здесь не нужно – нужна точность. Если система начнёт «улучшать» подборку треками из похожих фильмов только потому, что пользователь их раньше слушал, качество плейлиста упадёт.

Звучит очевидно? До этого исследования большинство систем либо персонализировали всё подряд, либо не персонализировали ничего. Промежуточного состояния не было. Традиционные гибриды с фиксированным весом смешения семантики и коллаборативной фильтрации перегибали в одну из сторон – либо нишевые треки лезли в подборку по «Queen», либо личные предпочтения игнорировались в настроенческих запросах.

Таксономия из 19 категорий: зачем столько?

Мы вручную разобрали тысячи реальных запросов пользователей из поисковой выдачи Звука и построили классификацию из 19 типов – с ограничением не более двух запросов от одного пользователя и полной анонимизацией логов. Вот что получилось на двух полюсах.

Запросы, которые должны персонализироваться:

  • Эмоциональное состояние – «когда тревожно», «для хорошего настроения»

  • Активность и спорт – «на вечернюю пробежку», «во время тренировки»

  • Работа и учёба – «для сосредоточенной работы», «фоном пока кодирую»

  • Домашняя обстановка – «готовлю ужин»

  • Романтический контекст – «свидание при свечах»

  • Фантазийные сценарии – «как будто я гном из Медной горы» (да, такие тоже бывают)

  • Жизненные ситуации – «мотивация перед важным выступлением»

  • Явная зависимость от вкуса – «инди-фолк под моё настроение»

  • Путешествия – «исследую Париж с женой»

Запросы, где персонализация вредит:

  • Конкретные исполнители и треки – «лучшие хиты Queen»

  • Саундтреки – «музыка из Ла-Ла Ленда»

  • Культурно-исторический контекст – «рождественские песни»

  • Чужая аудитория – «для дня рождения дочери-подростка»

  • Тренды – «треки из ТТ 2025»

  • Конкретные музыкальные характеристики – «техно-пиано с эмбиент текстурами»

  • Запросы «в стиле X» – «поп-хип-хоп как у Taylor Swift»

  • Публичные места – «фоном для ресторана»

  • Детский контент – «на детский праздник»

  • Бессмысленные строки – «ckfdf vthkje pfghtnbnm» (система должна уметь работать и с ними)

Разметили 5 тысяч реальных запросов вручную. Схема строгая: каждый запрос оценивали три музыкальных эксперта независимо, финальная метка – большинством голосов. Из этих 5 тысяч запросов 58% оказались каталожными и лишь 42% – персональными.

Среди персональных лидирует эмоциональное состояние – 22.4%. За ним домашняя обстановка (12.9%), жизненные ситуации (11.2%), активность и спорт (11.2%), путешествия (10.2%). Среди каталожных доминирует запрос конкретного исполнителя или жанра – 52.1%. Далее: запросы «в стиле X» (7.2%), культурно-исторический контекст (7.1%), конкретные музыкальные характеристики (6.2%) и бессмысленные строки – 5.3% (система должна уметь работать и с ними).

Как это работает технически

Система устроена в два этапа.

Сначала LLM-эмбеддер переводит текстовый запрос в векторное представление и по косинусному расстоянию находит в каталоге кандидатов – треки, чьи метаданные семантически близки к запросу. Метаданные трека включают title, artist, genres, mood, tempo, instruments, vocalness, language, release year и lyrics. Мы используем GigaEmbedder, дообученный с InfoNCE констрантивной функцией потерь (contrastive loss) на триплетах (anchor, positive, negative). Поиск кандидатов происходит по индексу ближайших соседей, возвращаем топ-N.

Затем подключается коллаборативная рекомендательная система – трансформерная модель, которая моделирует последовательные зависимости в истории взаимодействий пользователя (прослушивания, скипы, лайки) и выдаёт скор, отражающий соответствие трека долгосрочным предпочтениям.

Итоговый скор – перемножение двух оценок:

s_{final}(u, x, t) = s_{llm}(x, t) \cdot s_{cf}(u, t)^{\alpha(x)}

Три аргумента описывают контекст, в котором принимается решение. u – пользователь: история прослушиваний, скипы, лайки – всё, что знает о нём коллаборативная система. x – текстовый запрос, который он ввёл. t – конкретный трек-кандидат из каталога, которому считается скор:

Два компонента работают независимо. s_{\text{llm}}(x, t) оценивает, насколько трек семантически подходит под запрос. s_{\text{cf}}(u, t) – насколько он соответствует личному вкусу пользователя. 

Ключевой параметр – \alpha(x), «степень персонализации», которую определяет классификатор запросов. Если \alpha равна нулю – s_{\text{cf}}^0 = 1 для любого трека, остаётся чистая семантика. Если \alpha равна единице – коллаборативный сигнал работает в полную силу. Важно: классификатор смотрит только на текст запроса, не на историю пользователя – один и тот же человек получит разную степень персонализации в зависимости от того, что написал. Именно это делает подход динамическим: \alpha не фиксирована глобально, а вычисляется заново для каждого запроса.

Почему именно такая формула, а не взвешенная сумма? Оба компонента – независимые продакшн-системы с независимыми циклами релизов. Аддитивные формулировки требуют калибровки шкал. Обученные альтернативы – MLP, cross-attention – пришлось бы переобучать при каждом обновлении любого из компонентов. Экспоненциальная форма работает без обучаемых параметров, монотонна и устойчива.

Почему степень, а не коэффициент: α как температура ранкера

Здесь стоит посмотреть на формулу под другим углом – он раскрывает кое-что неочевидное.

Что если ранкер возвращает не произвольное число, а вероятностное распределение по трекам – то есть softmax, где сумма скоров по всем N кандидатам равна 1? Тогда возведение в степень \alpha – это буквально temperature scaling:

\text{softmax}(\text{logits} / T) \leftrightarrow s_{\text{cf}}^{\alpha}

При \alpha < 1 распределение сглаживается: персональные фавориты перестают доминировать, каталожные треки получают шанс. При  \alpha \to 0 персонализация исчезает совсем. При \alpha > 1 любимые треки ещё сильнее отрываются от остальных.

Иными словами, классификатор предсказывает не просто «персонализировать или нет», а температуру, с которой ранкер выражает свои предпочтения. Один параметр управляет тем, насколько система «слышит» пользователя – и насколько она «слышит» запрос.

Посмотрим на примере. Запрос: «Космическое путешествие с корешами» – смешанный: тут и личный вкус, и каталожные треки (саундтреки, эмбиент). Классификатор даёт  \alpha = 0.78.

До ранжирования. Исходный порядок треков (по убыванию s_llm)
До ранжирования. Исходный порядок треков (по убыванию s_llm)
После ранжирования. «Да» = трек попал в плейлист,  «Нет» = не попал
После ранжирования. «Да» = трек попал в плейлист,  «Нет» = не попал

Daft Punk и Star Wars попадают в плейлист несмотря на s_{\text{cf}} = 0.02–0.03 – пользователь их почти не слушал, но s_{\text{llm}} тянет их наверх. Кин-Дза-Дза и Vangelis не выживают – s_{\text{llm}} чуть ниже, и температура 0.78 уже не даёт им запаса. При \alpha = 0.5 в топ-10 прорвался бы Vangelis, но Кин-Дза-Дза всё равно нет – его вытесняет Kiasmos с более высоким s_{\text{cf}}. При \alpha = 1.0 Star Wars и Vangelis выпадают, но Daft Punk держится на 8-м месте за счёт относительно высокого s_{\text{cf}} среди каталожных треков.

Расширение запроса через лайки и дизлайки: красивая идея с ограниченной ��тдачей

Есть ещё один механизм, который можно подключить на этапе семантического поиска. Если у пользователя есть история явных лайков и дизлайков, их средние эмбеддинги добавляются к вектору запроса:

v_{\text{exp}}(u, x) = \frac{q \cdot v_{\text{query}} + p \cdot v_{\text{pos}} - n \cdot v_{\text{neg}}}{q + p - n}

Три веса: q – исходный запрос, p – понравившиеся треки, n – скрытые и отклонённые. Отрицательный знак перед v_{\text{neg}} буквально «отталкивает» вектор от нелюбимого контента в эмбеддинговом пространстве. Изящно.

На практике идея не оправдала ожиданий. В раунде 2 пользовательского исследования Embed500+RecSys без расширения одержал 75 побед (ELO 1443), а Embed500+Expand+RecSys с расширением – только 43 победы (ELO 1422). Embed+Expand в чистом виде – расширение без коллаборативного ранкера – оказался худшим из трёх: ELO 1372. Расширение запроса не только не помогает коллаборативному ранкеру, но незначительно ему мешает – и в любом случае существенно уступает динамической персонализации, которая в раунде 3 набрала ELO 1471.

Вероятная причина: когда коллаборативный ранкер уже хорошо знает предпочтения пользователя, добавление эмбеддингов лайков лишь дублирует сигнал. Для холодного старта, когда истории почти нет, механизм потенциально полезнее – но это в исследовании отдельно не проверялось.

Классификатор: чем проще, тем лучше в продакшне

Сравнили восемь предобученных трансформерных моделей с прицелом на русскоязычную базу. В пуле: RuModernBERT-base, sbert_large_nlu_ru, семейство Qwen3 (0.6B, 1.7B, 4B), Qwen2-0.5B, Falcon-H1-0.5B и Qwen3-Embedding-0.6B.

Стратегия обучения зависела от архитектуры. Энкодерные модели (sbert, RuModernBERT) дообучались с частичным размораживанием слоёв. Декодерные и эмбеддинговые (Qwen, Falcon) использовались как замороженные экстракторы – обучалась только линейная голова поверх среднего пулинга последнего слоя. Данные разбиты 70/20/10, оптимизатор AdamW с взвешенной кросс-энтропией (обратно-частотные веса классов), 100 эпох, максимальная длина 128 токенов.

Победил RuModernBERT-base: точность 96.9%, задержка 17 мс, overhead в пайплайне ~3 мс. Архитектурные оптимизации (чередующееся локально-глобальное внимание, sequence unpadding, Rotary Position Embeddings) ориентированы на длинные контексты, что создаёт небольшой overhead на коротких запросах в 128 токенов. Классический BERT-large на таких длинах работает на 37% быстрее – но в требования продакшна RuModernBERT всё равно укладывается.

Интересный компромисс: sbert_large_nlu_ru работает в 1.6 раза быстрее (10.8 мс) и выдаёт почти вдвое больше запросов в секунду – при точности на 1.1% ниже. Для очень высоконагруженного сервиса это может быть обоснованным выбором.

Пользовательское исследование: 63 человека, 856 сравнений

Слепое исследование с попарными сравнениями плейлистов. Каждый участник вводил собственный запрос на родном языке, видел два анонимных плейлиста и выбирал лучший – или ставил равенство. Интерфейс не раскрывал, какая модель что сгенерировала. Рейтинг агрегировался по схеме ELO, различия между моделями проверялись критерием Манна-Уитни.

Исследование разбито на три раунда:

Раунд 1 (199 сравнений): что важнее – размер кандидатного пула или коллаборативный сигнал? Embed500+RecSys – 66 побед, ELO 1465. Embed200+RecSys – 40 побед, ELO 1454. Чистый эмбеддер – 31 победа, ELO 1384. Обе системы с коллаборативным ранкером статистически значимо превосходят чистый эмбеддер (p = 0.004 и p ≈ 0 по критерию Манна-Уитни). А вот между собой Embed500 и Embed200 неразличимы – p = 0.058, 56% ничьих. Вывод: прирост даёт коллаборативный сигнал, а не размер пула кандидатов. Тем не менее в продакшне мы взяли 500: после retrieval часть треков отсеивается по explicit-фильтрам, языку и доступности в регионе – с пулом в 200 после фильтрации остаётся меньше кандидатов для ранжирования, что критично для редких запросов.

Раунд 2 (381 сравнение): помогает ли query expansion? Embed500+RecSys – 75 побед, ELO 1443. Embed500+Expand+RecSys – 43 победы, ELO 1422. Embed+Expand без коллаборативного ранкера – 62 победы, ELO 1372. Обе системы с RecSys значимо лучше чистого расширения без ранкера (p = 0.033 и p = 0.046). Но между собой Embed500+RecSys и Embed500+Expand+RecSys неразличимы – p = 0.207, 56% ничьих. Query expansion не даёт измеримого прироста поверх коллаборативного ранкера, зато добавляет сложности. В третий раунд пошёл Embed500+RecSys – по принципу бритвы Оккама: если два решения неотличимы по качеству, выбирают то, что проще в поддержке и дешевле в инференсе.

Раунд 3 (276 сравнений): финальное сравнение – динамическая персонализация против кросс-энкодера. DynPers – это полная система из предыдущих разделов: классификатор на основе RuModernBERT предсказывает \alpha(x) для каждого запроса, итоговый скор считается по формуле s_{\text{final}} = s_{\text{llm}} \cdot s_{\text{cf}}^{\alpha(x)}. CrossEnc – альтернативный подход: кросс-энкодер напрямую оценивает пару (запрос, трек) и выдаёт бинарное суждение – подходит трек или нет. Никакого \alpha, никакой адаптации под тип запроса.

DynPers – 114 побед (41.3%), CrossEnc – 55 (19.9%), 107 ничьих. ELO-разрыв – 217 очков (1471 против 1254), p < 0.01. На каталожных запросах обе системы близки – кросс-энкодер справляется, когда нужно просто проверить соответствие. На персональных DynPers выигрывает за счёт точного \alpha. Наибольший разрыв – на пограничных, смешанных запросах: именно там бинарное «подходит / не подходит» не работает, а непрерывный регулятор попадает точнее.

Что в итоге

Главный инсайт звучит просто: персонализация – это не переключатель «вкл/выкл», а непрерывный регулятор. И правильная позиция этого регулятора зависит от того, что именно спрашивает пользователь.

Технически это реализуется через лёгкий классификатор (17 мс, ~3 мс накладных расходов в пайплайне), который предсказывает «насколько личным» является запрос. Предсказанная вероятность напрямую становится температурой, с которой коллаборативный ранкер выражает свои предпочтения.

Просто, дёшево в поддержке и работает. Редкое сочетание.

А/Б тест подтвердил это в продакшне: тестовая группа показала статистически значимый лифт +3.0% по метрике «среднее количество активных дней на пользователя» за период теста. Лифт достиг минимального детектируемого эффекта, заложенного в предварительном анализе мощности.

Но есть кое-что поважнее технической элегантности. Этот подход честнее по отношению к пользователю. Когда человек просит «лучшие хиты Queen» – он не просит систему угадывать его настроение. Он просит Queen. Хорошая рекомендательная система должна уметь не только персонализировать, но и знать, когда этого делать не надо.

Следующий логичный шаг – персонализация, которая учитывает не только тип запроса, но и контекст момента. Один и тот же «что-нибудь для пробежки» в семь утра в по��едельник и в пятницу вечером – это, строго говоря, разные запросы. Пока системы этого не различают. Но уже различают, что Queen – это не настроение. 

Эта система – результат работы Рината, Дарьи, Ильи, Олега, Камиля, Романа, Анастасии и Ивана. Но не только. Спасибо команде музыкальных экспертов, которые составили 500 запросов для финальной оценки и честно говорили нам, когда результаты были плохими. И отдельно – 63 участникам пользовательского исследования, которые потратили своё время на то, чтобы мы могли написать «p < 0.01».