Predicción de series temporales usando LSTM con PyTorch en Python

Los datos de series temporales cambian con el tiempo. En este artículo, utilizaremos PyTorch para analizar datos de series temporales y predecir valores futuros mediante el aprendizaje profundo.

Los datos de series temporales, como su nombre indica, son un tipo de datos que cambian con el tiempo. Por ejemplo, la temperatura en un período de 24 horas, el precio de varios productos en un mes, los precios de las acciones de una empresa en particular en un año. Los modelos avanzados de aprendizaje profundo como Redes de memoria a largo plazo (LSTM), son capaces de capturar patrones en los datos de series temporales y, por lo tanto, pueden ser Se utiliza para hacer predicciones sobre la tendencia futura de los datos. En este artículo, verá cómo usar el algoritmo LSTM para hacer predicciones futuras utilizando datos de series temporales.

En uno de mis artículos anteriores, expliqué cómo realizar análisis de series temporales usando LSTM en la biblioteca Keras para predecir los precios futuros de las acciones. En este artículo, usaremos la biblioteca PyTorch, que es una de las bibliotecas de Python más utilizadas para el aprendizaje profundo.

Antes de continuar, se supone que tiene una competencia de nivel intermedio con el lenguaje de programación Python y ha instalado la biblioteca PyTorch. Además, ayudará el conocimiento de conceptos básicos de aprendizaje automático y conceptos de aprendizaje profundo. Si no ha instalado PyTorch, puede hacerlo con el siguiente comando pip:

1
$ pip install pytorch

Conjunto de datos y definición del problema {#conjunto de datos y definición del problema}

El conjunto de datos que usaremos viene integrado con la Biblioteca Python Seaborn. Importemos primero las bibliotecas requeridas y luego importaremos el conjunto de datos:

1
2
3
4
5
6
7
8
import torch
import torch.nn as nn

import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

Imprimamos la lista de todos los conjuntos de datos que vienen integrados con la biblioteca Seaborn:

1
sns.get_dataset_names()

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
['anscombe',
 'attention',
 'brain_networks',
 'car_crashes',
 'diamonds',
 'dots',
 'exercise',
 'flights',
 'fmri',
 'gammas',
 'iris',
 'mpg',
 'planets',
 'tips',
 'titanic']

El conjunto de datos que usaremos es el conjunto de datos flights. Carguemos el conjunto de datos en nuestra aplicación y veamos cómo se ve:

1
2
flight_data = sns.load_dataset("flights")
flight_data.head()

Producción:

cabecera del conjunto de datos de vuelos

El conjunto de datos tiene tres columnas: año, mes y pasajeros. La columna pasajeros contiene el número total de pasajeros que viajan en un mes específico. Tracemos la forma de nuestro conjunto de datos:

1
flight_data.shape

Producción:

1
(144, 3)

Puede ver que hay 144 filas y 3 columnas en el conjunto de datos, lo que significa que el conjunto de datos contiene un registro de viaje de 12 años de los pasajeros.

La tarea es predecir el número de pasajeros que viajaron en los últimos 12 meses en base a los primeros 132 meses. Recuerda que tenemos un registro de 144 meses, lo que significa que los datos de los primeros 132 meses se utilizarán para entrenar nuestro modelo LSTM, mientras que el rendimiento del modelo se evaluará con los valores de los últimos 12 meses.

Grafiquemos la frecuencia de los pasajeros que viajan por mes. La siguiente secuencia de comandos aumenta el tamaño de trama predeterminado:

1
2
3
4
fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 15
fig_size[1] = 5
plt.rcParams["figure.figsize"] = fig_size

Y este siguiente script traza la frecuencia mensual del número de pasajeros:

1
2
3
4
5
6
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.xlabel('Months')
plt.grid(True)
plt.autoscale(axis='x',tight=True)
plt.plot(flight_data['passengers'])

Producción:

graficando frecuencia mensual de pasajeros

El resultado muestra que a lo largo de los años aumentó el número promedio de pasajeros que viajaban por vía aérea. La cantidad de pasajeros que viajan dentro de un año fluctúa, lo que tiene sentido porque durante las vacaciones de verano o invierno, la cantidad de pasajeros que viajan aumenta en comparación con otras partes del año.

Preprocesamiento de datos {#preprocesamiento de datos}

Los tipos de las columnas en nuestro conjunto de datos son objeto, como se muestra en el siguiente código:

1
flight_data.columns

Producción:

1
Index(['year', 'month', 'passengers'], dtype='object')

El primer paso de preprocesamiento es cambiar el tipo de la columna pasajeros a flotante.

1
all_data = flight_data['passengers'].values.astype(float)

Ahora, si imprime la matriz numpy all_data, debería ver los siguientes valores de tipo flotante:

1
print(all_data)

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[112. 118. 132. 129. 121. 135. 148. 148. 136. 119. 104. 118. 115. 126.
 141. 135. 125. 149. 170. 170. 158. 133. 114. 140. 145. 150. 178. 163.
 172. 178. 199. 199. 184. 162. 146. 166. 171. 180. 193. 181. 183. 218.
 230. 242. 209. 191. 172. 194. 196. 196. 236. 235. 229. 243. 264. 272.
 237. 211. 180. 201. 204. 188. 235. 227. 234. 264. 302. 293. 259. 229.
 203. 229. 242. 233. 267. 269. 270. 315. 364. 347. 312. 274. 237. 278.
 284. 277. 317. 313. 318. 374. 413. 405. 355. 306. 271. 306. 315. 301.
 356. 348. 355. 422. 465. 467. 404. 347. 305. 336. 340. 318. 362. 348.
 363. 435. 491. 505. 404. 359. 310. 337. 360. 342. 406. 396. 420. 472.
 548. 559. 463. 407. 362. 405. 417. 391. 419. 461. 472. 535. 622. 606.
 508. 461. 390. 432.]

A continuación, dividiremos nuestro conjunto de datos en conjuntos de entrenamiento y de prueba. El algoritmo LSTM se entrenará en el conjunto de entrenamiento. Luego, el modelo se usará para hacer predicciones en el conjunto de prueba. Las predicciones se compararán con los valores reales en el conjunto de prueba para evaluar el rendimiento del modelo entrenado.

Los primeros 132 registros se usarán para entrenar el modelo y los últimos 12 registros se usarán como conjunto de prueba. El siguiente script divide los datos en conjuntos de entrenamiento y prueba.

1
2
3
4
test_data_size = 12

train_data = all_data[:-test_data_size]
test_data = all_data[-test_data_size:]

Ahora imprimamos la duración de los conjuntos de prueba y entrenamiento:

1
2
print(len(train_data))
print(len(test_data))

Producción:

1
2
132
12

Si ahora imprime los datos de prueba, verá que contiene los últimos 12 registros de la matriz numpy all_data:

1
print(test_data)

Producción:

1
[417. 391. 419. 461. 472. 535. 622. 606. 508. 461. 390. 432.]

Nuestro conjunto de datos no está normalizado en este momento. El número total de pasajeros en los años iniciales es mucho menor en comparación con el número total de pasajeros en los años posteriores. Es muy importante normalizar los datos para las predicciones de series temporales. Realizaremos una escala mínima/máxima en el conjunto de datos que normaliza los datos dentro de un cierto rango de valores mínimos y máximos. Usaremos la clase MinMaxScaler del módulo sklearn.preprocessing para escalar nuestros datos. Para obtener más detalles sobre la implementación del escalador min/max, visite este enlace.

El siguiente código normaliza nuestros datos utilizando el escalador mínimo/máximo con valores mínimos y máximos de -1 y 1, respectivamente.

1
2
3
4
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler(feature_range=(-1, 1))
train_data_normalized = scaler.fit_transform(train_data .reshape(-1, 1))

Ahora imprimamos los primeros 5 y los últimos 5 registros de nuestros datos de trenes normalizados.

1
2
print(train_data_normalized[:5])
print(train_data_normalized[-5:])

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[[-0.96483516]
 [-0.93846154]
 [-0.87692308]
 [-0.89010989]
 [-0.92527473]]
[[1.        ]
 [0.57802198]
 [0.33186813]
 [0.13406593]
 [0.32307692]]

Puede ver que los valores del conjunto de datos ahora están entre -1 y 1.

Es importante mencionar aquí que la normalización de datos solo se aplica en los datos de entrenamiento y no en los datos de prueba. Si se aplica la normalización a los datos de prueba, existe la posibilidad de que parte de la información se filtre del conjunto de entrenamiento al conjunto de prueba.

El siguiente paso es convertir nuestro conjunto de datos en tensores, ya que los modelos de PyTorch se entrenan con tensores. Para convertir el conjunto de datos en tensores, simplemente podemos pasar nuestro conjunto de datos al constructor del objeto FloatTensor, como se muestra a continuación:

1
train_data_normalized = torch.FloatTensor(train_data_normalized).view(-1)

El último paso de preprocesamiento es convertir nuestros datos de entrenamiento en secuencias y etiquetas correspondientes.

Puede usar cualquier longitud de secuencia y depende del conocimiento del dominio. Sin embargo, en nuestro conjunto de datos es conveniente utilizar una longitud de secuencia de 12 ya que tenemos datos mensuales y hay 12 meses en un año. Si tuviéramos datos diarios, una longitud de secuencia mejor habría sido 365, es decir, la cantidad de días en un año. Por lo tanto, estableceremos la longitud de la secuencia de entrada para el entrenamiento en 12.

1
train_window = 12

A continuación, definiremos una función llamada create_inout_sequences. La función aceptará los datos de entrada sin procesar y devolverá una lista de tuplas. En cada tupla, el primer elemento contendrá una lista de 12 elementos correspondientes a la cantidad de pasajeros que viajan en 12 meses, el segundo elemento de la tupla contendrá un elemento, es decir, la cantidad de pasajeros en el mes 12+1.

1
2
3
4
5
6
7
8
def create_inout_sequences(input_data, tw):
    inout_seq = []
    L = len(input_data)
    for i in range(L-tw):
        train_seq = input_data[i:i+tw]
        train_label = input_data[i+tw:i+tw+1]
        inout_seq.append((train_seq ,train_label))
    return inout_seq

Ejecute el siguiente script para crear secuencias y etiquetas correspondientes para el entrenamiento:

1
train_inout_seq = create_inout_sequences(train_data_normalized, train_window)

Si imprime la longitud de la lista train_inout_seq, verá que contiene 120 elementos. Esto se debe a que aunque el conjunto de entrenamiento contiene 132 elementos, la longitud de la secuencia es 12, lo que significa que la primera secuencia consta de los primeros 12 elementos y el elemento 13 es la etiqueta de la primera secuencia. De manera similar, la segunda secuencia comienza en el segundo elemento y termina en el elemento 13, mientras que el elemento 14 es la etiqueta de la segunda secuencia y así sucesivamente.

Ahora imprimamos los primeros 5 elementos de la lista train_inout_seq:

1
train_inout_seq[:5]

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[(tensor([-0.9648, -0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066,
          -0.8593, -0.9341, -1.0000, -0.9385]), tensor([-0.9516])),
 (tensor([-0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593,
          -0.9341, -1.0000, -0.9385, -0.9516]),
  tensor([-0.9033])),
 (tensor([-0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341,
          -1.0000, -0.9385, -0.9516, -0.9033]), tensor([-0.8374])),
 (tensor([-0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000,
          -0.9385, -0.9516, -0.9033, -0.8374]), tensor([-0.8637])),
 (tensor([-0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385,
          -0.9516, -0.9033, -0.8374, -0.8637]), tensor([-0.9077]))]

Puede ver que cada elemento es una tupla donde el primer elemento consta de los 12 elementos de una secuencia y el segundo elemento de la tupla contiene la etiqueta correspondiente.

Creación del modelo LSTM

Hemos preprocesado los datos, ahora es el momento de entrenar nuestro modelo. Definiremos una clase LSTM, que hereda de la clase nn.Module de la biblioteca PyTorch. Consulta mi último artículo para ver cómo crear un modelo de clasificación con PyTorch. Ese artículo lo ayudará a comprender lo que sucede en el siguiente código.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class LSTM(nn.Module):
    def __init__(self, input_size=1, hidden_layer_size=100, output_size=1):
        super().__init__()
        self.hidden_layer_size = hidden_layer_size

        self.lstm = nn.LSTM(input_size, hidden_layer_size)

        self.linear = nn.Linear(hidden_layer_size, output_size)

        self.hidden_cell = (torch.zeros(1,1,self.hidden_layer_size),
                            torch.zeros(1,1,self.hidden_layer_size))

    def forward(self, input_seq):
        lstm_out, self.hidden_cell = self.lstm(input_seq.view(len(input_seq) ,1, -1), self.hidden_cell)
        predictions = self.linear(lstm_out.view(len(input_seq), -1))
        return predictions[-1]

Permítanme resumir lo que está sucediendo en el código anterior. El constructor de la clase LSTM acepta tres parámetros:

  1. input_size: corresponde al número de características en la entrada. Aunque la longitud de nuestra secuencia es 12, para cada mes solo tenemos 1 valor, es decir, el número total de pasajeros, por lo que el tamaño de entrada será 1.
  2. hidden_layer_size: especifica el número de capas ocultas junto con el número de neuronas en cada capa. Tendremos una capa de 100 neuronas.
  3. output_size: la cantidad de elementos en la salida, dado que queremos predecir la cantidad de pasajeros para 1 mes en el futuro, el tamaño de la salida será 1.

A continuación, en el constructor creamos las variables hidden_layer_size, lstm, linear y hidden_cell. El algoritmo LSTM acepta tres entradas: estado oculto anterior, estado de celda anterior y entrada actual. La variable hidden_cell contiene el estado oculto y de celda anterior. Las variables de capa lstm y linear se utilizan para crear las capas LSTM y lineal.

Dentro del método forward, input_seq se pasa como un parámetro, que primero se pasa a través de la capa lstm. La salida de la capa lstm son los estados ocultos y de celda en el paso de tiempo actual, junto con la salida. La salida de la capa lstm se pasa a la capa linear. El número previsto de pasajeros se almacena en el último elemento de la lista de “predicciones”, que se devuelve a la función de llamada.

El siguiente paso es crear un objeto de la clase LSTM(), definir una función de pérdida y el optimizador. Como estamos resolviendo un problema de clasificación, usaremos la pérdida de entropía cruzada. Para la función de optimizador, utilizaremos el optimizador de adam.

1
2
3
model = LSTM()
loss_function = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

Imprimamos nuestro modelo:

1
print(model)

Producción:

1
2
3
4
LSTM(
  (lstm): LSTM(1, 100)
  (linear): Linear(in_features=100, out_features=1, bias=True)
)

Entrenando al modelo {#entrenando al modelo}

Entrenaremos nuestro modelo durante 150 épocas. Puedes probar con más épocas si quieres. La pérdida se imprimirá después de cada 25 épocas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
epochs = 150

for i in range(epochs):
    for seq, labels in train_inout_seq:
        optimizer.zero_grad()
        model.hidden_cell = (torch.zeros(1, 1, model.hidden_layer_size),
                        torch.zeros(1, 1, model.hidden_layer_size))

        y_pred = model(seq)

        single_loss = loss_function(y_pred, labels)
        single_loss.backward()
        optimizer.step()

    if i%25 == 1:
        print(f'epoch: {i:3} loss: {single_loss.item():10.8f}')

print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')

Producción:

1
2
3
4
5
6
7
epoch:   1 loss: 0.00517058
epoch:  26 loss: 0.00390285
epoch:  51 loss: 0.00473305
epoch:  76 loss: 0.00187001
epoch: 101 loss: 0.00000075
epoch: 126 loss: 0.00608046
epoch: 149 loss: 0.0004329932

Puede obtener valores diferentes ya que, de forma predeterminada, los pesos se inicializan aleatoriamente en una red neuronal PyTorch.

Hacer predicciones

Ahora que nuestro modelo está entrenado, podemos comenzar a hacer predicciones. Dado que nuestro conjunto de prueba contiene los datos de pasajeros de los últimos 12 meses y nuestro modelo está entrenado para hacer predicciones utilizando una longitud de secuencia de 12. Primero filtraremos los últimos 12 valores del conjunto de entrenamiento:

1
2
3
4
fut_pred = 12

test_inputs = train_data_normalized[-train_window:].tolist()
print(test_inputs)

Producción:

1
[0.12527473270893097, 0.04615384712815285, 0.3274725377559662, 0.2835164964199066, 0.3890109956264496, 0.6175824403762817, 0.9516483545303345, 1.0, 0.5780220031738281, 0.33186814188957214, 0.13406594097614288, 0.32307693362236023]

Puede comparar los valores anteriores con los últimos 12 valores de la lista de datos train_data_normalized.

Inicialmente, el elemento test_inputs contendrá 12 elementos. Dentro de un bucle for, estos 12 elementos se utilizarán para hacer predicciones sobre el primer elemento del conjunto de prueba, es decir, el número de elemento 133. El valor de predicción se agregará a la lista test_inputs. Durante la segunda iteración, nuevamente los últimos 12 elementos se usarán como entrada y se realizará una nueva predicción que luego se agregará a la lista test_inputs nuevamente. El bucle for se ejecutará 12 veces ya que hay 12 elementos en el conjunto de prueba. Al final del bucle, la lista test_inputs contendrá 24 elementos. Los últimos 12 elementos serán los valores previstos para el conjunto de prueba.

El siguiente script se utiliza para hacer predicciones:

1
2
3
4
5
6
7
8
model.eval()

for i in range(fut_pred):
    seq = torch.FloatTensor(test_inputs[-train_window:])
    with torch.no_grad():
        model.hidden = (torch.zeros(1, 1, model.hidden_layer_size),
                        torch.zeros(1, 1, model.hidden_layer_size))
        test_inputs.append(model(seq).item())

Si imprime la longitud de la lista test_inputs, verá que contiene 24 elementos. Los últimos 12 elementos previstos se pueden imprimir de la siguiente manera:

1
test_inputs[fut_pred:]

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[0.4574652910232544,
 0.9810629487037659,
 1.279405951499939,
 1.0621851682662964,
 1.5830546617507935,
 1.8899496793746948,
 1.323508620262146,
 1.8764172792434692,
 2.1249167919158936,
 1.7745600938796997,
 1.7952896356582642,
 1.977765679359436]

Es pertinente mencionar nuevamente que puede obtener diferentes valores dependiendo de los pesos utilizados para entrenar el LSTM.

Dado que normalizamos el conjunto de datos para el entrenamiento, los valores pronosticados también se normalizan. Necesitamos convertir los valores predichos normalizados en valores predichos reales. Podemos hacerlo pasando los valores normalizados al método inverse_transform del objeto escalador mínimo/máximo que usamos para normalizar nuestro conjunto de datos.

1
2
actual_predictions = scaler.inverse_transform(np.array(test_inputs[train_window:] ).reshape(-1, 1))
print(actual_predictions)

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[[435.57335371]
 [554.69182083]
 [622.56485397]
 [573.14712578]
 [691.64493555]
 [761.46355206]
 [632.59821111]
 [758.38493103]
 [814.91857016]
 [735.21242136]
 [739.92839211]
 [781.44169205]]

Ahora representemos los valores pronosticados contra los valores reales. Mira el siguiente código:

1
2
x = np.arange(132, 144, 1)
print(x)

Producción:

1
[132 133 134 135 136 137 138 139 140 141 142 143]

En el script anterior, creamos una lista que contiene valores numéricos de los últimos 12 meses. El primer mes tiene un valor de índice de 0, por lo tanto, el último mes estará en el índice 143.

En el siguiente guión, trazaremos el número total de pasajeros durante 144 meses, junto con el número previsto de pasajeros para los últimos 12 meses.

1
2
3
4
5
6
7
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.grid(True)
plt.autoscale(axis='x', tight=True)
plt.plot(flight_data['passengers'])
plt.plot(x,actual_predictions)
plt.show()

Producción:

graficando numero total de pasajeros

Las predicciones hechas por nuestro LSTM están representadas por la línea naranja. Puede ver que nuestro algoritmo no es demasiado preciso, pero aun así ha podido capturar la tendencia ascendente del número total de pasajeros que viajaron en los últimos 12 meses junto con fluctuaciones ocasionales. Puedes probar con una mayor cantidad de épocas y con una mayor cantidad de neuronas en la capa LSTM para ver si puedes obtener un mejor rendimiento.

Para tener una mejor vista de la salida, podemos graficar el número real y pronosticado de pasajeros para los últimos 12 meses de la siguiente manera:

1
2
3
4
5
6
7
8
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.grid(True)
plt.autoscale(axis='x', tight=True)

plt.plot(flight_data['passengers'][-train_window:])
plt.plot(x,actual_predictions)
plt.show()

Producción:

trazando el número previsto de pasajeros

Nuevamente, las predicciones no son muy precisas, pero el algoritmo pudo capturar la tendencia de que la cantidad de pasajeros en los meses futuros debería ser mayor que en los meses anteriores con fluctuaciones ocasionales.

Conclusión

LSTM es uno de los algoritmos más utilizados para resolver problemas de secuencias. En este artículo vimos cómo hacer predicciones futuras utilizando datos de series temporales con LSTM. También vio cómo implementar LSTM con la biblioteca PyTorch y luego cómo trazar los resultados pronosticados contra los valores reales para ver qué tan bien se está desempeñando el algoritmo entrenado. do.