Композитор с долгой кратковременной памятью

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Автоматическое сочинение музыки

Почти сразу после того, как я научился программированию, мне хотелось создать ПО, способное сочинять музыку. Я в течение нескольких лет предпринимал примитивные попытки автоматического сочинения музыки для Visions of Chaos. В основном при этом использовались простые математические формулы или генетические мутации случайных последовательностей нот. Добившись недавно скромного успеха в изучении и применении TensorFlow и нейронных сетей для поиска клеточных автоматов, я решил попробовать использовать нейронные сети для создания музыки.

Как это работает

Композитор обучает нейросеть с долгой кратковременной памятью (Long short-term memory, LSTM). LSTM-сети хорошо подходят для предсказания того, «что встретится дальше» в последовательностях данных. Подробнее о LSTM можно прочитать здесь.

LSTM-сеть получает различные последовательности нот (в данном случае это одноканальные файлы midi). После достаточного обучения она получает возможность создавать музыку, схожую с обучающими материалами.
Схемы внутреннего устройства LSTM могут казаться пугающими, но использование TensorFlow и/или Keras сильно упрощают создание LSTM и эксперименты с ними.

Исходная музыка для обучения моделей

Для таких простых LSTM-сетей нам достаточно, чтобы исходные композиции представляли собой один канал midi. Отлично для этого подходят файлы midi с соло на пианино. Я нашёл файлы midi с пианинными соло на Classical Piano Midi Page и mfiles, и использовал их для обучения своих моделей. Музыку разных композиторов я поместил в отдельные папки. Благодаря этому пользователь может выбрать Баха, нажать на кнопку Compose и получить композицию, которая (надеюсь) будет походить на Баха.

Модель LSTM

Моделью, на основании которого я писал код, был выбран этот пример автора Sigur?ur Sk?li Sigurgeirsson, о котором он подробнее пишет здесь. Я запустил выполнение скрипта lstm.py и через 15 часов он завершил обучение. Когда я запустил predict.py для генерации файлов midi, то был разочарован, потому что они состояли из одной повторяющейся ноты. Дважды повторив обучение, я получил такие же результаты. Исходная модель

model = Sequential() model.add(CuDNNLSTM(512,input_shape=(network_input.shape[1], network_input.shape[2]),return_sequences=True)) model.add(Dropout(0.3)) model.add(CuDNNLSTM(512, return_sequences=True)) model.add(Dropout(0.3)) model.add(CuDNNLSTM(512)) 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',metrics=["accuracy"])

Добавив в скрипт вывод графиков, я увидел, почему моя модель не работала. Точность не росла со временем, как должна была. См. ниже в посте хорошие графики, на которых видно, как должна выглядеть рабочая модель.

Я понятия не имел, почему так получилось. но отказался от этой модели и начал настраивать параметры.
model = Sequential() model.add(CuDNNLSTM(512, input_shape=(network_input.shape[1], network_input.shape[2]), return_sequences=True)) model.add(Dropout(0.2)) model.add(BatchNormalization()) model.add(CuDNNLSTM(256)) model.add(Dropout(0.2)) model.add(BatchNormalization()) model.add(Dense(128, activation="relu")) model.add(Dropout(0.2)) model.add(BatchNormalization()) model.add(Dense(n_vocab)) model.add(Activation('softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam',metrics=["accuracy"])

Она компактнее и в ней меньше слоёв LSTM. Также я добавил BatchNormalization, увидев его в видео sentdex. Скорее всего, существуют более качественные модели, но эта вполне неплохо работала во всех моих сеансах обучения. Заметьте, что в обеих моделях я заменил LSTM на CuDNNLSTM. Так я добился гораздо более быстрого LSTM-обучения благодаря использованию Cuda. Если у вас нет GPU с поддержкой Cuda, то придётся пользоваться LSTM. Спасибо sendtex за этот совет. Обучение новых моделей и сочинение файлов midi при использовании CuDNNLSTM выполняется примерно в пять раз быстрее.

Как долго следует обучать модель

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

Но как узнать на каком количестве эпох остановиться?

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

Вот результаты графиков одного прогона с сохранением каждых 50 эпохах, объединённые в один анимированный GIF.

Именно такие графики мы и хотим увидеть. Потери должны падать и оставаться низкими. Точность должна повышаться и оставаться близкой к 100%. Нужно использовать модель с количеством эпох, соответствующим моменту, когда графики впервые достигли своих пределов. Для показанного выше графика это будет 150 эпох. Если использовать более «старые» модели, то они будут переобучены и скорее всего приведут к простому копированию исходного материала.
Соответствующая этим графам модель обучалась на файлах midi категории «Гимны» (Anthems), взятых отсюда. Выходные midi-данные в модели с 150 эпохами. Выходные midi-данные в модели с 100 эпохами. Даже модель с 100 эпохами может слишком точно копировать исходник. Это может происходить из-за относительно малой выборки файлов midi для обучения. При большем количестве нот обучение проходит лучше.

Когда обучение проходит плохо

На изображении выше показан пример того, что иногда может происходить и происходит во время обучения. Потери уменьшаются, а точность повышается, как и обычно, но внезапно они начинают сходить с ума. На этом этапе возможно тоже стоит остановиться. Модель больше не будет (по крайней мере, на моём опыте) снова обучаться правильно. В этом случае сохранённая модель с 100 эпохами ещё слишком случайна, а с 150 эпохами уже прошла момент сбоя модели. Теперь я сохраняюсь через каждые 25 эпох, чтобы точно найти идеальный момент модели с наилучшим обучением, ещё до того, как она переобучится и придёт к сбою.
Ещё один пример ошибки обучения. Эта модель обучалась на файлах midi, взятых отсюда. В этом случае она хорошо держалась чуть дольше, чем 200 эпох. При использовании модели с 200 эпохами получается следующий результат в Midi. Без создания графиков мы бы никогда не узнали, есть ли у модели проблемы и когда они возникли, а также не смогли бы получить хорошую модель, не начиная всё с нуля.

Другие примеры

Модель с 75 эпохами, созданная на основании композиций Шопена. Модель с 50 эпохами, основанная на Midi-файлах рождественских композиций. Модель с 100 эпохами, основанная на Midi-файлах рождественских композиций. Но очень ли они «рождественские»? Модель с 300 эпохами, основанная на Midi-файлах Баха, взятых отсюда и отсюда. Модель с 200 эпохами, основанная на единственном Midi-файле Балакирева, взятого здесь. Модель с 200 эпохами, основанная на композициях Дебюсси. Модель с 175 эпохами, основанная на композициях Моцарта. Модель с 100 эпохами, основанная на композициях Шуберта. Модель с 200 эпохами, основанная на композициях Шумана. Модель с 200 эпохами, основанная на композициях Чайковского. Модель с 175 эпохами, основанная на народных песнях. Модель с 100 эпохами, основанная на колыбельных. Модель с 100 эпохами, основанная на музыке для свадеб. Модель с 200 эпохами, основанная на моих собственных файлах midi, взятых из моих саундтреков к видео на YouTube. Возможно, она немного переобучена, потому что в основном генерирует копии моих коротких одно- и двухтактных файлов midi.

Партитуры

Получив файлы midi, можно воспользоваться онлайн-инструментами наподобие SolMiRe, чтобы преобразовать их в партитуры. Ниже показана партитура представленного выше файла midi Softology с 200 эпохами.

Где можно протестировать композитора

LSTM Composer теперь включён в Visions of Chaos.

Выберите стиль из раскрывающегося списка и нажмите Compose. Если у вас установлены минимально необходимые Python и TensorFlow (инструкции см. здесь), то за несколько секунд (если у вас быстрый GPU) вы получите новый сочинённый машиной файл midi, который можно слушать и использовать для любой другой цели. Никаких авторских прав, никаких лицензионных отчислений. Если вам не понравятся результаты, то можете снова нажать на Compose и через несколько секунд будет готова новая композиция. Результаты пока нельзя считать полноценными композициями, но в них есть интересные небольшие последовательности нот, которые я буду использовать для создания музыки в будущем. В этом отношении LSTM-композитор может быть хорошим источником вдохновения для написания новых композиций.

Исходники на Python

Ниже приведён использованный мной код скриптов на Python для обучения LSTM и прогнозирования. Чтобы эти скрипты работали, необязательно устанавливать Visions of Chaos, а обучение и генерирование midi будут работать из командной строки.

Вот скрипт обучения lstm_music_train.py

lstm_music_train.py

# based on code from https://github.com/Skuldur/Classical-Piano-Composer # to use this script pass in; # 1. the directory with midi files # 2. the directory you want your models to be saved to # 3. the model filename prefix # 4. how many total epochs you want to train for # eg python -W ignore "C:LSTM Composerlstm_music_train.py" "C:LSTM ComposerBach" "C:LSTM Composer" "Bach" 500  import os import tensorflow as tf  # ignore all info and warning messages os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'  tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)  import glob import pickle import numpy import sys import keras import matplotlib.pyplot as plt  from music21 import converter, instrument, note, chord from datetime import datetime from keras.models import Sequential from keras.layers.normalization import BatchNormalization from keras.layers import Dense from keras.layers import Dropout from keras.layers import CuDNNLSTM from keras.layers import Activation from keras.utils import np_utils from keras.callbacks import TensorBoard from shutil import copyfile  # name of midi file directory, model directory, model file prefix, and epochs mididirectory = str(sys.argv[1]) modeldirectory = str(sys.argv[2]) modelfileprefix = str(sys.argv[3]) modelepochs = int(sys.argv[4])  notesfile = modeldirectory + modelfileprefix + '.notes'  # callback to save model and plot stats every 25 epochs class CustomSaver(keras.callbacks.Callback): 	def __init__(self): 		self.epoch = 0	 	# This function is called when the training begins 	def on_train_begin(self, logs={}): 		# Initialize the lists for holding the logs, losses and accuracies 		self.losses = [] 		self.acc = [] 		self.logs = [] 	def on_epoch_end(self, epoch, logs={}): 		# Append the logs, losses and accuracies to the lists 		self.logs.append(logs) 		self.losses.append(logs.get('loss')) 		self.acc.append(logs.get('acc')*100) 		# save model and plt every 50 epochs 		if (epoch+1) % 25 == 0: 			sys.stdout.write(" Auto-saving model and plot after {} epochs to ".format(epoch+1)+" "+modeldirectory + modelfileprefix + "_" + str(epoch+1).zfill(3) + ".model "+modeldirectory + modelfileprefix + "_" + str(epoch+1).zfill(3) + ".png  ") 			sys.stdout.flush() 			self.model.save(modeldirectory + modelfileprefix + '_' + str(epoch+1).zfill(3) + '.model') 			copyfile(notesfile,modeldirectory + modelfileprefix + '_' + str(epoch+1).zfill(3) + '.notes'); 			N = numpy.arange(0, len(self.losses)) 			# Plot train loss, train acc, val loss and val acc against epochs passed 			plt.figure() 			plt.subplots_adjust(hspace=0.7) 			plt.subplot(2, 1, 1) 			# plot loss values 			plt.plot(N, self.losses, label = "train_loss") 			plt.title("Loss [Epoch {}]".format(epoch+1)) 			plt.xlabel('Epoch') 			plt.ylabel('Loss') 			plt.subplot(2, 1, 2) 			# plot accuracy values 			plt.plot(N, self.acc, label = "train_acc") 			plt.title("Accuracy % [Epoch {}]".format(epoch+1)) 			plt.xlabel("Epoch") 			plt.ylabel("Accuracy %") 			plt.savefig(modeldirectory + modelfileprefix + '_' + str(epoch+1).zfill(3) + '.png') 			plt.close() 			 # train the neural network def train_network():  	sys.stdout.write("Reading midi files...  ") 	sys.stdout.flush()  	notes = get_notes()  	# get amount of pitch names 	n_vocab = len(set(notes))  	sys.stdout.write(" Preparing note sequences... ") 	sys.stdout.flush()  	network_input, network_output = prepare_sequences(notes, n_vocab)  	sys.stdout.write(" Creating CuDNNLSTM neural network model... ") 	sys.stdout.flush()  	model = create_network(network_input, n_vocab)  	sys.stdout.write(" Training CuDNNLSTM neural network model...  ") 	sys.stdout.flush()  	train(model, network_input, network_output)  # get all the notes and chords from the midi files def get_notes():  	# remove existing data file if it exists 	if os.path.isfile(notesfile): 		os.remove(notesfile) 	 	notes = []  	for file in glob.glob("{}/*.mid".format(mididirectory)): 		midi = converter.parse(file)  		sys.stdout.write("Parsing %s ... " % file) 		sys.stdout.flush()  		notes_to_parse = None  		try: # file has instrument parts 			s2 = instrument.partitionByInstrument(midi) 			notes_to_parse = s2.parts[0].recurse()  		except: # file has notes in a flat structure 			notes_to_parse = midi.flat.notes  		for element in notes_to_parse: 			if isinstance(element, note.Note): 				notes.append(str(element.pitch)) 			elif isinstance(element, chord.Chord): 				notes.append('.'.join(str(n) for n in element.normalOrder))  	with open(notesfile,'wb') as filepath: 		pickle.dump(notes, filepath)  	return notes  # prepare the sequences used by the neural network def prepare_sequences(notes, n_vocab): 	sequence_length = 100  	# get all pitch names 	pitchnames = sorted(set(item for item in notes))  	 # create a dictionary to map pitches to integers 	note_to_int = dict((note, number) for number, note in enumerate(pitchnames))  	network_input = [] 	network_output = []  	# create input sequences and the corresponding outputs 	for i in range(0, len(notes) - sequence_length, 1): 		sequence_in = notes[i:i + sequence_length] # needs to take into account if notes in midi file are less than required 100 ( mod ? ) 		sequence_out = notes[i + sequence_length]  # needs to take into account if notes in midi file are less than required 100 ( mod ? ) 		network_input.append([note_to_int[char] for char in sequence_in]) 		network_output.append(note_to_int[sequence_out])  	n_patterns = len(network_input)  	# reshape the input into a format compatible with CuDNNLSTM layers 	network_input = numpy.reshape(network_input, (n_patterns, sequence_length, 1)) 	# normalize input 	network_input = network_input / float(n_vocab)  	network_output = np_utils.to_categorical(network_output)  	return (network_input, network_output)  # create the structure of the neural network def create_network(network_input, n_vocab):  	''' 	""" create the structure of the neural network """ 	model = Sequential() 	model.add(CuDNNLSTM(512, input_shape=(network_input.shape[1], network_input.shape[2]), return_sequences=True)) 	model.add(Dropout(0.3)) 	model.add(CuDNNLSTM(512, return_sequences=True)) 	model.add(Dropout(0.3)) 	model.add(CuDNNLSTM(512)) 	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',metrics=["accuracy"]) 	''' 	 	model = Sequential() 	 	model.add(CuDNNLSTM(512, input_shape=(network_input.shape[1], network_input.shape[2]), return_sequences=True)) 	model.add(Dropout(0.2)) 	model.add(BatchNormalization()) 	 	model.add(CuDNNLSTM(256)) 	model.add(Dropout(0.2)) 	model.add(BatchNormalization()) 	 	model.add(Dense(128, activation="relu")) 	model.add(Dropout(0.2)) 	model.add(BatchNormalization()) 	 	model.add(Dense(n_vocab)) 	model.add(Activation('softmax')) 	model.compile(loss='categorical_crossentropy', optimizer='adam',metrics=["accuracy"]) 	 	return model  # train the neural network def train(model, network_input, network_output): 	 	# saver = CustomSaver() 	# history = model.fit(network_input, network_output, epochs=modelepochs, batch_size=50, callbacks=[tensorboard]) 	history = model.fit(network_input, network_output, epochs=modelepochs, batch_size=50, callbacks=[CustomSaver()])  	# evaluate the model 	print(" Model evaluation at the end of training") 	train_acc = model.evaluate(network_input, network_output, verbose=0) 	print(model.metrics_names) 	print(train_acc) 	 	# save trained model 	model.save(modeldirectory + modelfileprefix + '_' + str(modelepochs) + '.model')  	# delete temp notes file 	os.remove(notesfile) 	 if __name__ == '__main__': 	train_network()

А вот скрипт генерации midi lstm_music_predict.py:

lstm_music_predict.py

# based on code from https://github.com/Skuldur/Classical-Piano-Composer # to use this script pass in; # 1. path to notes file # 2. path to model # 3. path to midi output # eg python -W ignore "C:LSTM Composerlstm_music_predict.py" "C:LSTM ComposerBach.notes" "C:LSTM ComposerBach.model" "C:LSTM ComposerBach.mid"  # ignore all info and warning messages import os os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'  import tensorflow as tf tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)  import pickle import numpy import sys import keras.models  from music21 import instrument, note, stream, chord from keras.models import Sequential from keras.layers import Dense from keras.layers import Dropout from keras.layers import Activation  # name of weights filename notesfile = str(sys.argv[1]) modelfile = str(sys.argv[2]) midifile = str(sys.argv[3])  # generates a piano midi file def generate(): 	sys.stdout.write("Loading notes data file...  ") 	sys.stdout.flush()  	#load the notes used to train the model 	with open(notesfile, 'rb') as filepath: 		notes = pickle.load(filepath)  	sys.stdout.write("Getting pitch names...  ") 	sys.stdout.flush()  	# Get all pitch names 	pitchnames = sorted(set(item for item in notes)) 	# Get all pitch names 	n_vocab = len(set(notes))  	sys.stdout.write("Preparing sequences...  ") 	sys.stdout.flush()  	network_input, normalized_input = prepare_sequences(notes, pitchnames, n_vocab)  	sys.stdout.write("Loading LSTM neural network model...  ") 	sys.stdout.flush()  	model = create_network(normalized_input, n_vocab)  	sys.stdout.write("Generating note sequence...  ") 	sys.stdout.flush()  	prediction_output = generate_notes(model, network_input, pitchnames, n_vocab)  	sys.stdout.write(" Creating MIDI file...  ") 	sys.stdout.flush()  	create_midi(prediction_output)  # prepare the sequences used by the neural network def prepare_sequences(notes, pitchnames, n_vocab): 	# map between notes and integers and back 	note_to_int = dict((note, number) for number, note in enumerate(pitchnames))  	sequence_length = 100 	network_input = [] 	output = [] 	for i in range(0, len(notes) - sequence_length, 1): 		sequence_in = notes[i:i + sequence_length] 		sequence_out = notes[i + sequence_length] 		network_input.append([note_to_int[char] for char in sequence_in]) 		output.append(note_to_int[sequence_out])  	n_patterns = len(network_input)  	# reshape the input into a format compatible with LSTM layers 	normalized_input = numpy.reshape(network_input, (n_patterns, sequence_length, 1)) 	# normalize input 	normalized_input = normalized_input / float(n_vocab)  	return (network_input, normalized_input)  # create the structure of the neural network def create_network(network_input, n_vocab): 	model = keras.models.load_model(modelfile) 	return model  # generate notes from the neural network based on a sequence of notes def generate_notes(model, network_input, pitchnames, n_vocab): 	# pick a random sequence from the input as a starting point for the prediction 	start = numpy.random.randint(0, len(network_input)-1)  	int_to_note = dict((number, note) for number, note in enumerate(pitchnames))  	pattern = network_input[start] 	prediction_output = []  	# generate 500 notes 	for note_index in range(500): 		prediction_input = numpy.reshape(pattern, (1, len(pattern), 1)) 		prediction_input = prediction_input / float(n_vocab)  		prediction = model.predict(prediction_input, verbose=0)  		index = numpy.argmax(prediction) 		result = int_to_note[index] 		prediction_output.append(result)  		pattern.append(index) 		pattern = pattern[1:len(pattern)]  		if (note_index + 1) % 50 == 0: 			sys.stdout.write("{} out of 500 notes generated ".format(note_index+1)) 			sys.stdout.flush()  	return prediction_output  # convert the output from the prediction to notes and create a midi file from the notes def create_midi(prediction_output): 	offset = 0 	output_notes = []  	# create note and chord objects based on the values generated by the model 	for pattern in prediction_output: 		# 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) 			new_chord.offset = offset 			output_notes.append(new_chord) 		# pattern is a note 		else: 			new_note = note.Note(pattern) 			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 += 0.5  	midi_stream = stream.Stream(output_notes)  	midi_stream.write('midi', fp=midifile)  if __name__ == '__main__': 	generate()

Размеры файлов моделей

Недостаток включения нейросетей в Visions of Chaos заключается в размере файлов. Если бы генерация модели была быстрее, то я бы просто добавил кнопку, чтобы конечный пользователь мог сам обучать модели. Но поскольку некоторые из сеансов обучения множества моделей могут занимать по несколько дней, это не особо практично. Мне показалось, что лучше выполнить всё обучение и тестирование самому, и добавить только самые лучшие работающие модели. Это также означает, что конечному пользователю достаточно просто нажать на кнопку, и обученные модели создадут музыкальные композиции.

Каждая из моделей имеет размер 22 мегабайта. В условиях современного Интернета это не так уж много, но за годы разработки Visions of Chaos наращивал свой размер постепенно, и только недавно он внезапно увеличился с 70 до 91 МБ (из-за модели поиска клеточных автоматов). Поэтому я пока добавил в основной установщик Visions of Chaos только одну модель. Для пользователей, которым хочется большего, я выложил ссылку на ещё 1 ГБ моделей. Также они могут воспользоваться приведённым выше скриптом, чтобы создавать собственные модели, основанные на выбранных ими файлах midi.

Что дальше?

На данном этапе LSTM-композитор является простейшим примером использования нейронных сетей для сочинения музыки.

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


Источник: habr.com

Комментарии: