Resolviendo problemas de secuencia con LSTM en Keras: Parte 2

Esta es la segunda y última parte de la serie de artículos de dos partes sobre cómo resolver problemas de secuencia con LSTM. En la parte 1 de la serie, expliqué cómo so...

Esta es la segunda y última parte de la serie de artículos de dos partes sobre cómo resolver 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 a través de 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 avanzados, 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 de uno a muchos son el tipo de problemas de secuencia en los que 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 muchos con una sola característica

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 requeridas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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á la salida:

1
2
[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 números 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 remodela nuestros datos según lo requiere el LSTM:

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

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

Solución mediante LSTM simple
1
2
3
4
5
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:

1
2
3
4
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 mediante LSTM apilado

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

1
2
3
4
5
6
7
8
9
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 a través de LSTM bidireccional

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)

El resultado que recibí es [11.035181 12.082813]

Problemas de secuencia de uno a muchos con múltiples 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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í:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[[ 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í sucesivamente.

Vamos a remodelar nuestros datos:

1
2
X = np.array(X).reshape(25, 1, 2)
Y = np.array(Y)
Solución mediante LSTM simple
1
2
3
4
5
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 vamos a crear nuestro punto de prueba y ver qué tan bien funciona nuestro algoritmo:

1
2
3
4
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 LSTM simple es [40.946873 60.941723] que está muy cerca de la salida esperada.

Solución mediante LSTM apilado
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 a través de LSTM bidireccional
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)

La salida obtenida es: [41.0975 61.159065]

Problemas de secuencias 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 múltiples valores. Según el problema, se puede considerar que un vector de salida que contiene múltiples valores tiene salidas únicas (ya que la salida contiene datos de un paso 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. Dichos modelos se pueden usar para resolver problemas de secuencias de muchos a muchos con longitudes variables.

Modelo codificador-decodificador

Para resolver tales 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 codificadora 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-descodificador. 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 secuencias de muchos a muchos con una sola función

En esta sección, resolveremos problemas de secuencias 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 vamos a crear nuestro conjunto de datos.

Crear el conjunto de datos
1
2
3
4
5
6
7
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í:

1
2
3
[[[  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:

1
2
3
[[[ 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 lo 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 a nuestros modelos. Entrenaremos modelos LSTM apilados y LSTM bidireccionales en las siguientes secciones.

Solución mediante LSTM apilado

El siguiente script crea el modelo de codificador-decodificador utilizando LSTM apilados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 decodificadora. Dado que la salida tiene la forma de un paso de tiempo, que es un formato 3D, return_sequences para el modelo del decodificador se ha establecido como True. La capa TimeDistributed se usa para predecir individualmente la salida para cada paso de tiempo.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 codificador-decodificador anterior.

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

Vamos a crear un punto de prueba y ver si nuestro modelo de codificador-decodificador puede predecir la salida de varios pasos. Ejecute el siguiente script:

1
2
3
4
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 próximos tres múltiplos de 5, es decir, 315, 320 y 325. Recibí la siguiente salida:

1
2
3
[[[316.02878]
  [322.27145]
  [328.5536 ]]]

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

Solución a través de LSTM bidireccional

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 sobre el punto de prueba, es decir, [300, 305, 310].

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

Aquí está la salida:

1
2
3
[[[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 secuencias de muchos a muchos con múltiples funciones

Como ya 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

Vamos a crear un conjunto de datos simple para nuestro problema:

1
2
3
4
5
6
7
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 de 20 a 315 (inclusive). Finalmente, la lista Y, que resulta ser la salida, contiene todos los múltiplos de 5 entre 35 y 330 (inclusive). La lista de entrada final X es una fusión por columnas de X1 y X2.

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

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

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

La primera muestra de la entrada se ve así:

1
2
3
[[ 5  20]
[ 10  25]
[ 15  30]]

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

1
2
3
[[ 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 mediante LSTM apilado

El siguiente script 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)

Ahora vamos a crear un punto de prueba que se usará para hacer una predicción.

1
2
3
4
5
6
7
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í:

1
2
3
[[[300 315]
  [305 320]
  [310 325]]]

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

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

La salida prevista es:

1
2
3
[[[324.5786 ]
  [328.89658]
  [335.67603]]]

La salida está lejos de ser correcta.

Solución a través de 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

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

Aquí está la salida:

1
2
3
[[[330.49133]
  [335.35327]
  [339.64398]]]

La salida lograda es bastante cercana a 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 "Resolviendo 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 usar el modelo codificador-decodificador para predecir salidas de varios pasos. El modelo codificador-decodificador se utiliza en una variedad de aplicaciones de procesamiento natural del lenguaje como la traducción automática neuronal y el desarrollo de chatbots.

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