オオハタの研究ノート

考えたこととか勉強したこととか、書いていきます。

ショパンっぽい音楽を生成する。

ショパンっぽい音楽を生成する。

この記事は群馬大学電子計算機研究会 IGGG Advent Calendar 2018 - Adventar 20日目の記事です。

なのに2だったんだよ!くやしいじゃん。僕の中学時代の音楽の成績です。 10段階評価でなかったのでせめてもの救いでしたかね。

いや、ほんと。つらい。自分がだめならコンピュータにやってもらえばいいじゃない。 ということで音楽の自動生成をLSTMでやってみました。kerasで実装しました。

ピアノの練習も続けますはい。

データ集め

機械学習はなにはともあれデータからです。

http://www.piano-midi.de/ このサイトから頂きました。ショパンの曲を全部つかいました。

余談ですが、音楽生成の場合、生成する音楽に合わせて適度に過学習させるのがよいかもと考えています。 ゆったりクラシックとハードなデスメタルとか同時に学習させても、それはそれで面白いですが、意義を見失いそうな気もします。 今回はショパンっぽい音楽を生成するという目標のため、ショパンのみで全曲使うという判断に至りました。 データも少なくなって学習コストも減るのでよいかと。
自信がないので小さい文字にしますね。

適当にmusic/とかに入れておく。

パッケージのインポート

Music21はMITが作っているMusicologyのためのライブラリらしいです。midiのパースをするために使います。

$pip install music21
from music21 import converter, instrument, note, chord, stream
import glob
import numpy as np
from keras.utils import np_utils
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, LSTM, Activation, Lambda
from tensorflow.keras.callbacks import ModelCheckpoint
from tensorflow.keras.optimizers import Adam
Using TensorFlow backend.

データの前処理

音楽ファイルに機械学習を適用するには数値表現にしなくてはいけません。

[f:id:KenjiOhata:20181220002558j:plain]

midiファイル -(music21)-> 文字ベースの表現 -(連想配列)-> 数値ベースの表現

という変換をして学習していきます。 コードか単音を並べたデータを整形しました。

簡単のため他の楽器の情報と音の間隔の情報を削除しました。

データ変換の部分を具体的に1曲でやってみます。

この曲でやってみます。

midiファイル -(music21)-> 文字ベースの表現

nocturne = converter.parse("music/chpn_op27_1.mid")

parts = instrument.partitionByInstrument(nocturne)

notes_to_parse = parts.parts[0].recurse()  # 対象のパートを一つに絞る

string_nocturne_notes = []

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

こんな風になります。

string_nocturne_notes
['C#2',
 'G#2',
 'G#3',
 'C#3',
 'G#2',
 'C#2',
 'G#2',
 '5.8',
 ...]

文字ベースの表現 -(連想配列)-> 数値ベースの表現

音の種類です。

notenames = sorted(set(string_nocturne_notes)) #音名
notenames
['0',
 '0.3',
 '0.3.6',
 '0.4',
 '0.4.6',
 '0.5',
 '0.6',
 '1',
 '1.3.7',
 ...,
 'G#3',
 'G#4',
 'G#5',
 'G2',
 'G3',
 'G4']

音名と数字を連想配列で対応付けておきます。

note2int = dict((string_note, number) for number, string_note in enumerate(notenames))
note2int
{'0': 0,
 '0.3': 1,
 '0.3.6': 2,
 '0.4': 3,
 '0.4.6': 4,
 '0.5': 5,
 ...,
 'G#3': 106,
 'G#4': 107,
 'G#5': 108,
 'G2': 109,
 'G3': 110,
 'G4': 111}
numerical_nocturne_notes = []
for string_note in string_nocturne_notes:
    numerical_nocturne_notes.append(note2int[string_note])
    
numerical_nocturne_notes
[73,
 105,
 106,
 74,
 105,
...]

これで音楽ファイルを数字ベースの表現になおすことができました。 続いてはこれを機械学習のモデルに渡せるように、整形します。

どういうモデルにするか?ということにつながりますが、今回は100個の音符を参考にして次の1音符を作るというモデルを作ることにします。

数値列の先頭から長さ100個の音の列をつくって、それを学習データとし、101個目をその正解データとします。 これを一個ずつ、ずらしながら学習データと正解データを作っていきます。

[f:id:KenjiOhata:20181216024555p:plain]

sequence_length = 100 #  1つの入力の長さ

network_input = []
network_output = []

for i in range(0, len(numerical_nocturne_notes) - sequence_length, 1):
    network_input.append(numerical_nocturne_notes[i:i + sequence_length])
    network_output.append(numerical_nocturne_notes[i + sequence_length])

network_inputはTensorflowで扱えるようにnumpyにしておきます。

