- BrainTools - https://www.braintools.ru -

NEWAVE. Делаем интеллектуальный ретривал музыки

Представьте: лежит условный Владик на диване, вайбует, хочет музычку послушать. Открывает «Мою Волну». Нажимает «Плей». Играет не то. Он нажимает «Настроить». Решает, какой активностью он занимается, скроллит, выбирает. Дальше решает, какое у него настроение – спокойное или грустное. Следующий фильтр: «Хм, а я хочу незнакомую музыку послушать или популярную? А если вся популярная музыка – незнакомая? То что тогда?..» – круговорот мыслей выбивает вайб с двух ног. Вот бы можно было просто лечь, нажать на кнопочку, и сказать «А накати ка мне чилловое что-то, но чтобы не заснуть. Что-то с гитарками может. Можно вообще инди какой-нибудь, или типа альт-рока. Или даже мягкую электронику.»

Так давайте поможем Владику!

Спотифай и Яндекс Музыка для поиска используют коллаборативную фильтрацию и метаданные: всё «вокруг» контента – исполнителя, альбом, похожих пользователей – но не сам контент. Давайте вместо этих метаданных смотреть исключительно на сам аудио-файл и текст песни.

Нашему Владику хочется, чтобы когда он что-то наговорил, алгоритм предложил ему песни, отвечающие его запросу (текст) и по звучанию (музыка), и по смыслу (тоже текст). Если закодировать все три эти составляющие в векторы некоторого одного смыслового пространства, то их можно сравнивать между собой и находить самые похожие. Нам не понадобится искать нотные записи для каждой песни, чтобы научиться понимать её тональность и ритм – мы хотим научиться понимать всё это исключительно по самому звучанию, хоть и неявно. Это называется Representation Learning.

Что у нас по Prior Work

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

Чтобы сводить разные сущности – будь то аудио и текст или тексты разной природы, – придумали Two‑Tower архитектуры. Идея простая: у нас есть два отдельных энкодера, каждый кодирует свой тип данных, а дальше модель учится «притягивать» их векторы друг к другу в общем пространстве.

Про аудио модальность

Одна из первых таких “двухбашенных” работ для аудио – MuLan от Google. Там аудио сначала переводят в мел‑спектрограмму (такую картинку, показывающую все аудиочастоты) и прогоняют через ViT, а текстовое описание кодируют BERT’ом – модель учится связывать то, как трек звучит, с тем, как его описывают словами. Позже появилась CLAP (CLIP, только для аудио) – тот же Two-Tower, но звук там кодируется иерархическим трансформером HTS-AT. По той же логике [1] работает и другая известная модель TTMR, но она больше выровнена под короткие текстовые запросы.

Итак, с пониманием аудио разобрались. Но сейчас мы не читаем текст песни. Существуют же треки типа «Pumped Up Kicks» от «Foster the People» c текстом о суициде, но из-за весёлого звучания наша система смело выдала бы её в подборку по запросу «happy songs.»

Про текстовую модальность

Для сравнения текстов есть два основных подхода:

  1. Cross-Encoder. Здесь мы склеиваем два текста в один и проходимся Self-Attention’ом по всей последовательности, строя большую матрицу внимания [2]. Так мы можем вывести точное отношение каждого токена к каждому токену, максимально детально соотнести запрос с текстом. Но это чудовищно медленно для поиска: даже для обработки одного запроса нам бы нужно было прогнать всю эту нейросеть, весь этот тяжелый трансформер, по каждому токену обоих текстов – это буквально сотни раз на каждую из миллионов треков, что невероятно долго.

  2. Bi-Encoder работает куда быстрее. Это тот же Two-Tower, как и CLAP, но энкодеры одинаковые. Во-первых, мы проходимся по запросу один раз, а во-вторых мы можем заранее векторизовать все тексты песен и потом просто через косинусное сходство найти самый подходящий запросу текст – это делается очень быстро в векторных БД.

Очевидно, выбираем второй вариант.

Учимся понимать звучание песен

Архитектура и тренировочный процесс CLAP

Архитектура и тренировочный процесс CLAP

