Repository: JoaquinAmatRodrigo/skforecast

Issue:
Error al ejecutar código de ejemplo de Deep Learning para pronóstico de series temporales en skforecast
Estimados desarrolladores de skforecast,

Me dirijo a ustedes con la esperanza de obtener su ayuda para resolver un error que he encontrado al intentar ejecutar el código de ejemplo de "Deep Learning para la predicción de series temporales: Redes Neuronales Recurrentes (RNN) y Long Short-Term Memory (LSTM)" disponible en la siguiente URL: https://cienciadedatos.net/documentos/py54-forecasting-con-deep-learning

He seguido cuidadosamente los pasos descritos en el ejemplo, utilizando la versión más reciente de la biblioteca skforecast (0.5.4) y TensorFlow (2.16.1). Sin embargo, al ejecutar el código en mi entorno de trabajo (Laptop con procesador AMD, Windows 11), obtengo el siguiente error:

```
  --------------------------------------------------------------------------- 
AttributeError     Traceback (most recent call last)
Cell In[11], line 3
1 # Creación del forecaster
2 # ==============================================================================
----> 3 forecaster = ForecasterRnn(
4 regressor=model,
5 levels=levels,
6 transformer_series=MinMaxScaler(),
7 fit_kwargs={
8 "epochs": 10, # Número de épocas para entrenar el modelo.
9 "batch_size": 32, # Tamaño del batch para entrenar el modelo.
10 "callbacks": [
11 EarlyStopping(monitor="val_loss", patience=5)
12 ], # Callback para detener el entrenamiento cuando ya no esté aprendiendo más.
13 "series_val": data_val, # Datos de validación para el entrenamiento del modelo.
14 },
15 )
16 forecaster
File c:\Users\luisc\AppData\Local\Programs\Python\Python311\Lib\site-packages\skforecast\ForecasterRnn\ForecasterRnn.py:226, in ForecasterRnn.__init__(self, regressor, levels, lags, steps, transformer_series, weight_func, fit_kwargs, forecaster_id, n_jobs, transformer_exog)
223 layer_init = self.regressor.layers[0]
225 if lags == "auto":
--> 226 self.lags = np.arange(layer_init.input_shape[0][1]) + 1
227 warnings.warn(
228 "Setting `lags` = 'auto'. `lags` are inferred from the regressor "
229 "architecture. Avoid the warning with lags=lags."
230 )
231 elif isinstance(lags, int):
AttributeError: 'InputLayer' object has no attribute 'input_shape' 
``` 

Mi versión actual de skforecast es la : 0.12.1, y la de tensorflow es la: 2.16.1, intenté bajar las versiones a las 0.12.0 y 2.15.1 respectivamente, y aún así recibo el mismo error.

Me gustaría saber si me pueden ayudar a solucionar el error y poder correr su código, de antemano muchas gracias.


Discussion:
Hola @Chuello 
Creo que el link no lo es el correcto.

@FernandoCarazoMelo 
> Hola @Chuello Creo que el link no lo es el correcto.
> 
> @FernandoCarazoMelo

Buenas tardes Fernando, si tienes razón, ya edité en link, gracias
Hola @Chuello ,

No me da ningún error al ejecutarlo con tensorflow 2.15. Qué versión de keras estás utilizando? Puedes imprimir el session info?
> Hola @Chuello ,
> 
> No me da ningún error al ejecutarlo con tensorflow 2.15. Qué versión de keras estás utilizando? Puedes imprimir el session info?

Buenas tardes Fernando, si por acá te dejo la salida de session_info:
-----
keras               3.3.3
matplotlib          3.9.0
numpy               1.26.4
pandas              2.2.2
plotly              5.22.0
session_info        1.0.0
skforecast          0.12.0
sklearn             1.4.2
tensorflow          2.16.1
-----
IPython             8.25.0
jupyter_client      8.6.2
jupyter_core        5.7.2
-----
Python 3.11.6 (tags/v3.11.6:8b6ee5b, Oct  2 2023, 14:57:12) [MSC v.1935 64 bit (AMD64)]
Windows-10-10.0.22631-SP0
-----
Session information updated at 2024-06-19 16:55
skforecast version: 0.12.0
tensorflow version: 2.16.1

Resolving patch:
diff --git a/skforecast/ForecasterRnn/ForecasterRnn.py b/skforecast/ForecasterRnn/ForecasterRnn.py
--- a/skforecast/ForecasterRnn/ForecasterRnn.py
+++ b/skforecast/ForecasterRnn/ForecasterRnn.py
@@ -14,6 +14,7 @@
 import matplotlib.pyplot as plt
 import numpy as np
 import pandas as pd
