🐷

AIミュージックバトル!『弁財天』 Starter Kit ver. 1.1(TensorFlow版) ~第二幕対応~

2023/08/04に公開

AIミュージックバトル!『弁財天』 Starter Kit ver. 1.1(TensorFlow版) ~第二幕対応~

2023年8月4日版

北原 鉄朗(日本大学 文理学部 情報科学科)

この文書は何か

この文書では、AIミュージックバトル!『弁財天』に出場したい人が、対戦ルールに準拠したメロディ生成プログラムを作るための、参考となるプログラムを提供します。この文書は、次のように利用されることを想定しています。

  • 掲載されているプログラムをそのまま Google Colaboratory にコピーして、実行してみる(メロディが付与されたMIDIファイルが生成され、聴くことができる)。
  • TensorFlowを用いてモデルを構築する部分を自分で考えたモデルに変更し、それ以外のコードはそのまま利用する。
  • 学習データを独自に用意し、モデルの仕様やコードなどはそのまま利用して学習する。
  • MIDIなどのデータの読み書き、one-hotベクトルへの変換など、前処理や後処理の関数のみ使いまわす。
  • ルールブックに記載された対戦ルールだけでは分かりにくい仕様に関して、掲載されたプログラムを解読して理解を深める。

この文書で提供しているプログラムは、サンプルプログラムとしての分かりやすさを重視したため、クオリティの高いメロディを生成するものにはなっていません。出場者の皆さんには、このプログラムを踏み台にして各種改良を行っていただくことを想定しています。

なお、Google Colabノートブックにコードを貼り付けたものもあります(ここ) ので、必要に応じてお使いください。

本文書が提供するプログラムのライセンス

本文書に掲載したプログラムは、MITライセンスにて提供します。著作権は放棄しませんが、MITライセンスの条項を厳守する限り、自由に使っていただいて構いません。

MITランセンスの本文は https://opensource.org/licenses/mit-license.php、その参考日本語訳は https://licenses.opensource.jp/MIT/MIT.html をご覧ください。copyright holder は「北原 鉄朗 (Tetsuro Kitahara)」、yearは「2023」とします。

前提条件

Google Colaboratory が利用できることを前提とします。 Google Colaboratory の利用方法は他書にてお調べください。もちろん、「弁財天」自体が Google Colaboratory の利用を義務付けているわけではありません。

サンプルプログラムの仕様

入出力

「弁財天」の対戦ルールに従い、次の通りとします。

  • 入力データ:伴奏MIDIファイル、コード進行を記したテキストファイル
    • 調については C major か A minor のどちらかですので、実行時に手作業で変数の内容を書き換えることにします。
  • 出力データ1:
    • 新規に空のMIDIファイルを作ってメロディを付与したもの[1]
  • 出力データ2
    • 伴奏用のMIDIファイルにメロディを付与したもの

対戦ルールに準拠し、メロディは8小節とします。対戦ルールでは、アウフタクト(0小節目)や9小節目に音を入れることが認められています(ただし、9小節目の音の追加は、9小節目に伴奏もある場合のみ)が、ここでは簡単のため、これらは考えないことにします。

また、対戦ルールによると、8小節の伴奏の前に、2小節のブランク、2小節の前奏があることになっています。そのため、生成したメロディは5小節目から挿入することにします。

第一幕では、伴奏として提供されたMIDIファイルにメロディを付与して提出することになっていましたが、第二幕では、メロディのみのMIDIファイルを提出するルールに変更されました。そこで、出力データ1(output1.mid)として、メロディのみのMIDIファイルを出力します。しかし、これだけでは伴奏との不協和音の有無などを確認するのに不便ですので、伴奏用のMIDIファイルにメロディを付与したものも出力します。これを出力データ2(output2.mid)とします。ブラウザ上で再生できるのは、出力データ2の方です。出力データ1は、Googleドライブに保存されます。提出時に間違えないように気を付けましょう[2]

学習データ

「Charlie Parker's Omnibook MusicXML data」にあるMusicXMLデータを使用します。

入手先:https://homepages.loria.fr/evincent/omnibook/

「弁財天」の対戦ルールには学習データの指定はありませんので、「弁財天」自体がこのデータを学習に用いることを義務付けるものではありません。

このMusicXMLデータは、様々な調の楽曲が入っていますが、調号が含まれていません。そのため、後述の「music21」で分析して得られた調の情報に基づいて、C major または A minor に移調することにします。

モデル

今回掲載するプログラムでは、LSTM-VAEの亜種(図1)を構築することにします。

モデルの入力と出力は次の通りです。

  • 入力:メロディ+コード進行
    • 16分音符ごとに、メロディを表すベクトルとコードを表すベクトルを結合したものが時系列として並んでいる
    • メロディ:ノートナンバー [36, 84) および休符に対応する49次元のone-hotベクトル
      • [36, 84) は、36から、84の直前(つまり83)までを表す
      • [36, 84) の各々に対応する要素が48個、それに休符要素を合わせて合計49個の要素が並ぶ
    • コード:各ピッチクラスに対応する12次元のmany-hotベクトル
  • 出力:メロディ
    • 16分音符ごとにメロディを表すone-hotベクトルを並べた時系列

メロディを生成する際は、入力側のメロディは全要素(休符要素を含む)を0とし、コードを表す要素のみに「1」がある状態にして、エンコーダおよびデコーダを実行します。この方法が本当に正しいかは正直よく分かりませんが、シンプルなコードで書けるのと、一応動くので、この方法を取ることにします。

(おそらく、Conditional VAEなどに拡張して、コード進行はconditionとして与えてデコーダのみ動かす方が、王道のやり方だと思います。ぜひ試してみてください。)

なお、8小節全体を丸々LSTM-VAEで学習するのは大変なので、U小節ごとに分割し、U小節のメロディを生成する処理を必要なだけ繰り返すことにします(デフォルトではU=4)。


図1 本プログラムで構築するモデル

サンプルプログラムおよびその実行手順

サンプルプログラムは、次の手順に基づいて、Google Colabにコピー&ペーストしながら実行してください。

Step 1 学習データのダウンロードおよびGoogleドライブへのコピー

次の手順で学習データを準備します。

  1. Googleドライブの「マイドライブ」直下に「adlib」というフォルダを作成する。
  2. 「adlib」フォルダの中に「omnibook」というフォルダを作成する。
  3. https://homepages.loria.fr/evincent/omnibook/ にあるZIPファイルをダウンロードし、すべてのファイルを展開する。
  4. MusicXMLファイル(拡張子:xml)が50個ほどあるので、これらを「omnibook」フォルダにすべてコピーする。

Step 2 新しいGoogle Colabノートブックを開く

新しいGoogle Colaboratoryノートブックを開きます。

Step 3 Googleドライブをマウントする

Googleドライブを Google Colaboratory にマウントします。 次の方法のどちらかで、Googleドライブを Google Colaboratory にマウントしてください。

  • 方法1:マウントのためのコードを実行する
    Google Colaboratory ノートブックのコードセルに、次のコードをコピー&ペーストして実行する。
from google.colab import drive
drive.mount('/content/drive')
  • 方法2:ファイルタブ内のマウントボタンを押す
    Google Colaboratory ノートブックの画面左のフォルダアイコンを開くとディレクトリツリーが表示され、上にいくつかのアイコンがある。そのうちの1つを押すことで、Googleドライブを Google Colaboratory にマウントすることができる。

どちらの方法でも、Google Colaboratory へのマウントを許可するか問い合わせるメッセージが表示されるので、マウントに用いるGoogleアカウント名を確認した上で、許可してください。

Step 4 必要なライブラリー、コマンドをインストールする

Google Colaboratory ノートブックのコードセルに、次のコマンドをコピー&ペーストして実行します。

!pip install mido
!pip install midi2audio
!apt install fluidsynth

Step 5 MIDIファイルの読み書きなどを行う関数を定義する

MusicXMLファイルを読み込む関数、音符列をone-hotベクトル列に変換する関数、コードシンボル列をmany-hotベクトル列に変換する関数、これらのデータをモデルの入出力形式に合うように整える関数、モデルの出力を整形してMIDIファイルを生成する関数、MIDIファイルをオーディオに変換して Google Colaboratory 上で再生できるようにする関数などを定義します。

Google Colaboratory ノートブックの末尾に新しいコードセルを作り、そこに次のコードをコピー&ペーストして実行します。実行しないと関数が読み込まれないので注意しましょう。また、関数が読み込まれるだけなので、何も出力されません。

import music21
import numpy as np
import matplotlib.pyplot as plt
import mido
import csv
import IPython.display as ipd
import midi2audio
import glob

TOTAL_MEASURES = 240        # 学習用MusicXMLを読み込む際の小節数の上限
UNIT_MEASURES = 4           # 1回の生成で扱う旋律の長さ
BEAT_RESO = 4               # 1拍を何個に分割するか(4の場合は16分音符単位)
N_BEATS = 4                 # 1小節の拍数(今回は4/4なので常に4)
NOTENUM_FROM = 36           # 扱う音域の下限(この値を含む)
NOTENUM_THRU = 84           # 扱う音域の上限(この値を含まない)
INTRO_BLANK_MEASURES = 4    # ブランクおよび伴奏の小節数の合計
MELODY_LENGTH = 8           # 生成するメロディの長さ(小節数)

###### 2023.08.04 追加
TICKS_PER_BEAT = 480        # 四分音符を何ticksに分割するか
MELODY_PROG_CHG = 73        # メロディの音色(プログラムチェンジ)
MELODY_CH = 0               # メロディのチャンネル

KEY_ROOT = "C"              # 生成するメロディの調のルート("C" or "A")
KEY_MODE = "major"          # 生成するメロディの調のモード("major" or "minor")

# MusicXMLデータからNote列とChordSymbol列を生成
# 時間分解能は BEAT_RESO にて指定
def make_note_and_chord_seq_from_musicxml(score):
  note_seq = [None] * (TOTAL_MEASURES * N_BEATS * BEAT_RESO)
  chord_seq = [None] * (TOTAL_MEASURES * N_BEATS * BEAT_RESO)

  for element in score.parts[0].elements:
    if isinstance(element, music21.stream.Measure):
      measure_offset = element.offset
      for note in element.notes:
        if isinstance(note, music21.note.Note):
          onset = measure_offset + note._activeSiteStoredOffset
          offset = onset + note._duration.quarterLength
          for i in range(int(onset * BEAT_RESO), int(offset * BEAT_RESO + 1)):
            note_seq[i] = note
        if isinstance(note, music21.harmony.ChordSymbol):
          chord_offset = measure_offset + note.offset
          for i in range(int(chord_offset * BEAT_RESO), 
                         int((measure_offset + N_BEATS) * BEAT_RESO + 1)):
            chord_seq[i] = note
  return note_seq, chord_seq

# Note列をone-hot vector列(休符はすべて0)に変換
def note_seq_to_onehot(note_seq):
  M = NOTENUM_THRU - NOTENUM_FROM
  N = len(note_seq)
  matrix = np.zeros((N, M))
  for i in range(N):
    if note_seq[i] != None:
      matrix[i, note_seq[i].pitch.midi - NOTENUM_FROM] = 1
  return matrix

# 音符列を表すone-hot vector列に休符要素を追加
def add_rest_nodes(onehot_seq):
  rest = 1 - np.sum(onehot_seq, axis=1)
  rest = np.expand_dims(rest, 1)
  return np.concatenate([onehot_seq, rest], axis=1)

# 指定された仕様のcsvファイルを読み込んで
# ChordSymbol列を返す
def read_chord_file(file):
  chord_seq = [None] * (MELODY_LENGTH * N_BEATS)
  with open(file) as f:
    reader = csv.reader(f)
    for row in reader:
      m = int(row[0]) # 小節番号(0始まり)
      if m < MELODY_LENGTH:
        b = int(row[1]) # 拍番号(0始まり、今回は0または2)
        chord_seq[m*4+b] = music21.harmony.ChordSymbol(root=row[2], 
                                                       kind=row[3], 
                                                       bass=row[4])
  for i in range(len(chord_seq)):
    if chord_seq[i] != None:
      chord = chord_seq[i]
    else:
      chord_seq[i] = chord
  return chord_seq

# コード進行からChordSymbol列を生成
# divisionは1小節に何個コードを入れるか
def make_chord_seq(chord_prog, division):
  T = int(N_BEATS * BEAT_RESO / division)
  seq = [None] * (T * len(chord_prog))
  for i in range(len(chord_prog)):
    for t in range(T):
      if isinstance(chord_prog[i], music21.harmony.ChordSymbol):
        seq[i * T + t] = chord_prog[i]
      else:
        seq[i * T + t] = music21.harmony.ChordSymbol(chord_prog[i])
  return seq

# ChordSymbol列をmany-hot (chroma) vector列に変換
def chord_seq_to_chroma(chord_seq):
  N = len(chord_seq)
  matrix = np.zeros((N, 12))
  for i in range(N):
    if chord_seq[i] != None:
      for note in chord_seq[i]._notes:
        matrix[i, note.pitch.midi % 12] = 1
  return matrix

