본문 바로가기
  • 우당탕탕속의 잔잔함
Programming/Sound Analysis

[Deep Learning] LSTM 예측 모델을 이용한 작곡가 프로그램 개발

by zpstls 2023. 2. 8.
반응형

 

 

이번 포스트에서는 예측 모델을 통한 작곡 프로그램을 만들어보고자 합니다.

이전에 Music21과 관련된 글을 작성했었는데 이 Toolkit을 이용해 음성 데이터를 다룰 것입니다.

 

 

우선 Music21과 관련된 글은 다음과 같습니다.

 

[Toolkit] 음악학에 활용되는 Music21 사용 방법

이번 포스트에서는 음악과 관련된 작업을 수행할 때 활용되는 Python Toolkit인 Music21에 대해 다뤄볼 것입니다. Music21이란 무엇인지, 어떻게 활용하면 좋을지 등에 관해 작성해봅니다. Music21이란, 음

mj-thump-thump-story.tistory.com

 

 

그럼, 본격적으로 개발을 진행해보겠습니다.

이번 개발의 목표는 어떠한 MIDI 음악 파일을 Input으로 하여 LSTM Model을 학습시키고 해당 학습 결과를 이용해 Input 되었던 음악과 비슷한 풍을 가진 새로운 음악을 생성하는 것입니다.

 

개발할 프로그램의 전반적인 Flow는 다음과 같습니다.

Whole Flow Of Program

Dataset들을 구성하고 해당 Dataset을 Load 하여 Pre-Processing을 수행할 것입니다. 이때 Processing 과정에서는 음성 데이터를 Note, Chord, Pitch 등의 요소로 분리하고 해당 데이터들을 Sequence로 나누고 Dictionary화 하고, Normalization, One-Hot Encoding 등의 과정을 수행합니다.

이후, Deep Learninig Model에 넣어 학습시키고 학습 결과 Model을 생성합니다. Random Seqeunce를 생성하여 결과 Model에 Input 하면 한 개의 음계가 예측되는데, 이러한 예측들을 모아 하나의 곡을 완성시킵니다.

 

먼저 Dataset으로 사용할 MIDI 음악을 수집하여 ZIP 파일로 묶습니다. 참고로 저는 모차르트의 음악으로 Dataset을 구성하였습니다. 데이터셋 구성 부분은 간단하므로 생략하도록 하겠습니다.

이제 다음과 같은 코드를 통해 Dataset에 해당되는 MIDI 파일을 Read 하고 Note, Chord, Tempo로 분류하여 저장합니다.

notes = []
for i,file in enumerate(glob.glob("midi_songs_dataset/*.mid")):
  midi = converter.parse(file) 
  
  notes_to_parse = None
  try:
    parts = instrument.partitionByInstrument(midi)
  except TypeError:
    print('{} file occur error.'.format(file))

  if parts:
    notes_to_parse = parts.parts[0].recurse() 
  else :
    notes_to_parse = midi.flat.notes

  for e in notes_to_parse:
    if isinstance(e, note.Note):
      notes.append(str(e.pitch))
    elif isinstance(e, chord.Chord):
      notes.append('.'.join(str(n) for n in e.normalOrder))

Music21에서는 Instrument라는 Class 구조가 있습니다. 여기에는 partId, partName, instrumentId, instrumentName, midiProgram, midiChannel, lowestNote, highestNote 등의 Arrtibute들을 포함하고 있는데, 이러한 구조를 통해 Load 한 File이 단일 스트림, 스코어, 멀티 파트 구조인지 확인하여 각 요소별로 구분해 저장할 수 있도록 합니다.

Read 한 File이 instrument parts를 가지고 있는 경우와 flat structure에 Notes를 가지고 있는 경우로 구분해 각각 처리해 줍니다. Note인 경우는 Pitch 값을, Chord인 경우는 각 Note의 Pitch 값을 나눠서 저장합니다.

 

이제, Load 한 MIDI 파일에 전처리 과정을 수행해주어야 합니다. 해당 부분에 대한 코드는 다음과 같습니다.

sequence_len = 100
pitchnames = sorted(set(item for item in notes))
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))

net_in = []
net_out = []

for i in range(0, len(notes) - sequence_len):
  seq_in = notes[i:i + sequence_len] 
  seq_out = notes[i + sequence_len]
  
  net_in.append([note_to_int[char] for char in seq_in])
  net_out.append(note_to_int[seq_out])

n_patterns = len(net_in)
n_vocab = (len(set(notes)))
net_in = np.reshape(net_in, (n_patterns, sequence_len, 1))
net_in = net_in / float(n_vocab)
net_out = np_utils.to_categorical(net_out)

LSTM에 넣을 Sequence 길이를 100으로 지정합니다. 100의 단위로 음악을 끊어서 넣을 것입니다. 따라서 n_patterns는 [전체 패턴 길이 - 100]을 갖게 됩니다.

