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