# 空(全要素がゼロ)のピアノロールを生成
def make_empty_pianoroll(length):
  return np.zeros((length, NOTENUM_THRU - NOTENUM_FROM + 1))

# ピアノロール(one-hot vector列)をノートナンバー列に変換
def calc_notenums_from_pianoroll(pianoroll):
  notenums = []
  for i in range(pianoroll.shape[0]):
    n = np.argmax(pianoroll[i, :])
    nn = -1 if n == pianoroll.shape[1] - 1 else n + NOTENUM_FROM
    notenums.append(nn)
  return notenums

# 連続するノートナンバーを統合して (notenums, durations) に変換
def calc_durations(notenums):
  N = len(notenums)
  duration = [1] * N
  for i in range(N):
    k = 1
    while i + k < N:
      if notenums[i] > 0 and notenums[i] == notenums[i + k]:
        notenums[i + k] = 0
        duration[i] += 1
      else:
        break
      k += 1
  return notenums, duration

####### 2023.08.04 追加、2023.10.13 修正
# MIDIトラックを生成(make_midiから呼び出される)
def make_midi_track(notenums, durations, transpose, ticks_per_beat):
  track = mido.MidiTrack()
  # Logic Proにインポートしたときに空白小節がトリミングされないように、
  # ダミーのチャンネルメッセージとして、オール・ノート・オフを挿入
  track.append(mido.Message('control_change', channel=MELODY_CH, control=123, value=0))
  init_tick = INTRO_BLANK_MEASURES * N_BEATS * ticks_per_beat
  prev_tick = 0
  for i in range(len(notenums)):
    if notenums[i] > 0:
      curr_tick = int(i * ticks_per_beat / BEAT_RESO) + init_tick
      track.append(mido.Message('note_on', channel=MELODY_CH, note=notenums[i]+transpose,
                                velocity=100, time=curr_tick - prev_tick))
      prev_tick = curr_tick
      curr_tick = int((i + durations[i]) * ticks_per_beat / BEAT_RESO) + init_tick
      track.append(mido.Message('note_off', channel=MELODY_CH, note=notenums[i]+transpose,
                                velocity=100, time=curr_tick - prev_tick))
      prev_tick = curr_tick
  return track

####### 2023.08.04 追加
# プログラムチェンジを指定したものに差し替え
def replace_prog_chg(midi):
  for track in midi.tracks:
    for msg in track:
      if msg.type == 'program_change' and msg.channel == MELODY_CH:
        msg.program = MELODY_PROG_CHG

####### 2023.08.04 追加
# MIDIファイル(提出用、伴奏なし)を生成
def make_midi_for_submission(notenums, durations, transpose, dst_filename):
  midi = mido.MidiFile(type=1)
  midi.ticks_per_beat = TICKS_PER_BEAT
  midi.tracks.append(make_midi_track(notenums, durations, transpose, TICKS_PER_BEAT))
  midi.save(dst_filename)  

####### 2023.08.04 修正
# MIDIファイル(チェック用、伴奏あり)を生成
def make_midi_for_check(notenums, durations, transpose, src_filename, dst_filename):
  midi = mido.MidiFile(src_filename)
  replace_prog_chg(midi)
  midi.tracks.append(make_midi_track(notenums, durations, transpose, midi.ticks_per_beat))
  midi.save(dst_filename)

####### 2023.08.04 修正
# ピアノロールを描画し、MIDIファイルを再生
def show_and_play_midi(pianoroll, transpose, src_filename, dst_filename1, dst_filename2):
  plt.matshow(np.transpose(pianoroll))
  plt.show()
  notenums = calc_notenums_from_pianoroll(pianoroll)
  notenums, durations = calc_durations(notenums)
  make_midi_for_submission(notenums, durations, transpose, dst_filename1)
  make_midi_for_check(notenums, durations, transpose, src_filename, dst_filename2)
  fs = midi2audio.FluidSynth(sound_font="/usr/share/sounds/sf2/FluidR3_GM.sf2")
  fs.midi_to_audio(dst_filename2, "output.wav")
  ipd.display(ipd.Audio("output.wav"))

