티스토리 뷰

실습

Keras Chopin LSTM Source Code

MaxedSet 2019. 11. 21. 04:42

Google Colab에서 작성된 코드입니다.

Jupyter Notebook 과 달리, Google Colab의 Tab은 띄어쓰기 4칸이 아닌 2칸입니다.

Jupyter Notebook 에 쓰실 때 이 점 유의하시길 바랍니다. 

 

다른 저장소로부터 Chopin 피아노곡의 MIDI 파일을 받아

Chopin풍의 노래를 작곡하는 LSTM 모델을 학습시켜 봅시다.

 

MIDI 파일은 구글 드라이브에 올려야하며, MIDI 파일을 가져와 쓰기 위해서 연동해주어야 합니다.

# 본인의 구글 드라이브 → 지금 실행중인 코드

# google.colab.drive : 구글 드라이브에서 파일을 가져오기 위한 코드를 담고 있다.
from google.colab import drive

# 본인의 구글 드라이브를 '/gdrive' 라는 경로로 하여 쓸 수 있다.
drive.mount('/gdrive', force_remount=True)

 

music21 라이브러리는 구글 서버에도 설치되어 있지 않는 경우가 많습니다.

!는 Jupyter notebook을 통해 리눅스 명령어를 사용 가능하게 해줍니다.

# 악보 파일 (MIDI 파일)을 다루기 위한 라이브러리 music21 설치
!pip install music21

 

현재 내 코드가 실행될 GPU의 정보를 확인할 수 있습니다.

!nvidia-smi

 

필요한 라이브러리들을 불러옵시다

import glob, pickle # 파일 불러오기에 유용한 라이브러리들

import numpy as np # 행렬 계산

# MIDI 파일을 다루기 위한 라이브러리
from music21 import converter, instrument, note, chord, stream 

# 순차 모델 생성을 위한 라이브러리
from keras.models import Sequential

# LSTM : CPU 동작 / CuDNNLSTM : GPU 동작
from keras.layers import Dense, Dropout, LSTM, CuDNNLSTM

# One-hot Vector 만들기 위한 라이브러리
from keras.utils import np_utils

 

MIDI 파일 내의 음정, 박자 정보를 통해 파일 구조를 파악합시다.

출력의 알파벳과 숫자는 Chord / Note 의 음 높낮이(Pitch), 뒤의 숫자(offset)는 박자를 나타냅니다.

# MIDI 파일을 잘 불러왔는지 테스트

# MIDI 파일을 불러오는 함수
midi = converter.parse("/gdrive/My Drive/Colab Notebooks/chopin/chpn-p9_format0.mid")

# MIDI 파일 내의 notes(음정, 박자를 포함하는 정보)를 불러온다
notes_to_parse = midi.flat.notes

# 불러온 notes의 갯수
print(np.shape(notes_to_parse))

# 10개 테스트 출력
for e in notes_to_parse[:20]:
  print(e, e.offset)

# Note / Chord 두 종류로 나뉜다, Chord는 Note의 집합이다

이번 코드에선 박자 (오른쪽 숫자) 는 고려하지 않았습니다.

 

여러 MIDI 파일의 음정 정보를 한꺼번에 가져와 하나의 데이터로 다룹니다.

이 코드에선 50개의 MIDI 파일을 하나로 합친 뒤 Note / Chord 단위로 나눠줍니다.

# Chopin 폴더의 모든 MIDI 파일의 정보를 뽑아 하나로 만든다

# MIDI 파일로부터 Note 정보만 뽑아서 저장할 리스트
notes = []

# chopin 폴더 내의 모든 MIDI 파일에 반복문으로 접근
# glob.glob() : *를 제외한 나머지 부분이 같은 파일 이름들을 배열로 저장
# enumerate : 파일이름 배열을 순차적으로 하나씩 file에 넣는다
# i : 0 부터 1씩 증가 / file : 각 파일 이름
for i,file in enumerate(glob.glob("/gdrive/My Drive/Colab Notebooks/chopin/*.mid")):

  # midi: MIDI 파일의 전체 정보를 담고 있다 ------------------------------------------
  midi = converter.parse(file) 
  print('\r', 'Parsing file ', i, " ",file, end='') # 현재 진행 상황 출력

  # notes_to_parse : MIDI 파일을 Notes로 나누어 다루기 위한 변수
  notes_to_parse = None

  # try / except : try 수행 중 에러 발생 시 except 수행 -----------------------------
  # MIDI 파일 구조 차이로 인한 에러 방지

  # MIDI 파일의 Note / Chord / Tempo 정보만 가져온다
  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

  # Note / Chord / Tempo 정보 중 Note, Chord 의 경우 따로 처리, Tempo 정보는 무시 ----
  for e in notes_to_parse:

    # Note 인 경우 높이(Pitch), Octave 로 저장
    if isinstance(e, note.Note):
      notes.append(str(e.pitch))

    # Chord 인 경우 각 Note의 음높이(Pitch)를 '.'으로 나누어 저장
    elif isinstance(e, chord.Chord):

      # ':'.join([0, 1, 2]) : [0, 1, 2] -> [0:1:2]
      # str(n) for n in e.normalOrder 
      #     => e.normalOrder 라는 배열 내의 모든 원소 n에 대해 str(n) 해준 새 배열을 만든다.
      #        ex) str(i) for i in [1, 2, 3] => ['1', '2', '3']
      notes.append('.'.join(str(n) for n in e.normalOrder))

해당 경로의 MIDI 파일 50개가 잘 Parsing 되었다는 출력 

 

각기 음정 (Note / Chord) 마다 각각 다른 정수에 할당합니다.

여기서 쓰는 각각의 음정이 456가지이므로 1 ~ 456 에 각각 음이 할당되게 됩니다.

# MIDI 파일 정보를 다루기 쉽게 바꿔준다

# n_vocab : 모델 출력의 가짓수를 정하기 위해 Note의 총 가짓수를 센다
# set() : 중복되는 원소는 한번만 쓴다 / ex) set("Hello") => {'e', 'H', 'l', 'o'}
n_vocab = (len(set(notes)))
print('Classes of notes : ', n_vocab, '\n')

print('notes : ', notes[:500])
print('length of notes : ', len(notes), '\n')

# pitchnames : notes 배열의 모든 가능한 Note / Chord 를 정렬해놓은 배열  
pitchnames = sorted(set(item for item in notes))
print('pitchnames : ', pitchnames)
print('length of pitchnames : ', len(pitchnames), '\n')

# create a dictionary to map pitches to integers
# 음높이(Pitch)를 정수에 매핑하는 dictionary 자료형 생성
# ex) dict = {'key': value} => dict['key'] = value
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))

print('note_to_int : ', note_to_int)

각 Note 및 Chord가 각각의 정수로 할당된 것을 알 수 있다.

 

LSTM 모델에 필요한 Dataset을 만들어줍니다.

시계열을 100개씩 잘라주는 이유는 LSTM 학습 시 BPTT가 무한하게 역전파될 수 없기 때문입니다.

# LSTM 모델을 위한 Training Dataset 생성
# 곡을 배끼는 것이 아닌 작곡이기 때문에 Validation이 필요없다

seq_len = 100 # 시퀀스 길이

# Pitch를 정수로 바꾸어 LSTM 모델의 입출력으로 만들어준다
net_in = []
net_out = []

# LSTM 모델의 입출력을 만들기 위해 ( 전체 길이 - 시퀀스 길이(=100) ) 만큼 반복
# ex) 입력 : 출력 짝지어주기
#     [0 ~ 99] : [100] / [1 ~ 100] : [101] / ... / [전체 길이-100 ~ 전체 길이-1] : [전체 길이]
for i in range(0, len(notes) - seq_len):

  # LSTM 모델 입력과 출력을 만들어준다
  seq_in = notes[i:i + seq_len] # ex) [0:100] => [0 ~ 99]
  seq_out = notes[i + seq_len]  # ex) [100]

  # LSTM은 문자열이 아닌 숫자를 입출력으로 하므로 문자열을 정수로 바꿔야 한다
  net_in.append([note_to_int[char] for char in seq_in]) # 배열 안의 모든 원소에 대해 실행
  net_out.append(note_to_int[seq_out]) # 출력값 하나에 대해 실행

print(np.shape(net_in))
print(np.shape(net_out))

