Идея создать собственный веб-плеер с музыкой лоу-фай пришла мне в голову однажды воскресным днём во время изучения глубоких генеративных моделей. Я занялась этим проектом и закончила его за время каникул. Веб-плеер имеет две опции: пользователи могут выбрать трек лоу-фай на основе реальной песни, преобразованной с помощью библиотеки Tone.js, или трек, сгенерированный искусственным интеллектом. В обоих случаях поверх накладываются барабанные ритмы, атмосферные звуки и цитаты, которые пользователь сам выбрал на предыдущем шаге. В этом посте речь пойдёт в основном о том, как использовать нейросети LSTM для генерации midi-треков, а в конце я кратко расскажу о том, как с помощью Tone.js создаются песни.
Ещё со времён колледжа я увлекаюсь музыкой в жанре лоу-фай хип-хоп. Она создаёт уютную, успокаивающую атмосферу. Несколько последовательностей джазовых аккордов, ритмичные барабанные биты, атмосферные звуки, цитаты из фильмов, которые вызывают ностальгию, — и мы получаем довольно прилично звучащий трек в стиле лоу-фай хип-хоп. Важный вклад в восприятие лоу-фай вносит визуальный ряд, который создаёт соответствующую атмосферу наряду со звуками природы: воды, ветра и огня.
Попробовать веб-плеер можно здесь. Рекомендую использовать браузер Chrome для компьютера.
О сети LSTM я рассказала в предыдущем посте. Если кратко, то это — особая разновидность рекуррентной нейронной сети (RNN), которая лучше работает с долгосрочными зависимостями. Она также имеет рекуррентную структуру, которая на текущем временном шаге принимает выходные данные с предыдущего временного шага. Чтобы лучше понять структуру LSTM, развернём эту нейросеть и представим её ячейку в виде нескольких копий одной и той же сети, каждая из которых передаёт сообщение на следующий временной шаг, как показано на схеме.
Развёрнутая сеть LSTM
Каждая ячейка содержит четыре основных компонента, которые позволяют ей лучше справляться с долгосрочными зависимостями:
Структура ячейки LSTM
Есть несколько вариантов формата музыкальных данных, на которых можно обучать модель:
Наша задача — сгенерировать основной трек (т. е. последовательность нот, аккордов и пауз) для дальнейшего наложения его на другие составляющие, например, барабанные петли, поэтому для нашей цели midi-файлы — самый простой и эффективный формат. Обучение на необработанных аудиофайлах требует больших вычислительных ресурсов. Чтобы оценить их объём, достаточно отметить, что сэмплирование аудио с частотой 48000 кГц соответствует 48000 точкам данных в одной секунде аудио. Даже если мы уменьшим частоту дискретизации до 8 кГц, всё равно останется 8000 точек данных в секунду. Кроме того, чистый звук только мелодии или последовательности аккордов встречается крайне редко. Конечно, мы могли бы попытаться найти midi-файлы, которые содержат только последовательность аккордов / мелодию.
Для этих целей я использовала несколько образцов лоу-фай midi, созданных Майклом Ким-Шенгом, который великодушно разрешил мне использовать его файлы, а ещё некоторые midi-файлы из этого инструментария Cymatics lo-fi, который лицензирован для коммерческого использования. Чтобы удостовериться, что я тренирую свою модель на качественных данных (правдоподобная последовательность аккордов и размер для лоу-фай хип-хопа), я прослушала подмножество треков из каждого источника и отфильтровала обучающий набор данных. Архитектура модели создана по мотивам фортепианной музыки композиторов-классиков из этого репозитория.
Для загрузки midi-файлов можно использовать пакет Python music21. Music21 считывает midi-файл и сохраняет каждую составляющую музыки в определённом объекте Python. То есть нота сохраняется как объект Note, аккорд — как объект Chord, а пауза — как объект Rest. Их название, продолжительность, высотный класс и другие свойства доступны в записи через точку. Music21 хранит аудио в виде иерархии, показанной ниже. Соответственно мы можем извлекать необходимую информацию. Если вас интересует, как использовать этот пакет, на веб-сайте пакета находится руководство пользователя для начинающих. Кроме того, у Валерио Велардо на канале The Sound of AI есть учебное пособие о том, как применять music21.
Как я уже упоминала, music21 хранит каждую ноту, паузу и аккорд в виде объекта Python, поэтому на следующем шаге необходимо их преобразовать и поставить в соответствие целым числам, на которых можно обучить модель. Выходные данные модели должны содержать не только ноты, но также аккорды и паузы, поэтому будем преобразовывать каждый тип отдельно и ставить в соответствие преобразованному значению целое число. Мы делаем это для всех midi-файлов и для обучения модели объединяем их в единую последовательность.
Загрузка и преобразование midi
Теперь у нас есть удобная кодировка midi-данных для модели. Следующий шаг — подготовка входящих и целевых пар для модели. В простой задаче машинного обучения с учителем — задаче классификации — фигурируют входные и целевые пары. Например, модель, которая классифицирует породу собак, будет иметь в качестве входных данных цвет шерсти, рост, вес и цвет глаз собаки, а меткой/целью будет конкретная порода, к которой принадлежит собака. В нашем случае входные данные — это последовательность длины k, начинающаяся с временного шага i, а соответствующее целевое значение — точка данных на временном шаге i+k. Итак, мы организуем цикл по преобразованной последовательности нот и создаём для модели входные и целевые пары. На последнем шаге мы изменяем размерность входных данных, преобразуем их в формат, совместимый с keras, а также осуществляем прямое кодирование выходных данных.
Как упоминалось ранее, в качестве основной структуры модели мы воспользуемся слоями нейросети LSTM. Кроме того, в этой нейросети будут следующие составляющие:
model = Sequential()
model.add(LSTM(
256,
input_shape=(network_input.shape[1], network_input.shape[2]),
return_sequences=True
))
model.add(Dropout(0.3))
model.add(LSTM(512, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256))
model.add(Dense(256))
model.add(Dropout(0.3))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')
model.fit(network_input, network_output, epochs=200, batch_size=128)
Здесь 3 слоя LSTM с 2 слоями отсева, каждый из которых следует за первыми двумя слоями LSTM. Затем идут 2 полностью соединённых плотных слоя, за которыми следует одна функция активации softmax. Наши выходные данные категориальные, поэтому целевой функцией станет категориальная перекрёстная энтропия. Применяется довольно распространённый для RNNS оптимизатор RMSProp. Добавлены контрольные точки, для того чтобы веса регулярно сохранялись на разных эпохах и могли использоваться до окончания обучения модели. Можно изменять структуру модели и пробовать различные оптимизаторы, количество эпох и размеры пакетов.
Генерация выходных данных аналогичен процессу обучения — мы подаём модели последовательность длиной m (для упрощения обозначения обозначим её последовательностью m) и просим её предсказать следующую точку данных. Эта последовательность m имеет начальный индекс, случайно выбранный из входной последовательности, но при желании можно указать конкретный начальный индекс. Выходные данные модели представляют собой список вероятностей из softmax, которые сообщают нам, насколько каждый класс подходит в качестве следующей точки данных. Выберем класс с наибольшей вероятностью. Чтобы сгенерировать последовательность длины j, мы повторим этот процесс, удалив первый элемент последовательности m и добавив недавно сгенерированную точку данных к этой последовательности m. И так до тех пор, пока модель не сгенерирует j новых точек данных.
Данные, сгенерированные по алгоритму из последнего абзаца, по-прежнему являются целым числом, поэтому мы преобразуем их обратно в ноту/аккорд/паузу через те же соответствия, что и при кодировании. Если это формат строки аккорда, прочитаем целочисленное обозначение из строки "#.#.#.#" и создадим объект music21.chord. Если это нота или пауза, мы создадим соответствующий объект note или rest. В то же время на каждом временном шаге мы добавляем к выходной последовательности прогноза новую сгенерированную точку данных. Для иллюстрации ниже приведён пример потока, где мы генерируем последовательность из 4 точек данных на основе входной последовательности из 3 точек данных.
Теперь у нас есть последовательность нот, аккордов и пауз. Мы могли бы поместить их в поток music21 и записать midi-файл, и в этом случае все ноты будут четвертными. Чтобы сделать вывод немного интереснее, я добавила фрагмент кода, который случайным образом выбирает длительность для каждой ноты или аккорда (распределение вероятностей по умолчанию составляет 0,65 для восьмых нот, 0,25 — для 16-х нот, 0,05 — для четвертных и половинных нот). Паузы по умолчанию равны 16-м <1----> паузам, чтобы избежать слишком долгих промежутков между нотами.
NOTE_TYPE = {
"eighth": 0.5,
"quarter": 1,
"half": 2,
"16th": 0.25
}
offset = 0
output_notes = []
for pattern in prediction_output:
curr_type = numpy.random.choice(list(NOTE_TYPE.keys()), p=[0.65,0.05,0.05, 0.25])
# pattern is a chord
if ('.' in pattern) or pattern.isdigit():
notes_in_chord = pattern.split('.')
notes = []
for current_note in notes_in_chord:
new_note = note.Note(int(current_note))
new_note.storedInstrument = instrument.Piano()
notes.append(new_note)
new_chord = chord.Chord(notes, type=curr_type)
new_chord.offset = offset
output_notes.append(new_chord)
elif str(pattern).upper() == "R":
curr_type = '16th'
new_rest = note.Rest(type=curr_type)
new_rest.offset = offset
output_notes.append(new_rest)
# pattern is a note
else:
new_note = note.Note(pattern, type=curr_type)
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
output_notes.append(new_note)
# increase offset each iteration so that notes do not stack
offset += NOTE_TYPE[curr_type]
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='test_output.mid')
Для того чтобы сгенерированные треки по звучанию были ближе к настоящей музыке, после запуска модели несколько раз с разными параметрами и подбора треков, которые нам нравятся, применим инструментальный эффект в стиле лоу-фай из любой цифровой звуковой рабочей станции (DAW). Затем создадим веб-плеер с помощью JavaScript.
Tone.js — это веб-фреймворк, предназначенный для создания интерактивной музыки в браузере. Вы можете использовать его для создания различных интерактивных веб-сайтов (см. демоверсии здесь). Но здесь он применяется для окончательного наложения аудио, чтобы обеспечить одновременное воспроизведение барабанных ритмов, атмосферных звуков, цитат и мелодии. Он также позволяет записывать музыкальное сопровождение, сэмплировать определённый инструмент, добавлять звуковые эффекты (реверберацию, усиление и т. д.) и создавать циклы прямо в JavaScript. Скелет программы разработала Kathryn. Если вы хотите быстро и эффективно освоить Tone.js, я настоятельно рекомендую примеры использования этой библиотеки на веб-сайте. Вот самый важный вывод: если мы хотим добавить звуковые эффекты к каждому звуковому событию, которое создаём, нужно подключить его к AudioDestinationNode (т. е. к нашим динамикам) через toDestination()
или через samplePlayer.chain(effect1, Tone.Tone.Destination)
. Затем через Tone.Transport
мы сможем запускать, приостанавливать и планировать события на выходе устройства.
Барабанные ритмы, атмосферные звуки, цитаты и треки, предварительно сгенерированные искусственным интеллектом, представляют собой аудиофайлы (.mp3 или .wav), загруженные в наш веб-плеер через класс Player. После загрузки пользователем с веб-сайта входящих событий они передаются в класс Tone.js для создания циклов.
Барабанные ритмы повторяются каждые 8 тактов, атмосферные звуки — каждые 12 тактов, а сами сгенерированные треки — каждые 30 тактов. Цитаты не зацикливаются и начинаются с 5-го такта.
Tone.js не предоставляет нам инструментальных опций, подобно программам DAW, только сэмплеры, которые позволяют нам сэмплировать наши собственные инструменты, загружая пару нот. Затем сэмплер автоматически повторно загрузит сэмплы и создаст ноты, которых в явном виде не было.
Теперь мы можем записать мелодию и последовательность аккордов, указав ноты и время, в течение которого должна звучать та или иная нота. Для нужного нам преобразования ритма я рекомендую использовать TransportTime. TransportTime представлено в виде "такты: четвертные: шестнадцатые", нумерация начинается с нуля. Например, "0:0:2" означает, что нота будет воспроизведена после двух шестнадцатых нот в первом такте. "2:1:0" означает, что нота будет воспроизведена в третьем такте после одной четвертной ноты. Таким образом я записала мелодию и последовательности аккордов для 3 существующих песен: Ylang Ylang (автор FKJ), La Festin (автор Camille) и See You Again (автор Tyler, the Creator).
Я добавила опции для изменения фона веб-плеера в зависимости от атмосферных звуков. Таким образом, для каждого контекста отображается более соответствующая анимация. Также есть визуализатор, выполненный с помощью p5.js и связанный с нотами песни.
Нейронная сеть LSTM
Веб-плеер
Получить код и обучающие наборы данных можно в репозитории GitHub. Это всего лишь простой лоу-фай веб-плеер, но мне доставила удовольствие работа с моделью LSTM и Tone.js. Возможность использовать технологии для работы с музыкой удивляет меня каждый раз.
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также
>Я занялась этим проектом
>О сети LSTM я рассказал в предыдущем посте.
Неплохо было бы к одному роду привести.
Спасибо, поправили. Перепроверили окончания глаголов. Ошибкам такого рода — и контролю качества в целом — у нас посвящён целый чек-лист, но из-за форс-мажора пострадали связанные с ним процессы, гарантирующие, в частности, отсутствие этой ошибки. Сейчас всё это потихоньку налаживаем и заново закручиваем гайки. Добавлю также, что на Хабре есть стандартный механизм сообщения об опечатках, срабатывает он по выделению опечатки и нажатию Ctrl+Enter, а замечание попадает автору поста в личные сообщения.
вы не указали на какой библиотеке это создано, или я пропустил? Это tensorflow?
Кроме tone.js, использовалась Keras. Автор пишет о ней вскользь, такое несложно упустить. Код здесь.