CNTK - Red neuronal recurrente

Ahora, entendamos cómo construir una red neuronal recurrente (RNN) en CNTK.

Introducción

Aprendimos a clasificar imágenes con una red neuronal, y es uno de los trabajos icónicos del aprendizaje profundo. Pero, otra área en la que sobresale la red neuronal y se están realizando muchas investigaciones son las redes neuronales recurrentes (RNN). Aquí, vamos a saber qué es RNN y cómo se puede utilizar en escenarios en los que necesitamos lidiar con datos de series de tiempo.

¿Qué es la red neuronal recurrente?

Las redes neuronales recurrentes (RNN) pueden definirse como la clase especial de NN que son capaces de razonar a lo largo del tiempo. Los RNN se utilizan principalmente en escenarios, en los que debemos tratar con valores que cambian con el tiempo, es decir, datos de series temporales. Para entenderlo de una mejor manera, hagamos una pequeña comparación entre las redes neuronales regulares y las redes neuronales recurrentes:

  • Como sabemos, en una red neuronal normal, solo podemos proporcionar una entrada. Esto lo limita a resultados en una sola predicción. Para darle un ejemplo, podemos traducir un texto usando redes neuronales regulares.

  • Por otro lado, en las redes neuronales recurrentes, podemos proporcionar una secuencia de muestras que resulten en una sola predicción. En otras palabras, usando RNN podemos predecir una secuencia de salida basada en una secuencia de entrada. Por ejemplo, ha habido bastantes experimentos exitosos con RNN en tareas de traducción.

Usos de la red neuronal recurrente

Los RNN se pueden utilizar de varias formas. Algunos de ellos son los siguientes:

Predecir una única salida

Antes de profundizar en los pasos, veamos cómo RNN puede predecir una salida única basada en una secuencia, veamos cómo se ve un RNN básico.

Como podemos en el diagrama anterior, RNN contiene una conexión de bucle de retorno a la entrada y cada vez que alimentamos una secuencia de valores, procesará cada elemento en la secuencia como pasos de tiempo.

Además, debido a la conexión de bucle invertido, RNN puede combinar la salida generada con la entrada para el siguiente elemento de la secuencia. De esta manera, RNN construirá una memoria sobre toda la secuencia que se puede utilizar para hacer una predicción.

Para hacer predicciones con RNN, podemos realizar los siguientes pasos:

  • Primero, para crear un estado oculto inicial, necesitamos alimentar el primer elemento de la secuencia de entrada.

  • Después de eso, para producir un estado oculto actualizado, necesitamos tomar el estado oculto inicial y combinarlo con el segundo elemento en la secuencia de entrada.

  • Por último, para producir el estado oculto final y predecir la salida para el RNN, necesitamos tomar el elemento final en la secuencia de entrada.

De esta manera, con la ayuda de esta conexión de bucle invertido, podemos enseñarle a un RNN a reconocer patrones que ocurren con el tiempo.

Predecir una secuencia

El modelo básico, discutido anteriormente, de RNN también se puede extender a otros casos de uso. Por ejemplo, podemos usarlo para predecir una secuencia de valores basados ​​en una sola entrada. En este escenario, para poder realizar predicciones con RNN podemos realizar los siguientes pasos:

  • Primero, para crear un estado oculto inicial y predecir el primer elemento en la secuencia de salida, necesitamos alimentar una muestra de entrada a la red neuronal.

  • Después de eso, para producir un estado oculto actualizado y el segundo elemento en la secuencia de salida, necesitamos combinar el estado oculto inicial con la misma muestra.

  • Por último, para actualizar el estado oculto una vez más y predecir el elemento final en la secuencia de salida, alimentamos la muestra otra vez.

Predicción de secuencias

Como hemos visto, cómo predecir un valor único en función de una secuencia y cómo predecir una secuencia en función de un valor único. Ahora veamos cómo podemos predecir secuencias para secuencias. En este escenario, para poder realizar predicciones con RNN podemos realizar los siguientes pasos:

  • Primero, para crear un estado oculto inicial y predecir el primer elemento en la secuencia de salida, necesitamos tomar el primer elemento en la secuencia de entrada.

  • Después de eso, para actualizar el estado oculto y predecir el segundo elemento en la secuencia de salida, necesitamos tomar el estado oculto inicial.

  • Por último, para predecir el elemento final en la secuencia de salida, necesitamos tomar el estado oculto actualizado y el elemento final en la secuencia de entrada.

Trabajo de RNN

Para comprender el funcionamiento de las redes neuronales recurrentes (RNN), primero debemos comprender cómo funcionan las capas recurrentes en la red. Primero, analicemos cómo podemos predecir la salida con una capa recurrente estándar.

Predicción de salida con capa RNN estándar

Como comentamos anteriormente, también, una capa básica en RNN es bastante diferente de una capa regular en una red neuronal. En la sección anterior, también demostramos en el diagrama la arquitectura básica de RNN. Para actualizar el estado oculto por primera vez, podemos usar la siguiente fórmula:

En la ecuación anterior, calculamos el nuevo estado oculto calculando el producto escalar entre el estado oculto inicial y un conjunto de pesos.

Ahora, para el siguiente paso, el estado oculto para el paso de tiempo actual se usa como el estado oculto inicial para el siguiente paso de tiempo en la secuencia. Es por eso que, para actualizar el estado oculto por el segundo paso de tiempo, podemos repetir los cálculos realizados en el primer paso de la siguiente manera:

A continuación, podemos repetir el proceso de actualización del estado oculto para el tercer y último paso de la secuencia como se muestra a continuación:

Y cuando hayamos procesado todos los pasos anteriores en la secuencia, podemos calcular la salida de la siguiente manera:

Para la fórmula anterior, hemos utilizado un tercer conjunto de pesos y el estado oculto del paso de tiempo final.

Unidades recurrentes avanzadas

El problema principal con la capa recurrente básica es el problema del gradiente que desaparece y, debido a esto, no es muy bueno para aprender correlaciones a largo plazo. En palabras simples, la capa recurrente básica no maneja muy bien las secuencias largas. Esa es la razón por la que algunos otros tipos de capas recurrentes que son mucho más adecuados para trabajar con secuencias más largas son los siguientes:

Memoria a largo plazo (LSTM)

Las redes de memoria a largo plazo a corto plazo (LSTM) fueron introducidas por Hochreiter & Schmidhuber. Resolvió el problema de conseguir una capa recurrente básica para recordar cosas durante mucho tiempo. La arquitectura de LSTM se muestra arriba en el diagrama. Como podemos ver, tiene neuronas de entrada, células de memoria y neuronas de salida. Para combatir el problema del gradiente que desaparece, las redes de memoria a corto y largo plazo utilizan una celda de memoria explícita (almacena los valores anteriores) y las siguientes puertas:

  • Forget gate- Como su nombre lo indica, le dice a la celda de memoria que olvide los valores anteriores. La celda de memoria almacena los valores hasta que la puerta, es decir, "puerta olvidada", le dice que los olvide.

  • Input gate- Como su nombre lo indica, agrega cosas nuevas a la celda.

  • Output gate- Como su nombre lo indica, la puerta de salida decide cuándo pasar los vectores de la celda al siguiente estado oculto.

Unidades recurrentes cerradas (GRU)

Gradient recurrent units(GRU) es una ligera variación de la red LSTM. Tiene una puerta menos y está cableado ligeramente diferente a los LSTM. Su arquitectura se muestra en el diagrama anterior. Tiene neuronas de entrada, células de memoria cerradas y neuronas de salida. La red de unidades recurrentes cerradas tiene las dos puertas siguientes:

  • Update gate- Determina las siguientes dos cosas:

    • ¿Qué cantidad de información debe mantenerse del último estado?

    • ¿Qué cantidad de información se debe dejar entrar de la capa anterior?

  • Reset gate- La funcionalidad de la puerta de reinicio es muy parecida a la de la puerta de olvido de la red LSTM. La única diferencia es que se ubica de manera ligeramente diferente.

A diferencia de la red de memoria a largo plazo, las redes de unidades periódicas cerradas son un poco más rápidas y fáciles de ejecutar.

Creando estructura RNN

Antes de que podamos comenzar a hacer predicciones sobre la salida de cualquiera de nuestras fuentes de datos, primero debemos construir RNN y la construcción de RNN es bastante similar a como construimos una red neuronal regular en la sección anterior. A continuación se muestra el código para construir uno

from cntk.losses import squared_error
from cntk.io import CTFDeserializer, MinibatchSource, INFINITELY_REPEAT, StreamDefs, StreamDef
from cntk.learners import adam
from cntk.logging import ProgressPrinter
from cntk.train import TestConfig
BATCH_SIZE = 14 * 10
EPOCH_SIZE = 12434
EPOCHS = 10

Replanteo de múltiples capas

También podemos apilar múltiples capas recurrentes en CNTK. Por ejemplo, podemos usar la siguiente combinación de capas:

from cntk import sequence, default_options, input_variable
from cntk.layers import Recurrence, LSTM, Dropout, Dense, Sequential, Fold
features = sequence.input_variable(1)
with default_options(initial_state = 0.1):
   model = Sequential([
      Fold(LSTM(15)),
      Dense(1)
   ])(features)
target = input_variable(1, dynamic_axes=model.dynamic_axes)

Como podemos ver en el código anterior, tenemos las siguientes dos formas en las que podemos modelar RNN en CNTK:

  • Primero, si solo queremos el resultado final de una capa recurrente, podemos usar el Fold capa en combinación con una capa recurrente, como GRU, LSTM o incluso RNNStep.

  • En segundo lugar, como una forma alternativa, también podemos utilizar el Recurrence bloquear.

Entrenamiento de RNN con datos de series de tiempo

Una vez que construimos el modelo, veamos cómo podemos entrenar a RNN en CNTK -

from cntk import Function
@Function
def criterion_factory(z, t):
   loss = squared_error(z, t)
   metric = squared_error(z, t)
   return loss, metric
loss = criterion_factory(model, target)
learner = adam(model.parameters, lr=0.005, momentum=0.9)

Ahora, para cargar los datos en el proceso de entrenamiento, debemos deserializar secuencias de un conjunto de archivos CTF. El siguiente código tiene elcreate_datasource , que es una función de utilidad útil para crear la fuente de datos de entrenamiento y prueba.

target_stream = StreamDef(field='target', shape=1, is_sparse=False)
features_stream = StreamDef(field='features', shape=1, is_sparse=False)
deserializer = CTFDeserializer(filename, StreamDefs(features=features_stream, target=target_stream))
   datasource = MinibatchSource(deserializer, randomize=True, max_sweeps=sweeps)
return datasource
train_datasource = create_datasource('Training data filename.ctf')#we need to provide the location of training file we created from our dataset.
test_datasource = create_datasource('Test filename.ctf', sweeps=1) #we need to provide the location of testing file we created from our dataset.

Ahora que hemos configurado las fuentes de datos, el modelo y la función de pérdida, podemos comenzar el proceso de entrenamiento. Es bastante similar a lo que hicimos en secciones anteriores con redes neuronales básicas.

progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
   features: train_datasource.streams.features,
   target: train_datasource.streams.target
}
history = loss.train(
   train_datasource,
   epoch_size=EPOCH_SIZE,
   parameter_learners=[learner],
   model_inputs_to_streams=input_map,
   callbacks=[progress_writer, test_config],
   minibatch_size=BATCH_SIZE,
   max_epochs=EPOCHS
)

Obtendremos una salida similar a la siguiente:

Salida−

average  since  average  since  examples
loss      last  metric  last
------------------------------------------------------
Learning rate per minibatch: 0.005
0.4      0.4    0.4      0.4      19
0.4      0.4    0.4      0.4      59
0.452    0.495  0.452    0.495   129
[…]

Validando el modelo

En realidad, rechazar con un RNN es bastante similar a hacer predicciones con cualquier otro modelo CNK. La única diferencia es que necesitamos proporcionar secuencias en lugar de muestras individuales.

Ahora, cuando nuestro RNN finalmente haya terminado con el entrenamiento, podemos validar el modelo probándolo usando algunas secuencias de muestras de la siguiente manera:

import pickle
with open('test_samples.pkl', 'rb') as test_file:
test_samples = pickle.load(test_file)
model(test_samples) * NORMALIZE

Salida−

array([[ 8081.7905],
[16597.693 ],
[13335.17 ],
...,
[11275.804 ],
[15621.697 ],
[16875.555 ]], dtype=float32)