network_input = np.reshape(network_input, (-1, sequence_length, 1))
network_input.shape, network_input
((1227, 100, 1),
array([[[ 73],
         [105],
         [106],
         ...,
         [ 93],
         [ 74],
         [105]],

        ...,

        [[ 73],
         [ 39],
         [ 73],
         ...,
         [102],
         [ 12],
         [ 11]]]))

今回は次にどの音がくるか、というクラス分けをしていると考えられるので、network_outputを1-hotエンコードしておきます。

network_output = np_utils.to_categorical(network_output)
network_output.shape, network_output
((1227, 112),
array([[0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        ...,
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.]], dtype=float32))

行数が一致してるとか、ちまちま確認しておきます。

本番

この作業をすべてのファイルにやります。この際に、各音楽ファイルの始まりと終わりがくっつかないようにします。

sequence_length = 100 #  1つの入力の長さ
string_flatten_network_input = []
string_network_output = []

for music in glob.glob("music/*.mid"):
    string_notes = []
    print("parsing " + music)
    midi = converter.parse(music)
    
    note_to_parse = None
    parts = instrument.partitionByInstrument(midi)
    
    if parts:
        note_to_parse = parts.parts[0].recurse()
    else:
        note_to_parse = music.flat.notes
        
    for element in note_to_parse:
        if isinstance(element, note.Note):
            string_notes.append(str(element.pitch))
        elif isinstance(element, chord.Chord):
            string_notes.append('.'.join(str(n) for n in element.normalOrder))
            
    for i in range(0, len(string_notes) - sequence_length, 1):
        for string_note in string_notes[i:i+sequence_length]:
            string_flatten_network_input.append(string_note)
            
        string_network_output.append(string_notes[i+sequence_length])
parsing music/chpn-p15.mid
parsing music/chpn_op33_4.mid
parsing music/chpn_op53.mid
...
len(string_flatten_network_input)
5864600
len(string_network_output)
58646
notenames = sorted(set(string_flatten_network_input + string_network_output))
note2int = dict((string_note, number) for number, string_note in enumerate(notenames))
notenames
['0',
 '0.1',
 '0.2',
 '0.2.5',
 '0.2.6',
 ...,
 'G5',
 'G6']
note2int
{'0': 0,
 '0.1': 1,
 '0.2': 2,
 '0.2.5': 3,
 ...,
 'G1': 311,
 'G2': 312,
 'G3': 313,
 'G4': 314,
 'G5': 315,
 'G6': 316}
network_input = []
for string_note in string_flatten_network_input:
    network_input.append(note2int[string_note])
    
network_input = np.reshape(network_input, (-1, sequence_length, 1))
#network_input = network_input / float(n_vocab)
network_input.shape, network_input
((58646, 100, 1),
array([[[302],
         [259],
         [307],
         ...,
         [ 36],
         [261],
         [307]],

        ...,

        [[ 29],
         [248],
         [312],
         ...,
         [295],
         [274],
         [295]]]))
network_output = []
for string_note in string_network_output:
    network_output.append(note2int[string_note])

network_output = np_utils.to_categorical(network_output)
network_output.shape, network_output
((58646, 317),
array([[0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        ...,
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.],
        [0., 0., 0., ..., 0., 0., 0.]], dtype=float32))
n_vocab = len(notenames) #  音の種類 入力の正規化用

Model Building

モデルはLSTMを使いました。 LSTM3層にDense3層の構造で、Dropoutによる正則化を用いています。

出力はどの音になるかという確率を表わすベクトルになります。

f:id:KenjiOhata:20181216024607p:plain

model = Sequential()
model.add(Lambda((lambda x: x/n_vocab), input_shape=(network_input.shape[1], network_input.shape[2])))
model.add(LSTM(256, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256))
model.add(Dropout(0.3))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))

損失関数にはカテゴリカルクロスエントロピーを、最適化にはadamアルゴリズムを用いました。

model.compile(loss="categorical_crossentropy", optimizer=Adam(lr=0.0001)) # このlrがうまくいきました。
model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lambda (Lambda)              (None, 100, 1)            0         
_________________________________________________________________
lstm (LSTM)                  (None, 100, 256)          264192    
_________________________________________________________________
dropout (Dropout)            (None, 100, 256)          0         
_________________________________________________________________
lstm_1 (LSTM)                (None, 100, 256)          525312    
_________________________________________________________________
dropout_1 (Dropout)          (None, 100, 256)          0         
_________________________________________________________________
lstm_2 (LSTM)                (None, 256)               525312    
_________________________________________________________________
dropout_2 (Dropout)          (None, 256)               0         
_________________________________________________________________
dense (Dense)                (None, 317)               81469     
_________________________________________________________________
activation (Activation)      (None, 317)               0         
=================================================================
Total params: 1,396,285
Trainable params: 1,396,285
Non-trainable params: 0
_________________________________________________________________

