티스토리 뷰
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))
각기 음정 (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)
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))
만들어준 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 모델을 구성해줍니다.
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()
기본적인 학습 방법을 쓰므로 fit 함수를 사용합니다.
# 모델 학습
# 음악 작곡이기 때문에 Validation Set이 없다
# LSTM : 13시간 / CuDNN : 3시간 반
# loss 는 0.7 ~ 0.8 만 되어도 충분히 작곡 능력을 갖는다.
model.fit(net_in, net_out, epochs=75, batch_size=64)
학습된 모델을 사용하기 위해 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)
이제 초기 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 작곡 결과를 다시 우리가 들을 수 있게 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 |