# メロディを表すone-hotベクトル、コードを表すmany-hotベクトルの系列に対して、
# UNIT_MEASURES小節分だけ切り出したものを返す
def extract_seq(i, onehot_seq, chroma_seq):
  o = onehot_seq[i*N_BEATS*BEAT_RESO : (i+UNIT_MEASURES)*N_BEATS*BEAT_RESO, :]
  c = chroma_seq[i*N_BEATS*BEAT_RESO : (i+UNIT_MEASURES)*N_BEATS*BEAT_RESO, :]
  return o, c

# メロディを表すone-hotベクトル、コードを表すmany-hotベクトルの系列から、
# モデルの入力、出力用のデータに整えて返す
def calc_xy(o, c):
  x = np.concatenate([o, c], axis=1)
  y = o
  return x, y

# メロディを表すone-hotベクトル、コードを表すmany-hotベクトルの系列から
# モデルの入力、出力用のデータを作成して、配列に逐次格納する
def divide_seq(onehot_seq, chroma_seq, x_all, y_all):
  for i in range(0, TOTAL_MEASURES, UNIT_MEASURES):
    o, c, = extract_seq(i, onehot_seq, chroma_seq)
    if np.any(o[:, 0:-1] != 0):
      x, y = calc_xy(o, c)
      x_all.append(x)
      y_all.append(y)

Step 6 学習用MusicXMLを読み込む

Step 1でアップロードした学習用のMusicXMLファイルを読み込み、Step 5で定義した関数を使って、モデルの入出力に適した形に変換して配列に格納します。新しいコードセルを作り、次のコードをコピー&ペーストして実行します。

basedir = "/content/drive/MyDrive/adlib/"
dir = basedir + "omnibook/"

x_all = []
y_all = []
for f in glob.glob(dir + "/*.xml"):
  print(f)
  score = music21.converter.parse(f)
  key = score.analyze("key")
  if key.mode == KEY_MODE:
    inter = music21.interval.Interval(key.tonic, music21.pitch.Pitch(KEY_ROOT))
    score = score.transpose(inter)
    note_seq, chord_seq = make_note_and_chord_seq_from_musicxml(score)
    onehot_seq = add_rest_nodes(note_seq_to_onehot(note_seq))
    chroma_seq = chord_seq_to_chroma(chord_seq)
    divide_seq(onehot_seq, chroma_seq, x_all, y_all)

x_all = np.array(x_all)
y_all = np.array(y_all)

x_all という配列に入力データが、y_all という配列に出力データが格納されます。また、ファイルを読み込むたびにファイル名が出力されます。もしも何も出力されない場合は、1つもファイルが読み込まれていないということです。原因として、Googleドライブのマウントを忘れている、Googleドライブに作成したフォルダ名がコード内で指定したものと違っている、Step 1でMusicXMLファイルを別のところにアップロードしている、などが考えられます。

今回使用しているMusicXMLデータには、残念ながら調号が入っていません。そのため、music21の機能を使って調を分析し、分析によって判定された調の情報に基づいて C major または A minor に移調しています。music21による調の分析がどの程度正確なのかは、未検証です。

Step 7 VAEのモデルを構築するための関数を定義する

TensorFlowを用いてVAEのモデルを構築します。新しいコードセルに次のコードをコピー&ペーストして実行します。VAEがどんなモデルかは、良書が多数ありますので、そちらをご参照ください。

import tensorflow as tf
import tensorflow_probability as tfp

encoded_dim = 32                   # 潜在空間の次元数
seq_length = x_all.shape[1]        # 時間軸上の要素数
input_dim = x_all.shape[2]         # 入力データにおける各時刻のベクトルの次元数
output_dim = y_all.shape[2]        # 出力データにおける各時刻のベクトルの次元数
lstm_dim = 1024                    # LSTM層のノード数

# VAEに用いる事前分布を定義
def make_prior():
  tfd = tfp.distributions
  prior = tfd.Independent(
      tfd.Normal(loc=tf.zeros(encoded_dim), scale=1), 
      reinterpreted_batch_ndims=1)
  return prior

# エンコーダを構築
def make_encoder(prior):
  encoder = tf.keras.Sequential()
  encoder.add(tf.keras.layers.LSTM(lstm_dim, 
                                   input_shape=(seq_length, input_dim),
                                   use_bias=True, activation="tanh", 
                                   return_sequences=False))
  encoder.add(tf.keras.layers.Dense(
      tfp.layers.MultivariateNormalTriL.params_size(encoded_dim), 
      activation=None))
  encoder.add(tfp.layers.MultivariateNormalTriL(
      encoded_dim, 
      activity_regularizer=tfp.layers.KLDivergenceRegularizer(
          prior, weight=0.001)))
  return encoder

