{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "Qjsf78ymkS0E" }, "source": [ "#
Taller de Aprendizaje Automático
\n", "##
Taller 8: Demanda de bicicletas compartidas. Predicciones con *Recurrent Neural Networks*
" ] }, { "cell_type": "markdown", "metadata": { "id": "I4y3HbLOkOVD" }, "source": [ "# Introducción\n", "\n", "Las Redes Neuronales Recurrentes o *Recurrent Neural Networks* (RNN), fueron diseñadas especialmente para aprender a partir de datos secuenciales como: audio, texto, series temporales, entre muchos otros ejemplos. Para trabajar sobre algunos conceptos fundamentales de estas redes, se recurre nuevamente al conjunto de datos [*Bike Sharing Demand*](https://www.kaggle.com/c/bike-sharing-demand). Como ya se sabe estos datos cuentan con una marca de tiempo, lo cual les da un contexto temporal. Esta información es lo que va a permitir poder tratar a estos datos como secuenciales.\n", "A nivel del problema la idea es predecir la demanda de bicicletas en el futuro a partir de una secuencia fija de datos del pasado.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "rY6vz2Ekj8ig" }, "source": [ "## Objetivos\n", "\n", "\n", "* Manipular secuencias de datos.\n", "* Comparar diferentes enfoques de modelos de RNN para un problema concreto. " ] }, { "cell_type": "markdown", "metadata": { "id": "common-destiny" }, "source": [ "## Formas de trabajo" ] }, { "cell_type": "markdown", "metadata": { "id": "moral-gallery" }, "source": [ "### Opción 1: Trabajar localmente\n", "\n", "Descargar los datos en su máquina personal y trabajar en su propio ambiente de desarrollo. Asumiendo que ya creo un entorno para los talleres anteriores sólo debería installar la librería faltantes. \n", "\n", "*conda activate TAA-py38* \n", "*pip install xgboost seaborn* \n", "*jupyter-notebook* " ] }, { "cell_type": "markdown", "metadata": { "id": "fantastic-happiness" }, "source": [ "Los paquetes faltantes se pueden instalar desde el notebook haciendo: \n", "*!pip install paquete_faltante*" ] }, { "cell_type": "markdown", "metadata": { "id": "lined-sport" }, "source": [ "### Opción 2: Trabajar en *Colab*." ] }, { "cell_type": "markdown", "metadata": { "id": "lined-candle" }, "source": [ "\n", " \n", "
\n", " Ejecutar en Google Colab\n", "
" ] }, { "cell_type": "markdown", "metadata": { "id": "expensive-jewel" }, "source": [ "Se puede trabajar en Google Colab. Para ello es necesario contar con una cuenta de **google drive** y ejecutar un notebook almacenado en dicha cuenta. De lo contrario, no se conservarán los cambios realizados en la sesión. En caso de ya contar con una cuenta, se puede abrir el notebook y luego ir a *Archivo-->Guardar una copia en drive*." ] }, { "cell_type": "markdown", "metadata": { "id": "committed-quarterly" }, "source": [ "En caso de estar trabajando desde un notebook en Colab, deberá:" ] }, { "cell_type": "markdown", "metadata": { "id": "considered-dispatch" }, "source": [ "a) Installar el paquete *kaggle* para acceder a los datos" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "sunrise-major" }, "outputs": [], "source": [ "!pip install kaggle" ] }, { "cell_type": "markdown", "metadata": { "id": "ahead-austria" }, "source": [ "b) realizar la configuración necesaria para obtener datos desde la plataforma Kaggle. Para ello deberá ir a la página de la competencia y en la sección *data* aceptar los términos. Luego ejecutar la siguiente celda y pasarle el *token* de su usuario (ver comentario en celda)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "rGyCgWE6Y2hh" }, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')\n", "from google.colab import files\n", "\n", "# El siguiente archivo solicitado es para habilitar la API de Kaggle en el entorno que está trabajando.\n", "# Este archivo se descarga entrando a su perfíl de Kaggle, en la sección API, presionando donde dice: Create New API Token\n", "\n", "uploaded = files.upload()\n", "\n", "for fn in uploaded.keys():\n", " print('User uploaded file \"{name}\" with length {length} bytes'.format(\n", " name=fn, length=len(uploaded[fn])))\n", "\n", "#Then move kaggle.json into the folder where the API expects to find it.\n", "!mkdir -p ~/.kaggle/ && mv kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json" ] }, { "cell_type": "markdown", "metadata": { "id": "stretch-sunglasses" }, "source": [ "Una vez guardado el *token* se pueden descargar los datos." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "SKYP0IiCZA9G" }, "outputs": [], "source": [ "# Descarga de datos\n", "!kaggle competitions download -c bike-sharing-demand\n", "!unzip bike-sharing-demand.zip\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "# opcional para visualización\n", "#import seaborn as sns\n", "#sns.set_theme(style=\"whitegrid\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "D4d-WW6pYYma" }, "outputs": [], "source": [ "df_test = pd.read_csv('test.csv')\n", "df_train_raw = pd.read_csv('train.csv')\n", "df_submission = pd.read_csv('sampleSubmission.csv')" ] }, { "cell_type": "markdown", "metadata": { "id": "iqpNNz7DILE5" }, "source": [ "# Preprocesamiento de datos\n", "\n", "## Parte 1: Separación en conjuntos de Entrenamiento y Validación, y Rellenado de Datos Faltantes\n", "\n", "* Del conjunto de entramiento separe un conjunto de validación. Se sugiere tomar los primeros 15 días del mes para entrenar y del día 16 al 19 para validar. (*Sugerencia:* Ajuste el código de ejemplo presentado a continuación para indexar según el día)\n", "\n", "Este conjunto cuenta con datos faltantes lo cual es un problema que no fue tenido en cuenta en los talleres anteriores, pero sí puede ser relevante para trabajar con secuencias. La falta de datos se debe a la inexistencia de filas, tanto en el conjunto de *train* como en el conjunto de *test* (sin tener en cuenta cómo fueron divididos estos conjuntos).\n", "\n", "* Completar la función presentada más abajo para rellenar los datos faltantes teniendo en cuenta que se tienen datos numéricos y categóricos. (*Sugerencias:* [*pd.DataFrame.interpolate()*](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html) y [*pd.DataFrame.fillna()*](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html))\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "oDRwQpE8HyQm" }, "outputs": [], "source": [ "## Ejemplo de indexado de datos de 2012 usando atributos de datetime\n", "años = pd.to_datetime(df_train_raw.datetime).dt.year\n", "df_train_2012 = df_train_raw[años == 2012].reset_index(drop=True)\n", "print(df_train_2012.shape)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "qIOwNDB-HlVX" }, "outputs": [], "source": [ "from datetime import datetime\n", "import calendar\n", "\n", "def FilledIn(df):\n", " df_aux = df.copy()\n", " df_out = pd.DataFrame(columns=df_aux.columns)\n", " df_aux['datetime'] = pd.to_datetime(df_aux['datetime'])\n", " df_aux = df_aux.set_index('datetime')\n", "\n", " for year in [2011, 2012]:\n", " for month in range(12):\n", " start_date = datetime(year, month+1, 1, 0, 0, 0)\n", " last_day_of_month = calendar.monthrange(year, month+1)[1]\n", " end_date = datetime(year, month+1, last_day_of_month, 23, 0, 0)\n", " # Se agregan las marcas de tiempo que faltan\n", " df_month = df_aux[start_date:end_date]\n", " df_month = df_month.resample('H').asfreq()\n", " # Rellenar los datos faltantes===========\n", "\n", " #Su código\n", "\n", " #========================================\n", " df_month = df_month.reset_index()\n", "\n", " df_out = pd.concat([df_out,df_month])\n", " df_out = df_out.reset_index(drop=True)\n", " return df_out" ] }, { "cell_type": "markdown", "metadata": { "id": "QUoyP5ubPrBi" }, "source": [ "## Parte 2: Ingeniería de características y estandarización de los datos\n", "\n", "\n", "* Se aplica la ingeniería de características utilizada en el Taller 3.\n", "* Se estandarizan los datos. Se recomienda utilizar [OneHotEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html) para los datos categóricos.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "3JALVu7fpeJ4" }, "outputs": [], "source": [ "from sklearn.base import BaseEstimator, TransformerMixin\n", "from sklearn.pipeline import Pipeline\n", "from sklearn.preprocessing import StandardScaler, OneHotEncoder\n", "from sklearn.compose import ColumnTransformer\n", "\n", "class TimeFeatures(BaseEstimator, TransformerMixin):\n", " def __init__(self):\n", " self\n", " def fit(self, X, y=None):\n", " # X debe ser un DataFrame\n", " return self\n", " def transform(self, X):\n", " X_aux = X.copy()\n", " X_aux['datetime'] = pd.to_datetime(X_aux['datetime'])\n", " X_aux['month'] = X_aux['datetime'].dt.month\n", " X_aux['weekday'] = X_aux['datetime'].dt.weekday\n", " X_aux['hour'] = X_aux['datetime'].dt.hour\n", " X_aux = X_aux.drop('datetime', axis=1)\n", " return X_aux\n", "\n", "cat_features = ['season', 'weather', 'month', 'weekday', 'hour']\n", "num_features = ['temp', 'atemp', 'humidity', 'windspeed']\n", "# [holiday, workingday] ya son onehot\n", "scaler = ColumnTransformer([('cat', OneHotEncoder(), cat_features),\n", " ('num', StandardScaler(), num_features),\n", " ], remainder='passthrough')\n", "\n", "preprocess_pipe = Pipeline([('timefeatures', TimeFeatures()),\n", " ('scaler', scaler)])\n", "\n", "## df_full_train y df_full_val son los datos de entrenamiento y validación con los datos faltantes ya completados\n", "df_x_train = df_full_train.copy()\n", "print(df_x_train.shape)\n", "df_x_val = df_full_val.copy()\n", "print(df_x_val.shape)\n", "X_train = preprocess_pipe.fit_transform(df_x_train.drop('count', axis=1)).toarray()\n", "print(X_train.shape)\n", "X_val = preprocess_pipe.transform(df_x_val.drop('count', axis=1)).toarray()\n", "print(X_val.shape)" ] }, { "cell_type": "markdown", "metadata": { "id": "weeArlhBHeXZ" }, "source": [ "## Parte 3: Secuencias\n", "Para trabajar con modelos RNN en este tipo de problemas es necesario crear un nuevo *dataset* que incluya las secuencias de entrada al modelo y los valores de *target* para comparar las predicciones. Para esto se sugiere utilizar la función [keras.preprocessing.timeseries_dataset_from_array()](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/timeseries_dataset_from_array).\n", "\n", "* Crear dicho conjunto para entrenar un modelo que permita predecir la demanda de la próxima hora, dado que se conocen los datos de las últimas 24 horas. Para evitar introducir ruido en el modelo debido a los saltos entre meses por la extracción de los datos de *test*, se sugiere crear un *tf.data.Dataset* por cada mes e ir concatenandolos a medida que se los va procesando. Puede ser útil reusar el *loop* de la función *FilledIn*." ] }, { "cell_type": "markdown", "metadata": { "id": "5GBTmVXnAxRE" }, "source": [ "# Modelos\n", "Para las siguientes partes se recomienda utilizar *Comet* para guardar los experimentos. Esta es una buena forma de comparar los diferentes modelos.\n", "\n", "## Parte 4: *Naive forecasting*\n", "De manera de tener una referencia de desempeño, en este tipo de problemas se utiliza como medida de base el desempeño de algún predictor muy simple como puede ser *Naive forecasting*. El cual simplemente predice un valor como el valor del dato anterior.\n", "\n", "* Calcular los valores de RMSLE y MAE para el predictor *naive forecasting* simplemente manipulando los indices del vector de *target* preprocesado (sobre los conjuntos de entrenamiento y validación).\n" ] }, { "cell_type": "markdown", "metadata": { "id": "747QqVc11AaQ" }, "source": [ "## Parte 5: *Seq-to-Vector*\n", "En esta parte se evaluará el desempeño de un modelo simple del tipo secuencia a vector (*seq-to-vector*) sobre el conjunto de la Parte 3. Este tipo de modelos son aquellos que reciben una secuencia a la entrada y devuelve un vector a la salida. Para esta parte se pretende que el mismo cuente con una sola capa recurrente de 64 unidades y una capa densa a la salida.\n", "\n", "* Utilizar el MSLE como función de costo. Agregar RMSLE y MAE como métricas.\n", "* Entrenar el modelo durante 50 épocas con un optimizador *Adam* (lr=1e-3) y manteniendo el resto de los hiperparámetros por defecto. ¿Cómo es el desempeño con respecto al modelo NF?\n", "* Justifique la cantidad de parámetros entrenables en base a las matrices de pesos y los vectores de bias de cada capa.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "QI6w33SM-HLU" }, "source": [ "## Parte 6: *Seq-to-Seq*\n", "Otro tipo de modelo para atacar el problema anterior son aquellos denominados secuencia a secuencia (*seq-to-seq*). Estos modelos reciben una secuencia a la entrada y devuelven una secuencia a la salida. Para este problema se puede utilizar este modelo para predecir el siguiente valor en cada celda de la red recurrente y no sólo en la última. De esta forma se puede mejorar el desempeño notablemente.\n", "Antes de pasar al entrenamiento se debe modificar el *dataset* de manera que el *target* sea una secuencia de valores.\n", "* Adaptar el ejemplo 3 de la documentación de la función [keras.preprocessing.timeseries_dataset_from_array()](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/timeseries_dataset_from_array) para crear el nuevo *dataset*. Puede ser útil la siguiente función: *tf.data.Dataset.zip((features_dataset, labels_dataset))*.\n", "\n", "* Modificar el modelo anterior de manera que la salida de la red sea una secuencia, pero manteniendo el valor de los hiperparámetros.\n", "\n", "* Entrenar el nuevo modelo y comparar con los anteriores. ¿Por qué este modelo logra un desempeño mejor? ¿Por qué se mantiene la cantidad de parámetros entrenables?\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "XQ4iiqNs0qRt" }, "source": [ "## Parte 7: LSTM\n", "En el modelo *seq-to-seq* sustituir la capa *SimpleRNN* por una capa LSTM\n", "\n", "\n", "* Entrenar la red y comparar el desempeño con los modelos anteriores.\n", "* Justificar la cantidad de parámetros entrenables (ver la ecuación 15-3 del libro).\n", "* Cambiar el largo de las secuencias. ¿Cómo varía el desempeño para este modelo?\n", "* ¿Es posible reducir la distancia entre las curvas *train* y *validation* con los hiperparámetros *dropout* y *recurrent_dropout*?, ¿A qué parámetros afecta cada uno?.\n", "* (Opcional) Probar otros métodos de regularización como penalización de los pesos. Probar cambiar otros hiperparámetros de la red.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "NjObGyJWHyQn" }, "source": [] } ], "metadata": { "colab": { "collapsed_sections": [ "Qjsf78ymkS0E", "rY6vz2Ekj8ig", "moral-gallery" ], "name": "taller8_demanda_de_bicicletas_rnn2022.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.9" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 0 }