モデルを逐次保存できるようにコールバック関数を作成します。 僕が学習する際はモデルの保存かボルトネックとなり、学習が遅くなっていました。環境に合わせてperiodの設定をしてみてください。

dir = "model_checkpoint/"
filepath = "{loss:.4f}-weights-improvement-{epoch:02d}.hdf5"

checkpoint = ModelCheckpoint(
    filepath = dir + filepath, monitor="loss", verbose=0, save_best_only=True, mode='min', period=10
)


callbacks_list = [checkpoint]

これで学習できます。

model.fit(network_input, network_output, epochs=500, batch_size=64, callbacks=callbacks_list)

と書きましたが、実際のところGoogle Colabで実行しました。手元のノートPCでは手に終えない計算量でした。それでも合わせて10時間以上かかりました。

最終的にweightは0.5487のあたりで変化がなくなりました。

音楽生成

データの前処理と逆のことをします。 midiファイル <-(music21)- 文字ベースの表現 <-(連想配列)- 数値ベースの表現

とすれば音楽が生成されます!

生成にあたって250個くらいの音を作ることにしました。

music_length = 250

生成の最初にはもとからあるデータの中から使って1音目をつくることにします。

start = np.random.randint(0, len(network_input)-1)
pattern = network_input[start]
pattern
array([[ 79],
       [ 75],
       ...,
       [261],
       [272],
       [274]])

数値から、文字ベースの表現へ変換する連想配列です。

int2note = dict((number, string_note) for number, string_note in enumerate(notenames))
int2note
{0: '0',
 1: '0.1',
 2: '0.2',
 3: '0.2.5',
 4: '0.2.6',
 5: '0.2.6.8',
 ...,
 311: 'G1',
 312: 'G2',
 313: 'G3',
 314: 'G4',
 315: 'G5',
 316: 'G6'}

入力を準備して、予測、追加、入力を更新、という作業をくりかえします。

[f:id:KenjiOhata:20181220001800p:plain]

numerical_prediction_output = []

for note_index in range(music_length):
  prediction_input = np.reshape(pattern, (1, len(pattern), 1))

  prediction = model.predict(prediction_input, verbose=0)
  
  numerical_note = np.argmax(prediction)
  numerical_prediction_output.append(numerical_note)
  
  #pattern = np.append(pattern, numerical_note/float(n_vocab))
  pattern = np.append(pattern, numerical_note)
  pattern = pattern[1:len(pattern)]

数値ベースの表現から文字ベースの表現へ変換

string_prediction_output = []

for i in range(music_length):
    string_prediction_output.append(int2note[numerical_prediction_output[i]])
string_prediction_output
['10.0.3.6',
 '10.0.3.6',
 '10.0.3.6',
 ...,
 '10.0.3.6',
 '10.0.3.6',
 '10.0.3.6']

文字ベースから、music21の形式へ変換する

音の間隔を考慮していませんでした。 offsetというもので設定していきます。

offset = 0
prediction_output = []

for string_note in string_prediction_output:
  if ('.' in string_note) or string_note.isdigit():
    notes_in_chord = string_note.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
    prediction_output.append(new_chord)
  else:
    new_note = note.Note(string_note)
    new_note.offset = offset
    new_note.storedInstrument = instrument.Piano()
    prediction_output.append(new_note)
    
  offset += 0.5
prediction_output
[<music21.chord.Chord B- C E- F#>,
 <music21.chord.Chord B- C E- F#>,
 <music21.chord.Chord B- C E- F#>,
 ...,
 <music21.chord.Chord B- C E- F#>,
 <music21.chord.Chord B- C E- F#>]

music21からmidi形式へ変換する。

midi_stream = stream.Stream(prediction_output)
midi_stream.write('midi', fp='production/test_output.mid')
'production/test_output.mid'

聞いてみてください
ブログ上のサンプルコードは未学習の状態のものなので、上の結果のやつではないです。

割といい感じではないでしょうか。びっくりです。こんなにうまくいくとは。

むすび

すごいいい感じになりましたよね。(強引ですかね)
ということで大円満的に終わりたいと思います。

しかし展望的なものも。 今回のやり方では音楽の間隔を一定にしているので長い音がなかったりします。 ちまたでは、繰り返しを求めて研究がなされているようですが、繰り返しだけが良い音楽というわけでもないと思いますので どういう音楽がいい音楽なのか。そういったところを突き詰められたらよいと思います。

Githubにあげておくかも。 LSTMセルの解説もするかもしれないです。

参考

音の変換とモデル構築の参考にしました。この記事なくしてはこの記事はありませんでした。すごくわかりやすい記事。
https://towardsdatascience.com/how-to-generate-music-using-a-lstm-neural-network-in-keras-68786834d4c5

LSTMの理解はこちらの本がすごくわかりやすかったです。 青とか緑の本ではなく。 https://www.oreilly.co.jp/books/9784873118345/