55535개의 데이터-라벨 쌍 생성

만들어준 Dataset에 전처리를 해줍니다.

데이터의 경우 1 ~ 456 범위에서 0 ~ 1 범위로,

라벨의 경우 1차원 값에서 456차원 One-hot Vector로 만들어줍니다.

# LSTM 모델 입출력에 맞게 Dataset 전처리

# 시퀀스 길이(100) 만큼을 빼고 반복했으므로 100개 적은 패턴이 생긴다
n_patterns = len(net_in)
print('n_patterns : ', n_patterns)

# reshape the input into a format compatible with LSTM layers
# LSTM 입력에 맞는 모양으로 바꿔준다 : (샘플 수, 시퀀스 길이, 자료의 차원)
net_in = np.reshape(net_in, (n_patterns, seq_len, 1))
print('shape of net_in : ', net_in.shape)

# 데이터 범위 정규화 : 0 ~ (n_vocab - 1) => 0 ~ 1
net_in = net_in / float(n_vocab)

# 분류이므로 출력을 One-hot Vector로 만들어주어야 한다.
net_out = np_utils.to_categorical(net_out)
print('shape of net_out : ', net_out.shape)

실제 LSTM에 쓰일 Dataset의 형태

LSTM 모델을 구성해줍니다.

3층 짜리 Stacked LSTM 으로 마지막 층은 Many to One 형태이며, 각 층에 Dropout을 적용하였습니다.

456가지의 음정 중 하나를 선택하는 작업이므로 분류 작업이며, 출력 층 활성화 함수는 Softmax가 됩니다.

# 모델 구성

# 데이터의 Feature(특징) 수 or Dimension(차원)
data_dim = net_in.shape[2]

# GPU 환경 : CuDNNLSTM() / CPU 환경 : LSTM()
model = Sequential(name="Chopin_LSTM")

# return_sequences : True : Many to Many / False : Many to One
# seq_len : 입력으로 넣을 시계열 데이터의 길이 / data_dim : 각 데이터의 차원

model.add(CuDNNLSTM(512, input_shape=(seq_len, data_dim), return_sequences=True))
# GPU / CUDA / CuDNN 이 없는 환경에선 CuDNNLSTM만 LSTM으로 바꾸어 쓰면 됩니다.
# model.add(LSTM(512, input_shape=(seq_len, data_dim), 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()

3층 LSTM + 2층 MLP

기본적인 학습 방법을 쓰므로 fit 함수를 사용합니다.

# 모델 학습
# 음악 작곡이기 때문에 Validation Set이 없다
# LSTM : 13시간 / CuDNN : 3시간 반
# loss 는 0.7 ~ 0.8 만 되어도 충분히 작곡 능력을 갖는다.
model.fit(net_in, net_out, epochs=75, batch_size=64)

Epoch을 더 줄여도 학습은 가능하다.

 

학습된 모델을 사용하기 위해 LSTM 입력을 다시 만들어주겠습니다.

# 작곡을 위해 LSTM 모델 입력을 다시 만든다
# 위와 동일하므로 주석 생략

net_in = []
output = []

for i in range(0, len(notes) - seq_len, 1):
  seq_in = notes[i:i + seq_len]
  seq_out = notes[i + seq_len]

  net_in.append([note_to_int[char] for char in seq_in])
  output.append(note_to_int[seq_out])
  
n_patterns = len(net_in)

 

LSTM 모델은 초기에 길이 100의 시계열 데이터 (시퀀스)를 받으면 그 뒤에 한 음씩 작곡해냅니다.

# LSTM 모델이 작곡을 시작하기 위해 시작점으로써 랜덤한 시퀀스를 골라야 한다

# pattern : Dataset의 입력 전체 시퀀스 중 랜덤하게 고른 시퀀스
start = np.random.randint(0, len(net_in)-1)
pattern = net_in[start]
print('Random Sequence : ', pattern)

# int_to_note: 정수를 다시 Note로 바꾸기 위한 dictionary 자료형
int_to_note = dict((number, note) for number, note in enumerate(pitchnames))
print('int_to_note : ', int_to_note)

1 ~ 456 사이의 값 100개를 랜덤하게 만든 뒤 음정에 할당된 값으로 바꿔줍니다. 

 