Для сведения музыки и её описаний выбираем модель CLAP, любезно предобученную на множестве разных звуков и выложенную в опен-сорс ресёрч-лабой LAION AI.

Тренировали модель с помощью InfoNCE лосса – базы в контрастивном обучении [3]: модели показывают пары входных данных (Звук лая собаки, Текст «Dog barking»), она выдаёт два соответствующих вектора. Далее мы считаем насколько они похожи. Для правильных пар данных мы склоняем модель выдавать как можно более похожие векторы, для неправильных – как можно более далёкие друг от друга. Contrastive Learning 101.

Таким же образом мы будем тренировать каждую следующую модель.

Domain Adaptation

Из особенностей: этот предобученный CLAP видел отрывки звуков только по 10 секунд. Первым шагом будем знакомить модель с 10-секундными отрывками песен. Это нужно было для так называемого domain adaptation – приноровить изначальную модель к определённому домену данных, с которым она будет работать потом. По сути, это просто дообучение. К тому же, уже существует идеальный для этого датасет: Google MusicCaps имеет больше пяти тысяч пар 10-секундных отрывков песен и их описаний.

Тут всплывает первая особенность работы с аудио датасетами: в самих csv табличках этого аудио как-бы нет. Его нужно скачивать, как правило, через Ютуб. Короче, несколько часов насилуем VPN, чтобы скачать соответствующие песни, около 5% которых не скачались из-за отсутствия файлов или прав доступа к ним.

Обучаем на десяти эпохах: метрики сошлись очень хорошо и плавно, точность – больше 99%. Правда, модель настолько хорошо предобучена, что уже на самой первой эпохе модель выбила 98%… Зато мы теперь точно знаем, что модель приспособлена к работе с песнями.

Context Expansion

Теперь нужно расширить контекст CLAP с десяти секунд до хотя бы четырёх минут, чтобы модель всё-таки понимала песни целиком. То же самое нужно сделать и с контекстным окном текстового энкодера – по умолчанию оно ограничивается 77 токенами.

Появились две гипотезы как это сделать:

  1. Взять и просто нарезать каждую песню на отрывки по 10 секунд, закодировать их моделью и через mean pooling или какой-нибудь weighted average собрать в один вектор – так как раз делали в статье про TTMR.

  2. Попробовать расширить контекстное окно модели, обойдя «заводские ограничения».

Первое работало бы в десятки раз дольше в следствие необходимости кодировать множество отрывков, к тому же mean pooling размывал бы эти векторы из разных частей песни, по итогу не представляя ни одну из них. Интереснее посмотреть, сможет ли вообще «рожденный ползать CLAP, взлететь»?

Во время тренировки мы расширяем контекстное окно как аудио, так и текстового входа. Для аудио мы наращивали длительность контекста до 240 секунд, для текста – вплоть до 512 токенов. Наращиваем контекст линейно, раз в несколько батчей увеличивая контекстную длину аудио на одну секунду и количество токенов текста на один. Для этого нам пришлось убрать программное обрезание контекста nb_max_samples у feature extractor, переписать некоторые функции, и добавить интерполяцию позиционных эмбеддингов.

Positional embeddings interpolation illustration. Сверху: аудио, снизу: текстовая модальность. Светло-зелёные – это как раз интерполированные эмбеддинги

Positional embeddings interpolation illustration. Сверху: аудио, снизу: текстовая модальность. Светло-зелёные – это как раз интерполированные эмбеддинги

Поподробнее про последнее: и у аудио, и у текста, как полагается любой sequence модели, есть эмбеддинги, помогающие понимать позицию элементов последовательности. У нас они выглядят как простые векторы, которые мы прибавляем к основным эмбеддингам. Мы интерполируем их линейной одномерной интерполяцией для текста, и бикубической – для аудио, так как в аудио мы работаем с, по сути, двумерным изображением мел-спектрограммы. BERT и ViT просто наращивают количество токенов в контексте, а позиционные эмбеддинги просто интерполируются вместо переобучения. Зачем так делать? Новые позиции эмбеддингов приходилось бы инициализировать с нуля, что приводило бы к большим нестабильностям в обучении. А так мы хитрим и просто «натягиваем» узкое внимание модели на длинные последовательности.

