В этой статье мы поговорим о том, как с помощью ИИ генерировать музыку. Использовать мы будем обученную на хоралах И. С. Баха минимальную по количеству параметров модель GPT-2. А сама музыка будет представлена в виде текста.

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

Идея использовать GPT-2 и текстовое представления музыки пришла из статьи https://arxiv.org/pdf/2008.06048.pdf. Авторы обучили GPT-2 используя собственный набор токенов и у них получились весьма неплохие результаты, с которыми можно ознакомиться по ссылке. Сам же метод текстового представления мелодии взят из статьи https://arxiv.org/pdf/1808.03715.pdf.

Основная идея здесь заключается в том, что мы вводим «открывающие» и «закрывающие» токены для каждого элемента музыкальной композиции. В частности, момент начала звучания ноты обозначается токеном NOTE_ON, а момент окончания – NOTE_OFF. При этом, чтобы обозначить высоту ноты используются числа от 0 до 127. Пример: NOTE_ON=76.

В музыкальном произведении чаще всего содержится несколько одновременно звучащих голосов, каждый такой голос представляет из себя отдельную дорожку и обозначается в нашем представлении токенами TRACK_START и TRACK_END. Если в исходном виде все дорожки музыкальной композиции звучат одновременно, то в текстовом представлении все они расположены последовательно друг за другом, что позволяет работать с музыкой как с обычным текстом.

В качестве исходного формата музыкальной композиции будем использовать MIDI. Этот формат удобен тем, что представляет музыку в удобном для чтении и интерпретации виде при минимальной потере информации о звучании.

Преобразование MIDI в текст

Итак, для парсинга MIDI будем использовать библиотеку music21. Далее будет показан полный процесс преобразования мелодии в вид, пригодный для GPT-2.

Для считывания мелодии будем пользоваться методом parse() из модуля converter. Далее будем в цикле проходить по каждой дорожке. В начале и конце каждой итерации будем добавлять в итоговую строку токены начала и конца дорожки (TRACK_START и TRACK_END).

def preprocess_score(score):
    """
    Обработка мелодии
    :param score: исходная мелодия, считанная из midi файла
    :return: текстовое представление исходной мелодии
    """
    cur_piece_str = [PIECE_START]  # переменная с текстовым представлением мелодии
    meta_info = {}  # словарь с информацией о тональности и размере произведения

    # идем по дорожкам исходной мелодии
    for part in score.parts:
        # добавляем токен начала дорожки
        cur_piece_str.append(TRACK_START)
        # получаем текстовое представление для дорожки
        cur_track_str = preprocess_track(part, meta_info)
        # добавляем текстовое представление дорожки в итоговую строку
        cur_piece_str.extend(cur_track_str)
        # добавляем токен окончания дорожки
        cur_piece_str.append(TRACK_END)

    return cur_piece_str

Каждая дорожка состоит из тактов. Будем в цикле проходить по тактам каждой дорожки, попутно вставляя токены начала и конца такта (BAR_START и BAR_END). При этом, мы не будем рассматривать композиции, где меняется тональность или размер, чтобы не смущать нашу модель.

def preprocess_track(track, meta_info):
    """
    Обработка одной дорожки музыкальной композиции
    :param track: исходная дорожка
    :param meta_info: информация о тональности и размере мелодии в дорожке
    :return: текстовое представление дорожки
    """
    # инициализируем список с текстовым представлением дорожки
    # в качестве инструмента указываем 0 - это фортепиано
    # в теории можно указать любой другой
    # DENSITY - это "разреженность" нот. Более подробно тут - https://arxiv.org/pdf/2008.06048.pdf.
    track_txt = [f'{INSTRUMENT}=0', 'DENSITY=1']

    # считываем текущую дорожку поэлементно
    for elem_part in track:
        # если текущий элемент является тактом, то обрабатываем такт
        if isinstance(elem_part, music21.stream.base.Measure):
            # добавляем токен начала такта
            track_txt.append(BAR_START)
            # получаем текстовое представление такта
            cur_bar_info = preprocess_bar(elem_part)

            # заполняем словарь с информацией о тональности и размере произведения
            for info_key in ['Key', 'Beat duration', 'Beat count']:
                if info_key in cur_bar_info.keys() and info_key not in meta_info.keys():
                    meta_info[info_key] = cur_bar_info[info_key]
                elif info_key in cur_bar_info.keys() and info_key in meta_info.keys():
                    # исключаем случаи, когда в произведении меняется тональность или размер
                    if cur_bar_info[info_key] != meta_info[info_key]:
                        raise ValueError('Key or time signature was changed')

            cur_bar_time_sig = meta_info['Beat count']
            # обработка случая пустого такта
            # если текущий такт пустой то заполняем его паузой такой длительности, чтоб она заполнила такт
            if not cur_bar_info['bar_txt']:
                track_txt.append(f'{TIME_SHIFT}={cur_bar_time_sig * 4}')
            else:
                # если в такте что-то есть, то вставляем эту информацию 
                # в список для текстового представления дорожки
                track_txt.extend(cur_bar_info['bar_txt'])
                
            # добавляем токен окончая такта
            track_txt.append(BAR_END)

        else:
            pass

    return track_txt