# デコーダを構築
def make_decoder():
  decoder = tf.keras.Sequential()
  decoder.add(tf.keras.layers.RepeatVector(seq_length, 
                                           input_dim=encoded_dim))
  decoder.add(tf.keras.layers.LSTM(lstm_dim, use_bias=True, 
                                   activation="tanh", 
                                   return_sequences=True))
  decoder.add(tf.keras.layers.Dense(output_dim, use_bias=True, 
                                    activation="softmax"))
  return decoder

# エンコーダとデコーダを構築し、それらを結合したモデルを構築する
# (入力:エンコーダの入力、
#  出力:エンコーダの出力をデコーダに入力して得られる出力)
def make_model():
  encoder = make_encoder(make_prior())
  decoder = make_decoder()
  vae = tf.keras.Model(encoder.inputs, decoder(encoder.outputs))
  vae.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0005), 
              loss="categorical_crossentropy", metrics="categorical_accuracy")
  return vae

Step 8 モデルを構築・学習する

Step 7 で定義した関数を使って、モデルを構築・学習します。次のコードを実行します。学習が完了すると、Googleドライブの「adlib」フォルダに「mymodel.h5」という名前で、学習済みのモデルが保存されます。

vae = make_model()                        # VAEモデルを構築
vae.fit(x_all, y_all, epochs=500)         # VAEモデルを学習
vae.save(basedir + "/mymodel.h5")         # 学習済みVAEモデルをファイルに保存

Step 9 メロディを生成する

Step 8 で学習したモデルを使って、メロディを生成します。メロディを生成するには、次の2つのファイルを用意する必要があります。

  • 伴奏MIDIファイル
  • コード進行ファイル(csv形式)

伴奏MIDIファイルは、メロディの生成自体には使用しませんが、伴奏と一緒に再生して生成結果を確認するのに使います(第一幕では、競技ルールとして、生成したメロディは伴奏MIDIファイルに追加した形で提出することになっていましたが、第二幕ではメロディのみのMIDIデータを提出することになりました。そのため、競技自体には伴奏MIDIファイルは無くても構いませんが、伴奏と一緒に再生せずにメロディの妥当性を判断するのは難しいと思います)[3]。コード進行ファイルは、モデルに与える入力データを作成するのに必要です。

まず、「弁財天」のwebサイトから、伴奏MIDIファイルとコード進行ファイルをダウンロードします。次に、これらをマイドライブの中に作った「adlib」フォルダにコピーします。その上で、次のコードを実行します。その際に、必ず、伴奏MIDIファイルとコード進行ファイルのファイル名を正しいものに変更してください。

実行すると、2つのMIDIファイルが出力されます(デフォルトでは、output1.mid、output2.mid)。前者がメロディのみの提出用MIDIファイル、後者が伴奏入りの確認用MIDIファイルです[4]。競技ルールによると、提出するMIDIファイルは「<エントリーネーム>.mid」という名前にすることになってます。あらかじめ変数output_file1の内容を変更して「<エントリーネーム>.mid」で出力できるようにするといいでしょう(もちろん、出力後に手動でファイル名を変更するのでもOK)。

backing_file = "sample1_backing.mid"       # 適宜変更すること
chord_file = "sample1_chord.csv"           # 適宜変更すること

# 2023.08.04 変更
output_file1 = "output1.mid"                # 自分のエントリーネームに変更すること
output_file2 = "output2.mid"

vae = make_model()
vae.load_weights(basedir + "/mymodel.h5")

chord_prog = read_chord_file(basedir + chord_file)
chroma_vec = chord_seq_to_chroma(make_chord_seq(chord_prog, N_BEATS))
pianoroll = make_empty_pianoroll(chroma_vec.shape[0])
for i in range(0, MELODY_LENGTH, UNIT_MEASURES):
  o, c= extract_seq(i, pianoroll, chroma_vec)
  x, y = calc_xy(o, c)
  y_new = vae.predict(np.array([x]))
  index_from = i * (N_BEATS * BEAT_RESO)
  pianoroll[index_from : index_from + y_new[0].shape[0], :] = y_new[0]
# 2023.08.04 変更
show_and_play_midi(pianoroll, 12, basedir + "/" + backing_file,
                   basedir + output_file1, basedir + output_file2)

