Resolver problemas de secuencia con LSTM en Keras: Parte 2

R

Esta es la segunda y última parte de la serie de artículos de dos partes sobre la resolución de problemas de secuencia con LSTM. En la parte 1 de la serie, expliqué cómo resolver problemas de secuencia uno a uno y muchos a uno usando LSTM. En esta parte, verá cómo resolver problemas de secuencia de uno a muchos y de muchos a muchos mediante LSTM en Keras.

Los subtítulos de imágenes son un ejemplo clásico de problemas de secuencia de uno a muchos en los que tiene una sola imagen como entrada y debe predecir la descripción de la imagen en forma de secuencia de palabras. De manera similar, la predicción del mercado de valores para los próximos X días, donde la entrada es el precio de las acciones de los Y días anteriores, es un ejemplo clásico de problemas de secuencia de muchos a muchos.

En este artículo, verá ejemplos muy básicos de problemas de uno a muchos y de muchos a muchos. Sin embargo, los conceptos aprendidos en este artículo sentarán las bases para resolver problemas de secuencia avanzada, como la predicción del precio de las acciones y los subtítulos de imágenes automatizados que veremos en los próximos artículos.

Problemas de secuencia de uno a muchos

Los problemas de secuencia uno a muchos son el tipo de problemas de secuencia donde los datos de entrada tienen un paso de tiempo y la salida contiene un vector de valores múltiples o pasos de tiempo múltiples. En esta sección, veremos cómo resolver problemas de secuencia de uno a muchos donde la entrada tiene una sola característica. Luego pasaremos a ver cómo trabajar con la entrada de múltiples características para resolver problemas de secuencia de uno a muchos.

Problemas de secuencia de uno a varios con una sola función

Primero creemos un conjunto de datos y entendamos el problema que vamos a resolver en esta sección.

Crear el conjunto de datos

El siguiente script importa las bibliotecas necesarias:

from numpy import array
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers.core import Activation, Dropout, Dense
from keras.layers import Flatten, LSTM
from keras.layers import GlobalMaxPooling1D
from keras.models import Model
from keras.layers.embeddings import Embedding
from sklearn.model_selection import train_test_split
from keras.preprocessing.text import Tokenizer
from keras.layers import Input
from keras.layers.merge import Concatenate
from keras.layers import Bidirectional

import pandas as pd
import numpy as np
import re

import matplotlib.pyplot as plt

Y el siguiente script crea el conjunto de datos:

X = list()
Y = list()
X = [x+3 for x in range(-2, 43, 3)]

for i in X:
    output_vector = list()
    output_vector.append(i+1)
    output_vector.append(i+2)
    Y.append(output_vector)

print(X)
print(Y)

Aquí está el resultado:

[1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43]
[[2, 3], [5, 6], [8, 9], [11, 12], [14, 15], [17, 18], [20, 21], [23, 24], [26, 27], [29, 30], [32, 33], [35, 36], [38, 39], [41, 42], [44, 45]]

Nuestra entrada contiene 15 muestras con un paso de tiempo y un valor de característica. Para cada valor en la muestra de entrada, el vector de salida correspondiente contiene los siguientes dos enteros. Por ejemplo, si la entrada es 4, el vector de salida contendrá los valores 5 y 6. Por lo tanto, el problema es un problema de secuencia simple de uno a muchos.

El siguiente script cambia la forma de nuestros datos según lo requiera el LSTM:

X = np.array(X).reshape(15, 1, 1)
Y = np.array(Y)

Ahora podemos entrenar nuestros modelos. Entrenaremos LSTM simples y apilados.

Solución a través de Simple LSTM
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(1, 1)))
model.add(Dense(2))
model.compile(optimizer="adam", loss="mse")
model.fit(X, Y, epochs=1000, validation_split=0.2, batch_size=3)

Una vez que el modelo está entrenado, podemos hacer predicciones sobre los datos de prueba:

test_input = array([10])
test_input = test_input.reshape((1, 1, 1))
test_output = model.predict(test_input, verbose=0)
print(test_output)

Los datos de prueba contienen un valor 10. En la salida, deberíamos obtener un vector que contiene 11 y 12. La salida que recibí es [10.982891 12.109697] que en realidad está muy cerca de la salida esperada.

Solución a través de Stacked LSTM

El siguiente script entrena LSTM apilados en nuestros datos y hace predicciones en los puntos de prueba:

model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(1, 1)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(2))
model.compile(optimizer="adam", loss="mse")
history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

test_output = model.predict(test_input, verbose=0)
print(test_output)

La respuesta es [11.00432 11.99205] que está muy cerca de la salida real.

Solución vía LSTM bidireccional

El siguiente script entrena un LSTM bidireccional en nuestros datos y luego hace una predicción en el conjunto de prueba.

from keras.layers import Bidirectional

model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(1, 1)))
model.add(Dense(2))
model.compile(optimizer="adam", loss="mse")

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)
test_output = model.predict(test_input, verbose=0)
print(test_output)

La salida que recibí es [11.035181 12.082813]

Problemas de secuencia de uno a varios con varias funciones

En esta sección veremos problemas de secuencia de uno a muchos donde las muestras de entrada tendrán un paso de tiempo, pero dos características. La salida será un vector de dos elementos.

Crear el conjunto de datos

Como siempre, el primer paso es crear el conjunto de datos:

nums = 25

X1 = list()
X2 = list()
X = list()
Y = list()

X1 = [(x+1)*2 for x in range(25)]
X2 = [(x+1)*3 for x in range(25)]

for x1, x2 in zip(X1, X2):
    output_vector = list()
    output_vector.append(x1+1)
    output_vector.append(x2+1)
    Y.append(output_vector)

X = np.column_stack((X1, X2))
print(X)

Nuestro conjunto de datos de entrada se ve así:

[[ 2  3]
 [ 4  6]
 [ 6  9]
 [ 8 12]
 [10 15]
 [12 18]
 [14 21]
 [16 24]
 [18 27]
 [20 30]
 [22 33]
 [24 36]
 [26 39]
 [28 42]
 [30 45]
 [32 48]
 [34 51]
 [36 54]
 [38 57]
 [40 60]
 [42 63]
 [44 66]
 [46 69]
 [48 72]
 [50 75]]

Puede ver que cada paso de tiempo de entrada consta de dos características. La salida será un vector que contiene los siguientes dos elementos que corresponden a las dos características en el paso de tiempo de la muestra de entrada. Por ejemplo, para la muestra de entrada [2, 3], la salida será [3, 4], y así.

Reformemos nuestros datos:

X = np.array(X).reshape(25, 1, 2)
Y = np.array(Y)
Solución a través de Simple LSTM
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(1, 2)))
model.add(Dense(2))
model.compile(optimizer="adam", loss="mse")
model.fit(X, Y, epochs=1000, validation_split=0.2, batch_size=3)

Ahora creemos nuestro punto de prueba y veamos qué tan bien funciona nuestro algoritmo:

test_input = array([40, 60])
test_input = test_input.reshape((1, 1, 2))
test_output = model.predict(test_input, verbose=0)
print(test_output)

La entrada es [40, 60], la salida debe ser [41, 61]. La salida predicha por nuestro simple LSTM es [40.946873 60.941723] que está muy cerca de la salida esperada.

Solución a través de Stacked LSTM
model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(1, 2)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(2))
model.compile(optimizer="adam", loss="mse")
history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

test_input = array([40, 60])
test_input = test_input.reshape((1, 1, 2))
test_output = model.predict(test_input, verbose=0)
print(test_output)

La salida en este caso es: [40.978477 60.994644]

Solución vía LSTM bidireccional
from keras.layers import Bidirectional

model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(1, 2)))
model.add(Dense(2))
model.compile(optimizer="adam", loss="mse")

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)
test_output = model.predict(test_input, verbose=0)
print(test_output)

El resultado obtenido es: [41.0975 61.159065]

Problemas de secuencia de muchos a muchos

En los problemas de secuencia de uno a muchos y de muchos a uno, vimos que el vector de salida puede contener varios valores. Dependiendo del problema, se puede considerar que un vector de salida que contiene múltiples valores tiene salidas simples (ya que la salida contiene datos de un intervalo de tiempo en términos estrictos) o múltiples (ya que un vector contiene múltiples valores).

Sin embargo, en algunos problemas de secuencia, queremos múltiples salidas divididas en pasos de tiempo. En otras palabras, para cada paso de tiempo en la entrada, queremos un paso de tiempo correspondiente en la salida. Estos modelos se pueden utilizar para resolver problemas de secuencia de muchos a muchos con longitudes variables.

Modelo codificador-decodificador

Para resolver estos problemas de secuencia, se ha diseñado el modelo codificador-decodificador. El modelo codificador-decodificador es básicamente un nombre elegante para la arquitectura de red neuronal con dos capas LSTM.

La primera capa funciona como una capa de codificador y codifica la secuencia de entrada. El decodificador también es una capa LSTM, que acepta tres entradas: la secuencia codificada del codificador LSTM, el estado oculto anterior y la entrada actual. Durante el entrenamiento, la salida real en cada paso de tiempo se usa para entrenar el modelo codificador-decodificador. Al hacer predicciones, la salida del codificador, el estado oculto actual y la salida anterior se utilizan como entrada para hacer predicciones en cada paso de tiempo. Estos conceptos serán más comprensibles cuando los vea en acción en una próxima sección.

Problemas de secuencia de varios a varios con una sola función

En esta sección resolveremos problemas de secuencia de muchos a muchos a través del modelo codificador-decodificador, donde cada paso de tiempo en la muestra de entrada contendrá una característica.

Primero creemos nuestro conjunto de datos.

Crear el conjunto de datos
X = list()
Y = list()
X = [x for x in range(5, 301, 5)]
Y = [y for y in range(20, 316, 5)]

X = np.array(X).reshape(20, 3, 1)
Y = np.array(Y).reshape(20, 3, 1)

La entrada X contiene 20 muestras donde cada muestra contiene 3 pasos de tiempo con una característica. Una muestra de entrada se ve así:

[[[  5]
  [ 10]
  [ 15]]

Puede ver que la muestra de entrada contiene 3 valores que son básicamente 3 múltiplos consecutivos de 5. La secuencia de salida correspondiente para la muestra de entrada anterior es la siguiente:

[[[ 20]
  [ 25]
  [ 30]]

La salida contiene los siguientes tres múltiplos consecutivos de 5. Puede ver que la salida en este caso es diferente a la que hemos visto en las secciones anteriores. Para el modelo de codificador-decodificador, la salida también debe convertirse a un formato 3D que contenga el número de muestras, pasos de tiempo y características. Esto se debe a que el decodificador genera una salida por paso de tiempo.

Hemos creado nuestro conjunto de datos; el siguiente paso es entrenar nuestros modelos. Entrenaremos modelos LSTM apilados y LSTM bidireccionales en las siguientes secciones.

Solución a través de Stacked LSTM

La siguiente secuencia de comandos crea el modelo de codificador-decodificador utilizando LSTM apilados:

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()

# encoder layer
model.add(LSTM(100, activation='relu', input_shape=(3, 1)))

# repeat vector
model.add(RepeatVector(3))

# decoder layer
model.add(LSTM(100, activation='relu', return_sequences=True))

model.add(TimeDistributed(Dense(1)))
model.compile(optimizer="adam", loss="mse")

print(model.summary())

En el script anterior, la primera capa LSTM es la capa del codificador.

A continuación, hemos agregado el vector de repetición a nuestro modelo. El vector de repetición toma la salida del codificador y la alimenta repetidamente como entrada en cada paso de tiempo al decodificador. Por ejemplo, en la salida tenemos tres pasos de tiempo. Para predecir cada paso de tiempo de salida, el decodificador utilizará el valor del vector de repetición, el estado oculto de la salida anterior y la entrada actual.

A continuación, tenemos una capa de decodificador. Dado que la salida tiene la forma de un intervalo de tiempo, que es un formato 3D, return_sequences para el modelo de decodificador se ha configurado True. los TimeDistributed La capa se utiliza para predecir individualmente la salida para cada paso de tiempo.

El resumen del modelo para el modelo de codificador-decodificador creado en el script anterior es el siguiente:

Layer (type)                 Output Shape              Param #
=================================================================
lstm_40 (LSTM)               (None, 100)               40800
_________________________________________________________________
repeat_vector_7 (RepeatVecto (None, 3, 100)            0
_________________________________________________________________
lstm_41 (LSTM)               (None, 3, 100)            80400
_________________________________________________________________
time_distributed_7 (TimeDist (None, 3, 1)              101
=================================================================
Total params: 121,301
Trainable params: 121,301
Non-trainable params: 0

Puede ver que el vector de repetición solo repite la salida del codificador y no tiene parámetros para entrenar.

El siguiente script entrena el modelo de codificador-decodificador anterior.

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

Creemos un punto de prueba y veamos si nuestro modelo de codificador-decodificador es capaz de predecir la salida de varios pasos. Ejecute el siguiente script:

test_input = array([300, 305, 310])
test_input = test_input.reshape((1, 3, 1))
test_output = model.predict(test_input, verbose=0)
print(test_output)

Nuestra secuencia de entrada contiene tres valores de paso de tiempo 300, 305 y 310. La salida debe ser los siguientes tres múltiplos de 5, es decir, 315, 320 y 325. Recibí la siguiente salida:

[[[316.02878]
  [322.27145]
  [328.5536 ]]]

Puede ver que la salida está en formato 3D.

Solución vía LSTM bidireccional

Creemos ahora un modelo de codificador-decodificador con LSTM bidireccionales y veamos si podemos obtener mejores resultados:

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()
model.add(Bidirectional(LSTM(100, activation='relu', input_shape=(3, 1))))
model.add(RepeatVector(3))
model.add(Bidirectional(LSTM(100, activation='relu', return_sequences=True)))
model.add(TimeDistributed(Dense(1)))
model.compile(optimizer="adam", loss="mse")

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

El script anterior entrena el modelo codificador-decodificador a través de LSTM bidireccional. Hagamos ahora predicciones en el punto de prueba, es decir [300, 305, 310].

test_output = model.predict(test_input, verbose=0)
print(test_output)

Aquí está el resultado:

[[[315.7526 ]
  [321.47153]
  [327.94025]]]

La salida que obtuve a través de LSTM bidireccionales es mejor que la que obtuve a través del modelo de codificador-decodificador basado en LSTM apilado simple.

Problemas de secuencia de varios a varios con varias funciones

Como ya lo habrá adivinado, en los problemas de secuencia de muchos a muchos, cada paso de tiempo en la muestra de entrada contiene múltiples características.

Crear el conjunto de datos

Creemos un conjunto de datos simple para nuestro problema:

X = list()
Y = list()
X1 = [x1 for x1 in range(5, 301, 5)]
X2 = [x2 for x2 in range(20, 316, 5)]
Y = [y for y in range(35, 331, 5)]

X = np.column_stack((X1, X2))

En el script anterior creamos dos listas X1 y X2. La lista X1 contiene todos los múltiplos de 5 de 5 a 300 (inclusive) y la lista X2 contiene todos los múltiplos de 5 desde 20 hasta 315 (inclusive). Finalmente, la lista Y, que resulta ser que la salida contiene todos los múltiplos de 5 entre 35 y 330 (inclusive). La lista de entrada final X es una fusión de columnas de X1 y X2.

Como siempre, necesitamos remodelar nuestra entrada X y salida Y antes de que puedan utilizarse para entrenar a LSTM.

X = np.array(X).reshape(20, 3, 2)
Y = np.array(Y).reshape(20, 3, 1)

Puedes ver la entrada X se ha remodelado en 20 muestras de tres pasos de tiempo con 2 características donde la salida se ha remodelado en dimensiones similares pero con 1 característica.

La primera muestra de la entrada se ve así:

[[ 5  20]
[ 10  25]
[ 15  30]]

La entrada contiene 6 múltiplos consecutivos de un número entero 5, tres cada uno en las dos columnas. Aquí está la salida correspondiente para la muestra de entrada anterior:

[[ 35]
[ 40]
[ 45]]

Como puede ver, la salida contiene los siguientes tres múltiplos consecutivos de 5.

Entrenemos ahora nuestro modelo de codificador-decodificador para aprender la secuencia anterior. Primero entrenaremos un codificador-decodificador basado en LSTM apilado simple.

Solución a través de Stacked LSTM

La siguiente secuencia de comandos entrena el modelo LSTM apilado. Puede ver que la forma de entrada ahora es (3, 2) correspondiente a tres pasos de tiempo y dos características en la entrada.

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()
model.add(LSTM(100, activation='relu', input_shape=(3, 2)))
model.add(RepeatVector(3))
model.add(LSTM(100, activation='relu', return_sequences=True))
model.add(TimeDistributed(Dense(1)))
model.compile(optimizer="adam", loss="mse")

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

Creemos ahora un punto de prueba que se utilizará para hacer una predicción.

X1 = [300, 305, 310]
X2 = [315, 320, 325]

test_input = np.column_stack((X1, X2))

test_input = test_input.reshape((1, 3, 2))
print(test_input)

El punto de prueba se ve así:

[[[300 315]
  [305 320]
  [310 325]]]

La salida real del punto de prueba anterior es [330, 335, 340]. Veamos qué son las predicciones del modelo:

test_output = model.predict(test_input, verbose=0)
print(test_output)

La salida prevista es:

[[[324.5786 ]
  [328.89658]
  [335.67603]]]

El resultado está lejos de ser correcto.

Solución vía LSTM bidireccional

Entrenemos ahora el modelo de codificador-decodificador basado en LSTM bidireccionales y veamos si podemos obtener mejores resultados. El siguiente script entrena el modelo.

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()
model.add(Bidirectional(LSTM(100, activation='relu', input_shape=(3, 2))))
model.add(RepeatVector(3))
model.add(Bidirectional(LSTM(100, activation='relu', return_sequences=True)))
model.add(TimeDistributed(Dense(1)))
model.compile(optimizer="adam", loss="mse")

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

El siguiente script hace predicciones sobre el conjunto de prueba:

test_output = model.predict(test_input, verbose=0)
print(test_output)

Aquí está el resultado:

[[[330.49133]
  [335.35327]
  [339.64398]]]

La salida lograda está bastante cerca de la salida real, es decir [330, 335, 340]. Por lo tanto, nuestro LSTM bidireccional superó al LSTM simple.

Conclusión

Esta es la segunda parte de mi artículo sobre “Solución de problemas de secuencia con LSTM en Keras” (parte 1 aquí). En este artículo, vio cómo resolver problemas de secuencia de uno a muchos y de muchos a muchos en LSTM. También vio cómo se puede utilizar el modelo codificador-decodificador para predecir salidas de varios pasos. El modelo codificador-decodificador se utiliza en una variedad de aplicaciones de procesamiento de lenguaje natural, como la traducción automática neuronal y el desarrollo de chatbot.

En el próximo artículo, veremos la aplicación del modelo codificador-decodificador en NLP.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad