Generando música MIDI con Redes Neuronales Recurrentes

Posted on by

Generar música MIDI con Machine Learning

Una de las técnicas de Machine learning más conocidas son las redes neuronales recurrentes, inventadas por John Hopfield en 1982. A continuación vamos a explicar sus fundamentos matemáticos y desarrollaremos un ejemplo que genera música en formato MIDI.

Las redes neuronales recurrentes se utilizan para predecir el siguiente elemento de una serie. Por ejemplo puede ser un caracter, una nota musical, la temperatura o el valor de una acción de bolsa. Pero para poder predecir dicho elemento es necesario entrenar nuestro sistema, de manera que dada una serie aprenderá cual es el elemento más probable que le suceda.

La estructura de estas redes es similar a las redes de alimentación directa, con la diferencia de que en las redes recurrentes tienen un estado oculto cuya activación depende del estado anterior en cada instante de tiempo.

A continuación veremos cuales son los fundamentos matemáticos de dichas redes basándonos en un ejemplo para generar texto con python.

Generar texto con de las Redes Neuronales Recurrentes

La fórmula de una RNN ( Red Neuronal Recurrente) se define mediante el estado oculto actual h(t), siendo este una función dependiente del estado oculto anterior h(t-1), y el estado actual x(t). Theta ( θ ) son los parámetros de la función f.

 \Large \boxed{h(t) = f[h(t-1), x(t); \Theta]}

La función de perdidas viene dada por la secuencia de valores de x emparejados con sus respectivos valores de y para todos los valores temporales hasta t.

 \Large \boxed{L({x(1),...,x(t)},{y(1),..., y(t)}) = \sum_{n=1}^{t} L(t) = \sum_{n=1}^{t} -\log y(t) }

Pero para entenderlo mejor vamos a verlo con el desarrollo de código en python.

Cargar el archivo de entrenamiento

En primer lugar tenemos que cargar un archivo de texto lo suficientemente grande como para que nuestro sistema pueda aprender. En este caso cargaremos el archivo shakespeare.txt de la siguiente manera:

data = open('shakespeare.txt', 'r').read()
chars = list(set(data))
data_size, vocab_size = len(data), len(chars)

Donde data_size es el tamaño de nuestro texto de entrenamiento y vocab_size es el numero de caracteres únicos que contiene el texto.

Las redes neuronales operan con vectores, por lo que necesitamos crear una forma de codificar nuestros caracteres en vectores y viceversa.

char_to_ix = { ch:i for i,ch in enumerate(chars)}
ix_to_char = { i:ch for i, ch in enumerate(chars)}

Estos diccionarios nos permiten crear un vector del tamaño de vocab_size donde todos los valores serán cero excepto la posición del caracter que tendrá un uno. Veamos un ejemplo con el caracter ‘a’.

import numpy as np

vector_for_char_a = np.zeros((vocab_size, 1))
vector_for_char_a[char_to_ix['a']] = 1
print vector_for_char_a.ravel()

Salida:

[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

Definir la Red Neuronal

La red neuronal esta compuesta por tres capas: la capa de entrada, la capa oculta y la capa de salida. Cada capa esta conectada a la capa siguiente, de manera que cada nodo de la capa esta conectado a todos los nodos de la siguiente capa como podemos ver en la imagen.

Red Neuronal

Para poder entrenar la red neuronal tenemos que definir sus hiperparámetros:

  • seq_length: Logitud de la secuencia de entrada.
  • hidden_size: Tamaño de la capa oculta
  • learning_rate: Ritmo de aprendizaje

 

También tenemos de definir los parámetros del modelo:

  • Wxhson los parámetros que conectan el vector que contiene una entrada a la capa oculta.
  • Whhson los parámetros que conectan la capa oculta consigo misma. Esta es la clave de las redes neuronales recurrentes ya que esto repercute mediente la inyección de los valores previos de la salida del estado oculto consigo misma en la siguiente iteración.
  • Why: son los parámetros que conectan la capa oculta con la capa de salida.
  • bh: contiene el sesgo oculto.
  • by: contiene el sesgo de salida.

 

Todo esto lo definimos en python de la siguiente manera:

#hyperparameters

hidden_size = 100
seq_length = 25
learning_rate = 1e-1

#model parameters
Wxh = np.random.randn(hidden_size, vocab_size) * 0.01 
Whh = np.random.randn(hidden_size, hidden_size) * 0.01
Why = np.random.randn(vocab_size, hidden_size) * 0.01
bh = np.zeros((hidden_size, 1))
by = np.zeros((vocab_size, 1))

Definir la función de pérdida

La pérdida es una clave fundamental para evaluar la red neuronal. Cuanto menor sea el valor de la perdida mejor será la predicción de nuestro modelo.

Durante la fase de entrenamiento nuestro objetivo será minimizar la pérdida.

La función de perdidas calcula: el siguiente caracter dándole un caracter de los datos de entrenamiento, la pérdida comparando el caracter que ha calculado con el caracter que corresponde según los datos de entrenamiento y además los gradientes.

Los datos de entrada de la función son:

  • Lista de caracteres de entrada.
  • Lista de caracteres de los datos de entrenamiento.
  • El estado oculto anterior.

Los datos de salida de la función son:

  • La perdida.
  • Los gradientes por cada parámetro entre capas.
  • El último estado oculto.

Forward pass

El forward pass usa los parámetros del modelo (Wxh, Whh, Why, bh, by) para calcular el siguiente caracter de una serie obtenida de los datos de entrenamiento.

  • xs[t] : vector que codifica el caracter de la posición t.
  • ps[t] : son las probabilidades del siguiente caracter.

 \Large \boxed{h_t = \phi[Wx_t + Uh_{t-1}]}

Backward pass

La forma más simple de calcular todos los gradientes sería recalcular la perdida en pequeñas variaciones por cada parámetro. Pero se perdería mucho tiempo.

Mediante la propagación backdrop podemos calcular todos los gradientes para todos los parámetros de una sola vez. Des esta forma los gradientes se calculan en el sentido contrario al forward pass.

Esta es la definición de la función de perdida en python.

def lossFun(inputs, targets, hprev):
  xs, hs, ys, ps, = {}, {}, {}, {} #Empty dicts
  xs, hs, ys, ps = {}, {}, {}, {}
  hs[-1] = np.copy(hprev)
  loss = 0
  for t in xrange(len(inputs)):
    xs[t] = np.zeros((vocab_size,1))                                                                                                                     
    xs[t][inputs[t]] = 1
    hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh)                                                                                                       
    ys[t] = np.dot(Why, hs[t]) + by                                                                                                    
    ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t])) # probabilities for next chars                                                                                                              
    loss += -np.log(ps[t][targets[t],0])                                                                                            
  dWxh, dWhh, dWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)
  dbh, dby = np.zeros_like(bh), np.zeros_like(by)
  dhnext = np.zeros_like(hs[0])
  for t in reversed(xrange(len(inputs))):
    dy = np.copy(ps[t])
    dy[targets[t]] -= 1 # backprop into y  
    dWhy += np.dot(dy, hs[t].T)
    dby += dy
    dh = np.dot(Why.T, dy) + dhnext                                                                                                                                         
    dhraw = (1 - hs[t] * hs[t]) * dh                                                                                                                     
    dbh += dhraw
    dWxh += np.dot(dhraw, xs[t].T)
    dWhh += np.dot(dhraw, hs[t-1].T)
    dhnext = np.dot(Whh.T, dhraw) 
  for dparam in [dWxh, dWhh, dWhy, dbh, dby]:
    np.clip(dparam, -5, 5, out=dparam)                                                                                                                 
  return loss, dWxh, dWhh, dWhy, dbh, dby, hs[len(inputs)-1]

 

Generar muestras basadas en el modelo

Una vez tengamos el modelo la función que calculará resultados será la siguiente:

def sample(h, seed_ix, n):
  x = np.zeros((vocab_size, 1))
  x[seed_ix] = 1
  ixes = []
  for t in xrange(n):
    h = np.tanh(np.dot(Wxh, x) + np.dot(Whh, h) + bh)
    y = np.dot(Why, h) + by
    p = np.exp(y) / np.sum(np.exp(y))
    ix = np.random.choice(range(vocab_size), p=p.ravel())
    x = np.zeros((vocab_size, 1))
    x[ix] = 1
    ixes.append(ix)
  txt = ''.join(ix_to_char[ix] for ix in ixes)

Entrenar el modelo y generar texto

De esta forma entrenaremos el modelo y generará muestras por cada 1000 interacciones.

n, p = 0, 0
mWxh, mWhh, mWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)
mbh, mby = np.zeros_like(bh), np.zeros_like(by) # memory variables for Adagrad                                                                                                                
smooth_loss = -np.log(1.0/vocab_size)*seq_length # loss at iteration 0                                                                                                                        
while n<=1000*100: if p+seq_length+1 >= len(data) or n == 0:
    hprev = np.zeros((hidden_size,1))                                                                                                                                       
    p = 0                                                                                                                                                        
  inputs = [char_to_ix[ch] for ch in data[p:p+seq_length]]
  targets = [char_to_ix[ch] for ch in data[p+1:p+seq_length+1]]
  loss, dWxh, dWhh, dWhy, dbh, dby, hprev = lossFun(inputs, targets, hprev)
  smooth_loss = smooth_loss * 0.999 + loss * 0.001
  if n % 1000 == 0:
    print 'iter %d, loss: %f' % (n, smooth_loss) # print progress
    sample(hprev, inputs[0], 200)
  for param, dparam, mem in zip([Wxh, Whh, Why, bh, by],
                                [dWxh, dWhh, dWhy, dbh, dby],
                                [mWxh, mWhh, mWhy, mbh, mby]):
    mem += dparam * dparam
    param += -learning_rate * dparam / np.sqrt(mem + 1e-8)                                                                                                                   
  p += seq_length # move data pointer                                                                                                                                                         
  n += 1 # iteration counter 

Guardar el modelo en un archivo

Una vez tengamos nuestro modelo entrenado, puede que nos lleve horas, si queremos guardarlo para hacer uso de el an otro momento lo podemos hacer de la siguiente manera:

outfile = open('model.dat', 'w')
np.savez(outfile, chars=chars, hprev=hprev, Wxh=Wxh, Whh=Whh, bh=bh,by=by, Why=Why, vocab_size=np.array([vocab_size]))

Cargar el modelo entrenado

Cuando tengamos el modelo entrenado podemos cargarlo en otro programa de la siguiente forma.

model_file  =  open('model.dat', 'r')
model =  np.load(model_file)

chars= model['chars']
#hprev= model['hprev']
hprev = np.zeros((100,1))
Wxh=model['Wxh']
Whh=model['Whh']
bh=model['bh']
by=model['by']
Why=model['Why']
vocab_size=model['vocab_size'][0]

char_to_ix = { ch:i for i,ch in enumerate(chars) }
ix_to_char = { i:ch for i,ch in enumerate(chars) }

Una vez cargado añadimos la función para generar muestras y ya podemos hacer uso de nuestra red neuronal recurrente.

Generar música MIDI con Redes Neuronales Recurrentes

Una vez que hemos visto las funciones y parámetros necesarios para crear una Red Neuronal Recurrente podemos crear un modelo que genere música en formato MIDI.

Si quieres descargarte el proyecto y probarlo aquí tienes el código en github.

El proyecto está formado por los siguientes archivos:

  • raw_music.py: Más de 600 caciones en formato texto convertidas desde formato MIDI.
  • rnn_midi.py: Generador del modelo de red recurrente neuronal basado en raw_music.py.
  • keyboard2midi.py: Programa que cargará el modelo generado por rnn_midi.py y genera un archivo MIDI.
  • rnn_midi_25_100_50000.dat : Modelo guardado con 50.000 iteraciones.
  • rnn_midi_25_100_200000.dat : Modelo guardado con 200.000 iteraciones.
  • rnn_midi_25_100_250000.dat : Modelo guardado con 250.000 iteraciones.

 

Los requisitos necesarios para poder utilizar este proyecto son:

  • Python 2.7 o superior.
  • Numpy: Librería matemática para python.
  • Mido: Librería de archivos MIDI para python

Entrenamiento del modelo

Aunque el proyecto contiene varios modelos ya entrenados con diferentes número de iteraciones podemos entrenarlo nosotros.

python rrn_midi.py

Esto genera el modelo entrenado en un archivo con extensión .dat.

Generar Archivos MIDI con el modelo entrenado

python keyboard2midi.py

Esto genera un archivo con la extensión .mid.

Reproducir archivos MIDI

Para reproducir archivos MIDI tenemos varias opciones, aunque una de las más sencillas en timidity, que además de reproducir MIDI puede convertirloas a WAV.

timidity song1.mid

Generar archivo MIDI, covertirlo a wav y reproducirlo en una sola línea.

python keyboard2midi.py; timidity --output-24bit --output-mono -A120 song1.mid -Ow -o song1.wav; aplay song1.wav

Convertir archivos MIDI a MP3

ffmpeg -i song1.wav -acodec libmp3lame song1.mp3

Pruebas realizadas

Estas son varias de las pruebas generadas por nuesta Inteligencia Artificial.

Canción de trompeta creada por Inteligencia Artificial:

Canción de harpa creada por Inteligencia Artificial:

Canción de violín creada por Inteligencia Artificial:

Comments are disabled