Для обучения на аудио такой длины берём Jamendo CC Catalog: около 50-ти тысяч треков под лицензией Creative Commons.

Изначальный план был прост: streaming=True с Hugging Face, и нет забот. Однако по какой-то причине соединение с удалённым кластером работало крайне нестабильно и мы упирались в тайм-ауты. «Не беда» – подумали мы – «скачаем датасет прям на сервер», но и тут столкнулись с ограничением нашего университетского датахаба по памяти [4] – терабайт данных влезать не хотел (ещё бы). Даже при частичной загрузке Hugging Face во время обучения распаковывал данные, по сути создавая их вторую копию, что захломляло память очень быстро.

В итоге мы поняли что эту загрузку и удаление копий нужно автоматизировать и сделали свой псевдо-стриминг: скачивали один parquet-шард, распаковывали, прогоняли на нём обучение с размером батча, кратным количеству данных в шарде, удаляли и брали следующий. Иногда отщипывали данных от шарда для валидации. Таким образом проходили датасет последовательно и не забивали диск. За целый день на одной A100 успели пройти порядка 10% датасета – этого хватило, чтобы довести контекст до нужной длины и адаптироваться к новым входам.

Обучение шумное, при этом в среднем и тренировочный, и валидационный лосс убывают, а точность растёт – просто без идеальной монотонности.

Датасеты, которые мы использовали, часть 1

Датасеты, которые мы использовали, часть 1

Тестирование

Помимо Jamendo используем ещё один датасет – Song Describer Dataset, или SDD: небольшую, вручную курируемую коллекцию очень качественных описаний песен. Судя по всему, ценим его не только мы, но и взрослые ребята, так как на нём сравниваются подходы в статьях, например, в CLaMP 3. Мы тоже на нём ничего не обучаем – только валидируем модели как в процессе обучения, так и в конце.  Это нужно для того, чтобы так же понимать и робастность моделей к данным вне распределения: описания музыки в MusicCaps, в Jamendo и в реальном мире – это три стилистически разных набора текстов, поэтому видеть прогресс и на стороннем датасете важно для нашей задачи. 

Что приятно, во время расширения контекста точность продолжает подниматься даже на выборке из SDD.

Для финальной оценки качества опираемся на стандартные метрики оценки поисковых систем:

  1. Recall@K – это доля запросов, для которых релевантный трек оказался среди первых K результатов выдачи. Отвечает на простой вопрос: есть ли вообще шанс, что пользователь увидит подходящую песню, если он пролистает первые несколько результатов.

  2. MRR (Mean Reciprocal Rank) определяется как среднее значение обратного ранга (степени релевантности) первого релевантного результата. Эта метрика особенно чувствительна к верхним позициям списка и хорошо отражает пользовательское ощущение качества поиска: чем раньше в выдаче появляется первый подходящий трек, тем лучше система воспринимается.

  3. Precision@K – это доля релевантных треков среди первых K найденных. В прикладном смысле она показывает, насколько чистая и не зашумлённая выдача в начале списка и не приходится ли пользователю пробираться через нерелевантные результаты.

Все метрики нужны, все метрики важны, но на Precision@K мы обращаем меньше внимания: в статьях, с которыми мы сравниваемся, чаще публикуют MRR и Recall@10, и нам важно держать общую шкалу. Мы сравниваемся с результатами статьи про CLaMP 3 2025-го года.

На SDD наша long‑context версия CLAP показывает уверенный результат по Recall@10 на уровне 96% от SOTA решения и на 99% от бейзлайна (обычного CLAP) по MRR  – то есть мы уже не сильно отстаём от взрослых дядь.

Учимся понимать слова песен

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

Архитектура

Архитектура и тренировочный процесс Bi-Encoder

Архитектура и тренировочный процесс Bi-Encoder

Тратим некоторое время на эксперименты и выбор оптимальной архитектуры энкодера.

Изначально мы взяли Longformer, рассчитывая сразу закрыть проблему длинных текстов за счёт увеличенного контекстного окна. На практике модель оказалась слишком тяжёлой: обучение шло крайне медленно и плохо масштабировалось на наших ограниченных ресурсах.