В каждом такте есть набор нот. Нам нужно пройти в цикле по каждой ноте и записать ее в текстовое представление, обозначая токенами NOTE_ON и NOTE_OFF, а также указывая высоту и длительность каждой ноты.

def preprocess_bar(bar):
    """
    Обработка такта
    :param bar: исходный такт
    :return: текстовое представление такта
    """
    bar_txt = []  # список для текстового представления такта
    bar_dict = {}  # вспомогательный словарь

    # предыдущее значение смещения ноты относительно начала произведения плюс ее длительность
    # измеряется в четвертях
    prev_offset = 0.0
    # считываем такт поэлементно
    for elem_measure in bar:
        # если текущий элемент является тональностью
        if isinstance(elem_measure, music21.key.Key):
            # добавляем в словарь информацию о тональности
            bar_dict['Key'] = str(elem_measure.asKey())
        # если текущий элемент является размером
        elif isinstance(elem_measure, music21.meter.base.TimeSignature):
            # добавляем информацию о размере
            bar_dict['Beat duration'] = str(elem_measure.beatDuration.quarterLength)
            bar_dict['Beat count'] = elem_measure.beatCount
            bar_dict['Time signature'] = elem_measure
        # если текущий элемент является нотой или паузой
        elif isinstance(elem_measure, music21.note.Note):
            if elem_measure.isRest:
                # если нашли паузу, то в текстовое представление добавляем токен TIME_SHIFT
                bar_txt.append(f'{TIME_SHIFT}={elem_measure.duration.quarterLength * 4}')
            else:
                # если элемент не пауза, значит - нота
                # добавляем токены начала о конца ноты и токен длительности TIME_SHIFT
                note_list = [f'{NOTE_ON}={elem_measure.pitch.midi}',
                             f'{TIME_SHIFT}={elem_measure.duration.quarterLength * 4}',
                             f'{NOTE_OFF}={elem_measure.pitch.midi}']
                # смещение текущей ноты относительно начала композиции
                cur_offset = elem_measure.offset
                # если смещение текущей ноты относительно начала произведения 
                # больше чем смещение предыдущей плюc ее длительность,
                # то нужно добавить паузу
                if cur_offset - prev_offset > 0:
                    shift_duration = cur_offset - prev_offset

                    bar_txt.append(f'{TIME_SHIFT}='
                                   f'{shift_duration * 4}')

                    prev_offset = cur_offset
                # добавляем в текстовое представление такта текстовое представление ноты
                bar_txt.extend(note_list)
                # обновляем смещение
                prev_offset += elem_measure.duration.quarterLength

        else:
            pass

    bar_dict['bar_txt'] = bar_txt
    return bar_dict

Модель для генерации текста

GPT-2 – хороший вариант для генерации текста. Есть несколько версий этой модели, в том числе и легковесная, что позволяет обучать ее на локальной машине, не прибегая к использованию внешних мощностей (Например, google colab).

Пользуясь проектом, опубликованным здесь, была обучена GPT-2 на кусочках хоралов длинной 2, 4 и 8 тактов. Проанализировав, результаты генераций всех трех версий, был сделан выбор в пользу 4-х тактовой модели.

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

def sample(priming_sample_file, result_file):
    """
    Генерация аккомпанемента по данной мелодии
    :param priming_sample_file: файл с исходной мелодией в текстовом виде
    :param result_file: файл, куда надо положить результат генерации в формате midi
    :return: нет возвращаемого значения
    """
    tokenizer_path = os.path.join(CUR_FILE_PATH, "gpt2model_4_bars", "tokenizer.json")
    tokenizer = PreTrainedTokenizerFast(tokenizer_file=tokenizer_path)
    tokenizer.add_special_tokens({'pad_token': '[PAD]'})

    model_path = os.path.join(CUR_FILE_PATH, "gpt2model_4_bars", "best_model")
    model = GPT2LMHeadModel.from_pretrained(model_path)

    logger.info("Model loaded.")
    with open(priming_sample_file, 'r') as hfile:
        priming_sample = hfile.read()

    # генерируем список четырехтактовых кусочков мелодии
    generated_list = generate_music(priming_sample, model, tokenizer)
    # соединяем все в единое целое
    full_generation = concat_gen_list(generated_list)
    # преобразовываем текст в midi и сохраняем в файл
    note_seq.note_sequence_to_midi_file(token_sequence_to_note_sequence(full_generation), result_file)