+import keras
 from sklearn.pipeline import Pipeline
 from sklearn.base import clone
 from sklearn.preprocessing import MinMaxScaler
@@ -41,14 +42,15 @@
 # TODO. Test Grid search
 class ForecasterRnn(ForecasterBase):
     """
-    This class turns any regressor compatible with the TensorFlow API into a
-    TensorFlow RNN multi-serie multi-step forecaster. A unique model is created
-    to forecast all time steps and series. See documentation for more details.
+    This class turns any regressor compatible with the Keras API into a
+    Keras RNN multi-serie multi-step forecaster. A unique model is created
+    to forecast all time steps and series. Keras enables workflows on top of 
+    either JAX, TensorFlow, or PyTorch. See documentation for more details.
 
     Parameters
     ----------
-    regressor : regressor or pipeline compatible with the TensorFlow API
-        An instance of a regressor or pipeline compatible with the TensorFlow API.
+    regressor : regressor or pipeline compatible with the Keras API
+        An instance of a regressor or pipeline compatible with the Keras API.
     levels : str, list
         Name of one or more time series to be predicted. This determine the series
         the forecaster will be handling. If `None`, all series used during training
@@ -83,8 +85,8 @@ class ForecasterRnn(ForecasterBase):
         
     Attributes
     ----------
-    regressor : regressor or pipeline compatible with the TensorFlow API
-        An instance of a regressor or pipeline compatible with the TensorFlow API.
+    regressor : regressor or pipeline compatible with the Keras API
+        An instance of a regressor or pipeline compatible with the Keras API.
         An instance of this regressor is trained for each step. All of them
         are stored in `self.regressors_`.
     levels : str, list
@@ -218,7 +220,11 @@ def __init__(
         layer_init = self.regressor.layers[0]
 
         if lags == "auto":
-            self.lags = np.arange(layer_init.input_shape[0][1]) + 1
+            if keras.__version__ < "3.0":
+                self.lags = np.arange(layer_init.input_shape[0][1]) + 1
+            else:
+                self.lags = np.arange(layer_init.output.shape[1]) + 1
+
             warnings.warn(
                 "Setting `lags` = 'auto'. `lags` are inferred from the regressor " 
                 "architecture. Avoid the warning with lags=lags."
@@ -239,7 +245,10 @@ def __init__(
         layer_end = self.regressor.layers[-1]
 
         try:
-            self.series = layer_end.output_shape[-1]
+            if keras.__version__ < "3.0":
+                self.series = layer_end.output_shape[-1]
+            else:
+                self.series = layer_end.output.shape[-1]
         # if does not work, break the and raise an error the input shape should
         # be shape=(lags, n_series))
         except:
@@ -248,7 +257,10 @@ def __init__(
             )
 
         if steps == "auto":
-            self.steps = np.arange(layer_end.output_shape[1]) + 1
+            if keras.__version__ < "3.0":
+                self.steps = np.arange(layer_end.output_shape[1]) + 1
+            else:
+                self.steps = np.arange(layer_end.output.shape[1]) + 1
             warnings.warn(
                 "`steps` default value = 'auto'. `steps` inferred from regressor "
                 "architecture. Avoid the warning with steps=steps."
@@ -263,7 +275,10 @@ def __init__(
             )
 
         self.max_step = np.max(self.steps)
-        self.outputs = layer_end.output_shape[-1]
+        if keras.__version__ < "3.0":
+            self.outputs = layer_end.output_shape[-1]
+        else:
+            self.outputs = layer_end.output.shape[-1]
 
         if not isinstance(levels, (list, str, type(None))):
             raise TypeError(
diff --git a/skforecast/ForecasterRnn/utils.py b/skforecast/ForecasterRnn/utils.py
--- a/skforecast/ForecasterRnn/utils.py
+++ b/skforecast/ForecasterRnn/utils.py
@@ -8,11 +8,11 @@
 from typing import Union, Any, Optional, Tuple, Callable
 import pandas as pd
 import re
+import os
 from ..utils import check_optional_dependency
 
 try:
-    import tensorflow as tf
-    from tensorflow import keras
+    import keras
     from keras.models import Model
     from keras.layers import Dense, Input, Reshape, LSTM, SimpleRNN
     from keras.optimizers import Adam
@@ -34,7 +34,7 @@ def create_and_compile_model(
     optimizer: object=Adam(learning_rate=0.01),
     loss: object=MeanSquaredError(),
     compile_kwars: dict={},
-) -> tf.keras.models.Model:
+) -> keras.models.Model:
     """
     Creates a neural network model for time series prediction with flexible recurrent layers.
 
