Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/es-419/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.

Generar música con una RNN

Este tutorial usa una sencilla red neuronal recurrente (RNN) para generar notas musicales. Usted entrenará un modelo usando una colección de archivos MIDI de piano del conjunto de datos MAESTRO. Su modelo aprenderá a predecir la siguiente nota de la secuencia, dada una secuencia de notas. Llame repetidamente al modelo para generar secuencias de notas más largas.

Este tutorial contiene el código completo para parsear y generar archivos MIDI. Consulte el tutorial Generación de texto con una RNN para saber más sobre cómo funcionan las RNN.

Preparación

Este tutorial usa la librería pretty_midi para crear y parsear archivos MIDI, y pyfluidsynth para generar la reproducción de audio en Colab.

!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

Descargar el conjunto de datos 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', )

El conjunto de datos contiene unos 1200 archivos MIDI.

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

Procesar un archivo MIDI

Lo primero que hay que hacer es usar pretty_midi para parsear un único archivo MIDI y comprobar el formato de las notas. Si quiere descargar el siguiente archivo MIDI para reproducirlo en su computadora, puede hacerlo con colab escribiendo files.download(sample_file).

sample_file = filenames[1] print(sample_file)

Genere un objeto PrettyMIDI para el archivo MIDI de muestra.

pm = pretty_midi.PrettyMIDI(sample_file)

Reproduzca el archivo de muestra. El widget de reproducción puede tardar varios segundos en cargarse.

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)

Inspeccione el archivo MIDI. ¿Qué instrumentos se usan?

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)

Extraer notas

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}')

Cuando entrene el modelo, usará tres variables para representar una nota: pitch, step y duration. Pitch (tono) es la calidad percibida del sonido como un número de nota MIDI. step es el tiempo transcurrido desde la nota anterior o el inicio de la pista. duration es cuánto tiempo se reproducirá la nota en segundos y es la diferencia entre su hora de finalización y la hora de inicio.

Extraiga las notas del archivo MIDI de muestra.

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()

Quizá sea más fácil interpretar los nombres de las notas en lugar de los tonos, así que puede usar la siguiente función para convertir los valores numéricos de tono en nombres de nota. El nombre de la nota muestra el tipo de nota, su alteración y el número de octava (por ejemplo, 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]

Para visualizar la pieza musical, dibuje el tono, el inicio y el final de las notas a lo largo de la pista (es decir, el teclado del piano). Empiece con las 100 primeras notas.

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)

Dibuje las notas de toda la pista.

plot_piano_roll(raw_notes)

Verifique la distribución de cada variable de nota.

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)

Crear un archivo MIDI

La siguiente función le permite crear su propio archivo MIDI a partir de una lista de notas.

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)

Reproduzca el archivo MIDI generado y revise si hay alguna diferencia.

display_audio(example_pm)

También aquí puede escribir files.download(example_file) para descargar y reproducir este archivo.

Crear el conjunto de datos de entrenamiento

Cree el conjunto de datos de entrenamiento extrayendo notas de los archivos MIDI. Empiece con un pequeño número de archivos y luego experimente con más. Esto puede llevarle unos cuantos minutos.

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)

Luego, cree un tf.data.Dataset a partir de las notas parseadas.

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

Va a entrenar este modelo con lotes de secuencias de notas. Cada ejemplo tiene una secuencia de notas como elemento de entrada y la nota siguiente como la etiqueta. De este modo, el modelo se entrena para predecir la siguiente nota de una secuencia. Puede encontrar un diagrama que explica este proceso (y más detalles) en Clasificación de texto con una RNN.

Puede usar la práctica función window con tamaño seq_length para crear las características y etiquetas en este formato.

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)

Especifique la longitud de la secuencia para cada ejemplo. Experimente con distintas longitudes (por ejemplo, 50, 100, 150) para encontrar la que mejor se adapte a los datos, o use ajuste de hiperparámetros. El tamaño (vocab_size) del vocabulario se establece en 128, lo que representa todos los tonos admitidos por pretty_midi.

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

La forma del conjunto de datos es (100,1), de modo que el modelo toma 100 notas como entrada y aprende a predecir la siguiente nota como salida.

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)

Procese los ejemplos por lotes y configure el conjunto de datos para enfocarse al rendimiento.

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

Cómo entrenar y crear su modelo

Hay tres salidas del modelo, una para cada variable de nota. Para step y duration se usará una función de pérdida personalizada basada en el error cuadrático medio para que el modelo produzca valores no negativos.

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()

Si probamos la función model.evaluate, veremos que la pérdida de pitch es significativamente mayor que las pérdidas de step y duration. Observe que loss es la pérdida total al sumar todas las demás pérdidas, y en este momento está dominada por la pérdida de pitch.

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

Se puede compensar usando el argumento loss_weights al compilar:

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

El loss se convierte entonces en la suma ponderada de las pérdidas individuales.

model.evaluate(train_ds, return_dict=True)

Entrenar el modelo.

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()

Generar notas

Para usar el modelo para generar notas, primero necesitará suministrar una secuencia inicial de notas. La siguiente función genera una nota a partir de una secuencia de notas.

Para el tono de la nota, tome una muestra de la distribución softmax de notas producidas por el modelo, en lugar de elegir simplemente la nota con mayor probabilidad. Si siempre se eligiera la nota con mayor probabilidad, se obtendrían secuencias repetitivas de notas.

El parámetro temperature sirve para controlar qué tan aleatorias son las notas generadas. Puede encontrar más detalles sobre la temperatura en Generación de texto con una 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)

Ahora genere algunas notas. Pruebe a jugar con la temperatura y la secuencia de inicio en next_notes, a ver qué pasa.

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)

Si quiere descargar el archivo de audio, añada las dos líneas siguientes:

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

Visualice las notas generadas.

plot_piano_roll(generated_notes)

Revisa las distribuciones de pitch{/código0}, step{/código1} y `duration{/código2}.```

plot_distributions(generated_notes)

Notará el cambio en la distribución de las variables de nota en los gráficos anteriores. Como existe un bucle de retroalimentación entre las salidas y las entradas del modelo, éste tiende a generar secuencias similares de salidas para reducir la pérdida, especialmente para step y duration, que usan la pérdida MSE. Para pitch, puede aumentar la aleatoriedad aumentando la temperature en predict_next_note.

Siguientes pasos

Este tutorial muestra la mecánica de usar una RNN para generar secuencias de notas a partir de un conjunto de datos de archivos MIDI. Si quiere saber más, puede consultar el tutorial relacionado Generación de texto con una RNN, que tiene más diagramas y explicaciones.

Las GAN son una de las alternativas a las RNN para la generación de música. En lugar de generar audio, un enfoque basado en GAN puede generar una secuencia completa en paralelo. Es impresionante el trabajo realizado por el equipo de Magenta con GANSynth. También puede encontrar muchos proyectos musicales y artísticos maravillosos y código fuente abierto en el sitio web del proyecto Magenta.