Потом глаз упал на легендарную all-MiniLM-L6-v2 от Microsoft. Она небольшая и её часто используют именно для задач текстового ретривала. Несмотря на хорошую скорость и стабильность обучения, у неё обнаружилось критическое ограничение – очень маленькое контекстное окно. Существенная часть текста просто не попадала в модель, а попытки обойти это ограничение с помощью прохода по всему тексту и последующего average pooling не дали заметного прироста качества.

Поэтому мы поменяли её на не менее популярную BGE-M3. Она уже могла вместить в себя 8К токенов и эффективно запоминать весь этот контекст, что по итогу улучшило метрики без выхода за рамки доступных ресурсов. Важным оказалось и то, что BGE-M3 архитектурно заточена именно под dense retrieval на длинных документах. Эта модель специально тренировалась сопоставлять короткие вопросы с развернутыми ответами. Для нас это идеальное попадание: ведь пользовательский запрос – это всегда короткая, сжатая мысль, а текст песни – уже длинная, размазанная по куплетам история.

Обучение

Для обучения такой системы нам нужен датасет из двух частей: (1) тексты песен и (2) человеческие описания смысла/интерпретации. Поэтому объединяем два источника. Первый – Music4All, в котором у каждого трека есть базовые метаданные и «псевдо кэпшены» – сгенерированные LLM описания треков. Второй – Song Interpretation Dataset (SID), содержит лирику и пользовательские интерпретации и при этом уже привязан к Music4All через ID. Через этот айдишник мы совмещаем два датасета и получаем пары из исходного текста песни, и составного описания pseudo captions + interpretations.

Снова внедряем негативы в обучение. Сначала мы пошли по классическому пути и использовали in-batch negatives – это когда негативными примерами для текущего трека считаются просто все остальные треки в батче. Это оказалось слишком просто для модели: если целевой трек и случайный негатив слишком сильно различаются, энкодеру достаточно уловить общую стилистику слов, чтобы развести их по углам. Лосс падал красиво и быстро, но реального прироста в качестве поиска это не давало, ведь модель учила только поверхностные паттерны.

Чтобы сломать эту ленивую стратегию, внедряем майнинг жёстких негативов, hard negatives по-английски. Используя метаданные из Music4All, специально подаём модели неудобные примеры: треки того же жанра, с похожими тегами или даже того же исполнителя, но с другим текстом. Это заставило модель перестать читерить на очевидных различиях и начать реально вчитываться в семантику.

Обучаем итоговую штуку с достаточно большим размером батча, чтобы стабилизировать обучение, и видим достаточно монотонно опускающийся лосс, как тренировочный, так и валидационный, и за 25 долгих эпох доходим до конкурентоспособных метрик: Recall и MRR (на обычной тестовой выборке) получились практически идентичные тем, что у CLAP!

Fusion модель

Имея две сильные модели, возникает вопрос: как их объединить? Можно было бы вообще изначально попытаться объединить все три модальности в одно пространство изначально – это называется early fusion – но для это пришлось бы обучать эти энкодеры с самого начала, куда дольше подбирать данные, а результат, несмотря на красоту решения, мог бы и не оправдать затрат. Не зря сейчас в мире так мало по-настоящему мультимодальных моделей.

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

Архитектура

Архитектура и тренировочный процесс Fusion MLP

Архитектура и тренировочный процесс Fusion MLP

Идём самым практичным путём: учим небольшой MLP мэпить разные представления в одно векторное пространство только в конце пайплайна. Это дешевле, проще, и называется late fusion.

Для песен считаем два эмбеддинга:

аудио трека → CLAP audio encoder → аудио-эмбеддинг

слова трека → bi-encoder → текстовый эмбеддинг

Конкатенируем, прогоняем через трёхслойный перцептрон и нормализуем – получаем один заветный эмбеддинг трека.

Для запроса делаем то же самое, только обе ветки получают один и тот же текст запроса:

текст запроса → CLAP text encoder → уже текстовый эмбеддинг

текст запроса → bi-encoder → тоже текстовый эмбеддинг

Дальше тот же late fusion, и получаем эмбеддинг запроса.

Сравнение этих двух сущностей становится совсем простым: для нормализованных векторов это cosine similarity.