Network의 Input(= Data)은 (Sample 수, Sequence 길이, Data의 차원)의 형태로 변환합니다. 그리고 0 ~ 1 사이의 값으로 정규화시켜 주기 위해 n_vocab, 즉 중복되지 않은 note들의 수로 나눠줍니다.

Network Output(= Label)은 One-Hot Vector로 만들어줍니다.

 

위와 같이 전처리된 데이터를 다음과 같은 Model에 넣어 학습시켜주어야 합니다. 우선, Model에 대한 부분은 다음과 같습니다.

model = Sequential(name="Music_Creator_LSTM")
model.add(CuDNNLSTM(512, input_shape=(net_in.shape[1], net_in.shape[2]), return_sequences=True))
model.add(Dropout(rate=0.3))
model.add(CuDNNLSTM(512, return_sequences=True))
model.add(Dropout(rate=0.3))
model.add(CuDNNLSTM(512))
model.add(Dense(256))
model.add(Dropout(rate=0.3))
model.add(Dense(n_vocab, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
model.summary()

Structure Of Model

LSTM Model을 이용해 Sequence가 있는 데이터를 처리해 줄 것입니다. CuDNNLSTM과 Dropout만으로 이루어진 간단한 구조입니다. 마지막 부분에, 예측(= 하나의 음정 예측)을 위해 Dense Layer를 배치하고 Activation 함수로 Softmax를 사용합니다.

 

이제 다음과 같은 기본적인 학습 방식을 통해 학습을 수행할 것입니다.

filepath = "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(net_in, net_out, epochs=100, batch_size=64, callbacks=callbacks_list)
model.save('Music_Creator_LSTM_lastver.h5')
del model

중간에 Callback 함수를 두어 다 나은 Loss 값일 경우 Model 파일을 저장하도록 합니다. 참고로 Loss가 0.7 ~ 0.8 정도 되면 작곡을 문제없이 수행할 수 있다고 합니다.

 

 

 

 

 

학습이 완료되었다면, 이제 Prediction을 통해 음악을 생성해야 합니다. 학습 결과 Model을 Load 합니다.

from keras.models import load_model

model = load_model('preTrained_model.hdf5')

이제, Prediction을 위한 Data Format을 만들어주어야 합니다. 해당 부분에 대한 코드는 다음과 같습니다.

start_seqence = np.random.randint(0, len(net_in)-1)
pattern = net_in[start_seqence]
int_to_note = dict((number, note) for number, note in enumerate(pitchnames))

prediction_output = []

for i in range(0, 500):
  pred_in = np.reshape(pattern, (1, len(pattern), 1))
  pred_in = pred_in / float(n_vocab)

  prediction = model.predict(pred_in, verbose=0)
  index = np.argmax(prediction)
  result = int_to_note[index]
  prediction_output.append(result)
  pattern.append(index)
  pattern = pattern[1:len(pattern)]

offset = 0 
output_notes = []

for pattern in prediction_output:
    
    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)

    else:
        new_note = note.Note(pattern)
        new_note.offset = offset
        new_note.storedInstrument = instrument.Piano()
        output_notes.append(new_note)

    offset += 0.5

midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='Creator_Output.mid')

Dataset의 전체 입력 시퀀스 중 랜덤 하게 선택하여 초기 Input 값(= 100 길이의 Sequence)을 생성합니다. 또한 정수 값을 Note로 바꿔주는 Dictionary 자료형을 생성해 줍니다.

이러한 값을 학습된 Model에 넣어줍니다. 그러면 한 음정에 대한 Prediction을 수행하게 되고 이들을 모아서 MIDI Stream으로 생성하고 저장합니다.

 

Loss 별로, 최종적으로 생성된 곡은 다음과 같습니다.

 

>> Loss : 0.619

 

>> Loss : 0.532

 

>> Loss : 0.300

 

>> Loss : 0.137

 

이 음악이 모차르트 풍인지는 모르겠으나, 결과물이 생각보다 좋게 나왔습니다. (너무 기대감이 없었을지도 모르네요...ㅎ)

실제 음악 전공자분들이 들으시면 이상한 곡이라고 생각될 수도 있겠지만, 중간중간 의미 있는 멜로디가 있긴 한 것 같습니다. 따라서 전체 곡을 사용하기보다는 특정 부분을 사용해 작곡에 참고할 수 있지 않을까 싶습니다.

 

이제까지 입력 곡과 비슷한 성향을 가진 어떠한 곡을 생성해 보는 과정을 수행해 보았습니다.

보통 GAN이 데이터 생성에 많이 사용된다고 하지만, LSTM과 같은 시계열 예측 모델을 통해서도 데이터를 생성할 수 있습니다. 어쩌면 실제 작곡도 이런 방식으로 머릿속에서 수행되는 것은 아닌지 싶네요...ㅎ

 

이번 포스트는 여기서 마무리하도록 하겠습니다.

 

 

반응형

댓글