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

Gere músicas com uma RNN

Este tutorial mostra como gerar notas musicais usando uma rede neural recorrente simples (RNN). Você treinará um modelo usando uma coleção de arquivos MIDI de piano do dataset MAESTRO. Dada uma sequência de notas, seu modelo aprenderá a prever a próxima nota na sequência. Você pode gerar sequências mais longas ao chamar o modelo repetidamente.

Este tutorial contém o código completo para processar e criar arquivos MIDI. Para saber mais sobre como as RNNs funcionam, acesse o tutorial Geração de texto com uma RNN.

Configuração

Este tutorial usa a biblioteca pretty_midi para criar e processar arquivos MIDI e pyfluidsynth para gerar a reprodução de áudio no 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

Baixe o dataset 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', )

Esse dataset contém cerca de 1.200 arquivos MIDI.

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

Processe um arquivo MIDI

Primeiro, use pretty_midi para processar um único arquivo MIDI e inspecionar o formato das notas. Se você quiser baixar o arquivo MIDI abaixo para reproduzir no seu computador, faça isso no Colab ao escrever files.download(sample_file).

sample_file = filenames[1] print(sample_file)

Gere um objeto PrettyMIDI para o arquivo MIDI de amostra.

pm = pretty_midi.PrettyMIDI(sample_file)

Reproduza o arquivo de amostra. O widget de reprodução pode levar vários segundos para carregar.

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)

Inspecione o arquivo MIDI. Que tipos de instrumentos são usados?

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)

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

Você usará três variáveis para representar uma nota ao treinar o modelo: pitch, step e duration. O tom (pitch) é a qualidade perceptiva do som como um número de nota MIDI. step é o tempo decorrido desde a nota anterior ou o início da faixa. duration é o tempo que a nota tocará em segundos e a diferença entre os horários de término e início da nota.

Extraia as notas do arquivo MIDI de amostra.

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

Talvez seja mais fácil interpretar os nomes das notas em vez dos tons. Então, você pode usar a função abaixo para converter os valores de tom numéricos para nomes de notas. O nome mostra o tipo, acidente e número da oitava da nota (por exemplo, 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 a composição musical, plote o tom da nota, o início e o término na duração da faixa (ou seja, rolo de piano). Comece com as primeiras 100 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)

Plote as notas para a faixa inteira.

plot_piano_roll(raw_notes)

Confira a distribuição de cada variável das notas.

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)

Crie um arquivo MIDI

Você pode gerar seu próprio arquivo MIDI a partir de uma lista de notas usando a função abaixo.

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)

Reproduza o arquivo MIDI gerado e veja se há alguma diferença.

display_audio(example_pm)

Como antes, você pode escrever files.download(example_file) para baixar e reproduzir esse arquivo.

Crie o dataset de treinamento

Crie o dataset de treinamento ao extrair as notas dos arquivos MIDI. Você pode começar usando um pequeno número de arquivos e depois testar com mais. Isso pode levar alguns 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)

Em seguida, crie um tf.data.Dataset a partir das notas processadas.

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

Você treinará o modelo com lotes de sequências de notas. Cada exemplo consistirá em uma sequência de notas como as características de entrada e a próxima nota como o rótulo. Assim, o modelo será treinado para prever a próxima nota em uma sequência. Confira um diagrama descrevendo esse processo (e mais detalhes) em Classificação de texto com uma RNN.

É possível usar a função útil window com o tamanho seq_length para criar as características e os rótulos nesse 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)

Defina a duração da sequência para cada exemplo. Teste diferentes durações (por exemplo, 50, 100 e 150) para encontrar a ideal para os dados ou use o ajuste de hiperparâmetros. O tamanho do vocabulário (vocab_size) é definido como 128, representando todos os tons compatíveis com pretty_midi.

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

The shape of the dataset is (100,1), meaning that the model will take 100 notes as input, and learn to predict the following note as output.

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)

Agrupe os exemplos e configure o dataset para melhor desempenho.

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

Crie e treine o modelo

O modelo terá três saídas, uma para cada variável das notas. Para step e duration, você usará uma função de perda personalizada com base no erro quadrático médio, que incentiva o modelo a gerar valores não 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()

Testando a função model.evaluate, é possível ver que a perda de pitch é significativamente maior que as perdas de step e duration. Observe que loss é a perda total computada ao somar todas as outras perdas e, no momento, é dominada pela perda de pitch.

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

Uma maneira de equilibrar isso seria usar o argumento loss_weights para compilar:

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

Assim, loss seria a soma ponderada das perdas individuais.

model.evaluate(train_ds, return_dict=True)

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

Gere notas

Para gerar notas com o modelo, primeiro você precisa fornecer uma sequência inicial de notas. A função abaixo gera uma nota a partir de uma sequência.

Para o tom da nota, é extraída uma amostra da distribuição softmax das notas produzidas pelo modelo, e não é apenas escolhida a nota com maior probabilidade. Caso a nota mais provável fosse sempre escolhida, seriam geradas sequências repetitivas.

O parâmetro temperature pode ser usado para controlar a aleatoriedade das notas geradas. Confira mais detalhes sobre a temperatura em Geração de texto com uma 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)

Agora, gere algumas notas. Você pode brincar com a temperatura e a sequência inicial em next_notes para ver o que acontece.

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)

Você também pode baixar o arquivo de áudio ao adicionar as duas linhas abaixo:

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

Visualize as notas geradas.

plot_piano_roll(generated_notes)

Confira as distribuições de pitch, step e duration.

plot_distributions(generated_notes)

Nos plots acima, você verá a mudança na distribuição das variáveis das notas. Como há um loop de feedback entre as saídas e as entradas, o modelo tende a gerar sequências semelhantes de saídas para reduzir a perda. Isso é especialmente relevante para step e duration, que usam a perda de EQM. Para pitch, você pode aumentar a aleatoriedade com o aumento da temperature em predict_next_note.

Próximos passos

Este tutorial demonstrou a mecânica de usar uma RNN para gerar sequências de notas a partir de um dataset de arquivos MIDI. Para saber mais, acesse o tutorial relacionado Geração de texto com uma RNN, com diagramas e explicações adicionais.

Uma das alternativas ao uso de RNNs para gerar músicas é usar GANs. Em vez de gerar áudio, uma abordagem baseada em GAN consegue gerar uma sequência inteira em paralelo. A equipe Magenta fez um ótimo trabalho nessa abordagem com o GANSynth. Você também pode encontrar vários projetos de música e arte incríveis, além de código aberto, no site do projeto Magenta.