Данные

Датасеты, которые мы использовали, часть 2

Датасеты, которые мы использовали, часть 2

Для обучения MLP нам нужны полноценные тройки: аудио, слова песни и текстовое описание – готового датасета в таком виде не существует.

Поэтому собираем его сами. Берём аудио из FMA (Free Music Archive), потому что там есть полные треки, а тексты и пользовательские интерпретации – из SID. Дальше нужно аккуратно сопоставить всё это между собой.

Используем fuzzy matching строк. Сначала стараемся искать совпадения в названиях только среди треков того же исполнителя – это и быстрее, и заметно снижает число ложных матчей. Если исполнитель не указан или качество совпадения оказывается ниже порога, мы падаем в глобальный поиск по строке вида “artist – title”. Похожесть оцениваем по расстоянию Левенштейна и просто отбрасываем все сомнительные пары.

Тот самый Fuzzy Matching

Тот самый Fuzzy Matching

Результаты

График оценки MRR метрики

График оценки MRR метрики
График оценки метрики Recall на первых десяти результатах

График оценки метрики Recall на первых десяти результатах

Late Fusion позволяет нам поднять метрики на SDD чуть ли не в два раза, заметно обгоняя SOTA: из первых десяти треков выдачи абсолютно соответствующими оказываются около половины, что кажется со стороны немного, но впечатляюще для систем такого рода.

Сбор демо

Мы не остановились на коде в ноутбуках и построили полноценное приложение.

Принцип работы демки

Принцип работы демки

Для начала – сервер. Написали небольшой backend на FastAPI и подняли базу данных на SQLite. В ней лежала таблица с путями до аудиофайлов песен, их текстами, путями до файлов обложек, разными метаданными, а также двумя векторами в BLOB‑формате: текстовым и музыкальным. Когда от клиента прилетает запрос, backend идёт в FAISS, делает поиск по векторной базе и находит 20 самых релевантных треков с помощью Fusion‑модели. Эти треки стримятся на в виде JSON, так что уже через секунду клиенту прилетает первая песня, которую можно сразу включить.

Но что за песни хранятся в базе? Ещё один датасет! 🤤🤤🤤 LyricCovers 2.0. Изначально это академический датасет для задачи cover song identification: список в духе «трек А – это кавер на трек Б».

Мы выбрали 1000 записей из LyricCovers 2.0 с упором на разнообразие (язык, декада, тип YouTube‑видео, cover vs original), дедуплицировали их и заранее отфильтровали строки с неполными метаданными.

Дальше для каждой записи скачали аудио по уже имеющемуся youtube_url через yt-dlp, обложку брали как YouTube‑thumbnail по video_id, а лирику вытаскивали из поля датасета. Если там оказывалось пусто, текст искали по ссылке на сервис Genius. Так и насобирали тысячу качественных и, что важно (нет), легальных для демонстрации песен.

Клиент – iOS‑приложение на SwiftUI, которое мы писали вместе с GLM 4.6 и Codex. Примерно 2 часа ушло на работу с GLM, потом ещё всего каких-то 6 часов – на дебаг с Codex, включая час попыток объяснить ему, что такое Liquid Glass и что, блин, да, «iOS 26» действительно существует. Тем не менее, за одну ночь у нас получился функциональный и невероятно красивый прототип. В нём даже не нужно набирать запрос – Apple SpeechRecognition Framework позволил легко сделать распознавание голоса, чтобы всё ощущалось ещё футуристичнее.

Так выглядит клиент

Так выглядит клиент

 В работе принимали участие:

  • Влад Калиниченко @vladotpad [5]

  • Полина Коробейникова @poinkaa

  • Жанна Иванова @i_jeannee

Автор: vladotpad

Источник [6]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/25067

URLs in this post:

[1] логике: http://www.braintools.ru/article/7640

[2] внимания: http://www.braintools.ru/article/7595

[3] обучении: http://www.braintools.ru/article/5125

[4] памяти: http://www.braintools.ru/article/4140

[5] @vladotpad: https://www.braintools.ru/users/vladotpad

[6] Источник: https://habr.com/ru/articles/989756/?utm_campaign=989756&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100