正常に実行が終わると、図2 のような画像が出力されます。この画像は、ほとんどピアノロールと同じものですが、縦軸の向きが異なる(下に行くほど音が高い)ので注意してください。色の明るさは事後確率を表します。一番下は、休符要素を表します。

この画像の下にオーディオ再生のボタンがあります。これを押すことで生成されたメロディを確認することができます。MIDIファイル自体は、マイドライブの中の「adlib」フォルダに格納されます。

show_and_play_midi関数の第2引数は、トランスポーズ量を表します。ここでは「12」を指定していますので、モデルから生成されたメロディを1オクターブ上げてMIDIファイルに書き出していることになります。


図2 メロディ生成結果

拡張のヒント

実際に実行してみると分かるように、今回提供したプログラムはシンプルさを重視したため、ハイクオリティなメロディを出力できるモデルにはなっていません。そこで、拡張のためのヒントを少しだけお伝えします。

  • モデルを変えてみる
     ニューラルネットワークのモデルは、どんどん新しいものが提案されています。CNN、Transformer、GANなどいろいろな物を試してみるといいでしょう。
     今回はLSTM-VAEを使っていますが、モデルの入力と出力の両方にメロディを与えないといけないため、苦肉の策として、メロディ生成時は、モデルの入力のメロディの部分をすべてゼロで埋めています。本来であれば、コード進行はConditional VAEの「条件」として与え、メロディ生成時はデコーダのみ動かす方がいいと思います。そうなるように、モデルを拡張するのもいいでしょう。
     また、メロディ生成の場合、「n番目の音を入力してn+1番目の音を出力する」というモデルにする場合も多いです。そういうモデルにすれば、先頭の音をランダムに決めて残りの音はモデルが生成するということもできます。
  • ランダム性を取り入れる
     対戦ルールによると、制限時間の範囲内であれば、複数の出力を聴き比べて人出で選別することが認められています。そのため、同じ入力に対して常に同じ出力を返すモデルにするよりは、毎回異なるものが出力されるモデルにした方が、選別時の幅が広がるでしょう。
  • 後処理を導入する
     あらかじめ用意したプログラムによる後処理も認められています(ただし、聴いてから後処理を適用するかどうかを決めたり、後処理の内容を選んだりするのは不可。もちろん、後から人手で書き換えるのも不可)。たとえば、ノンダイアトニックノートをダイアトニックノートに変更する、1オクターブ上げるなどの後処理が考えられます。
  • コード進行に関する前処理を導入する
     学習の状況によっては、実際のコードがFmaj7だとしてもFとしてメロディ生成した方が、マトモなメロディが生成されることも考えられるでしょう。Fmaj7のままで生成したメロディとFに変更して生成したメロディの両方を用意して選別するのもいいでしょう。
  • 伴奏の内容を考慮してメロディを生成する
     今回のプログラムでは、メロディ生成時にはコード進行しか考慮していませんが、伴奏MIDIファイルの内容を分析し、伴奏に合ったメロディを生成することも考えられます。もちろん、かなり高度なことではありますが、ベースやギターなどのリズムに合わせてメロディのリズムを調整するなどの処理は、可能ではあると思います。また、伴奏MIDIファイルにはテンポの情報が書いてありますので、スローテンポかハイテンポかでモデルを使い分けるなども可能です。

おわりに

本文書では、AIミュージックバトル!『弁財天』出場者向けに、同バトルの競技ルールに準拠したMIDIファイルを生成するサンプルプログラムを紹介しました。

このサンプルプログラムでは、TensorFlowを使ってVAEを構築し、既存のソロメロディを使ってVAEモデルを学習しました。しかし、「弁財天」は「AIミュージック」と銘打っているものの、機械学習に限定しているわけではありません。ルールベースのメロディ生成アルゴリズムでも構いませんし、機械学習にルールベースの前処理/後処理を組み合わせたもの、確率モデルを用いたものなど、様々な方法が考えられます。ぜひ、競技ルールの範囲内で、魅力的なメロディ生成方法を考えてみていただければと思います。出場者1人1人の独創性に富んだメロディ生成システムがお披露目されることを楽しみにしております。

以上

脚注
  1. 第二幕にともなう変更点 ↩︎

  2. 第二幕に伴う変更点 ↩︎

  3. 第二幕にともなう変更点 ↩︎

  4. 第二幕にともなう変更点 ↩︎

Discussion