import IPython from IPython.display import Image, Audio from midi2audio import FluidSynth from music21 import corpus, converter, instrument, note, stream, chord, duration import matplotlib.pyplot as plt import time import pickle import keras from keras.callbacks import ModelCheckpoint, EarlyStopping from keras.utils.vis_utils import plot_model import os import numpy as np import glob from keras.layers import LSTM, Input, Dropout, Dense, Activation, Embedding, Concatenate, Reshape from keras.layers import Flatten, RepeatVector, Permute, TimeDistributed from keras.layers import Multiply, Lambda, Softmax import keras.backend as K from keras.models import Model from tensorflow.keras.optimizers import RMSprop from keras.utils import np_utils import seaborn as sns
Bach의 악보를 불러오는 작업
dataset_name = 'data/bach' filename = 'bach_846' file = "{}/{}.mid".format(dataset_name, filename) original_score = converter.parse(file).chordify()
- file 변수에 불러올 MIDI 형식의 악보 파일 경로를 문자열로 생성
- converter.parse(file)를 사용하여 MIDI 파일을 music21의 악보 객체로 파싱
- chordify() 메서드를 호출하여 악보를 화음(Chord)으로 변환
악보를 처리하고 음표와 박자 정보를 추출하여 두 개의 리스트에 저장
notes = [] durations = []
for element in original_score.flat: if isinstance(element, chord.Chord): notes.append('.'.join(n.nameWithOctave for n in element.pitches)) durations.append(element.duration.quarterLength)
if isinstance(element, note.Note): if element.isRest: notes.append(str(element.name)) durations.append(element.duration.quarterLength) else: notes.append(str(element.nameWithOctave)) durations.append(element.duration.quarterLength)
- flat : 악보를 평탄화(악보 안에 있는 모든 음표와 화음을 하나의 리스트로 만듦)
- if isinstance(element, chord.Chord) : 현재 처리 중인 요소가 화음인지 확인
- notes.append('.'.join(n.nameWithOctave for n in element.pitches)) : 화음 내의 음표들의 이름과 옥타브 정보를 추출하여 이들을 점으로 연결한 문자열을 notes 리스트에 추가
- durations.append(element.duration.quarterLength) : 화음의 박자 정보를 추출하여 durations 리스트에 추가, quarterLength 속성은 해당 박자의 길이를 쿼터노트 단위
- if isinstance(element, note.Note) : 현재 처리 중인 요소가 개별 음표(note.Note 객체)인지 확인
- if element.isRest : 음표가 쉼표인지 확인하고 쉼표인 경우, 해당 쉼표의 이름을 문자열로 변환하여 notes 리스트에 추가, 그렇지 않은 경우, 음표의 이름과 옥타브 정보를 추출하여 notes 리스트에 추가
- durations.append(element.duration.quarterLength) : 개별 음표의 박자 정보를 추출하여 durations 리스트에 추가
notes 와 durations 리스트에 저장된 처음 50개의 음표와 박자 정보를 출력
print('\nduration', 'pitch') idx = 0 for n,d in zip(notes,durations): if idx < 50: print(d, '\t', n) idx = idx + 1
- 음표와 박자 정보를 동시에 가져오기 위해 zip 함수를 사용
- 처음 50개의 음표와 박자 정보만 출력하고 나머지는 출력하지 않겠다는 제한
- 현재 음표의 박자 정보(d)와 음표(n)를 탭('\t')으로 구분하여 출력
MIDI 파일이 저장된 폴더에서 MIDI 파일 목록과 music21의 파서(converter) 객체를 가져오는 함수
def get_music_list(data_folder): file_list = glob.glob(os.path.join(data_folder, "*.mid")) parser = converter return file_list, parser
음악 생성을 위한 신경망 모델을 만드는 함수
- 모델은 음표와 박자 정보를 입력으로 받아 다음 음표와 박자를 출력으로 생성
def create_network(n_notes, n_durations, embed_size = 100, rnn_units = 256, use_attention = False): notes_in = Input(shape = (None,)) durations_in = Input(shape = (None,)) x1 = Embedding(n_notes, embed_size)(notes_in) x2 = Embedding(n_durations, embed_size)(durations_in) x = Concatenate()([x1,x2]) x = LSTM(rnn_units, return_sequences=True)(x)
- n_notes : 음표의 종류 수
n_durations : 박자의 종류 수
embed_size : 임베딩 차원 크기 (기본값: 100)
rnn_units : LSTM 유닛 수 (기본값: 256)
use_attention : 어텐션 메커니즘 사용 여부 (기본값: False)if use_attention: x = LSTM(rnn_units, return_sequences=True)(x) e = Dense(1, activation='tanh')(x) e = Reshape([-1])(e) alpha = Activation('softmax')(e) alpha_repeated = Permute([2, 1])(RepeatVector(rnn_units)(alpha)) c = Multiply()([x, alpha_repeated]) c = Lambda(lambda xin: K.sum(xin, axis=1), output_shape=(rnn_units,))(c) else: c = LSTM(rnn_units)(x)
notes_out = Dense(n_notes, activation = 'softmax', name = 'pitch')(c) durations_out = Dense(n_durations, activation = 'softmax', name = 'duration')(c) model = Model([notes_in, durations_in], [notes_out, durations_out])
if use_attention: att_model = Model([notes_in, durations_in], alpha) else: att_model = None
opti = RMSprop(lr = 0.001) model.compile(loss=['categorical_crossentropy', 'categorical_crossentropy'], optimizer=opti) return model, att_model
주어진 요소(음표, 박자)의 고유한 값들과 그 개수를 가져오는 함수
def get_distinct(elements): # Get all pitch names element_names = sorted(set(elements)) n_elements = len(element_names) return (element_names, n_elements)
요소 이름을 정수로 매핑하는 딕셔너리를 생성하는 함수
def create_lookups(element_names): # create dictionary to map notes and durations to integers element_to_int = dict((element, number) for number, element in enumerate(element_names)) int_to_element = dict((number, element) for number, element in enumerate(element_names)) return (element_to_int, int_to_element)
학습 데이터를 준비하여 LSTM 모델에 사용할 수 있는 형식으로 변환하는 함수
def prepare_sequences(notes, durations, lookups, distincts, seq_len =32): note_to_int, int_to_note, duration_to_int, int_to_duration = lookups note_names, n_notes, duration_names, n_durations = distincts notes_network_input = [] notes_network_output = [] durations_network_input = [] durations_network_output = []
- notes : 음표 시퀀스
durations : 박자 시퀀스
lookups : 요소를 정수로 매핑하는 딕셔너리들의 튜플
distincts : 요소의 고유한 값들과 개수의 튜플
seq_len : 시퀀스 길이 (기본값: 32)for i in range(len(notes) - seq_len): notes_sequence_in = notes[i:i + seq_len] notes_sequence_out = notes[i + seq_len] notes_network_input.append([note_to_int[char] for char in notes_sequence_in]) notes_network_output.append(note_to_int[notes_sequence_out]) durations_sequence_in = durations[i:i + seq_len] durations_sequence_out = durations[i + seq_len] durations_network_input.append([duration_to_int[char] for char in durations_sequence_in]) durations_network_output.append(duration_to_int[durations_sequence_out])
n_patterns = len(notes_network_input)
notes_network_input = np.reshape(notes_network_input, (n_patterns, seq_len)) durations_network_input = np.reshape(durations_network_input, (n_patterns, seq_len)) network_input = [notes_network_input, durations_network_input] notes_network_output = np_utils.to_categorical(notes_network_output, num_classes=n_notes) durations_network_output = np_utils.to_categorical(durations_network_output, num_classes=n_durations) network_output = [notes_network_output, durations_network_output] return (network_input, network_output)
주어진 확률 분포로부터 샘플을 추출하는 함수
def sample_with_temp(preds, temperature): if temperature == 0: return np.argmax(preds) else: preds = np.log(preds) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) return np.random.choice(len(preds), p=preds)
- 온도(temperature)에 따라 다음 음표나 박자를 생성
- 온도가 높은 경우 (높은 다양성):
온도가 높을수록 확률 분포가 균등해지고, 각 항목의 확률이 비슷해진다
이로 인해 다양한 결과가 생성되며, 예측이 더 무작위적이고 다양해진다- 온도가 낮은 경우 (낮은 다양성):
온도가 낮을수록 확률 분포가 뾰족해지고, 가장 확률이 높은 항목이 선택될 가능성이 높아진다
이로 인해 생성된 결과가 더 일관되며, 예측이 더 예측 가능해진다
음악 생성을 위한 학습과 관련된 설정과 경로를 정의
# run params run_folder = 'data/' store_folder = os.path.join(run_folder, 'store') data_folder ='data/bach'
- run_folder : 프로그램 실행과 관련된 파일과 폴더의 경로
- store_folder : 학습 중에 생성되는 중간 결과물을 저장할 폴더 경로
- data_folder : MIDI 파일이 저장된 폴더의 경로
if not os.path.exists('store'): os.mkdir(os.path.join(run_folder, 'store')) os.mkdir(os.path.join(run_folder, 'output')) os.mkdir(os.path.join(run_folder, 'weights')) os.mkdir(os.path.join(run_folder, 'viz'))
- 'store', 'output', 'weights', 'viz' 폴더가 존재하지 않는 경우, 이를 생성
mode = 'build' # data params intervals = range(1) seq_len = 32 # model params embed_size = 100 rnn_units = 256 use_attention = True
- 학습 모드를 정의('build' 모드)
- intervals = range(1) : 음악 생성에 사용할 시간 간격(interval)을 정의
mode 변수의 값이 'build'인 경우와 그렇지 않은 경우를 처리
if mode == 'build': music_list, parser = get_music_list(data_folder) print(len(music_list), 'files in total') notes = [] durations = []
for i, file in enumerate(music_list): print(i+1, "Parsing %s" % file) print(file) original_score = parser.parse(file).chordify() for interval in intervals: score = original_score.transpose(interval) notes.extend(['START'] * seq_len) durations.extend([0]* seq_len)
- for interval in intervals : 정의된 시간 간격(interval) 목록에 대해 아래의 코드를 실행
- score = original_score.transpose(interval) : 현재 interval을 사용하여 악보를 (transpose)이동
- notes.extend(['START'] * seq_len) : 학습 데이터의 시작 부분에 'START' 문자열을 seq_len 만큼 추가
- durations.extend([0] * seq_len) : 학습 데이터의 시작 부분에 0을 seq_len 만큼 추가
for element in score.flat: if isinstance(element, note.Note): if element.isRest: notes.append(str(element.name)) durations.append(element.duration.quarterLength) else: notes.append(str(element.nameWithOctave)) durations.append(element.duration.quarterLength) if isinstance(element, chord.Chord): notes.append('.'.join(n.nameWithOctave for n in element.pitches)) durations.append(element.duration.quarterLength)
- if isinstance(element, note.Note) : 현재 요소가 음표(note.Note)인 경우
if element.isRest : 음표가 쉼표인 경우, 해당 쉼표의 이름을 문자열로 변환하여 'notes' 리스트에 추가하고, 박자 정보를 'durations' 리스트에 추가
그렇지 않은 경우, 음표의 이름과 옥타브 정보를 문자열로 변환하여 'notes' 리스트에 추가하고, 박자 정보를 'durations' 리스트에 추가- if isinstance(element, chord.Chord) : 현재 요소가 화음(chord.Chord)인 경우
화음 내의 음표들의 이름과 옥타브 정보를 결합하여 하나의 문자열로 만들고, 이 문자열을 'notes' 리스트에 추가하고, 박자 정보를 'durations' 리스트에 추가with open(os.path.join(store_folder, 'notes'), 'wb') as f: pickle.dump(notes, f) with open(os.path.join(store_folder, 'durations'), 'wb') as f: pickle.dump(durations, f) else: with open(os.path.join(store_folder, 'notes'), 'rb') as f: notes = pickle.load(f) with open(os.path.join(store_folder, 'durations'), 'rb') as f: durations = pickle.load(f)
- 'notes'와 'durations' 리스트를 바이너리 파일로 저장
- 저장된 'notes'와 'durations' 리스트를 바이너리 파일에서 읽어옴
- 바이너리 파일은 텍스트 파일과는 다르게, 텍스트가 아닌 이진(binary) 형태로 데이터를 저장한다
이진 형태는 0과 1로 구성된 이진 코드로 표현됨
바이너리 파일은 텍스트 파일처럼 각 데이터 값 사이에 공백이나 줄 바꿈 문자가 없음
음표와 박자에 대한 고유한 값들을 가져오고, 이를 정수와 문자열 간의 매핑을 위한 딕셔너리로 만들어 저장
# get the distinct sets of notes and durations note_names, n_notes = get_distinct(notes) duration_names, n_durations = get_distinct(durations) distincts = [note_names, n_notes, duration_names, n_durations]
with open(os.path.join(store_folder, 'distincts'), 'wb') as f: pickle.dump(distincts, f)
- 'distincts' 리스트를 바이너리 파일로 저장
# make the lookup dictionaries for notes and dictionaries and save note_to_int, int_to_note = create_lookups(note_names) duration_to_int, int_to_duration = create_lookups(duration_names) lookups = [note_to_int, int_to_note, duration_to_int, int_to_duration]
with open(os.path.join(store_folder, 'lookups'), 'wb') as f: pickle.dump(lookups, f)
- 'lookups' 리스트를 바이너리 파일로 저장
note_to_int 딕셔너리의 내용을 출력
print('\nnote_to_int') for i, item in enumerate(note_to_int.items()): if i < 10: print(item)
학습 데이터를 모델 학습에 사용할 수 있는 형식으로 변환
network_input, network_output = prepare_sequences(notes, durations, lookups, distincts, seq_len)
네트워크의 입력 및 출력 데이터 예제를 출력
print('pitch input') print(network_input[0][0]) print('duration input') print(network_input[1][0]) print('pitch target') print(network_output[0][0]) print('duration target') print(network_output[1][0])
- "pitch input"은 음표 입력, "duration input"은 박자 입력, "pitch target"은 음표 출력, "duration target"은 박자 출력
create_network 함수를 사용하여 모델을 생성
model, att_model = create_network(n_notes, n_durations, embed_size, rnn_units, use_attention) model.summary()
- 음표와 박자 정보를 입력으로 받고, 음표와 박자를 출력으로 생성하는 신경망 모델을 만든다
모델의 구조를 시각화하여 이미지 파일로 저장
plot_model(model, to_file=os.path.join(run_folder ,'viz/model.png'), show_shapes = True, show_layer_names = True)
콜백(callbacks)을 설정하고 모델을 학습
weights_folder = os.path.join(run_folder, 'weights')
- 모델 가중치(weight) 파일이 저장될 폴더의 경로를 설정
checkpoint1 = ModelCheckpoint( os.path.join(weights_folder, "weights-improvement-{epoch:02d}-{loss:.4f}-bigger.h5"), monitor='loss', verbose=0, save_best_only=True, mode='min' )
- 모델 학습 중에 가장 낮은 손실(loss)을 가진 모델 가중치를 저장하기 위한 체크포인트(callbacks)를 설정
- 첫 번째 체크포인트(checkpoint1)는 에포크(epoch)와 손실 값을 포함하는 파일 이름으로 모델 가중치를 저장
checkpoint2 = ModelCheckpoint( os.path.join(weights_folder, "weights.h5"), monitor='loss', verbose=0, save_best_only=True, mode='min' )
- 두 번째 체크포인트(checkpoint2)는 단순히 "weights.h5"로 모델 가중치를 저장
early_stopping = EarlyStopping( monitor='loss' , restore_best_weights=True , patience = 10 )
- 조기 종료(callback)를 설정
- 이 콜백은 학습 중에 손실이 더 이상 감소하지 않을 때 학습을 조기 종료하고, 최상의 가중치로 복원한다
- patience 매개변수는 손실이 개선되지 않은 에포크 수를 나타낸다
- 여기서는 10 에포크 동안 손실이 감소하지 않으면 조기 종료한다
callbacks_list = [ checkpoint1 , checkpoint2 , early_stopping ]
model.save_weights(os.path.join(weights_folder, "weights.h5")) model.fit(network_input, network_output , epochs=2000000, batch_size=32 , validation_split = 0.2 , callbacks=callbacks_list , shuffle=True )
- 모델의 초기 가중치를 "weights.h5" 파일로 저장
- 모델을 학습한다
- network_input은 모델의 입력 데이터, network_output은 모델의 출력 데이터
- callbacks에는 설정한 콜백들을 전달한다
음악 생성에 사용되는 매개변수와 초기 시퀀스를 설정
# prediction params notes_temp=0.5 duration_temp = 0.5 max_extra_notes = 50 max_seq_len = 32 seq_len = 32
- notes_temp=0.5 및 duration_temp = 0.5 : 음표와 박자를 생성할 때 사용되는 온도값을 설정
- max_extra_notes = 50 : 생성할 수 있는 최대 추가 음표 개수를 설정
- max_seq_len = 32 : 생성될 시퀀스의 최대 길이를 설정
- seq_len = 32 : 모델에 입력되는 시퀀스의 길이를 설정
notes = ['START'] durations = [0]
- notes = ['START'] 및 durations = [0] : 초기 음표와 박자 시퀀스를 설정
- 'START'는 학습 데이터의 시작을 나타내는 문자열로, 초기 시퀀스의 첫 번째 요소로 사용된다(초기 박자는 0으로 설정)
if seq_len is not None: notes = ['START'] * (seq_len - len(notes)) + notes durations = [0] * (seq_len - len(durations)) + durations
- notes = ['START'] * (seq_len - len(notes)) + notes : 입력 시퀀스 길이(seq_len)와 현재 음표 시퀀스 길이(len(notes))를 비교하여 부족한 부분을 'START' 문자열로 채워넣는다
이렇게 하면 입력 시퀀스의 길이가 seq_len이 됨- durations = [0] * (seq_len - len(durations)) + durations : 입력 시퀀스 길이(seq_len)와 현재 박자 시퀀스 길이(len(durations))를 비교하여 부족한 부분을 0으로 채워넣는다
이렇게 하면 입력 시퀀스의 길이가 seq_len이 됨sequence_length = len(notes)
- 최종적으로 설정된 시퀀스의 길이를 sequence_length 변수에 저장
모델을 사용하여 음악을 생성