Redes Neuronales aplicadas a Series Temporales

Objetivo

El objetivo de este cuaderno es introducir algunos ejemplos de ajuste de redes neuronales en el contexto de análsiis de series temporales. En particular:

  • Discutir cómo convertir el problema de ajuste de modelos a un problema de aprendizaje supervisado.

  • Analizar la relación entre RNN clásicas y los modelos lineales ya vistos.

  • Discutir algunos ejemplos más complejos de redes (CNN, RNN, LSTM).

  • Observar cómo se puede realizar la predicción.

Nos basaremos en la biblioteca tensorflow, por lo que es necesario una instalación de Python con tensorflow para que funcione. En particular usaremos la biblioteca keras para interactuar de manera sencilla con tensorflow

Ejemplo

Trabajaremos en un principio con la serie de mortalidad que ya vimos:

cmort = astsa.cmort
cmort.plot();
../_images/244c076301a4fe7532cbb6dda8f533b04c7336920cd04161870752d848d91491.png

Estacionarización

Como vimos antes, resulta útil llevar primero la serie a algo estacionario. En este caso, le quitamos la tendencia.

from statsmodels.formula.api import ols

time = pd.Series([idx.ordinal for idx in cmort.index], index=cmort.index, name="Semana")
data = pd.concat([cmort, time], axis=1).dropna()

fit = ols(formula="cmort~time", data=data).fit()
fit.summary()
OLS Regression Results
Dep. Variable: cmort R-squared: 0.211
Model: OLS Adj. R-squared: 0.209
Method: Least Squares F-statistic: 135.0
Date: Wed, 12 Jun 2024 Prob (F-statistic): 8.03e-28
Time: 19:43:45 Log-Likelihood: -1829.9
No. Observations: 508 AIC: 3664.
Df Residuals: 506 BIC: 3672.
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 96.6510 0.790 122.335 0.000 95.099 98.203
time -0.0312 0.003 -11.618 0.000 -0.036 -0.026
Omnibus: 67.579 Durbin-Watson: 0.576
Prob(Omnibus): 0.000 Jarque-Bera (JB): 97.699
Skew: 0.906 Prob(JB): 6.09e-22
Kurtosis: 4.156 Cond. No. 590.


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
x=fit.resid
x.plot()
plt.title("Residuos de mortalidad sin tendencia");
../_images/2a0569e298260476c2858cefed864c84f79376ca47fe3e5460d9fb56074fdcb9.png

Aprendizaje en series temporales

Las redes neuronales sirven para realizar aprendizaje supervisado, esto es, a partir de ejemplos, encontrar los coeficientes de la red que minimizan una función de loss o pérdida. En este caso:

  • Los ejemplos son “ventanas” de valores en el tiempo de la serie, y uno o más features que nos interese incorporar:

    • Por ejemplo, el valor de la semana del año en este caso importa debido a la variación anual.

    • Pueden ser también diferentes “features” como la temperatura y partículas que ya vimos.

  • El valor a predecir es por ejemplo, el siguiente valor de la serie, o una ventana hacia adelante.

  • En base a esto, se arma una arquitectura de red y se entrena usando backpropagation.

Diagrama

Para el caso por ejemplo de tomar 6 lags:

split window

Preprocesamiento

En este caso, haremos varios modelos. El primero simplemente usa como feature la propia serie, usando una cantidad window de lags hacia atrás. Separamos ademas una parte para testear predicciones.

Ahora debemos rearmar los datos para el formato tensorflow:

  • Cada feature es un vector conteniendo una ventana de window datos de la serie.

  • Cada valor observado es un vector (en este caso escalar) con los datos a predecir pred.

La función keras.utils.timeseries_dataset_from_array() nos permite hacer esto ordenado (aunque con algunos pitfalls):

window = 3 #lags a mirar

input_data = x.values[:-window]
targets = x.values[window:]

dataset = keras.utils.timeseries_dataset_from_array( input_data, targets, 
                                                     sequence_length=window,
                                                     batch_size=16)

train, test = keras.utils.split_dataset(dataset,0.8)
2024-06-12 19:57:22.267907: W tensorflow/core/framework/local_rendezvous.cc:404] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence
train.as_numpy_iterator().next()
(array([[  1.23010434,   8.05125614,  -2.19759207],
        [  8.05125614,  -2.19759207,   1.52355972],
        [ -2.19759207,   1.52355972,  -0.64528849],
        [  1.52355972,  -0.64528849,  -0.4841367 ],
        [ -0.64528849,  -0.4841367 ,  -7.8029849 ],
        [ -0.4841367 ,  -7.8029849 ,  -5.55183311],
        [ -7.8029849 ,  -5.55183311,  -4.31068132],
        [ -5.55183311,  -4.31068132,  -7.58952953],
        [ -4.31068132,  -7.58952953,  -1.70837773],
        [ -7.58952953,  -1.70837773,  -3.41722594],
        [ -1.70837773,  -3.41722594,   1.77392585],
        [ -3.41722594,   1.77392585,  -8.57492236],
        [  1.77392585,  -8.57492236,   1.21622943],
        [ -8.57492236,   1.21622943, -12.91261877],
        [  1.21622943, -12.91261877,  -9.52146698],
        [-12.91261877,  -9.52146698,  -5.40031519]]),
 array([  1.52355972,  -0.64528849,  -0.4841367 ,  -7.8029849 ,
         -5.55183311,  -4.31068132,  -7.58952953,  -1.70837773,
         -3.41722594,   1.77392585,  -8.57492236,   1.21622943,
        -12.91261877,  -9.52146698,  -5.40031519, -13.1991634 ]))

Modelo 1: una única neurona densa.

Esto, sin agregar no linealidades, debería coincidir con el modelo autorregresivo que ya vimos.

model = keras.Sequential([
    keras.Input(shape=(window,)),
    keras.layers.Dense(units=1)
])

model.summary()
Model: "sequential_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense_1 (Dense)                 │ (None, 1)              │             4 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 4 (16.00 B)
 Trainable params: 4 (16.00 B)
 Non-trainable params: 0 (0.00 B)
model.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model.fit(
    train,
    epochs=300, 
    verbose=False,
    validation_data=test
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/7bae5c03977f23908da87a8c3ef09a0d5d06a594257042afad04d93c0f971cb4.png
pred = model.predict(dataset)
32/32 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
plt.plot(x.values)
plt.plot(range(window,x.size-window+1),pred);
../_images/bd1bc74a711372a6d91a085bc07a958000636636e32a8ece018234350081b447.png
model.get_weights()
[array([[0.0187821 ],
        [0.45386124],
        [0.36643973]], dtype=float32),
 array([-0.10907853], dtype=float32)]
from statsmodels.tsa.api import ARIMA

x_train=x.iloc[:406]
arima = ARIMA(x_train,order=(window,0,0), trend='c').fit()
arima.summary()
SARIMAX Results
Dep. Variable: y No. Observations: 406
Model: ARIMA(3, 0, 0) Log Likelihood -1283.337
Date: Wed, 12 Jun 2024 AIC 2576.675
Time: 20:11:50 BIC 2596.707
Sample: 01-04-1970 HQIC 2584.603
- 10-16-1977
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
const -0.3526 1.865 -0.189 0.850 -4.007 3.302
ar.L1 0.3794 0.045 8.471 0.000 0.292 0.467
ar.L2 0.4591 0.050 9.098 0.000 0.360 0.558
ar.L3 0.0041 0.052 0.079 0.937 -0.098 0.106
sigma2 32.4955 2.127 15.275 0.000 28.326 36.665
Ljung-Box (L1) (Q): 0.00 Jarque-Bera (JB): 8.96
Prob(Q): 0.97 Prob(JB): 0.01
Heteroskedasticity (H): 0.73 Skew: 0.30
Prob(H) (two-sided): 0.07 Kurtosis: 3.40


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
arima.mse
32.52697246853836
model.evaluate(train)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 486us/step - loss: 33.2681 - mse: 33.2681
[32.54787826538086, 32.54787826538086]

Modelo 2: múltiples capas densas

Agreguemos algunas capas para darle no linealidad al modelo. Usamos como función de activación relu, es decir \(a(x)=\max\{x,0\}\).

model2 = keras.Sequential([
    keras.Input(shape=(window,)),
    keras.layers.Dense(units=32, activation='relu'),
    keras.layers.Dense(units=1)
])

model2.summary()
Model: "sequential_5"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense_8 (Dense)                 │ (None, 32)             │           128 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_9 (Dense)                 │ (None, 1)              │            33 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 161 (644.00 B)
 Trainable params: 161 (644.00 B)
 Non-trainable params: 0 (0.00 B)
model2.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model2.fit(
    train,
    epochs=200, 
    verbose=False,
    validation_data=test
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/46516879b3d25dd725340655762ba24f4ee2b1bd7b4ce23e78fdcff12da01b22.png
model.evaluate(test)
model2.evaluate(test)
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 807us/step - loss: 30.3401 - mse: 30.3401
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 28.1381 - mse: 28.1381
[27.77257537841797, 27.77257537841797]
pred = model2.predict(dataset)
32/32 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step 
plt.plot(x.values)
plt.plot(range(window,x.size-window+1),pred);
../_images/5cda3b734712692dff04af3d439fe8ce261260ecc0b860f2b8d96af763d7b178.png

Modelo 3: agregando características

Agreguemos ahora al conjunto de entrenamiento algunas funciones del tiempo. Por ejemplo, la semana del año:

Preprocesamiento

window = 3 #lags a mirar

n = x.size-window
week = [x.index[i].weekofyear for i in range(0,n)]
#cost = np.cos(2*np.pi*freq*t)
#sint = np.sin(2*np.pi*freq*t)

features = 2
input_data = np.stack([x.values[:-window],week], axis=1)

targets = x.values[window:]

dataset = keras.utils.timeseries_dataset_from_array(input_data, targets, sequence_length=window, batch_size=4)

train, test = keras.utils.split_dataset(dataset,0.8)
2024-06-12 20:23:05.802270: W tensorflow/core/framework/local_rendezvous.cc:404] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence

Ajuste

model3 = keras.Sequential([
    keras.Input(shape=(window,features)),
    keras.layers.Reshape((1,window*features)),
    tf.keras.layers.Dense(units=1)
])

model3.summary()
Model: "sequential_7"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ reshape_1 (Reshape)             │ (None, 1, 6)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_11 (Dense)                │ (None, 1, 1)           │             7 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 7 (28.00 B)
 Trainable params: 7 (28.00 B)
 Non-trainable params: 0 (0.00 B)
model3.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model3.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/1d2a51635c160a03f465bec493f4888ca373ea1d3bf9f1f581e137e85861a0df.png
model3.evaluate(test)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 491us/step - loss: 25.1285 - mse: 25.1285
[25.332748413085938, 25.332748413085938]
pred = model3.predict(dataset)
pred = np.reshape(pred,(len(x)-2*window+1,1))
126/126 ━━━━━━━━━━━━━━━━━━━━ 0s 764us/step
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred);
../_images/af29ff47901337d0dd16410dc745440fa12365a784ec636abb37a4a049361ede.png

Modelo 4: características y capas

Agreguemos un par de capas densas intermedias

model4 = keras.Sequential([
    keras.Input(shape=(window,features)),
    keras.layers.Reshape((1,window*features)),
    keras.layers.Dense(units=16,activation="relu"),
    keras.layers.Dense(units=8,activation="relu"),
    tf.keras.layers.Dense(units=1)
])

model4.summary()
Model: "sequential_13"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ reshape_7 (Reshape)             │ (None, 1, 6)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_27 (Dense)                │ (None, 1, 16)          │           112 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_28 (Dense)                │ (None, 1, 8)           │           136 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_29 (Dense)                │ (None, 1, 1)           │             9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 257 (1.00 KB)
 Trainable params: 257 (1.00 KB)
 Non-trainable params: 0 (0.00 B)
model4.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model4.fit(
    train,
    epochs=50,
    verbose=False,
    validation_data=test
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/b3d204c701f88170cc563c15b04327203d208610f785c5d19ff65cebb281cc28.png
model4.evaluate(test)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 580us/step - loss: 27.8429 - mse: 27.8429
[27.725839614868164, 27.725839614868164]
pred = model4.predict(dataset)
pred = np.reshape(pred,(len(x)-2*window+1,1))
126/126 ━━━━━━━━━━━━━━━━━━━━ 0s 983us/step
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred);
../_images/f8ae5b2f975313bbcdcb098ac6c993d2b53d6fae2b01c9a697d89c5810988e24.png

Modelo 5: Convolutional Neural Network

La idea de la capa convolucional o CNN es similar a la de los procesos autorregresivos y a lo que veníamos haciendo antes, solo que simplifica un poco la escritura del modelo. El de abajo es esencialmente el mismo modelo 4 pero usando CNNs.

CNN

Preprocesamiento

window = 3 #lags a mirar

n = x.size-window
week = [x.index[i].weekofyear for i in range(0,n)]
#cost = np.cos(2*np.pi*freq*t)
#sint = np.sin(2*np.pi*freq*t)

features = 2
input_data = np.stack([x.values[:-window],week], axis=1)

targets = x.values[window:]

dataset = keras.utils.timeseries_dataset_from_array(input_data, targets, sequence_length=window, batch_size=4)

train, test = keras.utils.split_dataset(dataset,0.8)
2024-06-12 20:33:07.797055: W tensorflow/core/framework/local_rendezvous.cc:404] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence
model5 = keras.Sequential([
    keras.Input(shape=(1,features)),
    keras.layers.Conv1D(filters=16, kernel_size=window, activation="relu", padding="causal"),
    keras.layers.Dense(units=8,activation="relu"),
    tf.keras.layers.Dense(units=1)
])

model5.summary()
Model: "sequential_14"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ conv1d (Conv1D)                 │ (None, 1, 16)          │           112 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_30 (Dense)                │ (None, 1, 8)           │           136 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_31 (Dense)                │ (None, 1, 1)           │             9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 257 (1.00 KB)
 Trainable params: 257 (1.00 KB)
 Non-trainable params: 0 (0.00 B)
model5.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model5.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test,
    callbacks=keras.callbacks.EarlyStopping(monitor='val_loss', patience=3)
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/179fcfc71aeac38f5df73f114d64bbbf3a2592e4636b4a6d0970a0bf098dbc25.png
model5.evaluate(test)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 485us/step - loss: 31.1125 - mse: 31.1125
[30.25593376159668, 30.25593376159668]
pred = model5.predict(dataset)
pred.shape
126/126 ━━━━━━━━━━━━━━━━━━━━ 0s 915us/step
(503, 3, 1)
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred[:,2,0]);
../_images/d27546530afb5d8f9e5d1ddb037e1946e148724fbf96a52a575dafd18d3f62ec.png

Múltiples capas convolucionales con dilation

window = 2 #lags a mirar

n = x.size-window

input_data = x.values[:-window]

targets = x.values[window:]

dataset = keras.utils.timeseries_dataset_from_array(input_data, targets, sequence_length=window, batch_size=1)

train, test = keras.utils.split_dataset(dataset,0.8)
2024-06-12 20:40:41.812653: W tensorflow/core/framework/local_rendezvous.cc:404] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence
model5 = keras.Sequential([
    keras.Input(shape=(1,1)),
    keras.layers.Conv1D(filters=16, kernel_size=window, activation="relu", padding="causal"),
    keras.layers.Conv1D(filters=8, kernel_size=2, dilation_rate = 2*window, activation="relu", padding="causal"),
    keras.layers.Conv1D(filters=4, kernel_size=2, dilation_rate = 4*window, activation="relu", padding="causal"),
    tf.keras.layers.Dense(units=1)
])

model5.summary()
Model: "sequential_15"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ conv1d_1 (Conv1D)               │ (None, 1, 16)          │            48 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv1d_2 (Conv1D)               │ (None, 1, 8)           │           264 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv1d_3 (Conv1D)               │ (None, 1, 4)           │            68 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_32 (Dense)                │ (None, 1, 1)           │             5 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 385 (1.50 KB)
 Trainable params: 385 (1.50 KB)
 Non-trainable params: 0 (0.00 B)
model5.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model5.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test,
    callbacks=keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/d5b63866c792210be352bbd25f3e049938f989c54f7c9310212d159bde3b0bb3.png
