From 868a0180c6d9753c0bc30354566a32b2738903a5 Mon Sep 17 00:00:00 2001 From: David Lim <59020919+neldivad@users.noreply.github.com> Date: Thu, 24 Dec 2020 16:30:44 +0800 Subject: [PATCH 1/3] Add files via upload --- musicLSTM.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++++ musicPredict.py | 174 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 musicLSTM.py create mode 100644 musicPredict.py diff --git a/musicLSTM.py b/musicLSTM.py new file mode 100644 index 0000000..a44b251 --- /dev/null +++ b/musicLSTM.py @@ -0,0 +1,184 @@ +""" +This module prepares midi file data and feeds it to the neural +network for training +""" + +import glob +import pickle +import numpy +import tensorflow.keras as keras + +from music21 import converter, midi, instrument, note, chord + +from keras.models import Sequential +from keras.layers import Dense +from keras.layers import Dropout +from keras.layers import LSTM +from keras.layers import Activation +from keras.layers import BatchNormalization as BatchNorm +from keras.utils import np_utils +from tensorflow.keras.callbacks import ModelCheckpoint + +def train_network(folder_name, save_as_filename, + seq_len= 100, + LSTM_node_count= 512, + Dropout_count= 0.3, + epoch= 1, batchsize= 128): + """ + Trains a Neural Network to generate music. + `Folder_name` = Folder containing MIDI files you want to train. + `Save_as_filename` = Name of output file to be later used to generate MIDI. + `Epoch` = Number of times you want the computer to train. + `Batchsize` = Number of training examples utilized per epoch. + """ + ###################################################### + notes = get_notes(folder_name, save_as_filename) + + # get amount of pitch names + n_vocab = len(set(notes)) + + # Preparing model + network_input, network_output = prepare_sequences(notes, n_vocab, seq_len) + model = create_network(network_input, n_vocab, LSTM_node_count, Dropout_count) + + # Training model + train(model, network_input, network_output, epoch, batchsize) + ######################################################### + +def open_midi(midi_path, remove_drums): + """ + Reads MIDI files into a stream format. + There is an option to remove drums if their inputs may tamper with chord analysis. + """ + mf = midi.MidiFile() + mf.open(midi_path) + mf.read() + mf.close() + if (remove_drums): + for i in range(len(mf.tracks)): + mf.tracks[i].events = [ev for ev in mf.tracks[i].events if ev.channel != 10] + # Since drums are traditionally located in MIDI channel 10. + + return midi.translate.midiFileToStream(mf) + + +def get_notes(folder_name, save_as_filename): + """ + Get all the notes and chords from the midi files in the ./midi_songs directory + """ + notes = [] + # I might want to add a new line that simplifies chords so that the trained + # weights are more generalized and can be used for more music during the prediction phase. + # Alternatively, complex chords are necessary for new MIDI inputs. + # The downside is the weights will be unique to a specific note file. + + + for file in glob.glob(folder_name + "/*.mid"): + #midi = converter.parse(file) + midi = open_midi(file, True) + + print("Parsing %s" % file) + + 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('notes_data/' + save_as_filename, 'wb') as filepath: + pickle.dump(notes, filepath) + + return notes + +def prepare_sequences(notes, n_vocab, seq_len): + """ Prepare the sequences used by the Neural Network """ + sequence_length = seq_len + + # 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] + sequence_out = notes[i + sequence_length] + 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 LSTM 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) + +def create_network(network_input, n_vocab, LSTM_node_count, Dropout_count): + """ create the structure of the neural network """ + model = Sequential() + model.add(LSTM( + LSTM_node_count, + input_shape=(network_input.shape[1], network_input.shape[2]), + recurrent_dropout= Dropout_count, + return_sequences=True + )) + model.add(LSTM( + LSTM_node_count, + return_sequences=True, + recurrent_dropout= Dropout_count,)) + model.add(LSTM(LSTM_node_count)) + model.add(BatchNorm()) + model.add(Dropout(Dropout_count)) + model.add(Dense(256)) + model.add(Activation('relu')) + model.add(BatchNorm()) + model.add(Dropout(Dropout_count)) + model.add(Dense(n_vocab)) + model.add(Activation('softmax')) + model.compile(loss='categorical_crossentropy', optimizer='rmsprop') + + return model + +def train(model, network_input, network_output, epoch, batchsize): + """ train the neural network """ + filepath = "trained_weights/" + "weights-improvement-{epoch:02d}-{loss:.4f}-bigger.hdf5" + checkpoint = ModelCheckpoint( + filepath, + monitor='loss', + verbose=0, + save_best_only= True, + mode='min' + ) + callbacks_list = [checkpoint] + + model.fit(network_input, + network_output, + epochs= epoch, + batch_size= batchsize, + callbacks= callbacks_list) + + + + + + + + + + diff --git a/musicPredict.py b/musicPredict.py new file mode 100644 index 0000000..811139f --- /dev/null +++ b/musicPredict.py @@ -0,0 +1,174 @@ +""" +This module generates notes for a midi file using the +trained neural network +""" + +import pickle +import numpy +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 LSTM +from keras.layers import BatchNormalization as BatchNorm +from keras.layers import Activation + +def generate_midi(test_output_name, notes_file, weight_file, + seq_len= 100, + LSTM_node_count= 512, Dropout_count= 0.3, + note_count= 50, + offset_count= 0.5): + """ + Generates a piano midi file. + + `notes_file` refers to the file generated using `train_network()`. + + `weight_file` refers to the file generated using `train_network()`. + + `note_count` = Number of notes you want to generate. 50 by default. + + `seq_len` = Number of notes in sequence to sample. 100 by default. + + `LSTM_node_count` = Number of nodes to be used in LSTM model per layer. 512 node by default. + + `Dropout_count` = Dropout parameter for LSTM mdoel. Accepted inputs from 0-1. Default is 0.3. + + `offset_count` = The lower the offset, the "faster" the tempo. Default is 0.5. + """ + ############################################ + #load the notes used to train the model + with open('notes_data/'+ notes_file, 'rb') as filepath: + notes = pickle.load(filepath) + + # Get all pitch names + pitchnames = sorted(set(item for item in notes)) + n_vocab = len(set(notes)) + + network_input, normalized_input = prepare_sequences(notes, pitchnames, n_vocab, seq_len) + model = create_network(normalized_input, n_vocab, weight_file, LSTM_node_count, Dropout_count) + prediction_output = generate_notes(model, network_input, pitchnames, n_vocab, note_count) + + create_midi(prediction_output, test_output_name, offset_count) + ############################################## + +def prepare_sequences(notes, pitchnames, n_vocab, seq_len): + """ Prepare the sequences used by the Neural Network """ + # map between notes and integers and back + note_to_int = dict((note, number) for number, note in enumerate(pitchnames)) + + sequence_length = seq_len + 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) + +def create_network(network_input, n_vocab, weight_file, LSTM_node_count, Dropout_count): + """ + Create the structure of the neural network. + `weight_file` refers to the file generated using `train_network()` + """ + model = Sequential() + model.add(LSTM( + LSTM_node_count, + input_shape=(network_input.shape[1], network_input.shape[2]), + recurrent_dropout= Dropout_count, + return_sequences=True + )) + model.add(LSTM( + LSTM_node_count, + return_sequences=True, + recurrent_dropout= Dropout_count,)) + model.add(LSTM(LSTM_node_count)) + model.add(BatchNorm()) + model.add(Dropout(Dropout_count)) + model.add(Dense(256)) + model.add(Activation('relu')) + model.add(BatchNorm()) + model.add(Dropout(Dropout_count)) + model.add(Dense(n_vocab)) + model.add(Activation('softmax')) + model.compile(loss='categorical_crossentropy', optimizer='rmsprop') + + # Load the weights to each node + model.load_weights("trained_weights/" + weight_file) + + return model + +def generate_notes(model, network_input, pitchnames, n_vocab, note_count): + """ + Generate notes from the neural network based on a sequence of notes + """ + # 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 N notes + for note_index in range(note_count): + 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)] + + return prediction_output + +def create_midi(prediction_output, test_output_name, offset_count): + """ + convert the output from the prediction to notes and create a midi file + from the notes. + `offset_count` = the note duration before the next one. The lower the offset, + the "faster" the melody. + """ + 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 += offset_count + + midi_stream = stream.Stream(output_notes) + + midi_stream.write('midi', fp= 'generated_songs/' + test_output_name + '.mid') + + From 1fd4e8558159aeeb6d9ca422f0d068f8b0c76e56 Mon Sep 17 00:00:00 2001 From: David Lim <59020919+neldivad@users.noreply.github.com> Date: Thu, 24 Dec 2020 16:34:35 +0800 Subject: [PATCH 2/3] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 806bdec..276f079 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This project allows you to train a neural network to generate midi music files t ## Training -To train the network you run **lstm.py**. +To train the network you run **musicLSTM.py**. E.g. @@ -27,7 +27,7 @@ The network will use every midi file in ./midi_songs to train the network. The m ## Generating music -Once you have trained the network you can generate text using **predict.py** +Once you have trained the network you can generate text using **musicPredict.py** E.g. From a006a5270c78e98d71048b1ff0c1e8466aaa27af Mon Sep 17 00:00:00 2001 From: David Lim <59020919+neldivad@users.noreply.github.com> Date: Thu, 24 Dec 2020 16:41:46 +0800 Subject: [PATCH 3/3] Update README.md --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 276f079..4898670 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ This project allows you to train a neural network to generate midi music files t ## Training -To train the network you run **musicLSTM.py**. +To train the network you run **musicLSTM.py**. Required to have a midi folder, a folder named `notes_data` to store the generated notes, and +a folder named `trained_weights` to store the .hdf5 files generated from training. E.g. ``` -python lstm.py +from musicLSTM import train_network +train_network() ``` The network will use every midi file in ./midi_songs to train the network. The midi files should only contain a single instrument to get the most out of the training. @@ -27,12 +29,14 @@ The network will use every midi file in ./midi_songs to train the network. The m ## Generating music -Once you have trained the network you can generate text using **musicPredict.py** +Once you have trained the network you can generate text using **musicPredict.py**. Select the note files from `notes_data` folder and weights file from `trained_weights` +folder, then save the generated midi output into a folder named `generated_songs`. E.g. ``` -python predict.py +from musicPredict import generate_midi +generate_midi() ``` You can run the prediction file right away using the **weights.hdf5** file