@@ -64,14 +64,31 @@ def create_and_compile_model(
         Optimization algorithm and learning rate.
     loss : object, default `MeanSquaredError()`
         Loss function for model training.
+    compile_kwargs : dict, default `{}` 
+        Additional arguments for model compilation.
 
     Returns
     -------
-    model : tf.keras.models.Model
+    model : keras.models.Model
         Compiled neural network model.
     
     """
-
+    
+    if keras.__version__ > "3":
+        print(f"keras version: {keras.__version__}")
+        print(f"Using backend: {keras.backend.backend()}")
+        if keras.backend.backend() == "tensorflow":
+            import tensorflow
+            print(f"tensorflow version: {tensorflow.__version__}")
+        elif keras.backend.backend() == "torch":
+            import torch
+            print(f"torch version: {torch.__version__}")
+        elif keras.backend.backend() == "jax":
+            import jax
+            print(f"jax version: {jax.__version__}")
+        else:
+            print("Backend not recognized")
+            
     err_msg = f"`series` must be a pandas DataFrame. Got {type(series)}."
 
     if not isinstance(series, pd.DataFrame):
@@ -158,7 +175,7 @@ def create_and_compile_model(
     # Output layer
     x = Dense(levels * steps, activation="linear")(x)
     # model = Model(inputs=input_layer, outputs=x)
-    output_layer = tf.keras.layers.Reshape((steps, levels))(x)
+    output_layer = keras.layers.Reshape((steps, levels))(x)
     model = Model(inputs=input_layer, outputs=output_layer)
 
     # Compile the model if optimizer, loss or compile_kwargs are passed
diff --git a/skforecast/utils/utils.py b/skforecast/utils/utils.py
--- a/skforecast/utils/utils.py
+++ b/skforecast/utils/utils.py
@@ -34,7 +34,7 @@
     ],
     'deeplearning': [
         'matplotlib>=3.3, <3.9',
-        'tensorflow>=2.13, <2.16',
+        'keras>=2.6, <4.0',
     ],
     'plotting': [
         'matplotlib>=3.3, <3.9', 


Test patch:
diff --git a/skforecast/ForecasterRnn/tests/test_create_lags.py b/skforecast/ForecasterRnn/tests/test_create_lags.py
--- a/skforecast/ForecasterRnn/tests/test_create_lags.py
+++ b/skforecast/ForecasterRnn/tests/test_create_lags.py
@@ -6,15 +6,15 @@
 import pandas as pd
 from skforecast.ForecasterRnn import ForecasterRnn
 from skforecast.ForecasterRnn.utils import create_and_compile_model
-import tensorflow as tf
+import keras
 
 
 lags = 6
 steps = 3
 levels = "l1"
 activation = "relu"
-optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
-loss = tf.keras.losses.MeanSquaredError()
+optimizer = keras.optimizers.Adam(learning_rate=0.01)
+loss = keras.losses.MeanSquaredError()
 recurrent_units = 100
 dense_units = [128, 64]
 
diff --git a/skforecast/ForecasterRnn/tests/test_create_train_X_y.py b/skforecast/ForecasterRnn/tests/test_create_train_X_y.py
--- a/skforecast/ForecasterRnn/tests/test_create_train_X_y.py
+++ b/skforecast/ForecasterRnn/tests/test_create_train_X_y.py
@@ -6,7 +6,7 @@
 import pandas as pd
 from skforecast.ForecasterRnn import ForecasterRnn
 from skforecast.ForecasterRnn.utils import create_and_compile_model
-import tensorflow as tf
+import keras
 
 from sklearn.compose import ColumnTransformer
 from sklearn.preprocessing import StandardScaler
@@ -18,8 +18,8 @@
 steps = 1
 levels = "1"
 activation = "relu"
-optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
-loss = tf.keras.losses.MeanSquaredError()
+optimizer = keras.optimizers.Adam(learning_rate=0.01)
+loss = keras.losses.MeanSquaredError()
 recurrent_units = 100
 dense_units = [128, 64]
 
diff --git a/skforecast/ForecasterRnn/tests/test_fit.py b/skforecast/ForecasterRnn/tests/test_fit.py
--- a/skforecast/ForecasterRnn/tests/test_fit.py
+++ b/skforecast/ForecasterRnn/tests/test_fit.py
@@ -4,7 +4,7 @@
 import numpy as np
 import pytest
 from skforecast.ForecasterRnn import ForecasterRnn
-import tensorflow as tf
+import keras
 from skforecast.ForecasterRnn.utils import create_and_compile_model
 
 
@@ -13,8 +13,8 @@
 steps = 1
 levels = "1"
 activation = "relu"
-optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
-loss = tf.keras.losses.MeanSquaredError()
+optimizer = keras.optimizers.Adam(learning_rate=0.01)
+loss = keras.losses.MeanSquaredError()
 recurrent_units = 100
 dense_units = [128, 64]
 
diff --git a/skforecast/ForecasterRnn/tests/test_init.py b/skforecast/ForecasterRnn/tests/test_init.py
--- a/skforecast/ForecasterRnn/tests/test_init.py
+++ b/skforecast/ForecasterRnn/tests/test_init.py
@@ -2,8 +2,7 @@
 from skforecast.ForecasterRnn.utils import create_and_compile_model
 import pandas as pd
 import numpy as np
-import tensorflow as tf
-from tensorflow import keras
+import keras
 from keras.losses import MeanSquaredError
 from keras.optimizers import Adam
 import pytest
diff --git a/skforecast/ForecasterRnn/tests/test_plot_history.py b/skforecast/ForecasterRnn/tests/test_plot_history.py
--- a/skforecast/ForecasterRnn/tests/test_plot_history.py
+++ b/skforecast/ForecasterRnn/tests/test_plot_history.py
@@ -4,7 +4,7 @@
 import numpy as np
 import pytest
 from skforecast.ForecasterRnn import ForecasterRnn
-import tensorflow as tf
+import keras
 from skforecast.ForecasterRnn.utils import create_and_compile_model
 import matplotlib.pyplot as plt
 
@@ -19,8 +19,8 @@
 steps = 4
 levels = ["1", "2"]
 activation = "relu"
-optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
-loss = tf.keras.losses.MeanSquaredError()
+optimizer = keras.optimizers.Adam(learning_rate=0.01)
+loss = keras.losses.MeanSquaredError()
 recurrent_units = 100
 dense_units = [128, 64]
 
diff --git a/skforecast/ForecasterRnn/tests/test_predict.py b/skforecast/ForecasterRnn/tests/test_predict.py
--- a/skforecast/ForecasterRnn/tests/test_predict.py
+++ b/skforecast/ForecasterRnn/tests/test_predict.py
@@ -4,8 +4,8 @@
 import numpy as np
 import pytest
 from skforecast.ForecasterRnn import ForecasterRnn
-import tensorflow as tf
 from skforecast.ForecasterRnn.utils import create_and_compile_model
+import keras
 
 series = pd.DataFrame(
     {
@@ -18,8 +18,8 @@
 steps = 4
 levels = ["1", "2"]
 activation = "relu"
-optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
-loss = tf.keras.losses.MeanSquaredError()
+optimizer = keras.optimizers.Adam(learning_rate=0.01)
+loss = keras.losses.MeanSquaredError()
 recurrent_units = 100
 dense_units = [128, 64]
 
diff --git a/skforecast/ForecasterRnn/tests/test_utils.py b/skforecast/ForecasterRnn/tests/test_utils.py
--- a/skforecast/ForecasterRnn/tests/test_utils.py
+++ b/skforecast/ForecasterRnn/tests/test_utils.py
@@ -1,7 +1,7 @@
 import pytest
 import pandas as pd
 import numpy as np
-import tensorflow as tf
+import keras
 from skforecast.ForecasterRnn.utils import create_and_compile_model
 
 # test with several parameters for dense_units and recurrent_units
@@ -34,8 +34,8 @@ def test_units(dense_units, recurrent_layer, recurrent_units):
     steps = 5
     levels = 1
     activation = "relu"
-    optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
-    loss = tf.keras.losses.MeanSquaredError()
+    optimizer = keras.optimizers.Adam(learning_rate=0.01)
+    loss = keras.losses.MeanSquaredError()
 
     # Call the function to create and compile the model
     model = create_and_compile_model(
@@ -56,7 +56,7 @@ def test_units(dense_units, recurrent_layer, recurrent_units):
         recurrent_units = [recurrent_units]
 
     # Assert that the model is an instance of tf.keras.models.Model
-    assert isinstance(model, tf.keras.models.Model)
+    assert isinstance(model, keras.models.Model)
 
     # Assert that the model has the correct number of layers
     if dense_units is None:
@@ -79,7 +79,7 @@ def test_units(dense_units, recurrent_layer, recurrent_units):
 def test_correct_input_type():
     # Test if the function works with the correct input type
     model = create_and_compile_model(series_data, lags_data, steps_data, levels_data)
-    assert isinstance(model, tf.keras.models.Model)
+    assert isinstance(model, keras.models.Model)
 
 
 def test_incorrect_series_type():