model5.evaluate(test)
101/101 ━━━━━━━━━━━━━━━━━━━━ 0s 568us/step - loss: 32.8034 - mse: 32.8034
[34.05792236328125, 34.05792236328125]
pred = model5.predict(dataset)
pred.shape
505/505 ━━━━━━━━━━━━━━━━━━━━ 0s 671us/step
(505, 2, 1)
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred[:,window-1,0]);
../_images/ea61f28a9bcb17fcd0a6e3087dcce2d802663b8e63e26c268d2a40a50cccd640.png

Recurrent Neural Networks

Estas redes permiten “guardar estado” y en algún sentido son la generalización no lineal del Dynamic Linear model que ya vimos. Permiten en algún sentido agregar memoria.

El proceso en una capa RNN es:

RNN

Modelo 6: Simple RNN

La red recurrente simple tiene “memoria corta” y presenta problemas de ajuste (“vanishing and exploding gradients”) cuando uno hace el algoritmo de Backpropagation adaptado a las mismas.

window = 3 #lags a mirar

n = x.size-window

input_data = x.values[:-window]

targets = x.values[window:]

dataset = keras.utils.timeseries_dataset_from_array(input_data, targets, sequence_length=window, batch_size=1)

train, test = keras.utils.split_dataset(dataset,0.8)
2024-06-12 20:46:40.197622: W tensorflow/core/framework/local_rendezvous.cc:404] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence
model6 = keras.Sequential([
    keras.Input(shape=(window,1)),
    keras.layers.Reshape((1,window)), #necesario porque los valores anteriores de la serie son considerados "features" en la RNN.
    keras.layers.SimpleRNN(units=8, activation="relu"),
    tf.keras.layers.Dense(units=1)
])

model6.summary()
Model: "sequential_16"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ reshape_8 (Reshape)             │ (None, 1, 3)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ simple_rnn (SimpleRNN)          │ (None, 8)              │            96 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_33 (Dense)                │ (None, 1)              │             9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 105 (420.00 B)
 Trainable params: 105 (420.00 B)
 Non-trainable params: 0 (0.00 B)
model6.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model6.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test,
    callbacks=keras.callbacks.EarlyStopping(monitor='val_loss', patience=3)
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/7251e24ff8518c48bd1448e9949b2f6d0c1364c67e452d69b1ddfce90384bab2.png
model6.evaluate(test)
101/101 ━━━━━━━━━━━━━━━━━━━━ 0s 479us/step - loss: 27.3341 - mse: 27.3341
[27.543731689453125, 27.543731689453125]
pred = model6.predict(dataset)
pred.shape
503/503 ━━━━━━━━━━━━━━━━━━━━ 0s 649us/step
(503, 1)
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred);
../_images/523086a4e624634de6ae2bf5480e82d4594988597a9a032e121057bad02890f3.png

RNN con features

window = 3 #lags a mirar

n = x.size-window
week = [x.index[i].weekofyear for i in range(0,n)]

features = 2
input_data = np.stack([x.values[:-window],week], axis=1)

targets = x.values[window:]

dataset = keras.utils.timeseries_dataset_from_array(input_data, targets, sequence_length=window, batch_size=4)

train, test = keras.utils.split_dataset(dataset,0.8)
2024-06-12 20:50:29.275157: W tensorflow/core/framework/local_rendezvous.cc:404] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence
model7 = keras.Sequential([
    keras.Input(shape=(window,features)),
    keras.layers.Reshape((1,window*features)), #necesario porque los valores anteriores de la serie son considerados "features" en la RNN.
    keras.layers.SimpleRNN(units=8, activation="relu"),
    tf.keras.layers.Dense(units=1)
])

model7.summary()
Model: "sequential_18"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ reshape_10 (Reshape)            │ (None, 1, 6)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ simple_rnn_2 (SimpleRNN)        │ (None, 8)              │           120 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_35 (Dense)                │ (None, 1)              │             9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 129 (516.00 B)
 Trainable params: 129 (516.00 B)
 Non-trainable params: 0 (0.00 B)
model7.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model7.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test,
    callbacks=keras.callbacks.EarlyStopping(monitor='val_loss', patience=3)
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/b72d4f8dbe93cc01f72b52eb1314ac03c6ad63add1e4b2d4193baf12bad233be.png
model7.evaluate(test)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 646us/step - loss: 26.6596 - mse: 26.6596
[27.382709503173828, 27.382709503173828]
pred = model7.predict(dataset)
pred.shape
126/126 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
(503, 1)
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred);
../_images/fb9f788b0502f10970214f8e775fa82c1f496c4d4d1cf105d4cc59d91977b856.png

Modelo 8: LSTM

La red recurrente LSTM funciona igual que la red RNN en principio, pero tiene más “gates” y parámetros internos para permitir guardar más estado interno. Estas redes son muy usadas para series temporales.

model8 = keras.Sequential([
    keras.Input(shape=(window,features)),
    keras.layers.Reshape((1,window*features)), #necesario porque los valores anteriores de la serie son considerados "features" en la RNN.
    keras.layers.LSTM(units=8, activation="relu"),
    tf.keras.layers.Dense(units=1)
])

model8.summary()
Model: "sequential_22"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ reshape_14 (Reshape)            │ (None, 1, 6)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_3 (LSTM)                   │ (None, 8)              │           480 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_39 (Dense)                │ (None, 1)              │             9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 489 (1.91 KB)
 Trainable params: 489 (1.91 KB)
 Non-trainable params: 0 (0.00 B)
model8.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model8.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test,
    callbacks=keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/500b59da8deedea354242cb22509be548cfa93385a0bc17dfe23202f70aa35ac.png
model8.evaluate(test)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 595us/step - loss: 26.3951 - mse: 26.3951
[27.157976150512695, 27.157976150512695]
pred = model8.predict(dataset)
pred.shape
126/126 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
(503, 1)
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred);
../_images/f80f2a9027a965c6c530bcf4f39c69c8ab399d0e8b0099cb60260fd353b16cd8.png

Modelo 8: LSTM + capas

Agreguemos alguna capa densa más.

model9 = keras.Sequential([
    keras.Input(shape=(window,features)),
    keras.layers.Reshape((1,window*features)), #necesario porque los valores anteriores de la serie son considerados "features" en la RNN.
    keras.layers.Dense(units=8),
    keras.layers.LSTM(units=8, activation="relu"),
    keras.layers.Dense(units=8, activation="relu"),
    keras.layers.Dense(units=1)
])

model9.summary()
Model: "sequential_23"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ reshape_15 (Reshape)            │ (None, 1, 6)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_40 (Dense)                │ (None, 1, 8)           │            56 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_4 (LSTM)                   │ (None, 8)              │           544 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_41 (Dense)                │ (None, 8)              │            72 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_42 (Dense)                │ (None, 1)              │             9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 681 (2.66 KB)
 Trainable params: 681 (2.66 KB)
 Non-trainable params: 0 (0.00 B)
model9.compile(loss="mse",
              optimizer = "adam",
              metrics = ["mse"])
history = model9.fit(
    train,
    epochs=200,
    verbose=False,
    validation_data=test,
    callbacks=keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)
)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model mse')
plt.ylabel('mse')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper left')
plt.show()
../_images/0eaad14f777890d345770c7fb458168253cc9dc570f140a429a7b9486062925b.png
model9.evaluate(test)
26/26 ━━━━━━━━━━━━━━━━━━━━ 0s 904us/step - loss: 24.9205 - mse: 24.9205
[26.245861053466797, 26.245861053466797]
pred = model9.predict(dataset)
pred.shape
126/126 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
(503, 1)
plt.plot(x.values)
plt.plot(range(window,len(x)-window+1),pred);
../_images/0891eb2cd791db5c0fd999b139e6d75542dbacfda1f427af3b948035e14646ee.png

Conclusiones

  • El problema de predicción en series temporales puede transformarse en un problema de regresión sobre:

    • Los propios valores anteriores de la serie.

    • Otras funciones del tiempo.

    • Otras variables exógenas.

  • Las redes convolucionales son una versión no lineal de los modelos tipo ARMA vistos en clase. Se pueden superponer en capas y usar dilation para tener en cuenta dependencias largas

  • Las redes recurrentes son una versión no lineal de los modelos en espacio de estados vistos anteriormente.

  • Las redes LSTM son una versión de redes recurrentes que permite capturar dependencias largas.

Pero la conclusión más importante es que se necesitan muchos datos y ser muy cuidadoso en la validación para entrenar este tipo de modelos, y obtener resultados que mejoren lo visto anteriormente.