DeepLearningで自動作曲。GoogleのMagentaを自力で組んでみる。
最近,GoogleのMagentaというプロジェクトが公開されました。 AIに自動作曲をさせようという試みで,Google謹製ライブラリのTensorflowを使います。
今回やり方だけを眺めて,同じやり方をchainerとかRubyとか使って組んで,自動作曲を行ってみました。
とりあえず作った曲
今回はBachのInvention4番の右手を学習させてみました。
作られた曲はそうですか...って感じ。130epochでこんなもの。
こっちは荒ぶってる。150epoch。
公式が上げてる自動作曲された曲もこんな感じなのでまぁ。
Magentaを使わない理由?
勉強会でMagentaやろうってなったけど,Bazelがまったく走らなかった。
途中でMagenta自体を自力で作った方が早いのでは?って話になり,chainerとか使って同じものを作ってみようって感じになりました。
まぁTensorflowなんとなく使いにくいですし。chainerのさらっと書ける感やばい。食わず嫌いかも?
というわけで実装
Magenta自体に三つ作曲方法が入っていますが,基本となるBasic RNNのやり方を真似てみました。
やることをざっくりと言うと,今の音を入力したら次の音は何か?を予測できるようなモデルを作ります。 リズムの情報が入ってくるのでそこが面倒。
手順
- MIDIを最小イベントごとに区切る
- 各Eventで音を鳴らすか,前回の状態を継続するか,音を止めるかのどれかを入力
- そこから次Eventの状態を予測
入力は[音の高さ128次元]+[継続or止める]の130次元のone-hotベクトルになります。 一番細かい音符が64分だったなら曲全体を64分ごとに区切ってLSTMに突っ込む感じ。
一曲を[音の高さ128次元]+[継続or止める]のイベントベクトルの時系列データとみなして,前のベクトルを入力したら次のベクトルを予測して出力します。 これで曲を作らせる感じ。
コード
勉強会が一時間くらいでやろうって雰囲気だったので,結構美しくないコードを書いています。あまりお気になさらず。
まず,RubyでMIDIを配列に書き換え。 MIDIファイルを読み込んで右手部分を配列にします。
require "midilib" Encoding.default_external="utf-8" seq = MIDI::Sequence.new() File.open("ファイル名.mid", 'rb') do |file| seq.read(file) end delta = seq.note_to_delta('quarter') midi = [] timesig = nil seq.each_with_index do |track,i| print i,"\n" break if i == 2 track.each do |event| if MIDI::TimeSig === event timesig = event.to_s.delete("time sig").split("/").map(&:to_f) print timesig,"\n" end if MIDI::NoteEvent === event and event.note if event.to_s =~/off/ on_off = 0 elsif event.to_s =~/on/ on_off = 1 end midi << [event.note,event.time_from_start,on_off] end end end bar = delta * 4 * (timesig[0]/timesig[1]) output = midi.map do |event| bar_num = event[1] / bar.to_i step = event[1] % bar.to_i [event[0],event[-1],bar_num,step/bar] end # => note_num, on_off, 小節 min_num = (音符の最小の長さ) #本当はベタ打ちすべきでは無い midi.map!{|m| m[1] = m[1]/min_num;m} hash = midi.group_by{|g| g[1]}.map do |key,value| if value.size > 1 value = value.select{|s| s.last == 1} end [key,*value] end.to_h list = (0..midi.last[1]).to_a.map{|m| hash[m]} list.map! do |event| if event if event.last == 0 ret = 129 elsif event.last == 1 ret = event.first else raise end else ret = 128 end ret end print list #=> MIDIの配列
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import numpy as np import chainer from chainer import cuda, Function, gradient_check, report, training, utils, Variable from chainer import datasets, iterators, optimizers, serializers from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L from chainer.training import extensions def each_cons(x, size): return [x[i:i+size] for i in range(len(x)-size+1)] class RNN(Chain): def __init__(self): super(RNN, self).__init__( embed=L.EmbedID(130, 100), mid=L.LSTM(100, 50), # the first LSTM layer out=L.Linear(50, 130), # the feed-forward output layer ) def reset_state(self): self.mid.reset_state() def __call__(self, input_vector): # Given the current word ID, predict the next word. x = self.embed(input_vector) h = self.mid(x) y = self.out(h) return y rnn = RNN() model = L.Classifier(rnn) optimizer = optimizers.SGD() optimizer.setup(model) midi = (MIDIの配列を入れる) loss = 0 count = 0 seqlen = len(midi[1:]) for x in range(100): print x rnn.reset_state() for cur_event,next_event in each_cons(midi,2): cur_event, next_event = np.asarray([cur_event]).astype(np.int32), np.asarray([next_event]).astype(np.int32) loss += model(cur_event, next_event) count += 1 if count % 30 == 0 or count == seqlen: #print loss.data model.zerograds() loss.backward() loss.unchain_backward() optimizer.update() model.predictor.reset_state() list = [] prev_event = chainer.Variable(np.array([64], np.int32)) prob = F.softmax(model.predictor(prev_event)) list.append(64) for i in range(200): prob = F.softmax(model.predictor(prev_event)) probability = prob.data[0].astype(np.float64) probability /= np.sum(probability) index = np.random.choice(range(len(probability)), p=probability) list.append(index) prev_event = chainer.Variable(np.array([index], dtype=np.int32)) print list #=>自動作曲の出力
require 'midilib' include MIDI Encoding.default_external="utf-8" seq = Sequence.new() m_track = Track.new(seq) seq.tracks << m_track m_track.events << Tempo.new(Tempo.bpm_to_mpq 120 ) m_track.events << MetaEvent.new(META_SEQ_NAME, 'wave') track = Track.new(seq) seq.tracks << track seq.name = 'Input Track' track_instrument = GM_PATCH_NAMES[0] track.events << ProgramChange.new(0, 1, 0) delta = seq.note_to_delta('quarter') list = (さっきの出力) track.events = list.map do |m| if m < 128 on = NoteOn.new(0,m,127,0) off = NoteOff.new(0,m,127,delta/2) else on = NoteOn.new(0,0,0,0) off = NoteOff.new(0,0,0,delta/2) end [on,off] end.flatten File.open('output.midi', 'wb'){|f| seq.write(f)}
という流れ。全部Pythonに統一した方が良いんだろうけどRubyから離れられない人。
これからやってみたいこと
- lookbackとattentionも実装したい
- たくさんのデータを詰め込んでみたいよね
- あとはコード進行データとかと同期させたり,和音に対応させたり
- 即興のお供作りたい
必要だと思う
- 転調しても曲になるから全調に転調して突っ込むべきな気がする