LoginSignup
55
53

More than 3 years have passed since last update.

音楽生成AIに取り組む

Last updated at Posted at 2021-02-10

この記事について

はじめにお断りさせていただきますが、私はエンジニアでもなければIT業界の人間でもないので、記事にはおかしな記述や慣習に従わない書き方のコードなどがあるかもしれません。

最終的な目標は「AIやDeepLearningの技術を用いて音楽を作る」ことです。
とはいえGoogleでさえまだ成功していないのに、素人がいきなり大掛かりなものを作ろうとしても失敗することが目に見えています。
画像や文章など他の分野での成果と比べ、これまで音楽生成がうまくいっていない原因を、一曲の要素をまるごとニューラルネットワークで扱っていることだと考えました。
そこで今回は第一歩としてテーマを音楽の三要素からハーモニーひとつにしぼり、芸大和声でお馴染みの「バス課題に和声をつける」ことを目標にLSTMのモデルを制作しました。

開発環境

Google Colaboratory
Keras

データの準備

バス課題をご存知のない方のために簡単に紹介しておくと、
bass.png
このようなバス譜例の上に、
harm.png
こういった三声の和音を載せていく音楽理論の勉強で、音大の入試などでも出題されています。
バスと上三声のmidiデータをそれぞれ用意し、バスを入力データ、上三声を正解データとしてコンピュータに学習させることにします。
和声 理論と実習 1から第四章までハ長調の課題をmidiにしました。
なお正解データは実際に課題を解いて鍵盤で打ち込んだものなので、細かな禁則を無視している可能性もありますがご容赦ください。
訓練データ
正解データ

Colabの設定

Colabはそのままでは楽譜が表示できません。
Colabで実行する場合、そのための設定が必要になります。
私はこちらの記事を参考にしました。
Qiita

ライブラリの読み込み

準備が整ったらまずはライブラリのインポートから始めていきましょう。

import numpy as np
from keras.layers import LSTM, Input, Dropout, Dense, Activation, Embedding
from keras.layers import Softmax
import keras.backend as K 
from keras.models import Model
from keras.optimizers import RMSprop
from keras.utils import to_categorical, np_utils
from music21 import*

深層学習でおなじみのライブラリに加え、music21というpythonでmidiや楽譜を扱うためのライブラリを読み込みます。

データの下ごしらえ

SMFファイルをそのままLSTMで扱うことはできないため、先程用意したmidiデータをmusic21で読み込み加工していきます。
パスは各自の環境に合わせて書き換えてください。

#学習データ
midiX = "/content/drive/MyDrive/x_train.midi"
bass = converter.parse(midiX)

#正解データ
midiY = "/content/drive/MyDrive/y_train.midi"
answer = converter.parse(midiY)

訓練データの中身を確認しておきましょう。

bass.show()

以下のようにいくつものバス課題を一挙につなげたものが表示されます。
ダウンロード (3).png

次にこのデータから音名を取り出し、表にまとめていきます。
このコードはGenerative Deep Learningを参考にしています。

#音名を取り出す関数
def notes(score):
  notes = []

  for element in score.flat:
    if isinstance(element, chord.Chord):
      notes.append('.'.join(n.nameWithOctave for n in element.pitches))   
    if isinstance(element, note.Note):
      if element.isRest:
        continue
      else:
        notes.append(str(element.nameWithOctave))
  return notes

#dictionaryに音名とそれに対応する番号を格納する関数
def create_lookups(element_names):
    element_to_int = dict((element, number) for number, element in enumerate(element_names))
    int_to_element = dict((number, element) for number, element in enumerate(element_names))

    return (element_to_int, int_to_element)

#実行
lookupsX = create_lookups(notes(bass))
lookupsY = create_lookups(notes(answer))

中身は
({'A2':19,
'A3':162,
:
:
164:'G3',
165:'C3'})
のようになっています。

次にLSTMで学習させるため、今作ったlookupsからシーケンスを作成していきます。こちらも上記の本をベースにしています。

def prepare_sequences(notes, lookups, seq_len =3):

    note_to_int, int_to_note = lookups
    notes_network_input = []

    for i in range(len(notes) - seq_len):
        notes_sequence_in = notes[i:i + seq_len]
        notes_network_input.append([note_to_int[char] for char in notes_sequence_in])

    n_patterns = len(notes_network_input)
    n_notes = len(notes)

    notes_network_input = np.reshape(notes_network_input, (n_patterns, seq_len))
    network_input = notes_network_input

    return (network_input)

#上のコードを実行
x = notes(bass)
x = prepare_sequences(x, lookupsX)
y = notes(answer)
y = prepare_sequences(y, lookupsY)
y = to_categorical(y, 166)

xの中身は
array([[165, 23, 165],
[23, 165, 165],
:
:
[164, 162, 163],
[162, 163, 164]])
のようなシーケンスになっています。
yのみ画像判定のモデルなどと同様に0,1のバイナリのクラス行列に変換しておきます。

モデル作成

データの準備ができたので、実際にネットワークを組み立てましょう。

def create_network(n_notes, embed_size = 100, rnn_units = 256):

    notes_in = Input(shape = (None,))
    x = Embedding(n_notes, embed_size)(notes_in)
    x = LSTM(rnn_units, return_sequences=True)(x)    
    x = LSTM(rnn_units, return_sequences=True)(x)                                   
    notes_out = Dense(n_notes, activation = 'softmax', name = 'out')(x)

    model = Model(notes_in, notes_out)

    opti = RMSprop(lr = 0.001)
    model.compile(loss='categorical_crossentropy', optimizer=opti)

    return model    

データが少ないので、ネットワークもシンプルなものにしました。

学習

ネットワークが完成したので学習を実行しましょう。

n_notes = 166
model = create_network(n_notes)
#model.summary() ネットワークの構造を確認したい場合はこの行を実行してください。
model.fit(x, y, batch_size = 32, epochs=100)

モデルの評価

学習が終わったら、実際にバス課題を与えて能力を試してみましょう。
まずはテストデータを作成します。

x_test1 = ('C3', 'G3', 'C3')

x2 = stream.Stream()
for i in range(len(x_test1)):
  f = x_test1[i]
  g = note.Note(f)
  x2.append(g)

x2.show()

ダウンロード (1).png
シンプルな「ドソド」という学習データの中に存在しないバス課題を新たに作成しました。

x_test2 = []
for i in x_test1:
  l = lookupsX[0][i]
  x_test2.append(l)

ypred = model.predict(x_test2)

用意したテストデータを和声モデルが読み込める形に変換し実行しています。
それでは最後にその結果を楽譜に出力してみましょう。

box = []

for i in range(3):
  n = np.argmax(ypred[i][0])
  box.append(n)

box2 = []
for i in box:
  f = lookupsY[1][i]
  print(f)
  box2.append(f)

d = stream.Stream()
for t in range(len(box2)):
  a = box2[t]
  b = a.split(".")
  c = chord.Chord(b)
  d.append(c)
d.show()

ダウンロード (2).png

I-V-Iの進行をばっちり正解できていますね。

まとめ

今回用意したデータにはハ長調しか含まれていないため、他の調や転調には対応できません。
さらに現時点では、単にバスの単音と上三声が一対一対応しているだけの可能性も残ります。LSTMがきちんと和声進行を理解した上で和音を選択しているか、転調などを含むより大きなデータで検証する必要がありそうです。

55
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
53