MoSyaMoSyaを公開します!
この記事は群馬大学電子計算機研究会 IGGG Advent Calendar 2018 - Adventar 23日目の記事です。
やったぜ!
長いことかかりました。なんとなくWebアプリをつくってみようと思ってから何ヶ月か。 だらだらしてたのもあるんですが。
なんとなく形にできたのが嬉しいです。
作ったのはこれ
絵の模写をするときにグリットだしたり白黒にしたりします。
Python Flask Pure.cssなんかを使いました。
やってないぜ!
SPAっぽくしようと思ってJavaScriptに手を出していました。 ホントはこっちを完成させて公開したかったんですけど。。今の今まで不具合が治らなくて。。。 見送りです。ほんとつらい。 というか日本語にしてないのも。。
バージョンアップは近日公開します!
ショパンっぽい音楽を生成する。
ショパンっぽい音楽を生成する。
この記事は群馬大学電子計算機研究会 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による正則化を用いています。
出力はどの音になるかという確率を表わすベクトルになります。
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/