이제 초기 100개 음정을 갖는 LSTM 모델이 500개의 음정을 작곡해냅니다.

# LSTM 모델이 만든 출력값을 저장하기 위한 빈 리스트
pred_out = []

# generate 500 notes
for i in range(0, 500):

  # 랜덤하게 고른 시퀀스를 LSTM 모델 입력에 맞게 바꿔준다
  pred_in = np.reshape(pattern, (1, len(pattern), 1))

  # 입력 범위 정규화 / 0 ~ (n_vocab -1) => 0 ~ 1
  pred_in = pred_in / float(n_vocab)

  # LSTM 모델 사용
  prediction = model.predict(pred_in, verbose=0)

  # 출력 중 값이 가장 큰 Index 선택
  index = np.argmax(prediction)

  # 정수 값을 Note 값으로 변경
  result = int_to_note[index]
  print('\r', 'Predicted ', i, " ",result, end='')
  
  # LSTM이 만든 Note를 하나씩 리스트에 담는다
  pred_out.append(result)

  # 다음 입력을 위해 입력에 새 값 추가 후 가장 과거 값 제거
  # ex) [0:99] -> [1:100] -> ... -> [n : n + 99]
  pattern.append(index)
  pattern = pattern[1:len(pattern)]

 

LSTM이 출력한 500개의 음정을 확인해봅시다.

학습이 제대로 안 된 경우, 같은 음정만을 출력할수도 있습니다.

print('length of pred_out : ', len(pred_out))
print('pred_out : ', pred_out)

LSTM이 생성해낸 음정(Chord / Note)들을 볼 수 있다

 

LSTM 작곡 결과를 다시 우리가 들을 수 있게 MIDI 파일로 만들어줍니다.

Code가 실행되면 Google Drive에 새로운 MIDI 파일이 생깁니다.

우리는 박자를 고려하지 않았기 때문에 모든 음이 같은 박자로 재생됩니다.

# LSTM 모델이 예측한 값들로부터 MIDI 파일을 만들어준다

offset = 0 # 음(Note/Chord)을 언제 들려줄지 정하는 timing offset (박자 정보를 대신 함)

# MIDI 파일 생성을 위한 빈 리스트
output_notes = []

# create note and chord objects based on the values generated by the model
# LSTM 모델 예측 값을 하나씩 처리
for pattern in pred_out:

    # pattern이 Chord 일 때
    if ('.' in pattern) or pattern.isdigit():
        notes_in_chord = pattern.split('.') # ['8.1'].split('.') => ['8', '1']
        notes = [] # Note 정보를 저장할 빈 리스트

        # notes_in_chord의 텍스트를 Note 정보로 바꿔준다
        for current_note in notes_in_chord:
            new_note = note.Note(int(current_note)) # Text => 정수 => Note
            new_note.storedInstrument = instrument.Piano() # 악기 정보 설정
            notes.append(new_note) # notes 리스트에 더해준다

        # Note => Chord
        new_chord = chord.Chord(notes)
        new_chord.offset = offset # 시간 정보 설정
        output_notes.append(new_chord)

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

    # 반복마다 offset을 증가시킨다 (고정 박자)
    offset += 0.5

# Note/Chord => Stream => MIDI File
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='/gdrive/My Drive/test_output.mid')

 

학습이 잘 된 LSTM 모델을 저장해줍시다.

# 모델은 h5 파일 형태로 저장됩니다
# 경로에 주의합시다
model.save('/gdrive/My Drive/Chopin_LSTM.h5')

# 모델을 불러오기 위해 지워줍니다
del model

# 저장되어 있는 모델을 불러오기 위한 load_model 함수
from keras.models import load_model

# 'model' 에 해당 모델을 불러옵니다
model = load_model('/gdrive/My Drive/Chopin_LSTM.h5')

'실습' 카테고리의 다른 글

Keras MNIST GAN Source Code  (0) 2019.11.21
Keras MNIST CNN DAE Source Code  (0) 2019.11.13
Keras CIFAR-10 CNN Upgraded Source Code  (0) 2019.11.13
Keras CIFAR-10 CNN Source Code  (0) 2019.11.13
Keras MNIST CNN Source Code  (0) 2019.11.13
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함