Полный код генерации аккомпанемента можно найти в этом репозитории: https://github.com/Vitaliy1234/muse_it/tree/gpt_4_bars_exp

Посмотреть на примеры сгенерированных мелодий можно здесь: https://github.com/Vitaliy1234/muse_it/tree/gpt_4_bars_exp/gpt2_model/generations

Итог

В этой статье мы, воспользовавшись текстовым представлением мелодии, смогли сгенерировать аккомпанемент с помощью модели GPT-2. Конечно, качество генерируемых мелодий можно улучшить путем обучения более тяжелой версии GPT-2, но тот результат, что мы имеем, тоже весьма неплох.

Комментарии (6)


  1. SADKO
    07.12.2022 10:28
    +1

    А по мне так результаты стёрмные, больше похожие на на банальное переобучение, когда вместо извлечения смысла сеть тупо запоминает обучающую выборку и фигачит ей по поводу и без, но GPT-2 не виноватая тут...
    ИМХО засада именно в некорректности представления музыки в текст, которое сделано без учёта понимания теории музыки, что выглядит несколько дико, ибо оная вполне себе формализована, имплементируй - не хочу!
    Это равносильно тому, что кормить GPT-2 рукописным текстом, пусть и бинаризированном, в надежде что она научится распознавать письмо и эмоциональное состояние писателя, а потом ещё как-то в курит написанное. Не, ребята, так не бывает, про проклятие размерности слышали? Это как раз оно самое стучится в дверь. Но дело даже не в нём, сеть не взлетит если кормить её предложениями вырванными из контекста фраз.

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


    1. VitaLeeY Автор
      07.12.2022 14:25

      Спасибо большое за развернутый комментарий! Учту замечания.
      По поводу представления соглашусь, что оно не в полной мере отражает музыкальную композицию - буду искать пути улучшения.

      Хочу сказать пару слов по поводу теории музыки и ее формализации:
      читал труды Р.Х.Зарипова на эту тему. Они меня очень впечатлили тем, что музыка создаваемая алгоритмами получается правильная с точки зрения теории и звучит очень хорошо для человеческого слуха. Но мне кажется, что алгоритмической музыки не хватает разнообразия, каких-то "изюминок", нестандартностей.
      Музыка же постоянно развивается, и те правила, которыми руководствовались композиторы одной эпохи, уже не так строго соблюдаются композиторами другой (взять хотя бы атональную музыку Шёнберга).
      Безусловно у любой музыки есть форма, гармония, но и они имеют кучу вариаций. И как всё это заходить я пока что плохо понимаю. Буду изучать)

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


      1. SADKO
        09.12.2022 09:58
        +1

        Да, и это как раз таки самое интересное, ибо здесь статистические методы могут быть потенциально лучше алгоритмических. Ваш путь вполне имеет право на существование и я не призываю идти иным, а лишь уточнить метод текстового описания дабы позволить реализовать потенциал GPT-2...
        Ведь если внимательно приглядеться, мелодия похожа на речь, не только ритмически, но и структурно. Так что должно прокатить.


  1. Gavr09
    07.12.2022 14:28

    Идея интересна, однако соглашусь с@SADKO - надо дорабатывать с учетом теории музыки. Иначе какой смысл от сгенерированного "Турецкого рондо" Моцарта, да еще и с нарушением правил гармонии?
    Как вариант, кстати, не замахиваться на полноценную генерацию мелодий, а сделать лишь конвертацию более современных произведений в стилистику того же Баха. Кстати, это примерно и происходит в примере с "В лесу родилась елочка", но только надо учесть ограничения, накладываемые теорией.


    1. SADKO
      09.12.2022 11:01

      Отрицательный результат, тоже результат, "нарушение правил гармонии" намекает нам на то, что вместо "извлечения смысла", сеть именно переобучилась. Ну и в данном случае ИМХО очевидно почему оно так произошло и не получилось иначе.

      Прелесть музыки, в отличии от прочих художеств в том, что она глубоко физиологична, и отражает сам процесс мышления. Здесь не получится задвинуть, я художник, я так вижу.
      Ибо обыватель безо всякого сольфеджио прочухивает, что " в этом мотиве есть какая-то фальш "...


  1. OBIEESupport
    08.12.2022 02:15

    Добрый день! Занят почти параллельной тематикой. Есть бесплатные нотные библиотеки, думал как людям помочь петь по нотам. Делал караоке для детей. Скачал сделанное вами. Извините, не впечатлило. Ясно, что система подстроилась под размер, шаг изменения тона и другие индивидуальные параметры композитора, "что-то такое" выделила и, усилив это гипотезами нейросети, излила на бумагу.