Генерация музыки с помощью GPT-2

от автора

В этой статье мы поговорим о том, как с помощью ИИ генерировать музыку. Использовать мы будем обученную на хоралах И. С. Баха минимальную по количеству параметров модель 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, но тот результат, что мы имеем, тоже весьма неплох.


ссылка на оригинал статьи https://habr.com/ru/post/703942/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *