Blog
Entendiendo el estado de las Redes Neuronales Recurrentes de LSTM
- Publicado por: Rafael Fernandez
- Categoría: Deep Learning
-En esta lección usted descubrirá exactamente cómo se mantiene el estado en las redes LSTM mediante el aprendizaje profundo de Keras.
-Cómo desarrollar una sencilla red LSTM para un problema de predicción de secuencias.
-Cómo administrar cuidadosamente el estado a través de lotes y características de una red LSTM.
-Cómo gestionar manualmente el estado en una red LSTM para la predicción de estado.
Una poderosa y popular red neuronal recurrente es la red modelo a largo-corto plazo o LSTM. Es ampliamente utilizada porque la arquitectura supera el problema del gradiente de fuga y explosión que afecta a todas las redes neuronales recurrentes, lo que permite crear redes muy grandes y muy profundas. Al igual que otras redes neuronales recurrentes, las redes LSTM mantienen el estado, y los detalles de cómo se implementan en el marco de Keras pueden ser confusos.
Descripción del problema: Aprenda el alfabeto
En este tutorial vamos a desarrollar y contrastar un número de LSTM recurrentes de diferentes modelos de redes neuronales. El contexto de estas comparaciones será una simple predicción de la secuencia del problema de aprendizaje del alfabeto. Es decir, dada una letra del alfabeto, predecir la siguiente letra del alfabeto. Este es un simple problema de predicción de secuencia que una vez que se entiende puede generalizarse a otros problemas de predicción de secuencias, como la predicción y secuencia de series temporales. Preparemos el problema con algún código Python que podamos reutilizar a modo de ejemplo. En primer lugar, vamos a importar todas las clases y funciones que pensamos utilizar en este tutorial.
import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils
A continuación, podemos sembrar el generador de números aleatorios para asegurarnos de que los resultados son los mismos cada vez que se ejecuta el código.
# semilla aleatoria de reproducibilidad numpy.random.seed(7)
Ahora podemos introducir nuestro conjunto de datos, el alfabeto. Definimos el alfabeto en mayúsculas para facilitar su lectura. Necesitamos mapear las letras del alfabeto a valores enteros. Podemos hacerlo fácilmente creando un diccionario (mapa) del índice de letras del carácter. También podemos crear una búsqueda inversa para volver a convertir las predicciones en caracteres que se utilizarán más adelante.
# definimos los datos en crudo alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a números enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet))
Ahora necesitamos crear nuestros pares de entrada y salida en los que entrenar nuestra red neuronal. Podemos hacer esto definiendo la longitud de una secuencia de entrada, y luego leyendo secuencias de la secuencia del alfabeto de entrada. Por ejemplo, usamos una longitud de entrada de 1. Comenzando por el principio de los datos de entrada brutos, podemos leer la primera letra A y la siguiente letra como la predicción B. Nos movemos a lo largo de un carácter y repetimos hasta que alcanzamos una predicción de Z.
#preparamos el conjunto de datos de entrada a los pares de salida codificados como enteros seq_length = 1 dataX = [] dataY = [] for i in range(0, len(alphabet) - seq_length, 1): seq_in = alphabet[i:i + seq_length] seq_out = alphabet[i + seq_length] dataX.append([char_to_int[char] for char in seq_in]) dataY.append(char_to_int[seq_out]) print(seq_in, '->', seq_out)
También imprimimos los pares de entrada para la comprobación. Si se ejecuta el código hasta este punto, se obtiene la siguiente salida, que resume las secuencias de entrada de la longitud 1 y un único carácter de salida.
A -> B B -> C C -> D D -> E E -> F F -> G G -> H H -> I ...
Necesitamos remodelar con NumPy a un formato esperado por las redes LSTM, es decir [muestras, pasos de tiempo, características].
# remodelamos como [muestras, pasos de tiempo, características] X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
Una vez remodelados, podemos entonces normalizar los números enteros de entrada al rango 0-a-1, el rango de las funciones de activación sigmoide usadas por la red LSTM.
# normalizacion X = X / float(len(alphabet))
Finalmente, podemos pensar en este problema como una tarea de clasificación secuencial, donde cada una de las 26 letras representa una clase diferente. Como tal, podemos convertir la salida (y) y hacer una codificación en caliente, usando la función incorporada de Keras to_categorical().
# codificacion en caliente ala salida y = np_utils.to_categorical(dataY)
Ahora ya estamos preparados para ajustar diferentes modelos a la LSTM
LSTM para el aprendizaje del mapeo de un carácter a carácter
Comencemos diseñando una LSTM simple para aprender a predecir el siguiente carácter en la sección el alfabeto dado el contexto de un solo personaje. Enmarcaremos este ejemplo como un problema aleatorio de entrada de una letra y de salida de una letra. Como veremos, se trata de un enmarcado dificil del problema para que la LSTM aprenda. Vamos a crear una red LSTM con 32 unidades y una salida usando la función de activación de softmax para hacer predicciones. Porque es un problema multiclase podemos usar la función de pérdida de registro (llamada cruzamiento categórico o categorical_crossentropy en Keras) y optimizar la red utilizando la función de optimización de ADAM. El modelo es ajustado con 500 épocas con un tamaño de lote de 1.
# se crea y se ajusta el modelo model = Sequential() model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2]))) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, epochs=500, batch_size=1, verbose=2)
Después de que tengamos el modelo, podemos evaluar y resumir el rendimiento de todo el conjunto de datos de entrenamiento
# sumariza el rendimiento del modelo scores = model.evaluate(X, y, verbose=0) print("Precisión del modelo: %.2f%%" % (scores[1]*100))
A continuación, podemos volver a ejecutar los datos de formación a través de la red y generar predicciones, convirtiendo los pares de entrada y salida de nuevo en su formato de carácter original para tener una idea visual de lo bien que la red aprendió el problema.
# demuestra la predicciones del modelo for pattern in dataX: x = numpy.reshape(pattern, (1, len(pattern), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result)
El código completo lo tenemos aquí:
# LSTM para Aprender Mapeo de un Char a un Char import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils # semilla aleatoria para reproducibilidad numpy.random.seed(7) # definimos los datos alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet)) # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros seq_length = 1 dataX = [] dataY = [] for i in range(0, len(alphabet) - seq_length, 1): seq_in = alphabet[i:i + seq_length] seq_out = alphabet[i + seq_length] dataX.append([char_to_int[char] for char in seq_in]) dataY.append(char_to_int[seq_out]) print(seq_in, '->', seq_out) # remodelar el vector [muestras, pasos de tiempo, características] X = numpy.reshape(dataX, (len(dataX), seq_length, 1)) # normalizacion X = X / float(len(alphabet)) # codificación en caliente a la salida y = np_utils.to_categorical(dataY) # # creacion y ajuste del modelo model = Sequential() model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2]))) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, epochs=500, batch_size=1, verbose=2) # sumario de rendimiento del modelo scores = model.evaluate(X, y, verbose=0) print("Precisión del modelo: %.2f%%" % (scores[1]*100)) # demostracion de algunas predicciones for pattern in dataX: x = numpy.reshape(pattern, (1, len(pattern), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result)
Ejecutando el código obtenemos:
... Epoch 495/500 - 0s - loss: 1.7006 - acc: 0.7600 Epoch 496/500 - 0s - loss: 1.7015 - acc: 0.8000 Epoch 497/500 - 0s - loss: 1.7009 - acc: 0.6800 Epoch 498/500 - 0s - loss: 1.7008 - acc: 0.7600 Epoch 499/500 - 0s - loss: 1.7014 - acc: 0.8000 Epoch 500/500 - 0s - loss: 1.6977 - acc: 0.8400 Precisión del modelo: 84.00% ['A'] -> B ['B'] -> B ['C'] -> D ['D'] -> E ['E'] -> F ['F'] -> G ['G'] -> H ['H'] -> I ['I'] -> J ['J'] -> K ['K'] -> L ['L'] -> M ['M'] -> N ['N'] -> O ...
Podemos ver que este problema es, en efecto, un problema difícil de aprender para la red. La razón es que el las unidades LSTM pobres no tienen ningún contexto con el que trabajar. Cada patrón de entrada-salida se muestra para la red en un orden aleatorio y el estado de la red se reajusta después de cada patrón (donde cada lote contiene un patrón). Esto es un abuso de la arquitectura de la red LSTM, tratándolo como un Perceptrón multicapa estándar. A continuación, intentemos un encuadre diferente con el fin de proporcionar más secuencia a la red de la que aprender.
LSTM para un mapeo de ventana de características a un carácter único
Un enfoque popular para añadir más contexto a los datos de los Perceptrones multicapa es usar el comando método de la ventana. Aquí es donde se proporcionan los pasos anteriores de la secuencia como entrada adicional a la características de la red. Podemos intentar el mismo truco para proporcionar más contexto a la red LSTM. Aquí, por ejemplo, aumentamos la longitud de la secuencia de 1 a 3:
# preparamos el conjunto de datos de entrada a pares de salida codificados como enteros seq_length = 3
Lo que crea patrones de entrenamiento como:
ABC -> D BCD -> E CDE -> F
Cada elemento de la secuencia se proporciona como una nueva característica de entrada a la red. Esto requiere una modificación de la forma en que las secuencias de entrada se remodelaron en el paso de preparación de datos:
# remodelamos X como [muestras, pasos de tiempo, características] X = numpy.reshape(dataX, (len(dataX), 1, seq_length))
También requiere una modulación de cómo se remodelan los patrones de la muestra cuando se demuestran las predicciones del modelo.
x = numpy.reshape(pattern, (1, 1, len(pattern)))
La lista completa de códigos se proporciona a continuación para mayor información.
# LSTM sencillo para aprender a mapear desde una ventana de tres caracteres a un mapa de un carácter import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils # semilla aleatoria para reproducibilidad numpy.random.seed(7) # definimos los datos alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet)) # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros seq_length = 3 dataX = [] dataY = [] for i in range(0, len(alphabet) - seq_length, 1): seq_in = alphabet[i:i + seq_length] seq_out = alphabet[i + seq_length] dataX.append([char_to_int[char] for char in seq_in]) dataY.append(char_to_int[seq_out]) print(seq_in, '->', seq_out) # remodelar el vector [muestras, pasos de tiempo, características] X = numpy.reshape(dataX, (len(dataX), 1, seq_length)) # normalizamos X = X / float(len(alphabet)) # codificación en caliente a la salida y = np_utils.to_categorical(dataY) # creacion y ajuste del modelo model = Sequential() model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2]))) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, epochs=500, batch_size=1, verbose=2) # sumario de rendimiento del modelo scores = model.evaluate(X, y, verbose=0) print("Precisión del modelo: %.2f%%" % (scores[1]*100)) # demostracion de algunas predicciones for pattern in dataX: x = numpy.reshape(pattern, (1, 1, len(pattern))) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result)
Corriendo el programa obtenemos:
Epoch 493/500 - 0s - loss: 1.6569 - acc: 0.7391 Epoch 494/500 - 0s - loss: 1.6552 - acc: 0.7391 Epoch 495/500 - 0s - loss: 1.6501 - acc: 0.8261 Epoch 496/500 - 0s - loss: 1.6522 - acc: 0.7391 Epoch 497/500 - 0s - loss: 1.6503 - acc: 0.7391 Epoch 498/500 - 0s - loss: 1.6517 - acc: 0.8261 Epoch 499/500 - 0s - loss: 1.6491 - acc: 0.7826 Epoch 500/500 - 0s - loss: 1.6453 - acc: 0.7826 Precisión del modelo: 86.96% ['A', 'B', 'C'] -> D ['B', 'C', 'D'] -> E ['C', 'D', 'E'] -> F ['D', 'E', 'F'] -> G ['E', 'F', 'G'] -> H ['F', 'G', 'H'] -> I ['G', 'H', 'I'] -> J ['H', 'I', 'J'] -> K ['I', 'J', 'K'] -> L ['J', 'K', 'L'] -> M ['K', 'L', 'M'] -> N ['L', 'M', 'N'] -> O ['M', 'N', 'O'] -> P ['N', 'O', 'P'] -> Q ['O', 'P', 'Q'] -> R ['P', 'Q', 'R'] -> S ['Q', 'R', 'S'] -> T ['R', 'S', 'T'] -> U ['S', 'T', 'U'] -> V ['T', 'U', 'V'] -> X ['U', 'V', 'W'] -> Z ['V', 'W', 'X'] -> Z ['W', 'X', 'Y'] -> Z
Podemos ver un pequeño aumento en el rendimiento que puede o no ser real. Este es un problema simple que todavía no hemos podido aprender con los LSTMs ni siquiera con el método de ventana. Una vez más, esto es un uso indebido de la red LSTM por un mal encuadramiento del problema. De hecho, las secuencias de letras son pasos de tiempo de una característica en lugar de un paso de tiempo de características separadas. Hemos dado más contexto a la red, pero no más secuencia como se esperaba. En la siguiente sección, daremos más contexto a la red en forma de pasos temporales.
LSTM para un paso de tiempo con ventana para un mapeo de una carácter
En Keras, el uso que se pretende dar a las LSTM es proporcionar un contexto en forma de pasos temporales. Podemos tomar nuestro primer ejemplo y simplemente cambiar la longitud de la secuencia de 1 a 3.
seq_length = 3
De esta manera las entradas y salidas serían:
ABC -> D BCD -> E CDE -> F DEF -> G
La diferencia es que la remodelación de los datos de entrada toma la secuencia como una secuencia de pasos temporales de una característica, en lugar de un solo paso temporal de múltiples características.
# remodelar el vector [muestras, pasos de tiempo, características] X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
Este es el uso previsto de proporcionar contexto de secuencia a su LSTM en Keras. El ejemplo de código completo se proporciona a continuación:
# LSTM sencilla que aprende tres pasos de tiempo de tres caracteres para el mapeo de un carácter import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils # semilla aleatoria para reproducibilidad numpy.random.seed(7) # definimos los datos alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet)) # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros seq_length = 3 dataX = [] dataY = [] for i in range(0, len(alphabet) - seq_length, 1): seq_in = alphabet[i:i + seq_length] seq_out = alphabet[i + seq_length] dataX.append([char_to_int[char] for char in seq_in]) dataY.append(char_to_int[seq_out]) print(seq_in, '->', seq_out) # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros X = numpy.reshape(dataX, (len(dataX), seq_length, 1)) # normalizacion X = X / float(len(alphabet)) # codificación en caliente a la salida y = np_utils.to_categorical(dataY) # creacion y ajuste del modelo model = Sequential() model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2]))) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, epochs=500, batch_size=1, verbose=2) # sumario de rendimiento del modelo scores = model.evaluate(X, y, verbose=0) print("Precision del modelo: %.2f%%" % (scores[1]*100)) # demostracion de algunas predicciones for pattern in dataX: x = numpy.reshape(pattern, (1, len(pattern), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result)
Cuando ejecutamos el código obtenemos
... Epoch 478/500 - 0s - loss: 0.2234 - acc: 1.0000 Epoch 479/500 - 0s - loss: 0.2158 - acc: 1.0000 Epoch 480/500 - 0s - loss: 0.2168 - acc: 1.0000 Epoch 481/500 - 0s - loss: 0.2139 - acc: 1.0000 Epoch 482/500 - 0s - loss: 0.2141 - acc: 1.0000 Epoch 483/500 - 0s - loss: 0.2155 - acc: 1.0000 Epoch 484/500 - 0s - loss: 0.2121 - acc: 1.0000 Epoch 485/500 - 0s - loss: 0.2093 - acc: 1.0000 Epoch 486/500 - 0s - loss: 0.2116 - acc: 1.0000 Epoch 487/500 - 0s - loss: 0.2099 - acc: 1.0000 Epoch 488/500 - 0s - loss: 0.2141 - acc: 1.0000 Epoch 489/500 - 0s - loss: 0.2106 - acc: 1.0000 Epoch 490/500 - 0s - loss: 0.2071 - acc: 1.0000 Epoch 491/500 - 0s - loss: 0.2104 - acc: 1.0000 Epoch 492/500 - 0s - loss: 0.2017 - acc: 1.0000 Epoch 493/500 - 0s - loss: 0.2032 - acc: 1.0000 Epoch 494/500 - 0s - loss: 0.2016 - acc: 1.0000 Epoch 495/500 - 0s - loss: 0.2032 - acc: 1.0000 Epoch 496/500 - 0s - loss: 0.2051 - acc: 1.0000 Epoch 497/500 - 0s - loss: 0.1989 - acc: 1.0000 Epoch 498/500 - 0s - loss: 0.2019 - acc: 1.0000 Epoch 499/500 - 0s - loss: 0.2010 - acc: 1.0000 Epoch 500/500 - 0s - loss: 0.1962 - acc: 1.0000 Precision del modelo: 100.00% ['A', 'B', 'C'] -> D ['B', 'C', 'D'] -> E ['C', 'D', 'E'] -> F ['D', 'E', 'F'] -> G ['E', 'F', 'G'] -> H ['F', 'G', 'H'] -> I ['G', 'H', 'I'] -> J ['H', 'I', 'J'] -> K ['I', 'J', 'K'] -> L ['J', 'K', 'L'] -> M ['K', 'L', 'M'] -> N ['L', 'M', 'N'] -> O ['M', 'N', 'O'] -> P ['N', 'O', 'P'] -> Q ['O', 'P', 'Q'] -> R ['P', 'Q', 'R'] -> S ['Q', 'R', 'S'] -> T ['R', 'S', 'T'] -> U ['S', 'T', 'U'] -> V ['T', 'U', 'V'] -> W ['U', 'V', 'W'] -> X ['V', 'W', 'X'] -> Y ['W', 'X', 'Y'] -> Z
Podemos ver que el modelo aprende el problema perfectamente, como lo demuestra la evaluación del modelo y los ejemplos de predicciones. Pero ha aprendido un problema más simple. Específicamente, ha aprendido para predecir la siguiente letra de una secuencia de tres letras en el alfabeto. Se puede mostrar cualquier secuencia aleatoria de tres letras del alfabeto y predecir la siguiente letra.
En realidad no puede enumerar el alfabeto. Espero que un sistema multicapa lo suficientemente grande como la red de Perceptron podría ser capaz de aprender el mismo mapeo usando el método de la ventana. Las redes LSTM están llenas de estados. Deberían ser capaces de aprender toda la secuencia del alfabeto, pero por defecto, la implementación de Keras restablece el estado de la red después de cada lote de entrenamiento.
Estado de LSTM mantenido entre muestras dentro de un lote
La implementación de Keras para las LSTM restablece el estado de la red después de cada lote. Esto sugiere que si tuviéramos un tamaño de lote lo suficientemente grande para contener todos los patrones de entrada y si todos los patrones de entrada se ordenaran secuencialmente, la LSTM podría utilizar el contexto de la secuencia dentro del lote para aprender mejor la secuencia. Podemos demostrarlo fácilmente modificando el primer ejemplo para aprender un mapeo uno a uno y aumentar el tamaño del lote de 1 al tamaño del conjunto de datos de formación. Además, Keras baraja el conjunto de datos de entrenamiento antes de cada época de entrenamiento. Para garantizar que los patrones de datos de entrenamiento permanezcan secuenciales, podemos desactivar esta mezcla.
model.fit(X, y, epochs=5000, batch_size=len(dataX), verbose=2, shuffle=False)
La red aprenderá a mapear los caracteres usando la secuencia dentro del lote, pero este contexto no estará disponible para la red cuando se hagan predicciones. Podemos evaluar tanto la capacidad de la red para hacer predicciones al azar y en secuencia. El código completo lo mostramos a continuación:
#LSTM sencilla para aprender a mapear de un carácter a un carácter con todos los datos de cada lote. import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils from keras.preprocessing.sequence import pad_sequences # semilla aleatoria para reproducibilidad numpy.random.seed(7) # definimos los datos alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet)) # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros seq_length = 1 dataX = [] dataY = [] for i in range(0, len(alphabet) - seq_length, 1): seq_in = alphabet[i:i + seq_length] seq_out = alphabet[i + seq_length] dataX.append([char_to_int[char] for char in seq_in]) dataY.append(char_to_int[seq_out]) print(seq_in, '->', seq_out) # convert list of lists to array and pad sequences if needed X = pad_sequences(dataX, maxlen=seq_length, dtype='float32') # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros X = numpy.reshape(dataX, (len(dataX), seq_length, 1)) # normalizacion X = X / float(len(alphabet)) # codificación en caliente a la salida y = np_utils.to_categorical(dataY) # creacion y ajuste del modelo model = Sequential() model.add(LSTM(16, input_shape=(X.shape[1], X.shape[2]))) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, epochs=5000, batch_size=len(dataX), verbose=2, shuffle=False) # sumario de rendimiento del modelo scores = model.evaluate(X, y, verbose=0) print("Precision del modelo: %.2f%%" % (scores[1]*100)) # demostracion de algunas predicciones for pattern in dataX: x = numpy.reshape(pattern, (1, len(pattern), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result) # Demostracion que predice patrones aleatorios print("Test a patrones aleatorios:") for i in range(0,20): pattern_index = numpy.random.randint(len(dataX)) pattern = dataX[pattern_index] x = numpy.reshape(pattern, (1, len(pattern), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result)
Corriendo el código obtenemos:
Epoch 4970/5000 - 0s - loss: 0.9056 - acc: 1.0000 Epoch 4971/5000 - 0s - loss: 0.9054 - acc: 1.0000 Epoch 4972/5000 - 0s - loss: 0.9052 - acc: 1.0000 Epoch 4973/5000 - 0s - loss: 0.9051 - acc: 1.0000 Epoch 4974/5000 - 0s - loss: 0.9049 - acc: 1.0000 Epoch 4975/5000 - 0s - loss: 0.9047 - acc: 1.0000 Epoch 4976/5000 - 0s - loss: 0.9045 - acc: 1.0000 Epoch 4977/5000 - 0s - loss: 0.9043 - acc: 1.0000 Epoch 4978/5000 - 0s - loss: 0.9042 - acc: 1.0000 Epoch 4979/5000 - 0s - loss: 0.9040 - acc: 1.0000 Epoch 4980/5000 - 0s - loss: 0.9038 - acc: 1.0000 Epoch 4981/5000 - 0s - loss: 0.9036 - acc: 1.0000 Epoch 4982/5000 - 0s - loss: 0.9034 - acc: 1.0000 Epoch 4983/5000 - 0s - loss: 0.9033 - acc: 1.0000 Epoch 4984/5000 - 0s - loss: 0.9031 - acc: 1.0000 Epoch 4985/5000 - 0s - loss: 0.9029 - acc: 1.0000 Epoch 4986/5000 - 0s - loss: 0.9027 - acc: 1.0000 Epoch 4987/5000 - 0s - loss: 0.9026 - acc: 1.0000 Epoch 4988/5000 - 0s - loss: 0.9024 - acc: 1.0000 Epoch 4989/5000 - 0s - loss: 0.9022 - acc: 1.0000 Epoch 4990/5000 - 0s - loss: 0.9020 - acc: 1.0000 Epoch 4991/5000 - 0s - loss: 0.9018 - acc: 1.0000 Epoch 4992/5000 - 0s - loss: 0.9017 - acc: 1.0000 Epoch 4993/5000 - 0s - loss: 0.9015 - acc: 1.0000 Epoch 4994/5000 - 0s - loss: 0.9013 - acc: 1.0000 Epoch 4995/5000 - 0s - loss: 0.9011 - acc: 1.0000 Epoch 4996/5000 - 0s - loss: 0.9010 - acc: 1.0000 Epoch 4997/5000 - 0s - loss: 0.9008 - acc: 1.0000 Epoch 4998/5000 - 0s - loss: 0.9006 - acc: 1.0000 Epoch 4999/5000 - 0s - loss: 0.9004 - acc: 1.0000 Epoch 5000/5000 - 0s - loss: 0.9002 - acc: 1.0000 Precision del modelo: 100.00% ['A'] -> B ['B'] -> C ['C'] -> D ['D'] -> E ['E'] -> F ['F'] -> G ['G'] -> H ['H'] -> I ['I'] -> J ['J'] -> K ['K'] -> L ['L'] -> M ['M'] -> N ['N'] -> O ['O'] -> P ['P'] -> Q ['Q'] -> R ['R'] -> S ['S'] -> T ['T'] -> U ['U'] -> V ['V'] -> W ['W'] -> X ['X'] -> Y ['Y'] -> Z Test a patrones aleatorios: ['K'] -> L ['A'] -> B ['N'] -> O ['S'] -> T ['G'] -> H ['K'] -> L ['T'] -> U ['X'] -> Y ['G'] -> H ['Q'] -> R ['X'] -> Y ['I'] -> J ['N'] -> O ['U'] -> V ['H'] -> I ['S'] -> T ['I'] -> J ['R'] -> S ['C'] -> D ['A'] -> B
Como esperábamos, la red es capaz de utilizar el contexto dentro de la secuencia para aprender el alfabeto, logrando una precisión del 100% en los datos de la capacitación. Es importante destacar que la red puede hacer predicciones precisas para la siguiente letra del alfabeto para los caracteres seleccionados al azar. Hemos conseguido algo Muy impresionante!
LSTM de estado para un mapeo de una carácter a otro carácter
Hemos visto que podemos dividir nuestros datos brutos en secuencias de tamaño fijo y que esta representación puede ser aprendida por la LSTM, pero sólo para aprender mapeos aleatorios de 3 caracteres a 1 carácter. También hemos visto que podemos pervertir el tamaño de los lotes para ofrecer más secuencia a la red, pero sólo durante la formación. Idealmente, queremos exponer la red a toda la secuencia y dejar que aprenda las interdependencias, en lugar de que nosotros definamos esas dependencias explícitamente en el marco del problema.
Podemos hacer esto en Keras haciendo que las capas de LSTM estén llenas de estados y reajustando manualmente el estado de la red al final de la época, que es también el final de la secuencia de entrenamiento. Esta es la verdadera manera de utilizar las redes LSTM. Y es que al permitir que la propia red aprenda las dependencias entre los caracteres, necesitamos una red más pequeña (la mitad del número de unidades) y menos épocas de formación (casi la mitad). Primero tenemos que definir nuestra capa de LSTM como de estado. Al hacerlo, debemos especificar explícitamente el tamaño del lote como una dimensión en la forma de entrada. Esto también significa que cuando evaluamos la red o hacemos predicciones, podemos también especificar y adherirse a este mismo tamaño de lote. Esto no es un problema ahora que estamos usando un tamaño de lote de 1. Esto podría introducir dificultades al hacer predicciones cuando el tamaño de lote no es uno, ya que las predicciones deberán hacerse por lotes y en secuencia.
batch_size = 1 model.add(LSTM(16, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
Una diferencia importante en el entrenamiento del LSTM de estado es que lo entrenamos manualmente una época a la vez y reseteamos el estado después de cada época. Podemos hacer esto en un bucle. Una vez más, no pegamos la entrada, preservando la secuencia en la que se crearon los datos de formación de entrada.
for i in range(300): model.fit(X, y, epochs=1, batch_size=batch_size, verbose=2, shuffle=False) model.reset_states()
Como ya se ha mencionado, especificamos el tamaño del lote al evaluar el rendimiento de la red en todo el conjunto de datos de formación.
# resumir el rendimiento del modelo scores = model.evaluate(X, y, batch_size=batch_size, verbose=0) model.reset_states() print("Precision del modelo: %.2f%%" % (scores[1]*100))
Finalmente, podemos demostrar que la red ha aprendido todo el alfabeto. Podemos sembrar con la primera letra A, solicitar una predicción, retroalimentar la predicción como una entrada y repetir el proceso hasta Z.
# Demostrar algunas predicciones del modelo seed = [char_to_int[alphabet[0]]] for i in range(0, len(alphabet)-1): x = numpy.reshape(seed, (1, len(seed), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) print(int_to_char[seed[0]], "->", int_to_char[index]) seed = [index] model.reset_states()
También podemos ver si la red puede hacer predicciones a partir de una letra arbitraria.
# demostrar un punto de partida aleatorio letter = "K" seed = [char_to_int[letter]] print("New start: ", letter) for i in range(0, 5): x = numpy.reshape(seed, (1, len(seed), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) print(int_to_char[seed[0]], "->", int_to_char[index]) seed = [index] model.reset_states()
La lista completa de códigos se proporciona a continuación para mayor información.
******
# LSTM de estado para aprender mapeo de un carácter a un carácter import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils # semilla aleatoria para reproducibilidad numpy.random.seed(7) # definimos los datos alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet)) # preparamos el conjunto de datos de entrada a pares de salida codificados como enteros seq_length = 1 dataX = [] dataY = [] for i in range(0, len(alphabet) - seq_length, 1): seq_in = alphabet[i:i + seq_length] seq_out = alphabet[i + seq_length] dataX.append([char_to_int[char] for char in seq_in]) dataY.append(char_to_int[seq_out]) print(seq_in, '->', seq_out) # reformar X para que sea[muestras, pasos de tiempo, características] X = numpy.reshape(dataX, (len(dataX), seq_length, 1)) # normalizacion X = X / float(len(alphabet)) # una codificación en caliente de la variable de salida y = np_utils.to_categorical(dataY) #creacion y ajuste del modelo batch_size = 1 model = Sequential() model.add(LSTM(16, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True)) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) for i in range(300): model.fit(X, y, epochs=1, batch_size=batch_size, verbose=2, shuffle=False) model.reset_states() # summario del rendimiento del modelo scores = model.evaluate(X, y, batch_size=batch_size, verbose=0) model.reset_states() print("Precision del modelo: %.2f%%" % (scores[1]*100)) # Demostrar algunas predicciones del modelo seed = [char_to_int[alphabet[0]]] for i in range(0, len(alphabet)-1): x = numpy.reshape(seed, (1, len(seed), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) print(int_to_char[seed[0]], "->", int_to_char[index]) seed = [index] model.reset_states() # demostrar un punto de partida aleatorio letter = "L" seed = [char_to_int[letter]] print("Comenzamos de nuevo: ", letter) for i in range(0, 5): x = numpy.reshape(seed, (1, len(seed), 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) print(int_to_char[seed[0]], "->", int_to_char[index]) seed = [index] model.reset_states()
Cuando ejecutamos el código obtenemos
- 0s - loss: 1.5422 - acc: 0.3600 Epoch 1/1 - 0s - loss: 1.5360 - acc: 0.3600 Epoch 1/1 - 0s - loss: 1.5326 - acc: 0.3200 Precision del modelo: 32.00% A -> B B -> C C -> C C -> D D -> E E -> F F -> G G -> H H -> I I -> I I -> L L -> M M -> M M -> P P -> P P -> P P -> R R -> R R -> S S -> W W -> U U -> W W -> Y Y -> Y Y -> Z Comenzamos de nuevo: L L -> B B -> C C -> C C -> D D -> D
Podemos ver que la red no ha memorizado todo el alfabeto perfectamente. Utilizó el contexto de las propias muestras y aprendió cualquier dependencia que necesitara para predecir las siguiente carácter de la secuencia. También podemos ver que si sembramos la red con la primera letra, que puede hacer sonar correctamente el resto del alfabeto. También podemos ver que sólo ha aprendido la secuencia alfabética completa y la de un arranque en frío. Cuando se le pide que prediga la siguiente letra de K que predice B y vuelve a regurgitar todo el alfabeto. Para predecir realmente K, el estado de la red necesitaría ser calentado iterativamente alimentando las letras de A a J. Esto nos dice que podríamos lograr la misma eect con un LSTM sin estado preparando la formación datos como:
---a -> b --ab -> c -abc -> d abcd -> e
Donde la secuencia de entrada se fija en 25 (a-hasta-y para predecir z) y los patrones se predeterminan con cero-relleno. Finalmente, esto plantea la cuestión de entrenar una red LSTM usando secuencias de entrada de longitud variable para predecir el siguiente caracter.
LSTM con entrada de longitud variable con un solo carácter de Salida
En la sección anterior descubrimos que el estado LSTM de Keras era realmente sólo un atajo a repetir las primeras n-secuencias, pero realmente no nos ayudó a aprender un modelo genérico del alfabeto. En esta sección exploramos una variación de las LSTM sin estado que aprende subsecuencias aleatorias del alfabeto y un esfuerzo para construir un modelo al que se le puedan dar letras arbitrarias o subsecuencias de letras y predecir la siguiente letra del alfabeto.
En primer lugar, estamos cambiando el marco del problema. Para simplificar, definiremos la longitud máxima de la secuencia de entrada y la ajustaremos a un valor pequeño como 5 para acelerar el entrenamiento. Esto indica la longitud máxima de las subsecuencias del alfabeto que se sorteará para la formación. En las extensiones, esto podría ser ajustado al alfabeto completo (26) o más largo si permitimos que el bucle retroceda hasta el comienzo de la secuencia. También tenemos que descifrar el número de secuencias aleatorias para crear, en este caso, 1,000.
# prepare the dataset of input to output pairs encoded as integers num_inputs = 1000 max_len = 5 dataX = [] dataY = [] for i in range(num_inputs): start = numpy.random.randint(len(alphabet)-2) end = numpy.random.randint(start, min(start+max_len,len(alphabet)-1)) sequence_in = alphabet[start:end+1] sequence_out = alphabet[end + 1] dataX.append([char_to_int[char] for char in sequence_in]) dataY.append(char_to_int[sequence_out]) print(sequence_in, '->', sequence_out)
Ejecutar este código en el contexto más amplio creará patrones de entrada que se verán como los siguientes:
O -> P OPQ -> R IJKLM -> N QRSTU -> V ABCD -> E X -> Y GHIJ -> K
Las secuencias de entrada varían en longitud entre 1 y max_len y, por lo tanto, requieren relleno cero. Aquí, usamos el relleno del lado izquierdo (prefijo) con la función Keras pad_sequences ().
X = pad_sequences(dataX, maxlen=max_len, dtype='float32')
El modelo entrenado es evaluado en patrones de entrada seleccionados al azar. Esto podría ser fácilmente nuevas secuencias de caracteres generadas al azar. También creo que esto podría ser una secuencia lineal sembrada con A con salidas retroalimentadas como entradas de un solo carácter. La lista completa de códigos se proporciona a continuación para mayor información.
#LSTM con secuencias de entrada de longitud variable a una salida de un carácter import numpy from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.utils import np_utils from keras.preprocessing.sequence import pad_sequences # Fijar las semillas aleatorias para la reproducibilidad numpy.random.seed(7) # definir el set de datos bruto alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # crear mapeo de caracteres a números enteros (0-25) y viceversa char_to_int = dict((c, i) for i, c in enumerate(alphabet)) int_to_char = dict((i, c) for i, c in enumerate(alphabet)) # preparar el conjunto de datos de entrada a pares de salida codificados como enteros num_inputs = 1000 max_len = 5 dataX = [] dataY = [] for i in range(num_inputs): start = numpy.random.randint(len(alphabet)-2) end = numpy.random.randint(start, min(start+max_len,len(alphabet)-1)) sequence_in = alphabet[start:end+1] sequence_out = alphabet[end + 1] dataX.append([char_to_int[char] for char in sequence_in]) dataY.append(char_to_int[sequence_out]) print(sequence_in, '->', sequence_out) # convertir la lista de listas en secuencias de arreglos y pads si es necesario X = pad_sequences(dataX, maxlen=max_len, dtype='float32') # reformar X para que sea[muestras, pasos de tiempo, características] X = numpy.reshape(X, (X.shape[0], max_len, 1)) # normalizacion X = X / float(len(alphabet)) # one hot encode the output variable y = np_utils.to_categorical(dataY) # una codificación en caliente de la variable de salida batch_size = 1 model = Sequential() model.add(LSTM(32, input_shape=(X.shape[1], 1))) model.add(Dense(y.shape[1], activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, epochs=500, batch_size=batch_size, verbose=2) # resumir el rendimiento del modelo scores = model.evaluate(X, y, verbose=0) print("Model Accuracy: %.2f%%" % (scores[1]*100)) # demostrar algunas predicciones del modelo for i in range(20): pattern_index = numpy.random.randint(len(dataX)) pattern = dataX[pattern_index] x = pad_sequences([pattern], maxlen=max_len, dtype='float32') x = numpy.reshape(x, (1, max_len, 1)) x = x / float(len(alphabet)) prediction = model.predict(x, verbose=0) index = numpy.argmax(prediction) result = int_to_char[index] seq_in = [int_to_char[value] for value in pattern] print(seq_in, "->", result)
Cuando ejecutamos el código obtenemos:
Podemos ver que aunque el modelo no aprendió el alfabeto perfectamente de las subsecuencias generadas al azar, lo hizo muy bien. El modelo no fue afinado y puede requerir más entrenamiento o una red más grande, o ambos (un ejercicio para el lector del post). Esta es una buena extensión natural a todos los ejemplos de entrada secuencial en cada modelo de alfabeto por lotes aprendido anteriormente, ya que puede manejar consultas ad hoc, pero esta vez de longitud de secuencia arbitraria (hasta la máximo longitud).
➡ Conviértete en un as de la Inteligencia Artificial en nuestro curso: