Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/ja/tutorials/audio/music_generation.ipynb
25118 views
Kernel: Python 3
#@title Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.

RNN を使って音楽を生成する

このチュートリアルでは、単純な RNN(回帰型ニューラルネットワーク)を使用して楽譜を生成する方法を説明します。モデルは、MAESTRO データセットのピアノ MIDI ファイルのコレクションを使ってトレーニングします。ノートのシーケンスを与えられることで、モデルはそのシーケンスの次のノートを予測するように学習します。モデルを繰り返し呼び出すことで、より長いノートのシーケンスを生成できます。

このチュートリアルには、MIDI ファイルを解析して作成するための完全なコードが含まれます。RNN の仕組みについては、RNN によるテキスト生成をご覧ください。

MNIST モデルをビルドする

このチュートリアルでは、MIDI ファイルの作成と解析を行う pretty_midi ライブラリと、Colab でオーディオ再生を生成する pyfluidsynth を使用します。

!sudo apt install -y fluidsynth
!pip install --upgrade pyfluidsynth
!pip install pretty_midi
import collections import datetime import fluidsynth import glob import numpy as np import pathlib import pandas as pd import pretty_midi import seaborn as sns import tensorflow as tf from IPython import display from matplotlib import pyplot as plt from typing import Optional
seed = 42 tf.random.set_seed(seed) np.random.seed(seed) # Sampling rate for audio playback _SAMPLING_RATE = 16000

Maestro データセットをダウンロードする

data_dir = pathlib.Path('data/maestro-v2.0.0') if not data_dir.exists(): tf.keras.utils.get_file( 'maestro-v2.0.0-midi.zip', origin='https://storage.googleapis.com/magentadata/datasets/maestro/v2.0.0/maestro-v2.0.0-midi.zip', extract=True, cache_dir='.', cache_subdir='data', )

データセットには、約 1,200 個の MIDI ファイルが含まれます。

filenames = glob.glob(str(data_dir/'**/*.mid*')) print('Number of files:', len(filenames))

MIDI ファイルを処理する

まず、pretty_midi を使用して、単一の MIDI ファイルを解析し、ノートのフォーマットを検査します。以下の MIDI ファイルをコンピュータにダウンロードして再生する場合は、Colab で files.download(sample_file) を記述してください。

sample_file = filenames[1] print(sample_file)

サンプル MIDI ファイルの PrettyMIDI オブジェクトを生成します。

pm = pretty_midi.PrettyMIDI(sample_file)

サンプルファイルを再生します。再生ウィジェットの読み込みには数秒かかることがあります。

def display_audio(pm: pretty_midi.PrettyMIDI, seconds=30): waveform = pm.fluidsynth(fs=_SAMPLING_RATE) # Take a sample of the generated waveform to mitigate kernel resets waveform_short = waveform[:seconds*_SAMPLING_RATE] return display.Audio(waveform_short, rate=_SAMPLING_RATE)
display_audio(pm)

MIDI ファイルを検査します。どのような楽器が使用されていますか?

print('Number of instruments:', len(pm.instruments)) instrument = pm.instruments[0] instrument_name = pretty_midi.program_to_instrument_name(instrument.program) print('Instrument name:', instrument_name)

ノートを抽出する

for i, note in enumerate(instrument.notes[:10]): note_name = pretty_midi.note_number_to_name(note.pitch) duration = note.end - note.start print(f'{i}: pitch={note.pitch}, note_name={note_name},' f' duration={duration:.4f}')

モデルをトレーニングする際に、pitchstepduration という 3 つの変数を使用してノートを表現します。pitch は、MIDI ノートナンバーとしてのサウンドの知覚的な質です。step は、前のノートまたは曲の始めから経過した時間です。duration は、ノートの再生秒数で、ノートの終了時間とノートの開始時間の差です。

サンプル MIDI ファイルからノートを抽出します。

def midi_to_notes(midi_file: str) -> pd.DataFrame: pm = pretty_midi.PrettyMIDI(midi_file) instrument = pm.instruments[0] notes = collections.defaultdict(list) # Sort the notes by start time sorted_notes = sorted(instrument.notes, key=lambda note: note.start) prev_start = sorted_notes[0].start for note in sorted_notes: start = note.start end = note.end notes['pitch'].append(note.pitch) notes['start'].append(start) notes['end'].append(end) notes['step'].append(start - prev_start) notes['duration'].append(end - start) prev_start = start return pd.DataFrame({name: np.array(value) for name, value in notes.items()})
raw_notes = midi_to_notes(sample_file) raw_notes.head()

