読者です 読者をやめる 読者になる 読者になる

7rpn’s blog: うわああああな日常

好きなことをつらつらと。

DeepLearningで自動作曲。GoogleのMagentaを自力で組んでみる。

最近,GoogleMagentaというプロジェクトが公開されました。 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止める]のイベントベクトルの時系列データとみなして,前のベクトルを入力したら次のベクトルを予測して出力します。 これで曲を作らせる感じ。

コード

勉強会が一時間くらいでやろうって雰囲気だったので,結構美しくないコードを書いています。あまりお気になさらず。

まず,RubyMIDIを配列に書き換え。 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の配列

Rubyで実行した配列をPythonに放り込んで学習。

#!/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 #=>自動作曲の出力

出力結果をRubyに渡してMIDIを作成。

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も実装したい
  • たくさんのデータを詰め込んでみたいよね
  • あとはコード進行データとかと同期させたり,和音に対応させたり
  • 即興のお供作りたい

必要だと思う

  • 転調しても曲になるから全調に転調して突っ込むべきな気がする