ピッチよりもノート名を解釈する方が簡単な場合があるため、以下の関数を使用して数値のピッチ値からノート名に変換します。ノート名は、ノートの種類、臨時記号、およびオクターブ番号(例: C#4)を示します。

get_note_names = np.vectorize(pretty_midi.note_number_to_name) sample_note_names = get_note_names(raw_notes['pitch']) sample_note_names[:10]

曲を視覚化するために、ノートピッチとトラック全体(ピアノロール)の開始と終了をプロットします。最初の 100 個のノートから始めます。

def plot_piano_roll(notes: pd.DataFrame, count: Optional[int] = None): if count: title = f'First {count} notes' else: title = f'Whole track' count = len(notes['pitch']) plt.figure(figsize=(20, 4)) plot_pitch = np.stack([notes['pitch'], notes['pitch']], axis=0) plot_start_stop = np.stack([notes['start'], notes['end']], axis=0) plt.plot( plot_start_stop[:, :count], plot_pitch[:, :count], color="b", marker=".") plt.xlabel('Time [s]') plt.ylabel('Pitch') _ = plt.title(title)
plot_piano_roll(raw_notes, count=100)

トラック全体のノートをプロットします。

plot_piano_roll(raw_notes)

各ノート変数の分布を確認します。

def plot_distributions(notes: pd.DataFrame, drop_percentile=2.5): plt.figure(figsize=[15, 5]) plt.subplot(1, 3, 1) sns.histplot(notes, x="pitch", bins=20) plt.subplot(1, 3, 2) max_step = np.percentile(notes['step'], 100 - drop_percentile) sns.histplot(notes, x="step", bins=np.linspace(0, max_step, 21)) plt.subplot(1, 3, 3) max_duration = np.percentile(notes['duration'], 100 - drop_percentile) sns.histplot(notes, x="duration", bins=np.linspace(0, max_duration, 21))
plot_distributions(raw_notes)

MIDI ファイルを作成する

以下の関数を使用して、ノートのリストから独自の MIDI を生成できます。

def notes_to_midi( notes: pd.DataFrame, out_file: str, instrument_name: str, velocity: int = 100, # note loudness ) -> pretty_midi.PrettyMIDI: pm = pretty_midi.PrettyMIDI() instrument = pretty_midi.Instrument( program=pretty_midi.instrument_name_to_program( instrument_name)) prev_start = 0 for i, note in notes.iterrows(): start = float(prev_start + note['step']) end = float(start + note['duration']) note = pretty_midi.Note( velocity=velocity, pitch=int(note['pitch']), start=start, end=end, ) instrument.notes.append(note) prev_start = start pm.instruments.append(instrument) pm.write(out_file) return pm
example_file = 'example.midi' example_pm = notes_to_midi( raw_notes, out_file=example_file, instrument_name=instrument_name)

生成した MIDI ファイルを再生し、何らかの違いがないか確認します。

display_audio(example_pm)

前と同様に、files.download(example_file) を記述すると、このファイルをダウンロードして再生できます。

トレーニングデータセットを作成する

MIDI ファイルからノートを抽出して、トレーニングデータセットを作成します。まず、少数のファイルを使って作業を開始し、後の方でさらに他のファイルを使用して実験することができます。これには数分かかることがあります。

num_files = 5 all_notes = [] for f in filenames[:num_files]: notes = midi_to_notes(f) all_notes.append(notes) all_notes = pd.concat(all_notes)
n_notes = len(all_notes) print('Number of notes parsed:', n_notes)

次に、解析したノートから tf.data.Dataset を作成します。

key_order = ['pitch', 'step', 'duration'] train_notes = np.stack([all_notes[key] for key in key_order], axis=1)
notes_ds = tf.data.Dataset.from_tensor_slices(train_notes) notes_ds.element_spec

バッチ化されたノートのシーケンスに対してモデルをトレーニングします。各 Example では、入力特徴量としてノートのシーケンス、ラベルとして次のノートが使用されます。このようにすることで、モデルはシーケンスの次のノートを予測するようにトレーニングされます。このプロセスを説明した図(およびその他の詳細)は、RNN によるテキスト分類をご覧ください。

このフォーマットで特徴量とラベルを作成するには、便利な window 関数とサイズ seq_length を使用できます。

def create_sequences( dataset: tf.data.Dataset, seq_length: int, vocab_size = 128, ) -> tf.data.Dataset: """Returns TF Dataset of sequence and label examples.""" seq_length = seq_length+1 # Take 1 extra for the labels windows = dataset.window(seq_length, shift=1, stride=1, drop_remainder=True) # `flat_map` flattens the" dataset of datasets" into a dataset of tensors flatten = lambda x: x.batch(seq_length, drop_remainder=True) sequences = windows.flat_map(flatten) # Normalize note pitch def scale_pitch(x): x = x/[vocab_size,1.0,1.0] return x # Split the labels def split_labels(sequences): inputs = sequences[:-1] labels_dense = sequences[-1] labels = {key:labels_dense[i] for i,key in enumerate(key_order)} return scale_pitch(inputs), labels return sequences.map(split_labels, num_parallel_calls=tf.data.AUTOTUNE)

各 Example のシーケンスの長さを設定します。さまざまな長さ(50、100, 150 など)を試してデータに最適なものを確認するか、ハイパーパラメータのチューニングを行います。語彙のサイズ(vocab_size)は 128 で、pretty_midi がサポートするすべてのピッチを表します。

seq_length = 25 vocab_size = 128 seq_ds = create_sequences(notes_ds, seq_length, vocab_size) seq_ds.element_spec

データセットの形状は (100,1) で、モデルは 100 個のノートを入力として取り、出力として移行のノートの予測を学習します。

for seq, target in seq_ds.take(1): print('sequence shape:', seq.shape) print('sequence elements (first 10):', seq[0: 10]) print() print('target:', target)

Example をバッチ処理し、パフォーマンスを得られるようにデータセットを構成します。

batch_size = 64 buffer_size = n_notes - seq_length # the number of items in the dataset train_ds = (seq_ds .shuffle(buffer_size) .batch(batch_size, drop_remainder=True) .cache() .prefetch(tf.data.experimental.AUTOTUNE))
train_ds.element_spec

モデルを作成してトレーニングする

このモデルには、ノート変数あたり 1 つの出力、計 3 つの出力があります。stepduration については、モデルが負でない値を出力するように、平均二条誤差に基づくカスタム損失関数を使用します。

def mse_with_positive_pressure(y_true: tf.Tensor, y_pred: tf.Tensor): mse = (y_true - y_pred) ** 2 positive_pressure = 10 * tf.maximum(-y_pred, 0.0) return tf.reduce_mean(mse + positive_pressure)
input_shape = (seq_length, 3) learning_rate = 0.005 inputs = tf.keras.Input(input_shape) x = tf.keras.layers.LSTM(128)(inputs) outputs = { 'pitch': tf.keras.layers.Dense(128, name='pitch')(x), 'step': tf.keras.layers.Dense(1, name='step')(x), 'duration': tf.keras.layers.Dense(1, name='duration')(x), } model = tf.keras.Model(inputs, outputs) loss = { 'pitch': tf.keras.losses.SparseCategoricalCrossentropy( from_logits=True), 'step': mse_with_positive_pressure, 'duration': mse_with_positive_pressure, } optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate) model.compile(loss=loss, optimizer=optimizer) model.summary()

model.evaluate 関数をテストすると、pitch の損失が stepduration の損失を大きく上回ることがわかります。loss はその他すべての損失を合計して算出された合計損失であり、現在 pitch 損失に占有されていることに注意してください。

losses = model.evaluate(train_ds, return_dict=True) losses

これを平衡化する方法として、loss_weights 引数を使用してコンパイルする方法が挙げられます。

model.compile( loss=loss, loss_weights={ 'pitch': 0.05, 'step': 1.0, 'duration':1.0, }, optimizer=optimizer, )

これにより、loss は個別の損失の重み付き合計になります。

model.evaluate(train_ds, return_dict=True)

モデルをトレーニングする。

callbacks = [ tf.keras.callbacks.ModelCheckpoint( filepath='./training_checkpoints/ckpt_{epoch}', save_weights_only=True), tf.keras.callbacks.EarlyStopping( monitor='loss', patience=5, verbose=1, restore_best_weights=True), ]
%%time epochs = 50 history = model.fit( train_ds, epochs=epochs, callbacks=callbacks, )
plt.plot(history.epoch, history.history['loss'], label='total loss') plt.show()

ノートを生成する

モデルを使用してノートを生成するには、まず、ノートの開始シーケンスを指定する必要があります。以下の関数は、ノートのシーケンスから 1 つのノートを生成します。

ノートのピッチについては、モデルが生成するノートのソフトマックス分布からサンプルが取り出され、最も高い確率ののノートが拾われるわけではありません。常に最も高い確率のノートを拾ってしまうと、生成されるノートのシーケンスが繰り返されてしまいます。

生成されるノートのランダム性を制御するには、temperature パラメータを使用できます。temperature の詳細については、RNN によるテキスト生成をご覧ください。

def predict_next_note( notes: np.ndarray, keras_model: tf.keras.Model, temperature: float = 1.0) -> tuple[int, float, float]: """Generates a note as a tuple of (pitch, step, duration), using a trained sequence model.""" assert temperature > 0 # Add batch dimension inputs = tf.expand_dims(notes, 0) predictions = model.predict(inputs) pitch_logits = predictions['pitch'] step = predictions['step'] duration = predictions['duration'] pitch_logits /= temperature pitch = tf.random.categorical(pitch_logits, num_samples=1) pitch = tf.squeeze(pitch, axis=-1) duration = tf.squeeze(duration, axis=-1) step = tf.squeeze(step, axis=-1) # `step` and `duration` values should be non-negative step = tf.maximum(0, step) duration = tf.maximum(0, duration) return int(pitch), float(step), float(duration)

では、ノートを生成してみましょう。next_notes の temperature と開始シーケンスを変更しながら、どのような結果になるか確認します。

temperature = 2.0 num_predictions = 120 sample_notes = np.stack([raw_notes[key] for key in key_order], axis=1) # The initial sequence of notes; pitch is normalized similar to training # sequences input_notes = ( sample_notes[:seq_length] / np.array([vocab_size, 1, 1])) generated_notes = [] prev_start = 0 for _ in range(num_predictions): pitch, step, duration = predict_next_note(input_notes, model, temperature) start = prev_start + step end = start + duration input_note = (pitch, step, duration) generated_notes.append((*input_note, start, end)) input_notes = np.delete(input_notes, 0, axis=0) input_notes = np.append(input_notes, np.expand_dims(input_note, 0), axis=0) prev_start = start generated_notes = pd.DataFrame( generated_notes, columns=(*key_order, 'start', 'end'))
generated_notes.head(10)
out_file = 'output.mid' out_pm = notes_to_midi( generated_notes, out_file=out_file, instrument_name=instrument_name) display_audio(out_pm)

以下の 2 行を追加して、音声ファイルをダウンロードすることもできます。

from google.colab import files files.download(out_file)

生成されたノートを視覚化します。

plot_piano_roll(generated_notes)

pitchstep、および duration の分布を確認します。

plot_distributions(generated_notes)

上記のプロットでは、ノート変数の分布の変化を確認できます。モデルの出力と入力の間にフィードバックループがあるため、モデルは、似たような出力のシーケンスを生成して損失を低下させる傾向にあります。これは特に、MSE 損失を使用する stepduration に関連しています。pitch については、predict_next_notetemperature を増加させて、ランダム性を高めることができます。

次のステップ

このチュートリアルでは、RNN を使用して、MIDI ファイルのデータセットからノートのシーケンスを生成する仕組みを説明しました。さらに詳しい内容については、関連性の高い RNN によるテキスト生成チュートリアルをご覧ください。追加のダイアグラムと説明が記載されています。

音楽生成では、RNN のほかに、GAN を使用することも可能です。GAN ベースのアプローチでは、オーディオを生成する代わりに、シーケンス全体を並行して生成することができます。Magenta チームは、GANSynth を使用してこのアプローチで圧巻の取り組みを達成しています。Magenta プロジェクトのウェブサイトには、素晴らしい音楽とアートのプロジェクトとオープンソースコードが多数掲載されています。