{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib as mpl\nimport seaborn as sn\nimport tensorflow as tf\nimport tensorflow_addons as tfa\n\nfrom tqdm.notebook import tqdm\nfrom sklearn.model_selection import train_test_split, GroupShuffleSplit\nfrom leven import levenshtein\n\nimport glob\nimport sys\nimport os\nimport math\nimport gc\nimport sys\nimport sklearn\nimport time\nimport json\n\n# TQDM Progress Bar With Pandas Apply Function\ntqdm.pandas()\n\nprint(f'Tensorflow Version {tf.__version__}')\nprint(f'Python Version: {sys.version}')","metadata":{"papermill":{"duration":9.205219,"end_time":"2023-07-02T11:33:08.731341","exception":false,"start_time":"2023-07-02T11:32:59.526122","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:46.857006Z","iopub.execute_input":"2023-07-31T07:31:46.857645Z","iopub.status.idle":"2023-07-31T07:31:56.122976Z","shell.execute_reply.started":"2023-07-31T07:31:46.85761Z","shell.execute_reply":"2023-07-31T07:31:56.121859Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Character 2 Ordinal Encoding","metadata":{"papermill":{"duration":0.022966,"end_time":"2023-07-02T11:33:08.779005","exception":false,"start_time":"2023-07-02T11:33:08.756039","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Read Character to Ordinal Encoding Mapping\nwith open('/kaggle/input/asl-fingerspelling/character_to_prediction_index.json') as json_file:\n    CHAR2ORD = json.load(json_file)\n    \n# Ordinal to Character Mapping\nORD2CHAR = {j:i for i,j in CHAR2ORD.items()}\n\n# convert dictionary to pandas dataframe\nCHAR2ORD_DF = pd.DataFrame(CHAR2ORD.values(),index=CHAR2ORD.keys(),columns=['Ordinal Encoding'])\nCHAR2ORD_DF.head()","metadata":{"papermill":{"duration":0.057156,"end_time":"2023-07-02T11:33:08.859134","exception":false,"start_time":"2023-07-02T11:33:08.801978","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:56.1252Z","iopub.execute_input":"2023-07-31T07:31:56.126279Z","iopub.status.idle":"2023-07-31T07:31:56.150549Z","shell.execute_reply.started":"2023-07-31T07:31:56.126239Z","shell.execute_reply":"2023-07-31T07:31:56.149498Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Hyperparameters\n\nDefinimos los hiperparámetros que vamos a usar posteriormente\n\n- `IS_INTERACTIVE`: Esta variable indica si el cuaderno se está ejecutando en modo interactivo, lo que significa que se está ejecutando en un entorno donde se pueden realizar cambios y experimentar de manera interactiva. En este modo, puedes ejecutar celdas de código individualmente, modificar el código y ver los resultados inmediatamente. Esto es útil cuando estás desarrollando y depurando tu código, ya que te permite realizar cambios rápidos y observar los efectos de esos cambios en tiempo real. **Si lo que se quiere es ejecutar el código completo (5 horas) dejar** `IS_INTERACTIVE=False`. **En caso contrario, comentar esa misma línea de código**\n\n- `SEED`: Esta es una semilla aleatoria global utilizada para reproducir los resultados. Al fijar una semilla, se garantiza que los resultados sean consistentes en diferentes ejecuciones.\n\n- `N_TARGET_FRAMES`: Esta variable indica el número de fotogramas a los que se redimensionarán las grabaciones. Esto se hace para **estandarizar la longitud de las secuencias de entrada**.\n\n- `DEBUG`: Esta variable indica si se está ejecutando en modo de depuración. Si es `True`, se ejecutará en modo de depuración, lo que puede implicar la ejecución de un subconjunto de los datos para una ejecución más rápida y para facilitar la depuración.\n\n- `N_UNIQUE_CHARACTERS0`: Esta variable representa el número de **caracteres únicos que se deben predecir en el modelo**. No incluye el token de padding (relleno), el token de inicio de oración y el token de fin de oración.\n\n- `N_UNIQUE_CHARACTERS`: Esta variable es similar a `N_UNIQUE_CHARACTERS0`, pero incluye el token de relleno, el token de inicio de oración y el token de fin de oración. Por lo tanto, representa el número total de clases que el modelo debe predecir.\n\n- `PAD_TOKEN`, `START_TOKEN` y `END_TOKEN`: Estas variables representan los índices de los tokens de padding, inicio de oración y fin de oración en el vocabulario. Se utilizan durante el procesamiento de los datos y la generación de las secuencias de texto objetivo.\n\n- `USE_VAL`: Esta variable controla si se utiliza el 10% de los datos para la validación durante el entrenamiento.\n\n- `BATCH_SIZE`: Esta variable determina el tamaño del lote para el entrenamiento.\n\n- `N_EPOCHS`: Esta variable establece el número de épocas para entrenar el modelo. Si `IS_INTERACTIVE` es True, se establece en 2 para un entrenamiento rápido en modo interactivo; de lo contrario, se establece en 100 para un entrenamiento normal.\n\n- `N_WARMUP_EPOCHS`: Esta variable define el número de épocas de calentamiento en el programador de la tasa de aprendizaje.\n\n- `LR_MAX`: Esta variable establece la tasa de aprendizaje máxima para el proceso de entrenamiento.\n\n- `WD_RATIO`: Esta variable representa el ratio de decaimiento de peso en función de la tasa de aprendizaje.\n\n- `MAX_PHRASE_LENGTH`: Esta variable define la longitud máxima de una frase, incluyendo el token de fin de frase.\n\n- `TRAIN_MODEL`: Esta variable controla si se entrena el modelo.\n\n- `LOAD_WEIGHTS`: Esta variable controla si se cargan pesos preentrenados para el modelo.\n\n- `WARMUP_METHOD`: Esta variable especifica el método de calentamiento para el programador de la tasa de aprendizaje, ya sea 'log' o 'exp'.  Si es 'log', la tasa de aprendizaje se aumenta gradualmente de manera logarítmica durante las primeras épocas de calentamiento. Esto significa que al principio se incrementa lentamente y luego se acelera a medida que avanzan las épocas de calentamiento. Es útil cuando se desea un calentamiento más suave y gradual. Si es 'exp', la tasa de aprendizaje se aumenta exponencialmente durante las primeras épocas de calentamiento. Esto implica un aumento más rápido al principio y una tasa de aprendizaje más alta en las primeras épocas. Es útil cuando se quiere un calentamiento más rápido y agresivo.","metadata":{"papermill":{"duration":0.022903,"end_time":"2023-07-02T11:33:08.905507","exception":false,"start_time":"2023-07-02T11:33:08.882604","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"Tal y como lo tenemos ahora, el notebook se ejecuta en modo interactivo, con verbosidad 1, sin subset de entrenamiento ni conjunto de validación (usaremos todo para entrenar). Utilizaremos una configuración que permita una rápida ejecución, con un número de épocas de 2.","metadata":{}},{"cell_type":"code","source":"# If Notebook Is Run By Committing or In Interactive Mode For Development\nIS_INTERACTIVE = os.environ['KAGGLE_KERNEL_RUN_TYPE'] == 'Interactive'\n#IS_INTERACTIVE = False # to run full code\n# Verbose Setting during training\nVERBOSE = 1 if IS_INTERACTIVE else 2\n# Global Random Seed\nSEED = 42\n# Number of Frames to resize recording to\nN_TARGET_FRAMES = 128\n# Global debug flag, takes subset of train\nDEBUG = False\n# Number of Unique Characters To Predict + Pad Token + SOS Token + EOS Token\nN_UNIQUE_CHARACTERS0 = len(CHAR2ORD)\nN_UNIQUE_CHARACTERS = len(CHAR2ORD) + 1 + 1 + 1\nPAD_TOKEN = len(CHAR2ORD) # Padding # 59\nSTART_TOKEN = len(CHAR2ORD) + 1 # Start Of Sentence # 60\nEND_TOKEN = len(CHAR2ORD) + 2 # End Of Sentence # 61\n# Whether to use 10% of data for validation\nUSE_VAL = False\n# Batch Size\nBATCH_SIZE = 64\n# Number of Epochs to Train for\nN_EPOCHS = 2 if IS_INTERACTIVE else 100\n# Number of Warmup Epochs in Learning Rate Scheduler\nN_WARMUP_EPOCHS = 10\n# Maximum Learning Rate\nLR_MAX = 1e-3\n# Weight Decay Ratio as Ratio of Learning Rate\nWD_RATIO = 0.05\n# Length of Phrase + EOS Token\nMAX_PHRASE_LENGTH = 31 + 1\n# Whether to Train The model\nTRAIN_MODEL = True\n# Whether to Load Pretrained Weights\nLOAD_WEIGHTS = False\n# Learning Rate Warmup Method [log,exp]\nWARMUP_METHOD = 'exp'","metadata":{"papermill":{"duration":0.034637,"end_time":"2023-07-02T11:33:08.963209","exception":false,"start_time":"2023-07-02T11:33:08.928572","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:56.152249Z","iopub.execute_input":"2023-07-31T07:31:56.152628Z","iopub.status.idle":"2023-07-31T07:31:56.160593Z","shell.execute_reply.started":"2023-07-31T07:31:56.152595Z","shell.execute_reply":"2023-07-31T07:31:56.159339Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Read an Prepare the Data\n\n# Read Train\n\nComo `DEBUG=False`, cargamos el conjunto de entrenamiento entero. Le añadimos una columna `file_path`, con la dirección o ruta completa de los archivos tipo parquet.","metadata":{"papermill":{"duration":0.023106,"end_time":"2023-07-02T11:33:09.114637","exception":false,"start_time":"2023-07-02T11:33:09.091531","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Read Train DataFrame\nif DEBUG:\n    train = pd.read_csv('/kaggle/input/asl-fingerspelling/train.csv').head(5000)\nelse:\n    train = pd.read_csv('/kaggle/input/asl-fingerspelling/train.csv')\n\n# this will be used to construct TFLite model\ntrain_sequence_id = train.set_index('sequence_id')\n\n# Get complete file path to file\ndef get_file_path(path):\n    return f'/kaggle/input/asl-fingerspelling/{path}'\n\ntrain['file_path'] = train['path'].apply(get_file_path)\n\ntrain.head()","metadata":{"papermill":{"duration":0.236261,"end_time":"2023-07-02T11:33:09.374134","exception":false,"start_time":"2023-07-02T11:33:09.137873","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:56.163442Z","iopub.execute_input":"2023-07-31T07:31:56.164154Z","iopub.status.idle":"2023-07-31T07:31:56.354165Z","shell.execute_reply.started":"2023-07-31T07:31:56.164114Z","shell.execute_reply":"2023-07-31T07:31:56.353191Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Example File Paths\n\nCargamos ahora los 10 elementos parquet que habíamos guardado en `train_landmark_subsets`","metadata":{"papermill":{"duration":0.023756,"end_time":"2023-07-02T11:33:09.552626","exception":false,"start_time":"2023-07-02T11:33:09.52887","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Unique Parquet Files\nINFERENCE_FILE_PATHS = pd.Series(\n        glob.glob('/kaggle/input//aslfr-eda-preprocessing-dataset-for-beginners/train_landmark_subsets/*')\n    )\n\nprint(f'Found {len(INFERENCE_FILE_PATHS)} Inference Pickle Files')","metadata":{"papermill":{"duration":0.038118,"end_time":"2023-07-02T11:33:09.614891","exception":false,"start_time":"2023-07-02T11:33:09.576773","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:56.355727Z","iopub.execute_input":"2023-07-31T07:31:56.356057Z","iopub.status.idle":"2023-07-31T07:31:56.367511Z","shell.execute_reply.started":"2023-07-31T07:31:56.356024Z","shell.execute_reply":"2023-07-31T07:31:56.366449Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Example Parquet\n\nRecordemos la forma que tiene un archivo parquet de los que hemos guardado previo a preprocesarlo:","metadata":{}},{"cell_type":"code","source":"# Read First Parquet File\n# example_parquet_df = pd.read_parquet(train['file_path'][0])\nexample_parquet_df = pd.read_parquet(INFERENCE_FILE_PATHS[0])\n\n# Each parquet file contains 1000 recordings\nprint(f'Number of Unique Recording: {example_parquet_df.index.nunique()}')\n# Display DataFrame layout\ndisplay(example_parquet_df.head())","metadata":{"papermill":{"duration":1.512276,"end_time":"2023-07-02T11:33:49.075196","exception":false,"start_time":"2023-07-02T11:33:47.56292","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:56.369126Z","iopub.execute_input":"2023-07-31T07:31:56.369486Z","iopub.status.idle":"2023-07-31T07:31:58.062456Z","shell.execute_reply.started":"2023-07-31T07:31:56.369451Z","shell.execute_reply":"2023-07-31T07:31:58.061355Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Load X/y\n\nDe igual forma, cargamos los ficheros de entrenamiento y validación. En nuestro caso, `USE_VAL=False`, por lo que cargamos `X` e `y` completas. La forma original de los ficheros `y` es de `[61955,128]`, debido al relleno que hemos hecho. Como hemos elegido como límite para la frase `31+1` espacios, siendo el último el token de fin de sentencia, restrignimos el tamaño de 128 a 32.","metadata":{"papermill":{"duration":0.023663,"end_time":"2023-07-02T11:33:09.662914","exception":false,"start_time":"2023-07-02T11:33:09.639251","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Train/Validation\nif USE_VAL:\n    # TRAIN\n    X_train = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/X_train.npy')\n    y_train = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/y_train.npy')[:,:MAX_PHRASE_LENGTH]\n    N_TRAIN_SAMPLES = len(X_train)\n    # VAL\n    X_val = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/X_val.npy')\n    y_val = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/y_val.npy')[:,:MAX_PHRASE_LENGTH]\n    N_VAL_SAMPLES = len(X_val)\n    # Shapes\n    print(f'X_train shape: {X_train.shape}, X_val shape: {X_val.shape}')\n    print(f'y_train shape: {X_train.shape}, y_val shape: {X_val.shape}')\n    \n# Train On All Data\nelse:\n    # TRAIN\n    X_train = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/X.npy')\n    y_train = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/y.npy')[:,:MAX_PHRASE_LENGTH]\n    N_TRAIN_SAMPLES = len(X_train)\n    print(f'X_train shape: {X_train.shape}')\n    print(f'y_train shape: {y_train.shape}')","metadata":{"papermill":{"duration":37.639487,"end_time":"2023-07-02T11:33:47.326164","exception":false,"start_time":"2023-07-02T11:33:09.686677","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:31:58.066048Z","iopub.execute_input":"2023-07-31T07:31:58.066353Z","iopub.status.idle":"2023-07-31T07:32:37.775019Z","shell.execute_reply.started":"2023-07-31T07:31:58.066327Z","shell.execute_reply":"2023-07-31T07:32:37.774023Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Example Batch\n\nAhora, creamos ejemplos de lotes (batches) con el fin de depurar (debugging) o probar el funcionamiento del código. Creamos dos conjuntos de ejemplos: uno con un tamaño de muestra más grande (`N_EXAMPLE_BATCH_SAMPLES`) y otro con un tamaño de muestra más pequeño (`N_EXAMPLE_BATCH_SAMPLES_SMALL`).\n\nEn cada conjunto de ejemplos, creamos un diccionario `X_batch` que contiene las características de entrada y un array `y_batch` que contiene las etiquetas de salida.\n\nEn el diccionario `X_batch`, copiamos las primeras `N_EXAMPLE_BATCH_SAMPLES` muestras entrenamiento `X_train`. El valor `frames` representa los datos de los cuadros (frames, en este caso filas, siendo casa fila un conjunto de `[1,128,164]`) y phrase representa las frases asociadas a los frames.\n\nEn el array `y_batch`, copiamos las primeras `N_EXAMPLE_BATCH_SAMPLES` etiquetas de entrenamiento `y_train`, que corresponden a las frases asociadas a los frames.\n\nRealizamos el mismo procedimiento para el conjunto más pequeño, utilizando las variables `X_batch_small` e `y_batch_small`.","metadata":{"papermill":{"duration":0.02373,"end_time":"2023-07-02T11:33:47.374154","exception":false,"start_time":"2023-07-02T11:33:47.350424","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Example Batch For Debugging\nN_EXAMPLE_BATCH_SAMPLES = 1024\nN_EXAMPLE_BATCH_SAMPLES_SMALL = 32\n# Example Batch\nX_batch = {\n    'frames': np.copy(X_train[:N_EXAMPLE_BATCH_SAMPLES]),\n    'phrase': np.copy(y_train[:N_EXAMPLE_BATCH_SAMPLES]),\n#     'phrase_type': np.copy(y_phrase_type_train[:N_EXAMPLE_BATCH_SAMPLES]),\n}\ny_batch = np.copy(y_train[:N_EXAMPLE_BATCH_SAMPLES])\n# Small Example Batch\nX_batch_small = {\n    'frames': np.copy(X_train[:N_EXAMPLE_BATCH_SAMPLES_SMALL]),\n    'phrase': np.copy(y_train[:N_EXAMPLE_BATCH_SAMPLES_SMALL]),\n#     'phrase_type': np.copy(y_phrase_type_train[:N_EXAMPLE_BATCH_SAMPLES_SMALL]),\n}\ny_batch_small = np.copy(y_train[:N_EXAMPLE_BATCH_SAMPLES_SMALL])","metadata":{"papermill":{"duration":0.093503,"end_time":"2023-07-02T11:33:47.491607","exception":false,"start_time":"2023-07-02T11:33:47.398104","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:37.776392Z","iopub.execute_input":"2023-07-31T07:32:37.777131Z","iopub.status.idle":"2023-07-31T07:32:37.923006Z","shell.execute_reply.started":"2023-07-31T07:32:37.777094Z","shell.execute_reply":"2023-07-31T07:32:37.922033Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(\"Primer elemento de frames:\", X_batch_small['frames'][0])\nprint(f'Shape de cada elemento frames: {X_batch_small[\"frames\"][0].shape} \\n\\n')\nprint(\"Primer elemento de phrase:\", X_batch_small['phrase'][0])\nprint(f'Shape de cada elemento phrase: {X_batch_small[\"phrase\"][0].shape}')","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:37.924573Z","iopub.execute_input":"2023-07-31T07:32:37.924951Z","iopub.status.idle":"2023-07-31T07:32:37.93641Z","shell.execute_reply.started":"2023-07-31T07:32:37.924917Z","shell.execute_reply":"2023-07-31T07:32:37.935157Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Landmark Indices\n\nDe igual forma que en el notebook de preprocesamiento, definimos la función `get_idxs` para obtener los índices de las manos izquierda derecha y los labios.","metadata":{"papermill":{"duration":0.039462,"end_time":"2023-07-02T11:33:49.150383","exception":false,"start_time":"2023-07-02T11:33:49.110921","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Get indices in original dataframe\ndef get_idxs(df, words_pos, words_neg=[], ret_names=True, idxs_pos=None):\n    idxs = []\n    names = []\n    for w in words_pos:\n        for col_idx, col in enumerate(example_parquet_df.columns):\n            # Exclude Non Landmark Columns\n            if col in ['frame']:\n                continue\n                \n            col_idx = int(col.split('_')[-1])\n            # Check if column name contains all words\n            if (w in col) and (idxs_pos is None or col_idx in idxs_pos) and all([w not in col for w in words_neg]):\n                idxs.append(col_idx)\n                names.append(col)\n    # Convert to Numpy arrays\n    idxs = np.array(idxs)\n    names = np.array(names)\n    # Returns either both column indices and names\n    if ret_names:\n        return idxs, names\n    # Or only columns indices\n    else:\n        return idxs","metadata":{"papermill":{"duration":0.051023,"end_time":"2023-07-02T11:33:49.235748","exception":false,"start_time":"2023-07-02T11:33:49.184725","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:37.942383Z","iopub.execute_input":"2023-07-31T07:32:37.942648Z","iopub.status.idle":"2023-07-31T07:32:37.951436Z","shell.execute_reply.started":"2023-07-31T07:32:37.942625Z","shell.execute_reply":"2023-07-31T07:32:37.950345Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Lips Landmark Face Ids\nLIPS_LANDMARK_IDXS = np.array([\n        61, 185, 40, 39, 37, 0, 267, 269, 270, 409,\n        291, 146, 91, 181, 84, 17, 314, 405, 321, 375,\n        78, 191, 80, 81, 82, 13, 312, 311, 310, 415,\n        95, 88, 178, 87, 14, 317, 402, 318, 324, 308,\n    ])\n\n# Landmark Indices for Left/Right hand without z axis in raw data\nLEFT_HAND_IDXS0, LEFT_HAND_NAMES0 = get_idxs(example_parquet_df, ['left_hand'], ['z'])\nRIGHT_HAND_IDXS0, RIGHT_HAND_NAMES0 = get_idxs(example_parquet_df, ['right_hand'], ['z'])\nLIPS_IDXS0, LIPS_NAMES0 = get_idxs(example_parquet_df, ['face'], ['z'], idxs_pos=LIPS_LANDMARK_IDXS)\nCOLUMNS0 = np.concatenate((LEFT_HAND_NAMES0, RIGHT_HAND_NAMES0, LIPS_NAMES0))\nN_COLS0 = len(COLUMNS0)\n# Only X/Y axes are used\nN_DIMS0 = 2\n\nprint(f'N_COLS0: {N_COLS0}')","metadata":{"papermill":{"duration":0.053701,"end_time":"2023-07-02T11:33:49.324986","exception":false,"start_time":"2023-07-02T11:33:49.271285","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:37.953015Z","iopub.execute_input":"2023-07-31T07:32:37.954299Z","iopub.status.idle":"2023-07-31T07:32:37.966683Z","shell.execute_reply.started":"2023-07-31T07:32:37.954242Z","shell.execute_reply":"2023-07-31T07:32:37.965531Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Landmark Indices in subset of dataframe with only COLUMNS selected\nLEFT_HAND_IDXS = np.argwhere(np.isin(COLUMNS0, LEFT_HAND_NAMES0)).squeeze()\nRIGHT_HAND_IDXS = np.argwhere(np.isin(COLUMNS0, RIGHT_HAND_NAMES0)).squeeze()\nLIPS_IDXS = np.argwhere(np.isin(COLUMNS0, LIPS_NAMES0)).squeeze()\nHAND_IDXS = np.concatenate((LEFT_HAND_IDXS, RIGHT_HAND_IDXS), axis=0)\nN_COLS = N_COLS0\n# Only X/Y axes are used\nN_DIMS = 2\n\nprint(f'N_COLS: {N_COLS}')","metadata":{"papermill":{"duration":0.050026,"end_time":"2023-07-02T11:33:49.408684","exception":false,"start_time":"2023-07-02T11:33:49.358658","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:37.968474Z","iopub.execute_input":"2023-07-31T07:32:37.968921Z","iopub.status.idle":"2023-07-31T07:32:37.986714Z","shell.execute_reply.started":"2023-07-31T07:32:37.968881Z","shell.execute_reply":"2023-07-31T07:32:37.985679Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Indices in processed data by axes with only dominant hand\nHAND_X_IDXS = np.array(\n        [idx for idx, name in enumerate(LEFT_HAND_NAMES0) if 'x' in name]\n    ).squeeze()\nHAND_Y_IDXS = np.array(\n        [idx for idx, name in enumerate(LEFT_HAND_NAMES0) if 'y' in name]\n    ).squeeze()\n# Names in processed data by axes\nHAND_X_NAMES = LEFT_HAND_NAMES0[HAND_X_IDXS]\nHAND_Y_NAMES = LEFT_HAND_NAMES0[HAND_Y_IDXS]\n\nprint(f'HAND_X_NAMES: {HAND_X_NAMES}')","metadata":{"papermill":{"duration":0.045648,"end_time":"2023-07-02T11:33:49.488024","exception":false,"start_time":"2023-07-02T11:33:49.442376","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:37.988349Z","iopub.execute_input":"2023-07-31T07:32:37.988879Z","iopub.status.idle":"2023-07-31T07:32:37.998421Z","shell.execute_reply.started":"2023-07-31T07:32:37.988847Z","shell.execute_reply":"2023-07-31T07:32:37.996638Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Mean/STD Loading\n\nCargamos la media y la desviación estándar de la media de aquellos valores que eran distintos de 0.","metadata":{"papermill":{"duration":0.037446,"end_time":"2023-07-02T11:33:49.563172","exception":false,"start_time":"2023-07-02T11:33:49.525726","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Mean/Standard Deviations of data used for normalizing\nMEANS = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/MEANS.npy').reshape(-1)\nSTDS = np.load('/kaggle/input/aslfr-eda-preprocessing-dataset-for-beginners/STDS.npy').reshape(-1)\n\nprint(f'First 5 values of MEANS: {MEANS[:5]}')\nprint(f'Shape of MEANS: {MEANS.shape}') # 164 values, 1 for every column","metadata":{"papermill":{"duration":0.051188,"end_time":"2023-07-02T11:33:49.648816","exception":false,"start_time":"2023-07-02T11:33:49.597628","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:37.999778Z","iopub.execute_input":"2023-07-31T07:32:38.000169Z","iopub.status.idle":"2023-07-31T07:32:38.018721Z","shell.execute_reply.started":"2023-07-31T07:32:38.000138Z","shell.execute_reply":"2023-07-31T07:32:38.017523Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Añadimos la capa de preprocesamiento de nuevo aquí, ya que la usaremos en el modelo de `TFLite`","metadata":{}},{"cell_type":"code","source":"class PreprocessLayer(tf.keras.layers.Layer):\n    def __init__(self):\n        super(PreprocessLayer, self).__init__()\n        self.normalisation_correction = tf.constant(\n                    # Add 0.50 to x coordinates of left hand (original right hand) and substract 0.50 of right hand (original left hand)\n                     [0.50 if 'x' in name else 0.00 for name in LEFT_HAND_NAMES0],\n                dtype=tf.float32,\n            )\n    \n    @tf.function(\n        input_signature=(tf.TensorSpec(shape=[None,N_COLS0], dtype=tf.float32),),\n    )\n    def call(self, data0, resize=True):\n        # Fill NaN Values With 0\n        data = tf.where(tf.math.is_nan(data0), 0.0, data0)\n        \n        # Hacky\n        data = data[None]\n        \n        # Empty Hand Frame Filtering\n        hands = tf.slice(data, [0,0,0], [-1, -1, 84])\n        hands = tf.abs(hands)\n        mask = tf.reduce_sum(hands, axis=2)\n        mask = tf.not_equal(mask, 0)\n        data = data[mask][None]\n        \n        # Pad Zeros\n        N_FRAMES = len(data[0])\n        if N_FRAMES < N_TARGET_FRAMES:\n            data = tf.concat((\n                data,\n                tf.zeros([1,N_TARGET_FRAMES-N_FRAMES,N_COLS], dtype=tf.float32)\n            ), axis=1)\n        # Downsample\n        data = tf.image.resize(\n            data,\n            [1, N_TARGET_FRAMES],\n            method=tf.image.ResizeMethod.BILINEAR,\n        )\n        \n        # Squeeze Batch Dimension\n        data = tf.squeeze(data, axis=[0])\n        \n        return data\n    \npreprocess_layer = PreprocessLayer()","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:38.020756Z","iopub.execute_input":"2023-07-31T07:32:38.021256Z","iopub.status.idle":"2023-07-31T07:32:41.047091Z","shell.execute_reply.started":"2023-07-31T07:32:38.021219Z","shell.execute_reply":"2023-07-31T07:32:41.046075Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train Dataset\n\nAhora, pasamos a crear el dataset de train  a través de la función `get_train_dataset`, que recibe como argumentos las variables `X`, `y` y el tamaño del lote o batch.\n\n- Dentro de la función, se crea un array llamado `sample_idxs` que contiene los índices de las muestras en el conjunto de datos.\n- Luego, se crea un bucle infinito con la sentencia `while True`, que permite iterar continuamente sobre el conjunto de datos en lotes.\n- En cada iteración, se obtienen índices aleatorios a partir de `sample_idxs` utilizando la función `np.random.choice`. Se seleccionan `batch_size` índices aleatorios sin reemplazo.\n- A continuación, se crean dos diccionarios: `inputs` y `outputs`. En `inputs`, se asignan los lotes de frames y phrase correspondientes a los índices aleatorios seleccionados. En `outputs`, se asigna el lote de `y` correspondiente a los índices aleatorios.\n- Por último, se utiliza la sentencia `yield` para devolver los diccionarios `inputs` y `outputs` como un par de valores en cada iteración del generador.\n\nDe esta manera, al llamar a la función `get_train_dataset` se obtiene un generador que puede ser utilizado en un bucle `for` para iterar sobre los lotes de entrenamiento en cada época de entrenamiento. Cada lote contiene los datos de entrada y salida correspondientes a los índices aleatorios seleccionados.\n\nEn el contexto del **entrenamiento de un modelo**, el generador de datos se utiliza para proporcionar lotes de muestras de entrenamiento de manera eficiente durante el proceso de entrenamiento.\n\nEl generador de datos se utiliza como el valor de x en el método `fit()` del modelo. En lugar de pasar todos los datos de entrenamiento directamente como un tensor grande, que podría ocupar mucha memoria y ser ineficiente, se utiliza un generador de datos para proporcionar lotes de muestras más pequeños en cada paso de entrenamiento.\n\nLa función `get_train_dataset` devuelve un generador infinito que, en cada iteración, toma un lote aleatorio de muestras de entrenamiento (`batch_size`) a partir del conjunto de datos de entrenamiento X e y. Cada lote de muestras es un diccionario con dos claves: 'frames' y 'phrase'. Los datos de entrada se encuentran en la clave 'frames', que contiene las secuencias de datos numéricos correspondientes a las \"frames\" (secuencias de entrada de datos). Los datos de salida (etiquetas) se encuentran en la clave 'phrase', que contiene las secuencias de índices enteros que representan las \"frases\" (secuencias de salida deseada).\n\nCuando el método fit() del modelo recibe este generador de datos (train_dataset) como x, se ejecuta en cada época de entrenamiento y toma lotes de muestras del generador para actualizar los pesos del modelo. Esto permite entrenar el modelo utilizando todos los datos de entrenamiento, pero sin cargarlos todos en la memoria al mismo tiempo, lo que es útil para conjuntos de datos grandes. El generador continuará proporcionando lotes de muestras indefinidamente durante el entrenamiento, lo que asegura que el modelo se entrene en todas las muestras de entrenamiento disponibles.","metadata":{"papermill":{"duration":0.023965,"end_time":"2023-07-02T11:33:52.934821","exception":false,"start_time":"2023-07-02T11:33:52.910856","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Train Dataset Iterator\ndef get_train_dataset(X, y, batch_size=BATCH_SIZE):\n    sample_idxs = np.arange(len(X))\n    while True:\n        # Get random indices\n        random_sample_idxs = np.random.choice(sample_idxs, batch_size)\n        \n        inputs = {\n            'frames': X[random_sample_idxs],\n            'phrase': y[random_sample_idxs],\n        }\n        outputs = y[random_sample_idxs]\n        \n        yield inputs, outputs","metadata":{"papermill":{"duration":0.033186,"end_time":"2023-07-02T11:33:52.992213","exception":false,"start_time":"2023-07-02T11:33:52.959027","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.048314Z","iopub.execute_input":"2023-07-31T07:32:41.048657Z","iopub.status.idle":"2023-07-31T07:32:41.055086Z","shell.execute_reply.started":"2023-07-31T07:32:41.048626Z","shell.execute_reply":"2023-07-31T07:32:41.054139Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Train Dataset\ntrain_dataset = get_train_dataset(X_train, y_train)\n\ntrain_dataset","metadata":{"papermill":{"duration":0.03572,"end_time":"2023-07-02T11:33:53.051965","exception":false,"start_time":"2023-07-02T11:33:53.016245","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.056346Z","iopub.execute_input":"2023-07-31T07:32:41.057261Z","iopub.status.idle":"2023-07-31T07:32:41.072557Z","shell.execute_reply.started":"2023-07-31T07:32:41.057228Z","shell.execute_reply":"2023-07-31T07:32:41.071636Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Training Steps Per Epoch\nTRAIN_STEPS_PER_EPOCH = math.ceil(N_TRAIN_SAMPLES / BATCH_SIZE)\nprint(f'TRAIN_STEPS_PER_EPOCH: {TRAIN_STEPS_PER_EPOCH}')","metadata":{"papermill":{"duration":0.034525,"end_time":"2023-07-02T11:33:53.111793","exception":false,"start_time":"2023-07-02T11:33:53.077268","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.073971Z","iopub.execute_input":"2023-07-31T07:32:41.07442Z","iopub.status.idle":"2023-07-31T07:32:41.083028Z","shell.execute_reply.started":"2023-07-31T07:32:41.074388Z","shell.execute_reply":"2023-07-31T07:32:41.082007Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Validation Dataset\n\nHcaemos lo mismo para el set de validación","metadata":{"papermill":{"duration":0.02478,"end_time":"2023-07-02T11:33:53.161179","exception":false,"start_time":"2023-07-02T11:33:53.136399","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Validation Set\ndef get_val_dataset(X, y, batch_size=BATCH_SIZE):\n    offsets = np.arange(0, len(X), batch_size)\n    while True:\n        # Iterate over whole validation set\n        for offset in offsets:\n            inputs = {\n                'frames': X[offset:offset+batch_size],\n                'phrase': y[offset:offset+batch_size],\n            }\n            outputs = y[offset:offset+batch_size]\n\n            yield inputs, outputs","metadata":{"papermill":{"duration":0.0344,"end_time":"2023-07-02T11:33:53.220231","exception":false,"start_time":"2023-07-02T11:33:53.185831","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.084112Z","iopub.execute_input":"2023-07-31T07:32:41.085495Z","iopub.status.idle":"2023-07-31T07:32:41.094493Z","shell.execute_reply.started":"2023-07-31T07:32:41.085462Z","shell.execute_reply":"2023-07-31T07:32:41.093396Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Validation Dataset\nif USE_VAL:\n    val_dataset = get_val_dataset(X_val, y_val)","metadata":{"papermill":{"duration":0.031785,"end_time":"2023-07-02T11:33:53.276318","exception":false,"start_time":"2023-07-02T11:33:53.244533","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.097565Z","iopub.execute_input":"2023-07-31T07:32:41.097868Z","iopub.status.idle":"2023-07-31T07:32:41.107083Z","shell.execute_reply.started":"2023-07-31T07:32:41.097833Z","shell.execute_reply":"2023-07-31T07:32:41.106089Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if USE_VAL:\n    N_VAL_STEPS_PER_EPOCH = math.ceil(N_VAL_SAMPLES / BATCH_SIZE)\n    print(f'N_VAL_STEPS_PER_EPOCH: {N_VAL_STEPS_PER_EPOCH}')","metadata":{"papermill":{"duration":0.032228,"end_time":"2023-07-02T11:33:53.33261","exception":false,"start_time":"2023-07-02T11:33:53.300382","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.108674Z","iopub.execute_input":"2023-07-31T07:32:41.109064Z","iopub.status.idle":"2023-07-31T07:32:41.118499Z","shell.execute_reply.started":"2023-07-31T07:32:41.109033Z","shell.execute_reply":"2023-07-31T07:32:41.117445Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model Config\n\nPrevio al proceso de modelizado, comentaremos en qué consiste un modelo de tipo transformer:\n\nA diferencia de las arquitecturas recurrentes tradicionales como las redes neuronales recurrentes (RNN) o las redes neuronales convolucionales (CNN), que se basan en conexiones recurrentes o convoluciones para capturar dependencias temporales en los datos secuenciales, el modelo Transformer se basa en mecanismos de atención para capturar estas dependencias. El enfoque principal del mecanismo de atención es calcular las relaciones entre todos los elementos de una secuencia para asignar ponderaciones o importancias a cada elemento en función de su relevancia para la tarea en cuestión.\n\nEl modelo Transformer consta de dos componentes principales: el codificador y el decodificador. Cada componente está compuesto por múltiples capas apiladas. El codificador se encarga de procesar la entrada de texto original y capturar información relevante de cada palabra o token en la secuencia. Este proceso se realiza mediante mecanismos de atención, que permiten al modelo focalizarse en las partes más importantes de la entrada en cada paso. En el codificador, se aplican capas de atención multi-cabezal y capas de redes neuronales completamente conectadas para capturar y procesar la información contextual de la entrada.\n\nPor otro lado, el decodificador se encarga de generar la salida deseada, como la traducción de la entrada original a otro idioma. El decodificador también utiliza mecanismos de atención, pero en este caso, se enfoca tanto en la entrada original (a través de la atención enmascarada) como en la salida generada hasta el momento. Esto permite al modelo generar palabras o tokens de salida basados en el contexto actual y las palabras generadas anteriormente.\n\nEn cada capa tanto del codificador como del decodificador, se aplican técnicas de normalización y regularización, como la normalización de capa y el dropout, para estabilizar y regularizar el proceso de aprendizaje. Estas técnicas ayudan a mejorar la generalización del modelo y prevenir el sobreajuste.\n\nEn resumen, el codificador procesa la entrada original y captura la información contextual utilizando mecanismos de atención, mientras que el decodificador utiliza la información del codificador y las salidas generadas hasta el momento para producir la salida deseada.\n\nProcedemos ahora a definir varias variables relacionadas con la arquitectura y configuración de un modelo Transformer. \n\n- `LAYER_NORM_EPS` : Representa el valor epsilon utilizado en la capa de normalización del modelo Transformer.\n- `UNITS_ENCODER` y `UNITS_DECODER`: Estas variables indican el tamaño de la salida final y de los embeddings del codificador y decodificador respectivamente.\n- `NUM_BLOCKS_ENCODER` y `NUM_BLOCKS_DECODER`: Representan el número de bloques (capas) en el codificador y decodificador del Transformer.\n- `NUM_HEADS`: Indica el número de cabezas de atención en el mecanismo de atención multi-cabeza del Transformer.\n- `MLP_RATIO`: Es el factor de multiplicación utilizado para calcular el tamaño de la capa de feed-forward en el bloque del Transformer.\n- `EMBEDDING_DROPOUT`, `MLP_DROPOUT_RATIO`, `MHA_DROPOUT_RATIO` y `CLASSIFIER_DROPOUT_RATIO`: Estas variables definen las tasas de dropout utilizadas en diferentes partes del modelo, como la capa de embedding, la capa de feed-forward y la capa clasificadora.\n- `INIT_HE_UNIFORM`, `INIT_GLOROT_UNIFORM` y `INIT_ZEROS`: Son inicializadores utilizados para inicializar los pesos de las capas del modelo. `INIT_HE_UNIFORM` utiliza la inicialización He Uniform, `INIT_GLOROT_UNIFORM` utiliza la inicialización Glorot Uniform e `INIT_ZEROS` inicializa los pesos con ceros.\n- `GELU`: Es una función de activación llamada GELU (Gaussian Error Linear Unit) utilizada en el modelo.","metadata":{"papermill":{"duration":0.0243,"end_time":"2023-07-02T11:33:53.381866","exception":false,"start_time":"2023-07-02T11:33:53.357566","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Epsilon value for layer normalisation\nLAYER_NORM_EPS = 1e-6\n\n# final embedding and transformer embedding size\nUNITS_ENCODER = 384\nUNITS_DECODER = 256\n\n# Transformer\nNUM_BLOCKS_ENCODER = 4\nNUM_BLOCKS_DECODER = 3\nNUM_HEADS = 4\nMLP_RATIO = 2\n\n# Dropout\nEMBEDDING_DROPOUT = 0.00\nMLP_DROPOUT_RATIO = 0.30\nMHA_DROPOUT_RATIO = 0.20\nCLASSIFIER_DROPOUT_RATIO = 0.10\n\n# Initiailizers\nINIT_HE_UNIFORM = tf.keras.initializers.he_uniform\nINIT_GLOROT_UNIFORM = tf.keras.initializers.glorot_uniform\nINIT_ZEROS = tf.keras.initializers.constant(0.0)\n# Activations\nGELU = tf.keras.activations.gelu","metadata":{"papermill":{"duration":0.036845,"end_time":"2023-07-02T11:33:53.443416","exception":false,"start_time":"2023-07-02T11:33:53.406571","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.119896Z","iopub.execute_input":"2023-07-31T07:32:41.120756Z","iopub.status.idle":"2023-07-31T07:32:41.132188Z","shell.execute_reply.started":"2023-07-31T07:32:41.120726Z","shell.execute_reply":"2023-07-31T07:32:41.131322Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Landmark Embedding\n\n\nEn este paso, definiremos una clase llamada `LandmarkEmbedding` que hereda de `tf.keras.Model` y se utiliza para realizar el embedding de un landmark utilizando capas completamente conectadas. El embedding de un landmark, se refiere a la transformación de una representación de coordenadas (landmarks), en un vector resultado de aplicar transformaciones no lineales y aprender representaciones más expresivas de los landmarks. Estas representaciones embebidas pueden capturar características importantes de los landmarks, como su posición relativa, forma o cualquier otra información relevante para la tarea en cuestión...\n\nLa clase `LandmarkEmbedding` tiene los siguientes componentes:\n\n- El método `__init__` se encarga de inicializar la clase y configurar sus atributos. Recibe dos parámetros: `units`, que representa la dimensión de la incrustación, y `name`, que es el nombre de la incrustación.\n- El método `build` se utiliza para construir la arquitectura del modelo. Recibe el `input_shape` que define la forma de los datos de entrada. En este caso, se construye una incrustación para los landmarks. \n    * Dentro del método `build`, se define un peso llamado `empty_embedding` utilizando `self.add_weight`. Este peso representa la incrustación para un landmark faltante en un frame y se inicializa con ceros.\n    * A continuación, se define una secuencia de capas densas utilizando `tf.keras.Sequential`. Estas capas se utilizan para realizar la incrustación de los landmarks. La primera capa densa tiene `units` unidades y utiliza la función de activación GELU (Gaussian Error Linear Unit). La segunda capa densa también tiene `units` unidades y se inicializa con el método He Uniform.\n- El método `call` es la función de llamada del modelo y se utiliza para realizar la incrustación de los datos de entrada `x`. En esta función, se utiliza`tf.where` para aplicar una condición. Si la suma de los landmarks en un frame es igual a cero, lo que significa que el landmark es nulo, se utiliza la incrustación vacía `empty_embedding`. De lo contrario, se utiliza la incrustación de los datos de landmark utilizando la secuencia de capas densas definida anteriormente.","metadata":{"papermill":{"duration":0.024508,"end_time":"2023-07-02T11:33:53.492645","exception":false,"start_time":"2023-07-02T11:33:53.468137","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Embeds a landmark using fully connected layers\nclass LandmarkEmbedding(tf.keras.Model):\n    def __init__(self, units, name):\n        super(LandmarkEmbedding, self).__init__(name=f'{name}_embedding')\n        self.units = units\n        self.supports_masking = True\n        \n    def build(self, input_shape):\n        # Embedding for missing landmark in frame, initialized with zeros\n        self.empty_embedding = self.add_weight(\n            name=f'{self.name}_empty_embedding',\n            shape=[self.units],\n            initializer=INIT_ZEROS,)\n        # Embedding: 2 dense layers\n        self.dense = tf.keras.Sequential([\n            tf.keras.layers.Dense(self.units, name=f'{self.name}_dense_1', use_bias=False, kernel_initializer=INIT_GLOROT_UNIFORM, activation=GELU),\n            tf.keras.layers.Dense(self.units, name=f'{self.name}_dense_2', use_bias=False, kernel_initializer=INIT_HE_UNIFORM),\n        ], name=f'{self.name}_dense')\n\n    def call(self, x):\n        # if the landmark = 0 -> use empty embedding (return 0s), else use dense embedding\n        return tf.where(\n                # Checks whether landmark is missing in frame\n                tf.reduce_sum(x, axis=2, keepdims=True) == 0,\n                # If so, the empty embedding is used\n                self.empty_embedding,\n                # Otherwise the landmark data is embedded\n                self.dense(x),\n            )","metadata":{"papermill":{"duration":0.03711,"end_time":"2023-07-02T11:33:53.554368","exception":false,"start_time":"2023-07-02T11:33:53.517258","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:41.133377Z","iopub.execute_input":"2023-07-31T07:32:41.134509Z","iopub.status.idle":"2023-07-31T07:32:41.146106Z","shell.execute_reply.started":"2023-07-31T07:32:41.134476Z","shell.execute_reply":"2023-07-31T07:32:41.145152Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Ejemplo : Definimos los landmarks de tres frames utilizando tensores constantes de TensorFlow. Cada frame tiene dos landmarks representados como pares de coordenadas `(x, y)`. Pasamos los landmarks por el modelo de embedding utilizando la función `call()` del modelo. Esto generará las representaciones embebidas de los landmarks. En cada print, se verán las representaciones embebidas de los landmarks en forma de tensores. Cada tensor tendrá la forma `(n_batch,num_landmarks, num_dimensions)`, donde `num_landmarks` es el número de landmarks en el frame y `num_dimensions` es el número de dimensiones de cada landmark (en este caso, 2 para las coordenadas x e y).","metadata":{}},{"cell_type":"code","source":"# Create an instance of the LandmarkEmbedding model\nembedding_model = LandmarkEmbedding(units=2, name='landmark')\n\n# Define the landmarks of the three frames\nframe_1 = tf.constant([[[1, 2], [3, 4]]])\nframe_2 = tf.constant([[[0, 0], [5, 6]], [[7, 8], [9, 10]]])\n\n# Pass the landmarks through the embedding model\nembedding_1 = embedding_model(frame_1)\nembedding_2 = embedding_model(frame_2)\n\n# Print the embedded representations of the landmarks\nprint(f'Frame 1 shape: {frame_1.shape} ')\nprint(\"Frame 1 embeddings:\")\nprint(embedding_1)\n\nprint(\"\\n\\nFrame 2 embeddings:\")\nprint(embedding_2)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:41.147336Z","iopub.execute_input":"2023-07-31T07:32:41.14818Z","iopub.status.idle":"2023-07-31T07:32:43.176193Z","shell.execute_reply.started":"2023-07-31T07:32:41.148147Z","shell.execute_reply":"2023-07-31T07:32:43.17523Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> **Output**: En la salida del primer frame , se muestra un tensor de forma `(1, 2, 2)` y tipo de dato *float32*. Esto significa que hay una sola muestra en el batch, dos landmarks en el frame (dos vectores) y dos dimensiones para cada landmark (x,y).\nCada valor en el tensor representa la representación embebida de un landmark en el frame. Por ejemplo, el primer valor `[0.00237609, -0.14469941]` corresponde a la representación embebida del primer landmark `[1,2]`, y el segundo valor `[0.00064886, -0.10494988]` corresponde a la representación embebida del segundo landmark.\n\n El modelo LandmarkEmbedding realiza una transformación de los valores originales de los landmarks utilizando las capas densas definidas en el método build.\n\n- Si el valor de un landmark es igual a cero, lo que indica que el landmark está faltante en el frame, se utiliza una incrustación vacía (representada por `self.empty_embedding`) en lugar de realizar la transformación.\n- Si el valor de un landmark es distinto de cero, se aplica la transformación utilizando las capas densas (`self.dense`). En este caso, se realiza una operación lineal seguida de una función de activación GELU para obtener el embedding final del landmark.\n\nEn este último caso, se utilizan dos capas densas consecutivas. La primera capa (`self.dense_1`) realiza una multiplicación matricial de los valores de entrada con una matriz de pesos. Los pesos de esta capa se inicializan utilizando la inicialización Glorot uniforme. Luego, se aplica una función de activación llamada GELU (Gaussian Error Linear Unit), que no es más que una versión suavizada de la ReLU, e introduce de igual forma no linealidad en la transformación. La segunda capa (`self.dense_2`) realiza una operación similar, multiplicando la salida de la capa anterior por otra matriz de pesos inicializada con el método He uniforme.\n\nEn el ejemplo proporcionado, el número de `units` del modelo de embedding se establece en '2'. Esto significa que la dimensión de la salida del embedding será de tamaño 2 (nos quedaremos por tanto con la misma forma de tensor que la original).\n\nEn general, el propósito del embedding no es necesariamente reducir la dimensión de los datos de entrada, sino más bien proporcionar una representación densa y más significativa de los datos originales. El objetivo es capturar características relevantes y relaciones semánticas entre los datos de entrada.\n\nA través de las capas densas del modelo, se aplican transformaciones lineales y no lineales a los datos de entrada, lo que puede permitir capturar patrones y relaciones más complejas entre los landmarks.\n\nPor tanto, aunque el tamaño de la dimensión de salida del embedding se mantiene igual al tamaño de la dimensión de entrada (en este caso, 2), las capas densas introducen pesos y sesgos que permiten una transformación no lineal de los datos. Esto puede ayudar a capturar características más discriminativas y a representar de manera más efectiva las relaciones entre los landmarks.","metadata":{}},{"cell_type":"markdown","source":"# Embedding\n\nLa clase `Embedding` es una subclase de `tf.keras.Model` que se utiliza para crear una representación embebida para cada frame de entrada. Veamos paso a paso la función de esta clase:\n\n- En el método`__init__`, se inicializa la clase Embedding y se establece la propiedad `supports_masking=True`. Esto indica que el modelo admite el uso de máscaras para ignorar ciertas entradas durante el entrenamiento o la inferencia.\n\n- En el método `build`, se construye el modelo y se definen los parámetros y capas necesarios. EEn este caso, se crea una capa especial llamada \"embedding posicional\" para cada frame y una capa llamada \"embedding de landmarks\".\n\n    - La capa de \"embedding posicional\" se utiliza para agregar información sobre la posición de cada frame en la secuencia. Se inicializa con un tensor de forma `[N_TARGET_FRAMES, UNITS_ENCODER]` lleno de ceros y se ajustará durante el entrenamiento para capturar patrones de posición.\n\n    - La capa de \"embedding de landmarks\" se utiliza para generar una representación especial de los landmarks de cada frame. Esta representación captura características importantes de los landmarks y se utilizará para realizar tareas específicas más adelante.\n\n- En el método `call`, se define la lógica de la propagación hacia adelante del modelo. Aquí se realiza el procesamiento de los datos de entrada:\n    - Primero, se normalizan los datos de entrada `x` restando la media y dividiendo por la desviación estándar. Esto asegura que los datos estén en una escala adecuada para el modelo.\n\n    - Luego, se pasa la entrada normalizada a través de la capa de embedding para los landmarks `dominant_hand_embedding`. Se aplica por tanto un embedding a los datos de los landmarks. Imagina los landmarks como puntos de referencia en una secuencia de imágenes. La incrustación de landmarks es similar a una transformación que coge estos puntos y los transforma en una representación más compacta y rica en información, para ayudar al modelo a capturar características importantes de los landmarks y hacer que sean más fáciles de procesar. \n    \n    - Después, se agrega información sobre la posición de cada frame en la secuencia de frames (vídeo). Esto es importante porque el orden de los frames puede ser relevante para entender el contexto y la relación entre ellos. Por ejemplo, en un video, el orden de los frames puede representar el flujo temporal de las acciones. La incrustación posicional agrega esta información de posición a los datos incrustados. En el código, utilizamos un tensor llamado `positional_embedding` que contiene el embedding posicional correspondiente a cada frame en la secuencia.\n\n- Finalmente, se devuelve la representación embebida resultante `x`.\n\nEn resumen, la clase Embedding se utiliza para generar una representación embebida para cada frame de entrada. Esto implica normalizar los datos de entrada, generar una representación embebida de los landmarks y agregar información posicional a la representación embebida resultante.","metadata":{"papermill":{"duration":0.023829,"end_time":"2023-07-02T11:33:53.60253","exception":false,"start_time":"2023-07-02T11:33:53.578701","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Creates embedding for each frame\nclass Embedding(tf.keras.Model):\n    def __init__(self):\n        super(Embedding, self).__init__()\n        self.supports_masking = True\n    \n    def build(self, input_shape):\n        # Positional embedding for each frame index\n        self.positional_embedding = tf.Variable(\n            initial_value=tf.zeros([N_TARGET_FRAMES, UNITS_ENCODER], dtype=tf.float32),\n            trainable=True,\n            name='embedding_positional_encoder',)\n        # Embedding layer for Landmarks\n        self.dominant_hand_embedding = LandmarkEmbedding(UNITS_ENCODER, 'dominant_hand')\n\n    def call(self, x, training=False):\n        # Normalize data before aplying embedding\n        x = tf.where(\n                tf.math.equal(x, 0.0),\n                0.0,\n                (x - MEANS) / STDS,)\n        # Dominant Hand: apply landmark embedding to extract information\n        x = self.dominant_hand_embedding(x)\n        # Add Positional Encoding\n        x = x + self.positional_embedding\n        \n        return x","metadata":{"papermill":{"duration":0.036321,"end_time":"2023-07-02T11:33:53.663162","exception":false,"start_time":"2023-07-02T11:33:53.626841","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.177686Z","iopub.execute_input":"2023-07-31T07:32:43.178304Z","iopub.status.idle":"2023-07-31T07:32:43.187013Z","shell.execute_reply.started":"2023-07-31T07:32:43.178252Z","shell.execute_reply":"2023-07-31T07:32:43.185762Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Ejemplo: *vamos a visualizar la parte posicional de la clase Embedding. Para ello, crearemos una clase que realice únicamente embedding posicional, ya que el ejemplo de la parte del landmark embedding ya ha sido mostrado. Posteriormente, veremos el efecto conjunto*.","metadata":{}},{"cell_type":"code","source":"# positional Embedding class\nclass PositionalEmbedding(tf.keras.Model):\n    def __init__(self):\n        super(PositionalEmbedding, self).__init__()\n    \n    def build(self, input_shape):\n        self.positional_embedding = tf.Variable(\n            initial_value=tf.zeros([1, 164], dtype=tf.float32),\n            trainable=True,\n            name='positional_embedding')\n    \n    def call(self, x):\n        return x + self.positional_embedding","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.191222Z","iopub.execute_input":"2023-07-31T07:32:43.191831Z","iopub.status.idle":"2023-07-31T07:32:43.226639Z","shell.execute_reply.started":"2023-07-31T07:32:43.191795Z","shell.execute_reply":"2023-07-31T07:32:43.225733Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create model instance\nembedding_model = PositionalEmbedding()\n\n# Define input data of dimension [1,1,164]\ninput_data = X_train[0,0,:].reshape(1,1,164)\n\n# make the embedding\nembedded_data = embedding_model(input_data)\n\nprint(f'Input Shape: {input_data.shape}')\n\nprint(f'\\nEmbedded Shape: {embedded_data.shape}')\n\n# Imprimir los datos de salida incrustados\nprint(\"\\nInput Data:\")\nprint(input_data)\n\nprint(\"\\n\\nEmbedded Data:\")\nprint(embedded_data)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.229555Z","iopub.execute_input":"2023-07-31T07:32:43.229823Z","iopub.status.idle":"2023-07-31T07:32:43.256411Z","shell.execute_reply.started":"2023-07-31T07:32:43.229799Z","shell.execute_reply":"2023-07-31T07:32:43.255342Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"El embedding posicional se calcula utilizando funciones matemáticas o patrones predefinidos que permiten asignar un **valor numérico único a cada posición en la secuencia**. Estos valores numéricos se utilizan para **representar la información de posición relativa**, que **el modelo puede utilizar** para realizar operaciones específicas, como **reconocer patrones de dependencia espacial o temporal en la secuencia**.\n\nEn este ejemplo, la secuencia se refiere a los datos de entrada que estás pasando a través del modelo. La forma de los datos de entrada es `[1, 164]`, lo que significa que tienes **una secuencia** de longitud 164.\n\nEn general, no se puede asumir que los valores de los elementos en un mismo índice de diferentes lotes (frames) serán iguales en un contexto general.\n\nSi tuviesemos datos de tipo `[N, 164]`, donde N representa el número de lotes (número de secuencias) y 164 es la longitud de la secuencia (número de columnas), **los valores de los elementos en un mismo índice pueden ser diferentes en diferentes secuencias**, a menos que haya una lógica específica en tus datos que garantice lo contrario.\n\nEs importante tener en cuenta que los valores de los elementos en un mismo índice de diferentes lotes pueden variar para capturar la variabilidad en los datos y permitir al modelo aprender patrones y generalizar mejor.\n\nVeamos ahora el efecto conjunto de ambos embeddings:","metadata":{"execution":{"iopub.status.busy":"2023-07-14T10:53:31.205937Z","iopub.execute_input":"2023-07-14T10:53:31.206404Z","iopub.status.idle":"2023-07-14T10:53:31.21732Z","shell.execute_reply.started":"2023-07-14T10:53:31.206367Z","shell.execute_reply":"2023-07-14T10:53:31.216247Z"}}},{"cell_type":"code","source":"# Create model instance\nembedding_model = Embedding()\n\n# Define input data of dimension [1,1,164]\ninput_data = X_train[0,0,:].reshape(1,1,164)\n\n# make the embedding\nembedded_data = embedding_model(input_data)\n\nprint(f'Input Shape: {input_data.shape}')\n\nprint(f'\\nEmbedded Shape: {embedded_data.shape}')\n\n# Imprimir los datos de salida incrustados\nprint(\"\\nInput Data:\")\nprint(input_data)\n\nprint(\"\\n\\nEmbedded Data:\")\nprint(embedded_data)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.264755Z","iopub.execute_input":"2023-07-31T07:32:43.265037Z","iopub.status.idle":"2023-07-31T07:32:43.347224Z","shell.execute_reply.started":"2023-07-31T07:32:43.265013Z","shell.execute_reply":"2023-07-31T07:32:43.346252Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Podemos ver como la forma de los datos ha cambiado, debido a que se realiza la siguiente transformación: \n\n* Landmark embedding: Los datos de entrada se pasan a través de la capa `dominant_hand_embedding` de la clase `LandmarkEmbedding`. Esta capa aplica una transformación lineal a los datos de entrada utilizando capas densas. Como la dimensión de la incrustación (`UNITS_ENCODER`) es 384, en este caso, cada elemento de entrada individual es transformado en una representación de forma `[1, 1, 384]`. Esto se debe a que la dimensión de la secuencia se mantiene en 1, ya que solo se tiene un elemento de entrada en la secuencia.\n\n* Embedding posicional: Después del landmark embedding, se agrega un embedding posicional a cada elemento transformado. El embedding posicional, almacenada en la variable `positional_embedding`, tiene una forma de `[N_TARGET_FRAMES, UNITS_ENCODER]`. En este caso, `N_TARGET_FRAMES` se establece en 128 y `UNITS_ENCODER` se establece en 384.\n\n* Resultado final: Al sumar la el landmark embedding y el embedding posicional, se obtiene el resultado final con shape `[1, 128, 384]`. La primera dimensión (1) se refiere al número de elementos en el lote, la segunda dimensión (128) representa los fotogramas objetivo y la tercera dimensión (384) es la dimensión de la incrustación resultante.\n\nVeamos ahora lo que sucede con unos datos de entrada de forma `[1,128,164]`:","metadata":{}},{"cell_type":"code","source":"# Create model instance\nembedding_model = Embedding()\n\n# Define input data of dimension [1,128,164]\ninput_data = X_train[0,:,:].reshape(1,128,164) # [128,164] -> [1,128,164]\n\n# Make embedding\nembedded_data = embedding_model(input_data)\n\n\nprint(f'Input Shape: {input_data.shape}')\nprint(f'\\nEmbedded Shape: {embedded_data.shape}')\n\nprint(\"\\nInput Data:\")\nprint(input_data)\n\nprint(\"\\n\\nEmbedded Data:\")\nprint(embedded_data)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.348669Z","iopub.execute_input":"2023-07-31T07:32:43.349226Z","iopub.status.idle":"2023-07-31T07:32:43.39832Z","shell.execute_reply.started":"2023-07-31T07:32:43.349192Z","shell.execute_reply":"2023-07-31T07:32:43.397331Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"En el ejemplo, los datos de entrada tienen una forma de `[1, 128, 164]`. Cuando se pasan a través de la clase `Embedding` , se realizan los embeddings de los landmarks y la posicion, lo que nos permite capturar información sobre los landmarks y a su vez guardar la posición de estos en el vídeo. \n\nEn el caso específico del embedding de landmarks, se utiliza la clase `LandmarkEmbedding` que tiene una capa densa final con unidades `UNITS_ENCODER`. En el código, `UNITS_ENCODER=384`. Por lo tanto, después de aplicar el embedding de landmarks, los datos se ajustan a la dimensión de 384.\n\nLuego, se agrega el embedding posicional, que es un tensor de forma `[384,]`, por lo que se sumará a cada uno de los datos, sin cambiar las dimensiones.\n\nEn el contexto del procesamiento de lenguaje natural, el embedding aumenta la dimensión de los datos, no lo contrario.\n\nCuando se aplica un embedding a datos de texto, como palabras o secuencias, se busca asignar a cada elemento (palabra o token) una representación vectorial en un espacio de mayor dimensión. Es decir, se transforma una representación en formato discreto (por ejemplo, una palabra representada como un índice) a una representación en formato vectorial con un conjunto de números reales que representan las características del elemento.\n\nPor ejemplo, si se tiene un vocabulario de tamaño 10,000 palabras y se quiere representar cada palabra como un vector de 300 dimensiones utilizando un embedding, entonces se aumentaría la dimensión de las palabras de 1 (el índice de la palabra en el vocabulario) a 300.\n\nEste aumento de dimensión es beneficioso porque el embedding puede capturar mejor la semántica y relaciones entre las palabras o tokens, lo que ayuda a mejorar el rendimiento en tareas posteriores, como clasificación de texto, generación de texto, entre otras.","metadata":{}},{"cell_type":"markdown","source":"# Transformer\n\nAhora, se implementa una capa de atención multi-cabeza utilizando la operación de producto escalar. La atención multi-cabeza es una componente clave en los modelos de Transformers utilizados en tareas de procesamiento de lenguaje natural y otros dominios.\n\nEn un Transformer con atención multi-cabeza, los conceptos de consultas, claves y valores son clave, ya que se utilizan para calcular la atención entre elementos de una secuencia. El mecanismo de atención en un Transformer es lo que le permite enfocarse en partes relevantes de la entrada durante la etapa de codificación o decodificación. **La atención se calcula en cada posición de la secuencia de entrada en función de las consultas, claves y valores**.\n\nA continuación, se explica en qué consisten estas tres partes:\n\n- Consultas (Queries): son vectores que representan la posición actual en la secuencia de entrada para la cual se quiere calcular la atención. En otras palabras, son las posiciones que se están tratando de codificar o decodificar. Cada consulta se utiliza para calcular el grado de relevancia o similitud entre esta posición y todas las demás posiciones en la secuencia.\n\n- Claves (Keys): son vectores que representan todas las posiciones en la secuencia de entrada. Se utilizan para calcular el grado de relevancia entre las consultas y las diferentes posiciones en la secuencia. Las claves son fundamentales para determinar qué partes de la entrada son importantes para cada consulta.\n\n- Valores (Values): son vectores que contienen la información real en cada posición de la secuencia de entrada. Se utilizan para calcular los pesos de atención y ponderar la importancia de cada posición en función de su relevancia con respecto a las consultas y las claves.\n\nEl cálculo de la atención implica medir la similitud entre las consultas y las claves para obtener los pesos de atención. Estos pesos de atención indican cuánta importancia se le debe dar a cada posición de la secuencia en función de su relación con la consulta actual. Luego, se ponderan los valores utilizando estos pesos de atención para obtener la representación atendida o contextualizada de la consulta actual.\n\nEn el contexto de la atención multi-cabeza, este proceso se realiza varias veces, cada vez con diferentes conjuntos de parámetros (consultas, claves y valores), lo que permite al Transformer capturar diferentes patrones y relaciones en la secuencia de entrada de manera más efectiva.\n\n\n\nDividimos la explicación de nuestro mecanismo de atención en pasos:\n\n- `scaled_dot_product` es una función que realiza el producto escalar escalado entre las consultas (q), claves (k) y valores (v) de la atención. El \"scaled dot product\" es una operación fundamental en la atención, y se utiliza para **calcular la relevancia entre las consultas y las claves**. En el contexto de la atención multi-cabeza, se aplica el \"scaled dot product\" de forma paralela en cada cabeza de atención para capturar diferentes relaciones y patrones en los datos. El proceso del \"scaled dot product\" se realiza en tres pasos:\n\n    - Producto escalar: Se calcula el producto escalar entre las consultas (q) y las claves (k) transpuestas. Esto se logra mediante la operación de multiplicación matricial. El resultado es una matriz que representa la relevancia entre cada par de consulta y clave.\n    - Escalamiento: Después de calcular el producto escalar, se escala dividiendo por la raíz cuadrada de la dimensión de las consultas (q). Esta operación de escalamiento ayuda a estabilizar el proceso de atención y asegura que los valores de atención no sean demasiado grandes.\n    - Softmax y ponderación: A continuación, se aplica una función de softmax a la matriz escalada. El softmax convierte los valores en una distribución de probabilidad, lo que significa que cada valor de atención representa la importancia relativa de la clave correspondiente para una consulta dada. Luego, se utiliza esta distribución de atención para ponderar los valores (v) y obtener una representación combinada de los valores ponderados. \n    \n    La razón por la que utilizamos esta capa de \"scaled dot product\" en la atención multi-cabeza es que **nos permite capturar las relaciones y dependencias entre las consultas y las claves** de manera más expresiva. Al realizar el \"scaled dot product\" en múltiples cabezas de atención, podemos capturar diferentes patrones y relaciones en paralelo, lo que mejora la capacidad de modelar información relevante en los datos. \n    \n- `MultiHeadAttention` es una capa de atención multi-cabeza. Recibe como entrada las consultas (q), claves (k) y valores (v) de la atención. También puede recibir una máscara de atención opcional para enmascarar ciertas entradas. Los parámetros principales de esta capa son:\n    - `d_model`: la dimensión del espacio de representación para las consultas, claves y valores.\n    - `num_of_heads`: el número de cabezas de atención que se utilizarán.\n    - `dropout`: la probabilidad de dropout para regularizar la salida.\n    - `d_out` (opcional): si se proporciona, especifica la dimensión de salida después de la capa de atención multi-cabeza.\n- En el método `call`, se realizan las siguientes operaciones:\n    - Para cada cabeza de atención, se aplica una transformación lineal independiente a las consultas (q), claves (k) y valores (v) utilizando capas densas (wq, wk, wv) para proyectarlos a subespacios más pequeños.\n    - Se llama a la función `scaled_dot_product` para calcular la atención con las proyecciones de q, k y v para cada cabeza de atención.\n    - Las salidas de todas las cabezas de atención se concatenan a lo largo de la última dimensión para obtener una representación combinada.\n    - La representación combinada pasa por una capa lineal final (wo) para obtener la salida de la capa de atención multi-cabeza.\n    - Se aplica una capa de dropout para regularizar la salida antes de retornarla.\n    \nEn resumen, esta implementación realiza una atención multi-cabeza en paralelo, donde cada cabeza aprende diferentes representaciones ponderadas de las entradas. Luego, combina las salidas de todas las cabezas en una representación final que puede ser utilizada en tareas de procesamiento de lenguaje natural y otros problemas de aprendizaje automático.\n\n> Inciso: \n> - `tf.math.sqrt()`: calcula la raíz cuadrada de un tensor. En el contexto del código, se utiliza para calcular la raíz cuadrada de la dimensión de las consultas (q). Proporciona el valor utilizado para escalar el producto escalar, controlando la magnitud de los valores de atención resultantes.\n> - `tf.cast()`: se utiliza para realizar una conversión a tensor. En el código, se utiliza para convertir la dimensión de las consultas `(q.shape[-1])` a tf.float32 antes de aplicar la función `sqrt()`. Esto asegura que el cálculo de la raíz cuadrada se realice correctamente y con el tipo de datos adecuado.\n> - `tf.matmul()`: realiza la multiplicación matricial entre dos tensores. En el código, se utiliza para calcular el producto escalar entre las consultas (q) y las claves (k) transpuestas. La transposición de k se realiza automáticamente debido al argumento `transpose_b=True`. El resultado es una matriz que representa la relevancia entre cada par de consulta y clave. Esta función también se utiliza al fial para realizar la multiplicación matricial, pero en este caso se utiliza para multiplicar los pesos de atención normalizados (softmax) por los valores (v). Esto resulta en una combinación ponderada de los valores, donde cada valor es escalado por su peso de atención correspondiente.\n> - `softmax()`: es una capa de softmax aplicada a una matriz. En el código, se utiliza para calcular los pesos de atención normalizados. La función softmax convierte los valores de la matriz en una distribución de probabilidad, donde cada valor representa la importancia relativa de la clave correspondiente para una consulta dada.","metadata":{"papermill":{"duration":0.025127,"end_time":"2023-07-02T11:33:53.713795","exception":false,"start_time":"2023-07-02T11:33:53.688668","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"> Ejemplo: Explicación de consultas, claves y valores en un Transformer con atención multi cabeza. Supongamos que tenemos la siguiente oración: `\"El gato está durmiendo en la alfombra\"`.El proceso de atención multi cabeza funcionaría así:\n\n1. Primero, dividimos la oración en vectores de palabras o tokens:\n\n    - **Consultas:**\n        - \"El\"\n        - \"gato\"\n        - \"está\"\n        - \"durmiendo\"\n        - \"en\"\n        - \"la\"\n        - \"alfombra\"\n\n    - **Claves:**\n        - \"El\"\n        - \"gato\"\n        - \"está\"\n        - \"durmiendo\"\n        - \"en\"\n        - \"la\"\n        - \"alfombra\"\n\n    - **Valores:**\n        - Vector de información correspondiente a cada palabra.\n\n2. A continuación, calculamos la similitud (producto escalar, por ejemplo) entre cada consulta y cada clave para obtener los pesos de atención.\n\n3. Los pesos de atención nos indican qué palabras son más relevantes para cada consulta. Estos pesos se aplican a los valores correspondientes a las palabras en la entrada para obtener el resultado final.\n\n4. Finalmente, la atención multi cabeza combina las representaciones ponderadas de las palabras para obtener una representación de alta calidad para la oración original.","metadata":{}},{"cell_type":"code","source":"# based on: https://stackoverflow.com/questions/67342988/verifying-the-implementation-of-multihead-attention-in-transformer\n# replaced softmax with softmax layer to support masked softmax\nclass MultiHeadAttention(tf.keras.layers.Layer):\n    def __init__(self, d_model, n_heads, dropout, d_out=None):\n        super(MultiHeadAttention,self).__init__()\n        # Number of Units in Model\n        self.d_model = d_model\n        # Number of Attention Heads\n        self.n_heads = n_heads\n        # Number of Units in Intermediate Layers\n        self.depth = d_model // 2\n        # Scaling Factor Of Values\n        self.scale = 1.0 / tf.math.sqrt(tf.cast(self.depth, tf.float32))\n        # Learnable Projections to Depth\n        self.wq = self.fused_mha(self.depth)\n        self.wk = self.fused_mha(self.depth)\n        self.wv = self.fused_mha(self.depth)\n        # Output Projection\n        self.wo = tf.keras.layers.Dense(d_model if d_out is None else d_out, use_bias=False)\n        # Softmax Activation Which Supports Masking\n        self.softmax = tf.keras.layers.Softmax()\n        # Reshaping Of Multiple Attention heads to Single Value\n        self.reshape = tf.keras.Sequential([\n            # [attention heads, number of frames, d_model] → [number of frames, n_heads, d_model // n_heads]\n            tf.keras.layers.Permute([2, 1, 3]),\n            # [number of frames, attention heads, d_model] → [number of frames, d_model]\n            tf.keras.layers.Reshape([N_TARGET_FRAMES, self.depth]),\n        ])\n        # Output Dropout\n        self.do = tf.keras.layers.Dropout(dropout)\n        self.supports_masking = True\n        \n    # Single dense layer for all attention heads\n    def fused_mha(self, dim):\n        return tf.keras.Sequential([\n            # Single dense layer\n            tf.keras.layers.Dense(dim, use_bias=False),\n            # Reshape to [number of frames, number of attention head, depth]\n            tf.keras.layers.Reshape([N_TARGET_FRAMES, self.n_heads, dim // self.n_heads]),\n            # Permutate to [number of attention heads, number of frames, depth]\n            tf.keras.layers.Permute([2, 1, 3]),\n        ])\n        \n    def call(self, q, k, v, attention_mask=None, training=False):\n        # Projections to attention heads\n        Q = self.wq(q)\n        K = self.wk(k)\n        V = self.wv(v)\n        # Matrix multiply QxK to acquire attention scores\n        x = tf.matmul(Q, K, transpose_b=True) * self.scale\n        # Softmax attention scores and Multiply with Values\n        x = self.softmax(x, mask=attention_mask) @ V\n        # Reshape to flatten attention heads\n        x = self.reshape(x)\n        # Output projection\n        x = self.wo(x)\n        # Dropout\n        x = self.do(x, training=training)\n        return x","metadata":{"papermill":{"duration":0.04073,"end_time":"2023-07-02T11:33:53.779389","exception":false,"start_time":"2023-07-02T11:33:53.738659","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.400666Z","iopub.execute_input":"2023-07-31T07:32:43.401087Z","iopub.status.idle":"2023-07-31T07:32:43.414645Z","shell.execute_reply.started":"2023-07-31T07:32:43.401052Z","shell.execute_reply":"2023-07-31T07:32:43.413538Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Encoder\n\n[source](https://keras.io/examples/nlp/neural_machine_translation_with_transformer/)\n\nAhora, crearemos un Encoder basado en bloques de mecanismos de atención (transformer blocks). Un Encoder toma una secuencia de entrada `x`  y la transforma en una representación vectorial, que luego puede ser utilizada para tareas posteriores como la traducción o la generación de texto. Procedemos a explicar la clas`Encoder`:\n\n\n- Se define una clase llamada `Encoder`, que hereda de `tf.keras.Model`. Esta clase representa el componente del Encoder en un modelo basado en la arquitectura Transformer.\n- En el constructor `__init__`, se inicializa el Encoder con el número de bloques de atención (`num_blocks`) que se utilizarán en el proceso de codificación.\n- El método `build` se encarga de crear los componentes necesarios para los bloques de atención. Estos componentes son:\n  - Listas `ln_1s`, `mhas`, `ln_2s` y `mlps` que almacenarán capas de normalización, atención multi-cabeza, capas de normalización adicionales y perceptrones multicapa, respectivamente. Se crea una lista para cada bloque de atención.\n  - Para cada bloque de atención, se crea una capa de normalización (`tf.keras.layers.LayerNormalization`) llamada `ln_1`, una capa de atención multi-cabeza llamada `mha`, otra capa de normalización `ln_2` y un perceptrón multicapa `mlp`.\n  - También se verifica si la dimensión del Encoder es diferente a la dimensión del Decoder. Si es así, se crea una capa de proyección (`dense_out`) para adaptar la dimensión de salida del Encoder a la dimensión del Decoder.\n- El método `call` se utiliza para realizar la codificación de la entrada (`x`). Toma como entrada `x` y una variable `x_inp` que contiene información sobre la secuencia de entrada (como máscaras de atención). Aquí se realiza el procesamiento en cada bloque de atención:\n    - Se crea una \"máscara de atención\" (attention_mask) para evitar que el modelo preste atención a partes no válidas o rellenas de la secuencia de entrada. Esta máscara se calcula a partir de `x_inp`, que es una variable que contiene información sobre la secuencia de entrada.\n    - Se itera sobre los bloques de atención definidos previamente (`ln_1`, `mha`, `ln_2` y `mlp`) y se aplican en secuencia a la entrada x.\n    - En cada iteración, primero se aplica una normalización (`ln_1`) sobre la entrada x más el resultado de la atención multi-cabeza (`mha`). Esto se conoce como una \"conexión residual\", donde la entrada original se suma a la salida de la atención multi-cabeza antes de aplicar la normalización. Esta técnica ayuda a evitar problemas de desvanecimiento del gradiente y mejora el flujo de información.\n    - Luego, se aplica otra normalización (`ln_2`) sobre la salida de la conexión residual más el resultado del perceptrón multicapa (`mlp`).\n    - Después de pasar por todos los bloques de atención y perceptron, si la dimensión del Encoder es diferente a la dimensión del Decoder (`UNITS_ENCODER != UNITS_DECODER`), se realiza una proyección (`dense_out`) para adaptar la dimensión de salida del Encoder a la dimensión del Decoder.\n    - Finalmente, la función devuelve la representación vectorial resultante después de aplicar todos los bloques de atención y el perceptrón multicapa.\n    \n    \n> Inciso: El perceptrón multicapa y el embedding son conceptos diferentes utilizados en el campo del aprendizaje automático y el procesamiento del lenguaje natural . A continuación, explicaremos y destacaremos sus diferencias:   \n\nEl perceptrón multicapa (MLP), o red neuronal con alimentación hacia adelante, es una arquitectura de red neuronal que consta de múltiples capas; incluidas al menos una capa de entrada, una o más capas ocultas y una capa de salida. Las neuronas de la capa se llaman nodos o perceptrones.\n\nEn el MLP, cada neurona de una capa se conecta con todas las neuronas de la capa anterior y posterior mediante conexiones ponderadas. Las conexiones ponderadas permiten que una neurona ajuste su \"sensibilidad\" a ciertos patrones en los datos de entrada. Durante el proceso de entrenamiento, los pesos se ajustan para minimizar el error entre las salidas predichas por la red y las salidas reales conocidas, en el caso del aprendizaje supervisado.\n\nLos pesos pueden ser positivos o negativos, lo que significa que pueden amplificar o reducir la contribución de una entrada en particular. Al aprender los pesos óptimos, la red neuronal puede encontrar patrones y relaciones relevantes en los datos de entrada, lo que le permite realizar tareas de clasificación, regresión u otras tareas específicas para las cuales ha sido entrenada. Durante el proceso de entrenamiento, los pesos de estas conexiones se ajustan para que el modelo pueda aprender a representar relaciones complejas y no lineales en los datos. \n\nEl propósito principal del MLP es aprender funciones no lineales para clasificación o regresión en conjuntos de datos complejos. A través de las capas ocultas y la no linealidad introducida por funciones de activación, el MLP puede aprender representaciones más complejas que las que se logran con modelos lineales.\n\nLa principal diferencia entre un perceptrón multicapa y un embedding radica en su propósito y función:\n\n- Perceptrón Multicapa (MLP): Es una arquitectura de red neuronal que se utiliza para aprender representaciones no lineales en conjuntos de datos complejos. Se aplica en tareas de clasificación y regresión en aprendizaje supervisado, y su objetivo es aprender funciones complejas a partir de características numéricas.\n- Embedding: Es una representación vectorial continua y densa de palabras o tokens en un espacio vectorial de baja dimensión. Se utiliza específicamente en NLP para mapear palabras o tokens desde el espacio de palabras (discreto) a un espacio vectorial (continuo). Su objetivo es capturar el significado y las relaciones semánticas entre las palabras en tareas de procesamiento del lenguaje natural.","metadata":{"papermill":{"duration":0.024245,"end_time":"2023-07-02T11:33:53.828275","exception":false,"start_time":"2023-07-02T11:33:53.80403","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Encoder based on multiple transformer blocks\nclass Encoder(tf.keras.Model):\n    def __init__(self, num_blocks):\n        super(Encoder, self).__init__(name='encoder')\n        self.num_blocks = num_blocks\n        self.supports_masking = True\n    \n    def build(self, input_shape):\n        self.ln_1s = []\n        self.mhas = []\n        self.ln_2s = []\n        self.mlps = []\n        # Make Transformer Blocks\n        for i in range(self.num_blocks):\n            # First Layer Normalisation\n            self.ln_1s.append(tf.keras.layers.LayerNormalization(epsilon=LAYER_NORM_EPS))\n            # Multi Head Attention\n            self.mhas.append(MultiHeadAttention(UNITS_ENCODER, NUM_HEADS, MHA_DROPOUT_RATIO))\n            # Second Layer Normalisation\n            self.ln_2s.append(tf.keras.layers.LayerNormalization(epsilon=LAYER_NORM_EPS))\n            # Multi Layer Perception\n            self.mlps.append(tf.keras.Sequential([\n                tf.keras.layers.Dense(UNITS_ENCODER * MLP_RATIO, activation=GELU, kernel_initializer=INIT_GLOROT_UNIFORM, use_bias=False),\n                tf.keras.layers.Dropout(MLP_DROPOUT_RATIO),\n                tf.keras.layers.Dense(UNITS_ENCODER, kernel_initializer=INIT_HE_UNIFORM, use_bias=False),\n            ]))\n            # Optional Projection to Decoder Dimension\n            if UNITS_ENCODER != UNITS_DECODER:\n                self.dense_out = tf.keras.layers.Dense(UNITS_DECODER, kernel_initializer=INIT_GLOROT_UNIFORM, use_bias=False)\n                self.apply_dense_out = True\n            else:\n                self.apply_dense_out = False\n                \n    def get_attention_mask(self, x_inp):\n        # Attention Mask\n        attention_mask = tf.math.count_nonzero(x_inp, axis=[2], keepdims=True, dtype=tf.int32)\n        attention_mask = tf.math.count_nonzero(attention_mask, axis=[2], keepdims=False)\n        attention_mask = tf.expand_dims(attention_mask, axis=1)\n        attention_mask = tf.expand_dims(attention_mask, axis=1)\n        return attention_mask\n        \n    def call(self, x, x_inp, training=False):\n        # Attention mask to ignore missing frames\n        attention_mask = self.get_attention_mask(x_inp)\n        # Iterate input over transformer blocks\n        for ln_1, mha, ln_2, mlp in zip(self.ln_1s, self.mhas, self.ln_2s, self.mlps):\n            x = ln_1(x + mha(x, x, x, attention_mask=attention_mask))\n            x = ln_2(x + mlp(x))\n            \n        # Optional Projection to Decoder Dimension\n        if self.apply_dense_out:\n            x = self.dense_out(x)\n    \n        return x","metadata":{"papermill":{"duration":0.041438,"end_time":"2023-07-02T11:33:53.894476","exception":false,"start_time":"2023-07-02T11:33:53.853038","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.416376Z","iopub.execute_input":"2023-07-31T07:32:43.416945Z","iopub.status.idle":"2023-07-31T07:32:43.43257Z","shell.execute_reply.started":"2023-07-31T07:32:43.416911Z","shell.execute_reply":"2023-07-31T07:32:43.431617Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Decoder\n\nDespués del `Encoder`, definimos la clase `Decoder`, que se encarga de utilizar las representaciones embeddings obtenidas por el Encoder para generar una secuencia de salida. Procedemos a desglosar los distintos pasos:\n\n- En el método `__init__`, se inicializa el modelo Decoder y se especifica el número de bloques de Transformer (`num_blocks`) que se utilizarán en la generación de la secuencia de salida. Además, se indica que el modelo es compatible con el enmascaramiento (`supports_masking = True`). El enmascaramiento es una técnica crucial que se utiliza para manejar secuencias de longitud variable durante el procesamiento.\n\n- En el método `build`, se construyen las capas necesarias para el modelo Decoder. Aquí es donde definimos las diferentes partes del modelo que se utilizarán durante el proceso de generación de la secuencia de salida. Estas capas son:\n\n - `positional_embedding`: Esta es la capa de embedding posicional, que se utiliza para asignar una representación numérica a cada posición en la secuencia de salida. Es decir, asigna una representación única a cada palabra o token en la secuencia.\n - `char_emb`: Esta es la capa de embedding de caracteres, que convierte las secuencias de caracteres (frases) en representaciones numéricas. Es un paso fundamental en el procesamiento del texto, ya que las redes neuronales trabajan con números, no con texto en bruto.\n - `pos_emb_mha`: Esta es la capa de atención multi-cabeza con enmascaramiento causal para el embedding posicional. La atención con enmascaramiento causal es una técnica que ayuda al modelo a aprender relaciones de dependencia temporal mientras genera la secuencia de salida. Es decir, permite que el modelo \"mire\" solo las palabras anteriores (o a sí mismo) durante la generación de la secuencia.\n - `pos_emb_ln`: Esta es la capa de normalización que se aplica después de la atención con enmascaramiento causal. La normalización es una técnica que ayuda a estabilizar y acelerar el entrenamiento del modelo.\n - `ln_1s`, `mhas`, `ln_2s`, y `mlps`: Estas son listas que contienen varias capas de normalización, atención multi-cabeza y capas de perceptrón multicapa (MLP), respectivamente. Se construyen `num_blocks` de estas capas para permitir que el Decoder realice múltiples iteraciones y capte relaciones más complejas en la secuencia de salida generada.\n\n- En el método `get_causal_attention_mask`, se crea una máscara de atención causal. Esta máscara se utiliza en la capa de atención con enmascaramiento causal para asegurar que cada posición solo atienda a las posiciones anteriores (o a sí misma), evitando que se produzca información del futuro durante la generación de la secuencia.\n\n- En el método `call`, se realiza el procesamiento de la secuencia de salida a través de los bloques de Transformer en el Decoder. A continuación, se describen los pasos:\n\n1. Se realiza el preprocesamiento de la secuencia de entrada (`phrase`) para convertirla en representaciones numéricas (embedding) utilizando la capa de embedding de caracteres (`char_emb`). Además, se agrega el token especial de inicio (`START_TOKEN`) al principio de cada secuencia y se añaden tokens de relleno (`PAD_TOKEN`) al final para alcanzar la longitud máxima deseada (`MAX_PHRASE_LENGTH`).\n\n2. Se crea la máscara de atención causal utilizando el método `get_causal_attention_mask` para evitar que el modelo acceda a información del futuro durante la generación de la secuencia de salida.\n\n3. Se obtienen las representaciones embeddings iniciales sumando la capa de embedding posicional (`positional_embedding`) y la capa de embedding de caracteres (`char_emb`) de la secuencia de entrada (`phrase`). Esta suma combina información espacial (posicional) y semántica (embedding de caracteres) para cada palabra en la secuencia.\n\n4. Se aplica una capa de normalización (`pos_emb_ln`) a las representaciones embeddings iniciales después de la atención con enmascaramiento causal (`pos_emb_mha`). La normalización ayuda a que el modelo se entrene de manera más estable y eficiente.\n\n5. Se iteran los bloques de Transformer (capas de normalización, atención multi-cabeza y MLP) a través de un bucle `for`. Cada bloque es responsable de procesar y mejorar las representaciones embeddings durante la generación de la secuencia de salida.\n\n6. Finalmente, se recorta la secuencia generada para eliminar cualquier token de relleno (`PAD_TOKEN`) y se obtiene la salida del modelo Decoder con una longitud de `MAX_PHRASE_LENGTH`.\n\nEn resumen, el modelo Decoder utiliza múltiples bloques de Transformer para generar una secuencia de salida basada en la información del Encoder. Aplica atención con enmascaramiento causal para capturar dependencias temporales y utiliza técnicas de normalización y MLP para procesar y mejorar las representaciones embeddings durante la generación de la secuencia. El objetivo es que el modelo genere secuencias de texto significativas y coherentes a partir de la información del Encoder.","metadata":{"papermill":{"duration":0.024624,"end_time":"2023-07-02T11:33:53.943518","exception":false,"start_time":"2023-07-02T11:33:53.918894","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Decoder based on multiple transformer blocks\nclass Decoder(tf.keras.Model):\n    def __init__(self, num_blocks):\n        super(Decoder, self).__init__(name='decoder')\n        self.num_blocks = num_blocks\n        self.supports_masking = True\n    \n    def build(self, input_shape):\n        # Causal Mask Batch Size 1\n        self.causal_mask = self.get_causal_attention_mask()\n        # Positional Embedding, initialized with zeros\n        self.positional_embedding = tf.Variable(\n            initial_value=tf.zeros([N_TARGET_FRAMES, UNITS_DECODER], dtype=tf.float32),\n            trainable=True,\n            name='embedding_positional_encoder',\n        )\n        # Character Embedding\n        self.char_emb = tf.keras.layers.Embedding(N_UNIQUE_CHARACTERS, UNITS_DECODER, embeddings_initializer=INIT_ZEROS)\n        # Positional Encoder MHA\n        self.pos_emb_mha = MultiHeadAttention(UNITS_DECODER, NUM_HEADS, MHA_DROPOUT_RATIO)\n        self.pos_emb_ln = tf.keras.layers.LayerNormalization(epsilon=LAYER_NORM_EPS)\n        # First Layer Normalisation\n        self.ln_1s = []\n        self.mhas = []\n        self.ln_2s = []\n        self.mlps = []\n        # Make Transformer Blocks\n        for i in range(self.num_blocks):\n            # First Layer Normalisation\n            self.ln_1s.append(tf.keras.layers.LayerNormalization(epsilon=LAYER_NORM_EPS))\n            # Multi Head Attention\n            self.mhas.append(MultiHeadAttention(UNITS_DECODER, NUM_HEADS, MHA_DROPOUT_RATIO))\n            # Second Layer Normalisation\n            self.ln_2s.append(tf.keras.layers.LayerNormalization(epsilon=LAYER_NORM_EPS))\n            # Multi Layer Perception\n            self.mlps.append(tf.keras.Sequential([\n                tf.keras.layers.Dense(UNITS_DECODER * MLP_RATIO, activation=GELU, kernel_initializer=INIT_GLOROT_UNIFORM, use_bias=False),\n                tf.keras.layers.Dropout(MLP_DROPOUT_RATIO),\n                tf.keras.layers.Dense(UNITS_DECODER, kernel_initializer=INIT_HE_UNIFORM, use_bias=False),\n            ]))\n            \n    def get_causal_attention_mask(self):\n        i = tf.range(N_TARGET_FRAMES)[:, tf.newaxis]\n        j = tf.range(N_TARGET_FRAMES)\n        mask = tf.cast(i >= j, dtype=tf.int32)\n        mask = tf.reshape(mask, (1, N_TARGET_FRAMES, N_TARGET_FRAMES))\n        mult = tf.concat(\n            [tf.expand_dims(1, -1), tf.constant([1, 1], dtype=tf.int32)],\n            axis=0,\n        )\n        mask = tf.tile(mask, mult)\n        mask = tf.cast(mask, tf.float32)\n        return mask\n    \n    def get_attention_mask(self, x_inp):\n        # Attention Mask\n        attention_mask = tf.math.count_nonzero(x_inp, axis=[2], keepdims=True, dtype=tf.int32)\n        attention_mask = tf.math.count_nonzero(attention_mask, axis=[2], keepdims=False)\n        attention_mask = tf.expand_dims(attention_mask, axis=1)\n        attention_mask = tf.expand_dims(attention_mask, axis=1)\n        return attention_mask\n        \n    def call(self, encoder_outputs, phrase, x_inp, training=False):\n        # Batch Size\n        B = tf.shape(encoder_outputs)[0]\n        # Cast to INT32\n        phrase = tf.cast(phrase, tf.int32)\n        # Prepend SOS Token\n        phrase = tf.pad(phrase, [[0,0], [1,0]], constant_values=START_TOKEN, name='prepend_sos_token')\n        # Pad With PAD Token\n        phrase = tf.pad(phrase, [[0,0], [0,N_TARGET_FRAMES-MAX_PHRASE_LENGTH-1]], constant_values=PAD_TOKEN, name='append_pad_token')\n        # Positional Embedding\n        x = self.positional_embedding + self.char_emb(phrase)\n        # Causal Attention\n        x = self.pos_emb_ln(x + self.pos_emb_mha(x, x, x, attention_mask=self.causal_mask))\n        # Attention mask to ignore missing frames\n        attention_mask = self.get_attention_mask(x_inp)\n        # Iterate input over transformer blocks\n        for ln_1, mha, ln_2, mlp in zip(self.ln_1s, self.mhas, self.ln_2s, self.mlps):\n            x = ln_1(x + mha(x, encoder_outputs, encoder_outputs, attention_mask=attention_mask))\n            x = ln_2(x + mlp(x))\n        # Slice 31 Characters\n        x = tf.slice(x, [0, 0, 0], [-1, MAX_PHRASE_LENGTH, -1])\n    \n        return x","metadata":{"papermill":{"duration":0.046582,"end_time":"2023-07-02T11:33:54.014771","exception":false,"start_time":"2023-07-02T11:33:53.968189","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.433947Z","iopub.execute_input":"2023-07-31T07:32:43.434836Z","iopub.status.idle":"2023-07-31T07:32:43.456719Z","shell.execute_reply.started":"2023-07-31T07:32:43.434805Z","shell.execute_reply":"2023-07-31T07:32:43.455443Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Creamos la máscara de atención causal fuera de la clase `Decoder `. La máscara de atención causal tiene como objetivo evitar que el modelo de Decoder acceda a información del futuro durante la generación de la secuencia de salida. En las tareas de generación de texto, traducción automática u otras tareas donde la secuencia de salida se genera de manera autoregresiva, es esencial que el modelo solo tenga acceso a la información de las posiciones anteriores a la que está prediciendo en ese momento. De lo contrario, el modelo podría utilizar información del futuro para hacer predicciones, lo que llevaría a resultados incoherentes y poco realistas (data leakage).\n\n- Esta función toma un único parámetro `B`. La función crea una secuencia numérica de longitud `N_TARGET_FRAMES` utilizando `tf.range(N_TARGET_FRAMES)`. Luego, se agregan dimensiones adicionales a esta secuencia mediante `[:, tf.newaxis]` para convertirla en un vector columna.\n\n- Se realiza un producto cartesiano entre la secuencia creada en el paso 2 y la secuencia original utilizando `i >= j`. Esto crea una matriz de ceros y unos, donde los elementos son cero en todas las posiciones que corresponden a posiciones futuras (columnas) en relación con la posición actual (fila).\n\n- La matriz de ceros y unos se remodela para tener una forma de `(1, N_TARGET_FRAMES, N_TARGET_FRAMES)`. La dimensión adicional al principio es necesaria para su uso posterior en el cálculo de la máscara final.\n\n- El valor de `B` se concatena con una constante `[1, 1]` mediante `tf.concat` para crear un tensor que especifica cuántas veces se replicará la máscara para manejar el tamaño del lote (batch) en el modelo.\n\n- Se utiliza `tf.tile` para replicar la máscara de atención causal `B` veces, donde `B` es el tamaño del lote (batch). Esto se hace para que la máscara pueda aplicarse a todos los elementos del lote en el modelo.\n\n- La máscara resultante se convierte en un tensor de tipo `float32` mediante `tf.cast` para asegurarse de que sea compatible con el cálculo de atención en TensorFlow. Finalmente, la función devuelve la máscara de atención causal resultante.","metadata":{}},{"cell_type":"code","source":"# Causal Attention to make decoder not attent to future characters which it needs to predict\ndef get_causal_attention_mask(B):\n    i = tf.range(N_TARGET_FRAMES)[:, tf.newaxis]\n    j = tf.range(N_TARGET_FRAMES)\n    mask = tf.cast(i >= j, dtype=tf.int32)\n    mask = tf.reshape(mask, (1, N_TARGET_FRAMES, N_TARGET_FRAMES))\n    mult = tf.concat(\n        [tf.expand_dims(B, -1), tf.constant([1, 1], dtype=tf.int32)],\n        axis=0,\n    )\n    mask = tf.tile(mask, mult)\n    mask = tf.cast(mask, tf.float32)\n    return mask\n\nget_causal_attention_mask(1)","metadata":{"papermill":{"duration":0.094639,"end_time":"2023-07-02T11:33:54.137348","exception":false,"start_time":"2023-07-02T11:33:54.042709","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.459637Z","iopub.execute_input":"2023-07-31T07:32:43.459988Z","iopub.status.idle":"2023-07-31T07:32:43.504683Z","shell.execute_reply.started":"2023-07-31T07:32:43.459954Z","shell.execute_reply":"2023-07-31T07:32:43.503823Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Procedemos a ver ahora paso por paso el proceso de la máscara de atención:","metadata":{}},{"cell_type":"code","source":"B = 1\ni = tf.range(N_TARGET_FRAMES)[:, tf.newaxis]\nj = tf.range(N_TARGET_FRAMES)\nprint(i[:5])\nprint(j)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.507671Z","iopub.execute_input":"2023-07-31T07:32:43.507941Z","iopub.status.idle":"2023-07-31T07:32:43.51951Z","shell.execute_reply.started":"2023-07-31T07:32:43.507917Z","shell.execute_reply":"2023-07-31T07:32:43.518462Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ahora, al realizar la operación `tf.cast(i >= j, dtype=tf.int32)`, en cada elemento `i` de los 128, obtendremos un array de tamaño 128 con 1 uno (`i=j`) y el resto ceros. Al repetir la operación a lo largo de `i` tendremos 128 elementos de tamaño 128","metadata":{}},{"cell_type":"code","source":"mask = tf.cast(i >= j, dtype=tf.int32)\nprint(mask)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.521202Z","iopub.execute_input":"2023-07-31T07:32:43.521782Z","iopub.status.idle":"2023-07-31T07:32:43.528505Z","shell.execute_reply.started":"2023-07-31T07:32:43.521749Z","shell.execute_reply":"2023-07-31T07:32:43.527458Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Redimensionamos el tensor añadiendole una dimensión a la izquierda y creamos `mult` para replicar mask `B` veces. Como en este caso `B=1`, no cambiará nada. `expand_dims` expande en una dimensión `B` en la posición que marque el segundo argumento. En este caso, añade una dimensión a `B` a la derecha, por lo que pasa a ser un array `[1]` en vez de un elemento `1`.","metadata":{}},{"cell_type":"code","source":"tf.expand_dims(B, -1)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.530078Z","iopub.execute_input":"2023-07-31T07:32:43.530489Z","iopub.status.idle":"2023-07-31T07:32:43.542366Z","shell.execute_reply.started":"2023-07-31T07:32:43.530457Z","shell.execute_reply":"2023-07-31T07:32:43.541319Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"mask = tf.reshape(mask, (1, N_TARGET_FRAMES, N_TARGET_FRAMES)) # reshape [128,128] to [1,128,128]  \nmult = tf.concat(\n        [tf.expand_dims(B, -1), tf.constant([1, 1], dtype=tf.int32)],\n        axis=0,)\n\nprint(mult)\n\nmask = tf.tile(mask, mult)\nprint(mask)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.543721Z","iopub.execute_input":"2023-07-31T07:32:43.544446Z","iopub.status.idle":"2023-07-31T07:32:43.55358Z","shell.execute_reply.started":"2023-07-31T07:32:43.544415Z","shell.execute_reply":"2023-07-31T07:32:43.552526Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"mask = tf.cast(mask, tf.float32) # turn into float\nmask","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.554984Z","iopub.execute_input":"2023-07-31T07:32:43.555816Z","iopub.status.idle":"2023-07-31T07:32:43.565507Z","shell.execute_reply.started":"2023-07-31T07:32:43.555783Z","shell.execute_reply":"2023-07-31T07:32:43.564409Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Non Pad/START/END Token Accuracy\n\nDefinimos ahora la clase `TopKAccuracy`. Se trata de una métrica personalizada que hereda de `tf.keras.metrics.Metric`. Su propósito es el de calcular la precisión top-k de un modelo con una salida de varias dimensiones.\n\nEn lugar de simplemente comparar la clase predicha por el modelo con la clase verdadera, el Top-K Accuracy considera si la clase verdadera se encuentra dentro de las K clases más probables predichas por el modelo.\n\nImagina que tienes un modelo de clasificación que tiene que predecir la categoría correcta de una imagen. Si el Top-K es 1 (Top-1 Accuracy), entonces el modelo debe predecir la categoría correcta como la de mayor probabilidad. Sin embargo, si el Top-K es 5 (Top-5 Accuracy), el modelo se considera acertado si la categoría correcta está dentro de las 5 categorías con mayores probabilidades predichas por el modelo.\n\nLa métrica se calcula de la siguiente manera:\n\n- Para cada muestra o ejemplo en el conjunto de datos, el modelo genera una distribución de probabilidad sobre todas las clases posibles.\n- El modelo selecciona las K clases con las mayores probabilidades y verifica si la clase verdadera está dentro de estas K clases.\n- Si la clase verdadera está entre las K clases más probables, entonces el modelo ha acertado esa muestra para el cálculo del Top-K Accuracy.\n- La métrica Top-K Accuracy se calcula tomando el porcentaje de muestras donde el modelo ha acertado en predecir la clase verdadera dentro de las K clases más probables.\n- El valor de K en el Top-K Accuracy permite ajustar el nivel de exigencia del modelo. Un Top-K con un valor alto (por ejemplo, Top-5 o Top-10) puede ser útil cuando algunas tareas de clasificación son difíciles y hay varias clases posibles cercanas en términos de probabilidad. Por otro lado, un Top-K con un valor bajo (como Top-1) es más estricto y solo considera la clase con la mayor probabilidad predicha.\n\nLos pasos son los siguientes:\n\n- En el método `__init__`, se inicializa la clase y recibe un parámetro `k`, que indica el valor de k en la precisión top-k. Por ejemplo, si `k=1`, se calculará la precisión top-1 (también conocida como precisión en el ranking de mayor probabilidad). Dentro del método __init__, se crea una instancia de `tf.keras.metrics.SparseTopKCategoricalAccuracy`, que es una métrica incorporada de TensorFlow que calcula la precisión top-k para salidas categóricas dispersas. Esta instancia se guarda en el atributo `self.top_k_acc` para su uso posterior.\n\n- En el método `update_state`, se realiza el cálculo de la precisión top-k actualizando el estado de la métrica. Este método toma tres argumentos:\n    - `y_true`: La etiqueta verdadera del modelo, que contiene los índices de las clases correctas para cada muestra.\n    - `y_pred`: La salida predicha por el modelo, que contiene las probabilidades de las clases para cada muestra.\n    - `sample_weight`: Pesos opcionales para las muestras.\n\n    - Se realiza una serie de transformaciones para preparar los datos para el cálculo de la precisión top-k:\n        - Ambos, `y_true` y `y_pred`, se redimensionan para convertirse en tensores unidimensionales. Esto se hace para asegurar que los datos estén en el formato correcto para la métrica de precisión top-k.\n        - Se obtienen los índices de aquellos `y_true` que son menores que `N_UNIQUE_CHARACTERS` (recordemos que había un total de 62 token o caracteres distintos). Esto se hace para filtrar solo los índices válidos en el cálculo de la métrica, ya que podría haber índices fuera del rango válido en ciertos casos.\n        - Se utiliza `tf.gather` para seleccionar solo las muestras correspondientes a los índices válidos tanto en `y_true` como en `y_pred`.\n\n    - Finalmente, se llama al método `update_state` de `self.top_k_acc`, pasándole los datos preprocesados `y_true` e `y_pred` para realizar el cálculo real de la precisión top-k.\n\n- Los métodos `result` y `reset_state` simplemente devuelven el resultado de la precisión top-k actual y restablecen el estado de la métrica, respectivamente.","metadata":{"papermill":{"duration":0.024935,"end_time":"2023-07-02T11:33:54.189242","exception":false,"start_time":"2023-07-02T11:33:54.164307","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# TopK accuracy for multi dimensional output\nclass TopKAccuracy(tf.keras.metrics.Metric):\n    def __init__(self, k, **kwargs):\n        super(TopKAccuracy, self).__init__(name=f'top{k}acc', **kwargs)\n        self.top_k_acc = tf.keras.metrics.SparseTopKCategoricalAccuracy(k=k)\n\n    def update_state(self, y_true, y_pred, sample_weight=None):\n        y_true = tf.reshape(y_true, [-1])\n        y_pred = tf.reshape(y_pred, [-1, N_UNIQUE_CHARACTERS])\n        character_idxs = tf.where(y_true < N_UNIQUE_CHARACTERS0)\n        y_true = tf.gather(y_true, character_idxs, axis=0)\n        y_pred = tf.gather(y_pred, character_idxs, axis=0)\n        self.top_k_acc.update_state(y_true, y_pred)\n\n    def result(self):\n        return self.top_k_acc.result()\n    \n    def reset_state(self):\n        self.top_k_acc.reset_state()","metadata":{"papermill":{"duration":0.037227,"end_time":"2023-07-02T11:33:54.251228","exception":false,"start_time":"2023-07-02T11:33:54.214001","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.566878Z","iopub.execute_input":"2023-07-31T07:32:43.567669Z","iopub.status.idle":"2023-07-31T07:32:43.577071Z","shell.execute_reply.started":"2023-07-31T07:32:43.567633Z","shell.execute_reply":"2023-07-31T07:32:43.576265Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Loss Weights\n\n","metadata":{"papermill":{"duration":0.024945,"end_time":"2023-07-02T11:33:54.301243","exception":false,"start_time":"2023-07-02T11:33:54.276298","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Create Initial Loss Weights All Set To 1\nloss_weights = np.ones(N_UNIQUE_CHARACTERS, dtype=np.float32)\n# Set Loss Weight Of Pad Token To 0\nloss_weights[PAD_TOKEN] = 0","metadata":{"papermill":{"duration":0.033995,"end_time":"2023-07-02T11:33:54.360318","exception":false,"start_time":"2023-07-02T11:33:54.326323","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.579062Z","iopub.execute_input":"2023-07-31T07:32:43.58024Z","iopub.status.idle":"2023-07-31T07:32:43.591739Z","shell.execute_reply.started":"2023-07-31T07:32:43.58019Z","shell.execute_reply":"2023-07-31T07:32:43.591105Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Sparse Categorical Crossentropy With Label Smoothing¶\n\nAhora, creamos la función `scce_with_ls`, que calcula la pérdida utilizando la función de pérdida `categorical_crossentropy` con soporte para el suavizado de etiquetas. Esta técnica se aplica para evitar que el modelo se vuelva demasiado confiado y \"sobreconfiado\" en sus predicciones.\n\nCuando entrenamos un modelo de clasificación, normalmente usamos etiquetas o salidas objetivo en formato \"one-hot\". En formato \"one-hot\", cada muestra tiene una etiqueta que es un vector binario con un \"1\" en la posición correspondiente a la clase verdadera y \"0\" en todas las demás posiciones. Por ejemplo, si tenemos un problema de clasificación con 3 clases, una muestra perteneciente a la clase 2 tendría una etiqueta `[0, 1, 0]`.\n\nEl suavizado de etiquetas consiste en disminuir la confianza absoluta del modelo en las etiquetas individuales, redistribuyendo una pequeña cantidad de probabilidad desde la clase verdadera a las otras clases. En lugar de utilizar \"1\" en la posición correspondiente a la clase verdadera, se utiliza un valor cercano a \"1\", y el resto del valor se distribuye entre las otras clases.\n\nPor ejemplo, si aplicamos un suavizado de etiquetas con un valor de suavizado (smoothing value) de 0.1 en el problema anterior, la etiqueta `[0, 1, 0]` podría convertirse en `[0.05, 0.9, 0.05]`. Esto significa que en lugar de estar completamente seguro de que la muestra pertenece a la clase 2, el modelo se vuelve un poco menos seguro y distribuye una pequeña cantidad de probabilidad a las otras clases. Este efecto generalmente resulta en una mejora en la generalización del modelo y evita el sobreajuste (overfitting).\n\n\nLa función contiene los siguientes elementos:\n\n1. `idxs = tf.where(y_true != PAD_TOKEN)`: En esta línea, se utiliza `tf.where` para encontrar las posiciones en `y_true` donde el valor es diferente del token de relleno (`PAD_TOKEN`). El objetivo es filtrar los índices que no corresponden a tokens de relleno, ya que no queremos considerarlos en el cálculo de la pérdida.\n\n2. `y_true = tf.gather_nd(y_true, idxs)`: Aquí, se utiliza `tf.gather_nd` para obtener los valores de las etiquetas de `y_true` que no son tokens de relleno. \n\n3. `y_pred = tf.gather_nd(y_pred, idxs)`: De manera similar, `tf.gather_nd` se utiliza para obtener las predicciones del modelo que corresponden a los índices filtrados de `y_true`. \n\n4. `y_true = tf.cast(y_true, tf.int32)`: Para el cálculo de la pérdida con `tf.keras.losses.categorical_crossentropy`, es necesario que `y_true` sea de tipo int32, por lo que se realiza una conversión.\n\n5. `y_true = tf.one_hot(y_true, N_UNIQUE_CHARACTERS, axis=1)`: La función `tf.one_hot` se utiliza para convertir las etiquetas `y_true` en una representación \"one-hot\".\n\n6. `loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred, label_smoothing=0.25, from_logits=True)`: Aquí se calcula la pérdida utilizando la función de pérdida `categorical_crossentropy`. La función tiene un parámetro opcional `label_smoothing`, que en este caso se establece en 0.25. El valor 0.25 indica cuánta \"confianza\" se debe agregar a la clase verdadera y se resta de la probabilidad de la clase verdadera y se suma a las otras clases en la pérdida. `from_logits=True` indica que `y_pred` no ha sido pasado por una función de activación softmax antes del cálculo de la pérdida.\n\n7. `loss = tf.math.reduce_mean(loss)`: Finalmente, la pérdida calculada en el paso anterior se reduce a un solo valor tomando su promedio utilizando `tf.math.reduce_mean`. Esto es necesario para obtener una única medida de pérdida que represente el rendimiento general del modelo en el conjunto de datos.","metadata":{"papermill":{"duration":0.024662,"end_time":"2023-07-02T11:33:54.409907","exception":false,"start_time":"2023-07-02T11:33:54.385245","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# source:: https://stackoverflow.com/questions/60689185/label-smoothing-for-sparse-categorical-crossentropy\ndef scce_with_ls(y_true, y_pred):\n    # Filter Pad Tokens\n    idxs = tf.where(y_true != PAD_TOKEN)\n    y_true = tf.gather_nd(y_true, idxs)\n    y_pred = tf.gather_nd(y_pred, idxs)\n    # One Hot Encode Sparsely Encoded Target Sign\n    y_true = tf.cast(y_true, tf.int32)\n    y_true = tf.one_hot(y_true, N_UNIQUE_CHARACTERS, axis=1)\n    # Categorical Crossentropy with native label smoothing support\n    loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred, label_smoothing=0.25, from_logits=True)\n    loss = tf.math.reduce_mean(loss)\n    return loss","metadata":{"papermill":{"duration":0.034321,"end_time":"2023-07-02T11:33:54.468872","exception":false,"start_time":"2023-07-02T11:33:54.434551","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:43.592943Z","iopub.execute_input":"2023-07-31T07:32:43.593923Z","iopub.status.idle":"2023-07-31T07:32:43.603197Z","shell.execute_reply.started":"2023-07-31T07:32:43.593888Z","shell.execute_reply":"2023-07-31T07:32:43.602554Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model\n\nUna vez definidos los distintos subelementos que conforman el modelo, creamos el modelo. \n\n- Dicho modelo tiene dos elementos de entrada `tf.keras.layers.Input` que corresponden a los elementos del diccionario `X_batch`. La primera entrada es para los datos de entrada llamados \"frames\" que hemos definido , que son secuencias de datos numéricos de forma `[N_TARGET_FRAMES, N_COLS]=[128,164]` . La segunda entrada es para los datos de entrada llamados \"phrase\", que son secuencias de palabras representadas por índices enteros de forma `[MAX_PHRASE_LENGTH]=32`.\n\n- Se inicia la construcción del modelo con los datos de entrada \"frames\" usando `x = frames_inp`. A continuación, se aplica una capa de `Masking` para ignorar valores nulos en los datos de entrada \"frames\". Esto se hace para lidiar con secuencias de longitud variable, donde los valores nulos representan puntos de tiempo sin información válida.\n\n- A continuación, se aplica la capa de `Embedding`. Acabaremos teniendo una dimensión de `[N,128,384]`.\n\n- Luego, se pasan los datos \"frames\" a través de bloques de Transformer usando la capa `Encoder(NUM_BLOCKS_ENCODER)(x, frames_inp)`. Los bloques de Transformer son responsables de procesar y extraer características relevantes de las secuencias de entrada.\n","metadata":{"papermill":{"duration":0.025057,"end_time":"2023-07-02T11:33:54.518693","exception":false,"start_time":"2023-07-02T11:33:54.493636","status":"completed"},"tags":[]}},{"cell_type":"code","source":"print(f' # elements in \"X_batch\": {len(X_batch)}') # frames and phrase\nprint(f' Shape of the first element \"frames\": {X_batch[\"frames\"].shape}') # [N_batch, TARGET_FRAMES, COLS] \nprint(f' Shape of the second element \"phrase\": {X_batch[\"phrase\"].shape}') # [N_batch, MAX_PHRASE_LENGTH]","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.604421Z","iopub.execute_input":"2023-07-31T07:32:43.60535Z","iopub.status.idle":"2023-07-31T07:32:43.615666Z","shell.execute_reply.started":"2023-07-31T07:32:43.605318Z","shell.execute_reply":"2023-07-31T07:32:43.614719Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Partimos de dos tensores con la misma forma que 'frames' y 'phrase'. Utilizamos la máscara para que no haya nulos y hacemos una representación densa de los datos, con Landmark Embedding y Embedding posicional. Luego, aplicamos el encoder para que aprenda y estudie los patrones","metadata":{}},{"cell_type":"code","source":"# Inputs\nframes_inp = tf.keras.layers.Input([N_TARGET_FRAMES, N_COLS], dtype=tf.float32, name='frames')\nphrase_inp = tf.keras.layers.Input([MAX_PHRASE_LENGTH], dtype=tf.int32, name='phrase')\n# Frames\nx = frames_inp\n\n# Masking to eliminate NaN values in frames\nx = tf.keras.layers.Masking(mask_value=0.0, input_shape=(N_TARGET_FRAMES, N_COLS))(x)\nprint(f' Input shape: {x.shape}')\n# Embedding\nx = Embedding()(x)\n\nprint(f' Shape post Embedding: {x.shape}')\n    \n# Encoder Transformer Blocks\nx = Encoder(NUM_BLOCKS_ENCODER)(x, frames_inp)\n\nprint(f' Shape post encoder: {x.shape}')\nprint(f' Units Decoder: {UNITS_DECODER}')","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:43.616994Z","iopub.execute_input":"2023-07-31T07:32:43.617673Z","iopub.status.idle":"2023-07-31T07:32:45.744332Z","shell.execute_reply.started":"2023-07-31T07:32:43.617642Z","shell.execute_reply":"2023-07-31T07:32:45.743343Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"- Después de pasar por los bloques del encoder, los datos se pasan a través de la capa `Decoder(NUM_BLOCKS_DECODER)(x, phrase_inp)`. Gracias a esta capa se generan secuencias de texto basadas en las características extraídas del encoder. \n\n- Luego, se agrega una capa `Sequential` que contiene una capa de `Dropout` para evitar el sobreajuste, seguida de una capa `Dense` que representa la capa de salida del modelo. Esta capa tiene una cantidad de neuronas igual a `N_UNIQUE_CHARACTERS`, que es el número total de clases o tokens posibles en el problema de generación de texto.\n\n- Los outputs del modelo son asignados a `x`.","metadata":{}},{"cell_type":"code","source":"# Decoder\nx = Decoder(NUM_BLOCKS_DECODER)(x, phrase_inp, frames_inp)\nprint(f'Shape post Decoder: {x.shape}')    \nprint(f'# of unique characters: {N_UNIQUE_CHARACTERS}')\n# Classifier\nx = tf.keras.Sequential([\n    # Dropout\n    tf.keras.layers.Dropout(CLASSIFIER_DROPOUT_RATIO), # CLASSIFIER_DROPOUT_RATIO = 0.1\n    # Output Neurons: 62 classes (different tokens)\n    tf.keras.layers.Dense(N_UNIQUE_CHARACTERS, activation=tf.keras.activations.linear,\n                          kernel_initializer=INIT_HE_UNIFORM, use_bias=False),\n    ], name='classifier')(x)\n    \noutputs = x","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:45.746026Z","iopub.execute_input":"2023-07-31T07:32:45.746469Z","iopub.status.idle":"2023-07-31T07:32:47.199137Z","shell.execute_reply.started":"2023-07-31T07:32:45.746434Z","shell.execute_reply":"2023-07-31T07:32:47.196057Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"- El modelo es construido utilizando `tf.keras.models.Model`, tomando las entradas y salidas definidas anteriormente.\n\n- Se define la función de pérdida `scce_with_ls`, que es la Categorical Crossentropy Loss con soporte para Label Smoothing, como la función de pérdida del modelo.\n\n- Se define el optimizador del modelo utilizando `RectifiedAdam`, una variante del optimizador Adam.\n\n- Se definen dos métricas de evaluación del modelo: la precisión Top-1 (`TopKAccuracy(1)`) y la precisión Top-5 (`TopKAccuracy(5)`).\n\n- El modelo se compila utilizando `model.compile`, especificando la función de pérdida, el optimizador, las métricas y los pesos de pérdida. Los pesos de pérdida son los que se definieron anteriormente y se utilizan para ajustar la contribución relativa de cada clase en el cálculo de la pérdida total del modelo.\n\nEl modelo que hemos construido se trata de seq2seq con atención, comúnmente utilizado en tareas de generación de texto y traducción automática.","metadata":{}},{"cell_type":"code","source":"# Create Tensorflow Model\nmodel = tf.keras.models.Model(inputs=[frames_inp, phrase_inp], outputs=outputs)\n    \n# Categorical Crossentropy Loss With Label Smoothing\nloss = scce_with_ls\n    \n# Adam Optimizer\noptimizer = tfa.optimizers.RectifiedAdam(sma_threshold=4)\noptimizer = tfa.optimizers.Lookahead(optimizer, sync_period=5)\n\n# TopK Metrics\nmetrics = [\n        TopKAccuracy(1),\n        TopKAccuracy(5),]\n    \nmodel.compile(\n    loss=loss,\n    optimizer=optimizer,\n    metrics=metrics,\n    loss_weights=loss_weights,\n    )\nmodel.summary()","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:47.202042Z","iopub.execute_input":"2023-07-31T07:32:47.202962Z","iopub.status.idle":"2023-07-31T07:32:47.333238Z","shell.execute_reply.started":"2023-07-31T07:32:47.202922Z","shell.execute_reply":"2023-07-31T07:32:47.331984Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Una vez vistos todos los elementos necesarios para la creación del modelo, definimos la función `get_model` que contiene todos ellos agrupados. ","metadata":{}},{"cell_type":"code","source":"def get_model():\n    # Inputs\n    frames_inp = tf.keras.layers.Input([N_TARGET_FRAMES, N_COLS], dtype=tf.float32, name='frames')\n    phrase_inp = tf.keras.layers.Input([MAX_PHRASE_LENGTH], dtype=tf.int32, name='phrase')\n    # Frames\n    x = frames_inp\n\n    # Masking\n    x = tf.keras.layers.Masking(mask_value=0.0, input_shape=(N_TARGET_FRAMES, N_COLS))(x)\n    \n    # Embedding\n    x = Embedding()(x)\n    \n    # Encoder Transformer Blocks\n    x = Encoder(NUM_BLOCKS_ENCODER)(x, frames_inp)\n    \n    # Decoder\n    x = Decoder(NUM_BLOCKS_DECODER)(x, phrase_inp, frames_inp)\n    \n    # Classifier\n    x = tf.keras.Sequential([\n        # Dropout\n        tf.keras.layers.Dropout(CLASSIFIER_DROPOUT_RATIO),\n        # Output Neurons\n        tf.keras.layers.Dense(N_UNIQUE_CHARACTERS, activation=tf.keras.activations.linear, kernel_initializer=INIT_HE_UNIFORM, use_bias=False),\n    ], name='classifier')(x)\n    \n    outputs = x\n    \n    # Create Tensorflow Model\n    model = tf.keras.models.Model(inputs=[frames_inp, phrase_inp], outputs=outputs)\n    \n    # Categorical Crossentropy Loss With Label Smoothing\n    loss = scce_with_ls\n    \n    # Adam Optimizer\n    optimizer = tfa.optimizers.RectifiedAdam(sma_threshold=4)\n    optimizer = tfa.optimizers.Lookahead(optimizer, sync_period=5)\n\n    # TopK Metrics\n    metrics = [\n        TopKAccuracy(1),\n        TopKAccuracy(5),\n    ]\n    \n    model.compile(\n        loss=loss,\n        optimizer=optimizer,\n        metrics=metrics,\n        loss_weights=loss_weights,\n    )\n    \n    return model","metadata":{"papermill":{"duration":0.03826,"end_time":"2023-07-02T11:33:54.581784","exception":false,"start_time":"2023-07-02T11:33:54.543524","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:47.335111Z","iopub.execute_input":"2023-07-31T07:32:47.335769Z","iopub.status.idle":"2023-07-31T07:32:47.347185Z","shell.execute_reply.started":"2023-07-31T07:32:47.335734Z","shell.execute_reply":"2023-07-31T07:32:47.346447Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Input data: X_batch['frames'] & X_batch['phrase']\nfor k, v in X_batch.items():\n    print(f'{k}: {v.shape}')","metadata":{"papermill":{"duration":0.035009,"end_time":"2023-07-02T11:33:54.641801","exception":false,"start_time":"2023-07-02T11:33:54.606792","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:47.34873Z","iopub.execute_input":"2023-07-31T07:32:47.349417Z","iopub.status.idle":"2023-07-31T07:32:47.364209Z","shell.execute_reply.started":"2023-07-31T07:32:47.349383Z","shell.execute_reply":"2023-07-31T07:32:47.363236Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"tf.keras.backend.clear_session()\n\nmodel = get_model()","metadata":{"papermill":{"duration":3.159973,"end_time":"2023-07-02T11:33:57.826278","exception":false,"start_time":"2023-07-02T11:33:54.666305","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:47.365715Z","iopub.execute_input":"2023-07-31T07:32:47.366363Z","iopub.status.idle":"2023-07-31T07:32:50.337292Z","shell.execute_reply.started":"2023-07-31T07:32:47.366329Z","shell.execute_reply":"2023-07-31T07:32:50.336319Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Plot model summary\nmodel.summary(expand_nested=True)","metadata":{"papermill":{"duration":0.237637,"end_time":"2023-07-02T11:33:58.088939","exception":false,"start_time":"2023-07-02T11:33:57.851302","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:50.339325Z","iopub.execute_input":"2023-07-31T07:32:50.33988Z","iopub.status.idle":"2023-07-31T07:32:50.49545Z","shell.execute_reply.started":"2023-07-31T07:32:50.339845Z","shell.execute_reply":"2023-07-31T07:32:50.49471Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Plot Model Architecture\ntf.keras.utils.plot_model(model, show_shapes=True, show_dtype=True, show_layer_names=True, expand_nested=True, show_layer_activations=True)","metadata":{"papermill":{"duration":0.307642,"end_time":"2023-07-02T11:33:58.432321","exception":false,"start_time":"2023-07-02T11:33:58.124679","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:50.496702Z","iopub.execute_input":"2023-07-31T07:32:50.497039Z","iopub.status.idle":"2023-07-31T07:32:50.796124Z","shell.execute_reply.started":"2023-07-31T07:32:50.497006Z","shell.execute_reply":"2023-07-31T07:32:50.795173Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Verify Training Flag\n\nComprobamos ahora si funciona correctamente:\n\nEn el primer assert se obtienen las predicciones del modelo utilizando el conjunto de datos de entrada `X_batch_small` con el indicador `training=False`. Esto significa que se está realizando una inferencia estática, donde el modelo no se actualizará ni cambiará durante este proceso. Las predicciones se almacenan en la variable `pred`.\n\nLuego, se realiza un bucle `for` diez veces para verificar si las predicciones son consistentes en múltiples ejecuciones de inferencia. Se hace esto comparando las predicciones originales con nuevas predicciones obtenidas utilizando nuevamente el conjunto de datos de entrada `X_batch_small` y el indicador de entrenamiento en `training=False`. Para esta comparación, se utiliza la función `tf.cast` para asegurarse de que las predicciones sean de tipo int8 (enteros de 8 bits) y luego se reduce el tensor resultante utilizando `tf.reduce_min`.\n\nLa función `tf.reduce_min` se utiliza para encontrar el valor mínimo en el tensor resultante, que debe ser igual a 1. Esto significa que todas las predicciones originales y las nuevas predicciones deben ser idénticas, lo que indica que el modelo produce resultados estáticos coherentes durante la inferencia.\n\nEn el segundo assert, nuevamente se obtienen las predicciones utilizando el conjunto de datos de entrada `X_batch_small`, pero esta vez con el indicador `training=True`. Esto simula el proceso de entrenamiento, donde el modelo utiliza la técnica de dropout para reducir el sobreajuste y mejorar la generalización. El dropout consiste en desactivar aleatoriamente algunas neuronas durante el entrenamiento para evitar que el modelo se vuelva demasiado dependiente de ciertas características específicas de los datos de entrenamiento.\n\nLuego, se realiza un bucle `for` diez veces para verificar si las predicciones varían significativamente en múltiples ejecuciones durante el entrenamiento. Para ello, se compara el tensor de predicciones originales (calculado en el paso 1) con nuevas predicciones obtenidas con el conjunto de datos `X_batch_small` y el indicador de entrenamiento en `True`. Se utiliza la función `tf.cast` para asegurarse de que las predicciones sean de tipo float32 (números de punto flotante) y luego se calcula la media utilizando `tf.reduce_mean`.\n\nSe espera que el valor medio resultante sea mayor que 0.99, lo que significa que al menos el 99% de las predicciones en cada ejecución durante el entrenamiento son diferentes entre sí debido al dropout aplicado. Esto es una indicación de que el modelo está utilizando diferentes subconjuntos de neuronas en diferentes ejecuciones, lo que es deseable para mejorar la generalización y reducir el sobreajuste.","metadata":{"papermill":{"duration":0.037937,"end_time":"2023-07-02T11:33:58.509486","exception":false,"start_time":"2023-07-02T11:33:58.471549","status":"completed"},"tags":[]}},{"cell_type":"code","source":"print(X_batch_small['frames'].shape)\nprint(X_batch_small['phrase'].shape)","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:32:50.797149Z","iopub.execute_input":"2023-07-31T07:32:50.797805Z","iopub.status.idle":"2023-07-31T07:32:50.804145Z","shell.execute_reply.started":"2023-07-31T07:32:50.797763Z","shell.execute_reply":"2023-07-31T07:32:50.803092Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def verify_correct_training_flag():\n    # Verify static output for inference\n    pred = model(X_batch_small, training=False)\n    for _ in tqdm(range(10)):\n        assert tf.reduce_min(tf.cast(pred == model(X_batch_small, training=False), tf.int8)) == 1\n\n    # Verify at least 99% varying output due to dropout during training\n    for _ in tqdm(range(10)):\n        assert tf.reduce_mean(tf.cast(pred != model(X_batch_small, training=True), tf.float32)) > 0.99\n        \nverify_correct_training_flag()","metadata":{"papermill":{"duration":5.527641,"end_time":"2023-07-02T11:34:04.075359","exception":false,"start_time":"2023-07-02T11:33:58.547718","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:50.805635Z","iopub.execute_input":"2023-07-31T07:32:50.806845Z","iopub.status.idle":"2023-07-31T07:32:54.42829Z","shell.execute_reply.started":"2023-07-31T07:32:50.806811Z","shell.execute_reply":"2023-07-31T07:32:54.427308Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Verify No NaN Predictions\n\nComprobamos ahora si hay valores NaN en las predicciones realizadas por el modelo.\n\nEl código comienza utilizando el método \"predict\" del modelo para obtener las predicciones del conjunto de datos de entrenamiento (\"train_dataset\"), ya que no se está utilizando el conjunto de validación. Se especifica la cantidad de pasos a realizar utilizando el parámetro `steps`, que se establece en `N_VAL_STEPS_PER_EPOCH` si se utiliza el conjunto de validación o en 100 si no se utiliza el conjunto de validación (usamos por tanto 100). El parámetro \"verbose\" se establece en 1.\n\nUna vez que se obtienen las predicciones, el código cuenta cuántos valores NaN hay en el resultado utilizando la función `np.isnan(y_pred).sum()`. Si el resultado de esta cuenta es mayor que cero, significa que hay valores NaN en las predicciones.\n\nLuego, el código muestra un histograma de las predicciones para visualizar la distribución de los logits. En este contexto, los logits son los valores de salida de una red neuronal antes de aplicar una función de activación, como la función softmax. Los logits representan las puntuaciones o puntajes asociados a cada clase en un problema de clasificación.\n\nPor ejemplo, supongamos que tienes un problema de clasificación con tres clases: \"perro\", \"gato\" y \"pájaro\". Después de entrenar una red neuronal, esta producirá un conjunto de valores numéricos como resultado para una entrada dada, que corresponderán a los logits para cada clase. Por ejemplo, podrías obtener [2.5, 1.8, 0.1] como logits para una imagen dada. Estos valores representan las puntuaciones o niveles de confianza que la red asigna a cada clase. En este caso, la red está más segura de que la imagen es un \"perro\" porque el valor más alto es 2.5, seguido de \"gato\" con 1.8 y \"pájaro\" con 0.1.\n\nDespués de obtener los logits, generalmente aplicamos una función de activación, como la función softmax, para convertir estos valores en probabilidades, de modo que sumen 1 y podamos interpretarlos como las probabilidades de que la entrada pertenezca a cada clase. En este ejemplo, después de aplicar la función softmax, podríamos obtener [0.64, 0.29, 0.07], lo que indica que la red tiene una probabilidad del 64% de que la imagen sea un \"perro\", del 29% de que sea un \"gato\" y del 7% de que sea un \"pájaro\".\n\nEsto es útil para comprender cómo se distribuyen las predicciones y si hay valores extremadamente altos o bajos que podrían indicar un problema.","metadata":{"papermill":{"duration":0.036771,"end_time":"2023-07-02T11:34:04.151722","exception":false,"start_time":"2023-07-02T11:34:04.114951","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Verify No NaN predictions\ndef verify_no_nan_predictions():\n    y_pred = model.predict(\n        val_dataset if USE_VAL else train_dataset,\n        steps=N_VAL_STEPS_PER_EPOCH if USE_VAL else 100,\n        verbose=VERBOSE,\n    )\n\n    print(f'# NaN Values In Predictions: {np.isnan(y_pred).sum()}')\n    \n    plt.figure(figsize=(15,8))\n    plt.title(f'Logit Predictions Initialized Model')\n    pd.Series(y_pred.flatten()).plot(kind='hist', bins=128)\n    plt.xlabel('Logits')\n    plt.grid()\n    plt.show()\n    \nverify_no_nan_predictions()","metadata":{"papermill":{"duration":9.257248,"end_time":"2023-07-02T11:34:13.446151","exception":false,"start_time":"2023-07-02T11:34:04.188903","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:32:54.429841Z","iopub.execute_input":"2023-07-31T07:32:54.430451Z","iopub.status.idle":"2023-07-31T07:33:03.348199Z","shell.execute_reply.started":"2023-07-31T07:32:54.430416Z","shell.execute_reply":"2023-07-31T07:33:03.347208Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Learning Rate Scheduler\n\nAhora, definimos la función llamada `lrfn`, que utilizaremos para calcular el learning rate durante el entrenamiento del modelo.\n\nLa función `lrfn` toma varios argumentos:\n\n- `current_step`: El paso de entrenamiento actual, que indica cuántos pasos se han completado hasta ahora en el entrenamiento del modelo.\n- `num_warmup_steps`: El número de pasos de entrenamiento que se utilizan para el calentamiento (`warm-up`) del `learning rate`. Durante el calentamiento, el `learning rate` aumenta gradualmente desde un valor muy pequeño hasta su valor máximo.\n- `lr_max`: El valor máximo que puede tomar el `learning rate` durante el entrenamiento. Después del calentamiento, el `learning rate` oscilará entre 0 y este valor máximo.\n- `num_cycles`: El número de ciclos completos de oscilación del `learning rate`. Un ciclo completo significa que el `learning rate` va desde su valor máximo hasta 0 y luego regresa a su valor máximo. Un valor de 0.50 indica que habrá medio ciclo completo.\n- `num_training_steps`: El número total de pasos de entrenamiento que se llevarán a cabo durante todo el entrenamiento del modelo.\n\nEl objetivo principal de la función `lrfn` es calcular el `learning rate` para cada paso de entrenamiento. Ello se divide en dos partes:\n\n1. Calentamiento del `Learning Rate`: Si el paso de entrenamiento actual es menor que el número de pasos de calentamiento (`num_warmup_steps`), entonces el `learning rate` aumentará gradualmente desde un valor muy pequeño hacia su valor máximo. Se utilizan dos métodos posibles de calentamiento: logarítmico (`'log'`) o exponencial (`2 ** -`). Esto permite que el modelo se adapte de manera más suave a los datos al comienzo del entrenamiento.\n2. Oscilación del `Learning Rate`: Después de los pasos de calentamiento, el `learning rate` oscilará entre 0 y su valor máximo (`lr_max`). La forma de esta oscilación sigue una función coseno modificada, que va desde 0 a 1 y luego vuelve a 0 a medida que avanzan los ciclos.","metadata":{"papermill":{"duration":0.037744,"end_time":"2023-07-02T11:34:13.521856","exception":false,"start_time":"2023-07-02T11:34:13.484112","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def lrfn(current_step, num_warmup_steps, lr_max, num_cycles=0.50, num_training_steps=N_EPOCHS):\n    \n    if current_step < num_warmup_steps:\n        if WARMUP_METHOD == 'log':\n            return lr_max * 0.10 ** (num_warmup_steps - current_step)\n        else:\n            return lr_max * 2 ** -(num_warmup_steps - current_step)\n    else:\n        progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))\n\n        return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))) * lr_max","metadata":{"papermill":{"duration":0.048796,"end_time":"2023-07-02T11:34:13.609084","exception":false,"start_time":"2023-07-02T11:34:13.560288","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:33:03.349866Z","iopub.execute_input":"2023-07-31T07:33:03.350234Z","iopub.status.idle":"2023-07-31T07:33:03.35804Z","shell.execute_reply.started":"2023-07-31T07:33:03.3502Z","shell.execute_reply":"2023-07-31T07:33:03.3569Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ahora, se define una función `plot_lr_schedule` que se utilizará para visualizarla tasa de aprendizaje. La función recibe dos argumentos:\n- `lr_schedule`: Una lista que contiene los valores de la tasa de aprendizaje para cada paso de entrenamiento (`epoch`).\n- `epochs`: El número total de `epochs` de entrenamiento.\n\n- Se traza una línea para el learning rate programado (`lr_schedule`). Se agrega un valor None al inicio y al final para evitar que la línea llegue hasta los bordes del gráfico.\n- Se trazan puntos en el gráfico para cada learning rate en lr_schedule. Dependiendo del número total de `epochs`, se muestran solo algunos puntos para evitar que el gráfico se vuelva demasiado denso. Los puntos se etiquetan con los valores del learning rate.\n\nLuego, se crea una lista llamada `LR_SCHEDULE` utilizando la función `lrfn` que calcula el learning rate para cada step en el número de épocas, que en este caso es 20, y dibujamos el gafico viendo el learning rate de cada época. \n\nFinalmente, se crea un callback de Keras llamado `lr_callback`, que se utilizará durante el entrenamiento del modelo. Este callback ajustará automáticamente la tasa de aprendizaje en cada `epoch` de entrenamiento utilizando la función lambda `step: LR_SCHEDULE[step]`. El valor de la tasa de aprendizaje para cada `epoch` se obtendrá de la lista `LR_SCHEDULE` que se creó anteriormente.","metadata":{}},{"cell_type":"code","source":"print(f'N_EPOCHS: {N_EPOCHS}')\nprint(f'N_WARMUP_EPOCHS: {N_WARMUP_EPOCHS}')","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:33:03.359385Z","iopub.execute_input":"2023-07-31T07:33:03.359977Z","iopub.status.idle":"2023-07-31T07:33:03.382831Z","shell.execute_reply.started":"2023-07-31T07:33:03.359941Z","shell.execute_reply":"2023-07-31T07:33:03.381769Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def plot_lr_schedule(lr_schedule, epochs):\n    fig = plt.figure(figsize=(20, 10))\n    plt.plot([None] + lr_schedule + [None])\n    # X Labels\n    x = np.arange(1, epochs + 1)\n    x_axis_labels = [i if epochs <= 40 or i % 5 == 0 or i == 1 else None for i in range(1, epochs + 1)]\n    plt.xlim([1, epochs])\n    plt.xticks(x, x_axis_labels) # set tick step to 1 and let x axis start at 1\n    \n    # Increase y-limit for better readability\n    plt.ylim([0, max(lr_schedule) * 1.1])\n    \n    # Title\n    schedule_info = f'start: {lr_schedule[0]:.1E}, max: {max(lr_schedule):.1E}, final: {lr_schedule[-1]:.1E}'\n    plt.title(f'Step Learning Rate Schedule, {schedule_info}', size=18, pad=12)\n    \n    # Plot Learning Rates\n    for x, val in enumerate(lr_schedule):\n        if epochs <= 40 or x % 5 == 0 or x is epochs - 1:\n            if x < len(lr_schedule) - 1:\n                if lr_schedule[x - 1] < val:\n                    ha = 'right'\n                else:\n                    ha = 'left'\n            elif x == 0:\n                ha = 'right'\n            else:\n                ha = 'left'\n            plt.plot(x + 1, val, 'o', color='black');\n            offset_y = (max(lr_schedule) - min(lr_schedule)) * 0.02\n            plt.annotate(f'{val:.1E}', xy=(x + 1, val + offset_y), size=12, ha=ha)\n    \n    plt.xlabel('Epoch', size=16, labelpad=5)\n    plt.ylabel('Learning Rate', size=16, labelpad=5)\n    plt.grid()\n    plt.show()\n\n# Learning rate for encoder\nLR_SCHEDULE = [lrfn(step, num_warmup_steps=N_WARMUP_EPOCHS, lr_max=LR_MAX, num_cycles=0.50) for step in range(N_EPOCHS)]\n# Plot Learning Rate Schedule\nplot_lr_schedule(LR_SCHEDULE, epochs=N_EPOCHS)\n# Learning Rate Callback\nlr_callback = tf.keras.callbacks.LearningRateScheduler(lambda step: LR_SCHEDULE[step], verbose=0)","metadata":{"papermill":{"duration":0.885914,"end_time":"2023-07-02T11:34:14.533559","exception":false,"start_time":"2023-07-02T11:34:13.647645","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:33:03.384215Z","iopub.execute_input":"2023-07-31T07:33:03.384885Z","iopub.status.idle":"2023-07-31T07:33:03.749196Z","shell.execute_reply.started":"2023-07-31T07:33:03.384851Z","shell.execute_reply":"2023-07-31T07:33:03.748302Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Weight Decay Callback","metadata":{"papermill":{"duration":0.038978,"end_time":"2023-07-02T11:34:14.612776","exception":false,"start_time":"2023-07-02T11:34:14.573798","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"\nEste fragmento de código define una clase de callback personalizado llamado WeightDecayCallback, que se utilizará durante el entrenamiento del modelo para actualizar el término de \"weight decay\" (decaimiento de peso) en el optimizador en función de la tasa de aprendizaje.\n\nEl weight decay, también conocido como \"L2 regularization\", es una técnica utilizada en el entrenamiento de modelos de aprendizaje automático para prevenir el sobreajuste (overfitting) y mejorar la generalización del modelo.\n\nEn términos simples, el weight decay consiste en agregar un término de penalización a la función de pérdida del modelo que está relacionado con los valores de los pesos del modelo. Este término de penalización es proporcional al cuadrado de los valores de los pesos y se agrega a la función de pérdida durante el proceso de optimización.\n\nEl objetivo del weight decay es penalizar los pesos grandes del modelo, lo que significa que el modelo tiene menos probabilidad de ajustar sus pesos a valores extremadamente grandes que podrían causar sobreajuste. Al penalizar los pesos grandes, el modelo se ve incentivado a utilizar pesos más pequeños, lo que puede mejorar la generalización y evitar que el modelo memorice los datos de entrenamiento en lugar de aprender patrones generales.\n\nEl callback WeightDecayCallback tiene un constructor __init__ que toma un parámetro opcional wd_ratio, que representa la proporción de weight decay respecto a la tasa de aprendizaje (por defecto es igual a WD_RATIO).\n\nEl callback tiene un método on_epoch_begin, que se ejecuta al comienzo de cada época durante el entrenamiento del modelo. En este método, se actualiza el weight decay en el optimizador multiplicando la tasa de aprendizaje actual del modelo por el valor del wd_ratio. Esto permite que el weight decay se ajuste automáticamente en función del learning rate, lo que puede ayudar a mejorar la regularización del modelo y prevenir el sobreajuste.\n\nAdemás, el método on_epoch_begin imprime en pantalla el valor actual de la tasa de aprendizaje y el weight decay, lo que proporciona información útil durante el entrenamiento para monitorear cómo estos valores están cambiando a lo largo de las épocas.","metadata":{}},{"cell_type":"code","source":"# Custom callback to update weight decay with learning rate\nclass WeightDecayCallback(tf.keras.callbacks.Callback):\n    def __init__(self, wd_ratio=WD_RATIO):\n        self.step_counter = 0\n        self.wd_ratio = wd_ratio\n    \n    def on_epoch_begin(self, epoch, logs=None):\n        model.optimizer.weight_decay = model.optimizer.learning_rate * self.wd_ratio\n        print(f'learning rate: {model.optimizer.learning_rate.numpy():.2e}, weight decay: {model.optimizer.weight_decay.numpy():.2e}')","metadata":{"papermill":{"duration":0.051216,"end_time":"2023-07-02T11:34:14.702691","exception":false,"start_time":"2023-07-02T11:34:14.651475","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:33:03.750575Z","iopub.execute_input":"2023-07-31T07:33:03.753995Z","iopub.status.idle":"2023-07-31T07:33:03.759783Z","shell.execute_reply.started":"2023-07-31T07:33:03.753961Z","shell.execute_reply":"2023-07-31T07:33:03.758734Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Evaluate Initialized Model\n\nEvaluamos ahora el modelo con el set de datos de entrenamiento","metadata":{"papermill":{"duration":0.038896,"end_time":"2023-07-02T11:34:14.781663","exception":false,"start_time":"2023-07-02T11:34:14.742767","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Evaluate Initialized Model On Validation Data\nmodel.evaluate(\n    val_dataset if USE_VAL else train_dataset,\n    steps=N_VAL_STEPS_PER_EPOCH if USE_VAL else TRAIN_STEPS_PER_EPOCH,\n    verbose=VERBOSE,\n)","metadata":{"papermill":{"duration":41.4984,"end_time":"2023-07-02T11:34:56.318989","exception":false,"start_time":"2023-07-02T11:34:14.820589","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:33:03.761351Z","iopub.execute_input":"2023-07-31T07:33:03.76203Z","iopub.status.idle":"2023-07-31T07:34:28.020806Z","shell.execute_reply.started":"2023-07-31T07:33:03.761995Z","shell.execute_reply":"2023-07-31T07:34:28.019803Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Baseline\n\nEste fragmento de código calcula la \"baseline accuracy\" o precisión de referencia para el modelo cuando solo se predice el token de relleno (`PAD_TOKEN`).\n\nEn este caso particular, la \"precisión de referencia\" se refiere a la precisión del modelo cuando solo se predice el token de relleno (`PAD_TOKEN`) para todas las muestras en el conjunto de datos. Esta precisión se calcula como la proporción de muestras en el conjunto de datos que contienen solo el token de relleno.\n\nEs importante calcular la \"precisión de referencia\" para tener una idea de cómo se comportaría un modelo simple o ingenuo que siempre predice el valor más común (el token de relleno). Si el modelo que estamos construyendo no supera significativamente esta precisión de referencia, indica que el modelo no está aprendiendo patrones relevantes en los datos y necesita mejorar para ser útil en la tarea que se está abordando.\n\nSi la variable `USE_VAL` es verdadera, significa que se está utilizando el conjunto de validación para calcular la precisión de referencia. En caso contrario, se utiliza el conjunto de entrenamiento.\n\nLa precisión de referencia se calcula comparando las etiquetas reales (`y_val` o `y_train`) con el valor del token de relleno (`PAD_TOKEN`). El token de relleno se utiliza en secuencias para indicar posiciones vacías o sin información relevante.\n\nEl cálculo de la precisión de referencia se realiza de la siguiente manera:\n\n- Si `USE_VAL=True`, se calcula la precisión de referencia utilizando el conjunto de validación: Se compara cada elemento en el conjunto de validación (`y_val`) con el valor del token de relleno (`PAD_TOKEN`).\n- Se obtiene un array booleano que indica si cada elemento es igual al token de relleno o no.\n- Se calcula la media de este array booleano para obtener la proporción de elementos que son iguales al token de relleno. Esta proporción representa la precisión de referencia en el conjunto de validación.\n- Si `USE_VAL=False`, se realiza el mismo proceso pero utilizando el conjunto de entrenamiento (y_train) en su lugar.\n\nUna precisión de referencia alta podría indicar que el modelo necesita mejorar para ser útil en la tarea que se está abordando.","metadata":{"papermill":{"duration":0.044168,"end_time":"2023-07-02T11:34:56.412094","exception":false,"start_time":"2023-07-02T11:34:56.367926","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# baseline accuracy when only pad token is predicted\nif USE_VAL:\n    baseline_accuracy = np.mean(y_val == PAD_TOKEN)\nelse:\n    baseline_accuracy = np.mean(y_train == PAD_TOKEN)\nprint(f'Baseline Accuracy: {baseline_accuracy:.4f}')","metadata":{"papermill":{"duration":0.056578,"end_time":"2023-07-02T11:34:56.511966","exception":false,"start_time":"2023-07-02T11:34:56.455388","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:34:28.021934Z","iopub.execute_input":"2023-07-31T07:34:28.022331Z","iopub.status.idle":"2023-07-31T07:34:28.033973Z","shell.execute_reply.started":"2023-07-31T07:34:28.022289Z","shell.execute_reply":"2023-07-31T07:34:28.03284Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train\n\nllamamos manualmente a gc.collect() para liberar la memoria no utilizada:","metadata":{"papermill":{"duration":0.039868,"end_time":"2023-07-02T11:34:56.591346","exception":false,"start_time":"2023-07-02T11:34:56.551478","status":"completed"},"tags":[]}},{"cell_type":"code","source":"gc.collect()","metadata":{"papermill":{"duration":0.33862,"end_time":"2023-07-02T11:34:56.969069","exception":false,"start_time":"2023-07-02T11:34:56.630449","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:34:28.036031Z","iopub.execute_input":"2023-07-31T07:34:28.036406Z","iopub.status.idle":"2023-07-31T07:34:28.332891Z","shell.execute_reply.started":"2023-07-31T07:34:28.036373Z","shell.execute_reply":"2023-07-31T07:34:28.331947Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Procedemos ahora a entrenar el modelo usando el set de datos `train_dataset`\n\nLos pasos en el entrenamiento del modelo son los siguientes:\n\n1. Primero se limpian todos los modelos anteriores en la GPU para liberar memoria. Esto se hace usando `tf.keras.backend.clear_session()`.\n\n2. Se obtiene un nuevo modelo utilizando la función `get_model()`. Se imprime un resumen del modelo utilizando `model.summary()` para verificar que se ha creado correctamente\n\n4. Se inicia el proceso de entrenamiento del modelo utilizando el método `fit()`. En este método, se proporcionan los siguientes argumentos:\n   - `x`: los datos de entrenamiento (`train_dataset`)\n   - `steps_per_epoch`: El número de pasos que se realizarán en cada época de entrenamiento. Cada paso es una actualización de los pesos del modelo basada en un lote de muestras.\n   - `epochs`: El número total de épocas que se realizarán durante todo el entrenamiento.\n   - `validation_data`: los datos de validación (`val_dataset`). En nuestro caso `USE_VAL=False`, por lo que se establece en `None`.\n   - `validation_steps`: El número de pasos que se realizarán durante la evaluación en cada época de validación. Tampoco los utilizamos.\n   - `callbacks`: Una lista de devoluciones de llamada que se ejecutan durante el entrenamiento. En este caso, se utiliza el callback `lr_callback` para actualizar el learning rate y el callback `WeightDecayCallback()` para actualizar el peso de decaimiento (weight decay) durante el entrenamiento.\n   - `verbose`: Un valor entero que controla la cantidad de información que se muestra durante el entrenamiento. Un valor de 0 muestra nada, un valor de 1 muestra una barra de progreso, y un valor de 2 muestra una barra de progreso y el resumen de cada época.","metadata":{}},{"cell_type":"code","source":"print(f'TRAIN_STEPS_PER_EPOCH: {TRAIN_STEPS_PER_EPOCH}')\nprint(f'N_EPOCHS: {N_EPOCHS}')\nprint(f'VERBOSE: {VERBOSE}')","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:34:28.335639Z","iopub.execute_input":"2023-07-31T07:34:28.336171Z","iopub.status.idle":"2023-07-31T07:34:28.346755Z","shell.execute_reply.started":"2023-07-31T07:34:28.336137Z","shell.execute_reply":"2023-07-31T07:34:28.345676Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if TRAIN_MODEL:\n    # Clear all models in GPU\n    tf.keras.backend.clear_session()\n\n    # Get new fresh model\n    model = get_model()\n\n    # Sanity Check\n    model.summary()\n    print('\\n\\n')\n    # Actual Training\n    history = model.fit(\n            x=train_dataset,\n            steps_per_epoch=TRAIN_STEPS_PER_EPOCH,\n            epochs=N_EPOCHS,\n            # Only used for validation data since training data is a generator\n            validation_data=val_dataset if USE_VAL else None,\n            validation_steps=N_VAL_STEPS_PER_EPOCH if USE_VAL else None,\n            callbacks=[\n                lr_callback,\n                WeightDecayCallback(),\n            ],\n            verbose = VERBOSE,\n        )","metadata":{"papermill":{"duration":12244.208248,"end_time":"2023-07-02T14:59:01.217198","exception":false,"start_time":"2023-07-02T11:34:57.00895","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:34:28.347978Z","iopub.execute_input":"2023-07-31T07:34:28.348801Z","iopub.status.idle":"2023-07-31T07:40:20.870311Z","shell.execute_reply.started":"2023-07-31T07:34:28.348767Z","shell.execute_reply":"2023-07-31T07:40:20.869251Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Load Weights\nif LOAD_WEIGHTS: # LOAD_WEIGHTS is set to False\n    model.load_weights('/kaggle/input/aslfr-training-python37/model.h5')\n    print(f'Successfully Loaded Pretrained Weights')","metadata":{"papermill":{"duration":0.069569,"end_time":"2023-07-02T14:59:01.347383","exception":false,"start_time":"2023-07-02T14:59:01.277814","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:40:20.872164Z","iopub.execute_input":"2023-07-31T07:40:20.872568Z","iopub.status.idle":"2023-07-31T07:40:20.877992Z","shell.execute_reply.started":"2023-07-31T07:40:20.872533Z","shell.execute_reply":"2023-07-31T07:40:20.876771Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Save Model Weights\nmodel.save_weights('model.h5')","metadata":{"papermill":{"duration":0.206194,"end_time":"2023-07-02T14:59:01.616525","exception":false,"start_time":"2023-07-02T14:59:01.410331","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:40:20.879529Z","iopub.execute_input":"2023-07-31T07:40:20.879933Z","iopub.status.idle":"2023-07-31T07:40:21.005531Z","shell.execute_reply.started":"2023-07-31T07:40:20.879898Z","shell.execute_reply":"2023-07-31T07:40:21.004542Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ahora, evaluamos el modelo cargado en el conjunto de datos especificado y calculamos la pérdida y las métricas del modelo en ese conjunto de datos. Es una forma de verificar que el modelo esté cargado correctamente y listo para su uso. Debido a que `train_dataset` es un objeto generador, cada vez que se accede a él se obtiene un nuevo lote de datos diferente. Por ello, no estamos evaluando con los mismos datos con los que entrenamos.\n\nLa función `evaluate()` nos imprime 3 valores por pantalla en forma de lista:\n\n- La pérdida (Sparse Categorical Cross Entropy with Label Smoothing en nuestro caso): medida de qué tan bien se ajustan las predicciones del modelo a las etiquetas verdaderas durante el entrenamiento. Una pérdida más baja indica un mejor ajuste del modelo a los datos de entrenamiento.\n- Precisión Top-1 (TopKAccuracy(1)): Esta es la precisión top-1, también conocida como precisión en el ranking de mayor probabilidad. Representa la fracción de muestras en las que el modelo predice correctamente la clase verdadera como la clase con la mayor probabilidad. Es decir, es la precisión para el caso en que se toma solo la clase más probable como predicción.\n- Precisión Top-5 (TopKAccuracy(5)): Esta es la precisión top-5, que representa la fracción de muestras en las que el modelo predice correctamente la clase verdadera como una de las cinco clases con las mayores probabilidades. En otras palabras, se consideran las cinco clases más probables y se verifica si la clase verdadera está presente en esas cinco clases.","metadata":{"execution":{"iopub.status.busy":"2023-07-26T17:48:41.772628Z","iopub.execute_input":"2023-07-26T17:48:41.77303Z","iopub.status.idle":"2023-07-26T17:48:42.5113Z","shell.execute_reply.started":"2023-07-26T17:48:41.772989Z","shell.execute_reply":"2023-07-26T17:48:42.509845Z"}}},{"cell_type":"code","source":"# Verify Model is Loaded Correctly\nmodel.evaluate(\n    val_dataset if USE_VAL else train_dataset,\n    steps=N_VAL_STEPS_PER_EPOCH if USE_VAL else TRAIN_STEPS_PER_EPOCH,\n    batch_size=BATCH_SIZE,\n    verbose=VERBOSE,\n)","metadata":{"papermill":{"duration":41.357991,"end_time":"2023-07-02T14:59:43.031109","exception":false,"start_time":"2023-07-02T14:59:01.673118","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:40:21.00712Z","iopub.execute_input":"2023-07-31T07:40:21.007496Z","iopub.status.idle":"2023-07-31T07:41:15.678332Z","shell.execute_reply.started":"2023-07-31T07:40:21.00746Z","shell.execute_reply":"2023-07-31T07:41:15.677383Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Levenshtein Distance","metadata":{"papermill":{"duration":0.056044,"end_time":"2023-07-02T14:59:43.142922","exception":false,"start_time":"2023-07-02T14:59:43.086878","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"Definimos una función llamada `outputs2phrase`, que se utiliza para convertir las salidas (`outputs`) del modelo en una secuencia de texto (frase). La función toma como entrada las salidas del modelo, que pueden ser un tensor de dos dimensiones o un tensor unidimensional. La función realiza lo siguiente:\n\n1. Si las salidas tienen dos dimensiones, se toma el índice de la clase con la probabilidad más alta para cada muestra utilizando `np.argmax(outputs, axis=1)`. Esto se hace asumiendo que las salidas son probabilidades de clases, y el modelo ha predicho la clase con la mayor probabilidad para cada muestra.\n\n2. Luego, la función recorre los índices de las clases predichas y utiliza un diccionario llamado `ORD2CHAR` para convertir los índices en caracteres. `ORD2CHAR` es un diccionario que mapea los índices de las clases a los caracteres correspondientes en la secuencia de texto. Por ejemplo, el índice 0 podría estar mapeado al carácter \"a\", el índice 1 al carácter \"b\" y así sucesivamente.\n\n3. Finalmente, la función une todos los caracteres para formar la secuencia de texto completa y la devuelve como resultado.\n\nEn resumen, esta función es útil para convertir las salidas del modelo, que son índices de clases o probabilidades, en una secuencia de texto legible para interpretar las predicciones del modelo en forma de texto.","metadata":{}},{"cell_type":"code","source":"# Output Predictions to string\ndef outputs2phrase(outputs):\n    if outputs.ndim == 2:\n        outputs = np.argmax(outputs, axis=1)\n    \n    return ''.join([ORD2CHAR.get(s, '') for s in outputs])","metadata":{"papermill":{"duration":0.066802,"end_time":"2023-07-02T14:59:43.266519","exception":false,"start_time":"2023-07-02T14:59:43.199717","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:15.679832Z","iopub.execute_input":"2023-07-31T07:41:15.68018Z","iopub.status.idle":"2023-07-31T07:41:15.685977Z","shell.execute_reply.started":"2023-07-31T07:41:15.680148Z","shell.execute_reply":"2023-07-31T07:41:15.684852Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ahora, definimos la función llamada `predict_phrase` que se utiliza para generar predicciones de texto basadas en una secuencia de frames de entrada utilizando un modelo de lenguaje entrenado previamente.\n\nAquí están los pasos detallados de la función `predict_phrase`:\n\n1. La función está decorada con `@tf.function()` para convertirla en un grafo de TensorFlow y obtener beneficios de rendimiento al ejecutarla.\n\n2. La función toma el tensor `frames` como entrada, secuencia de frames igual que hemos visto anteriormente (`X_batch['frames']`).\n\n3. Se agrega una dimensión batch dimension al tensor `frames` utilizando `tf.expand_dims()`. Esto es necesario porque el modelo espera una entrada con una dimensión de lote, incluso si estamos haciendo una predicción para un solo ejemplo.\n\n4. Se inicializa la variable `phrase` con la forma `[1, MAX_PHRASE_LENGTH]` y se llena con el token de relleno (`PAD_TOKEN`). Esta variable se utiliza para almacenar la secuencia de texto generada.\n\n5. Se inicia un bucle que se ejecutará `MAX_PHRASE_LENGTH` veces, que es el máximo número de tokens en la secuencia de texto generada.\n\n6. Dentro del bucle, se realiza lo siguiente:\n\n   * Se convierte `phrase` a tipo `int8`.\n   * Se llama al modelo con las entradas `X = frames` e `y = phrase` utilizando el método `model()` para obtener las predicciones del modelo para el siguiente token en la secuencia de texto. Estas predicciones se almacenan en la variable `outputs`.\n   *  `phrase = tf.cast(phrase, tf.int32)`: Se convierte el tensor `phrase` al tipo de datos int32. Esto es necesario para asegurarnos de que podamos utilizar la función `tf.where()` correctamente, ya que requiere que los argumentos tengan el mismo tipo de datos.\n   * `tf.range(MAX_PHRASE_LENGTH) < idx + 1`: Se crea un tensor booleano con forma `[MAX_PHRASE_LENGTH]=32`, donde cada elemento es `True` si su índice es menor que `idx + 1`, y `False` en caso contrario. Esto crea una máscara que indica qué posiciones de la secuencia de texto aún no han sido predichas.\n   * `tf.argmax(outputs, axis=2, output_type=tf.int32)`: Se utiliza la función `tf.argmax()` para encontrar el índice del token con la mayor probabilidad en las predicciones (`outputs`). El argumento `axis=2` indica que la búsqueda del máximo se realizará a lo largo del tercer eje de `outputs`, que corresponde a las diferentes clases o tokens posibles en la secuencia de texto. El argumento `output_type=tf.int32` asegura que el resultado de `tf.argmax()` será de tipo entero `int32`.\n   * `tf.where(condition, x, y)`: Esta función realiza una operación de \"selección condicional\". Toma tres argumentos: `condition`, `x` e `y`. Si `condition` es `True` en una posición, el valor de `x` en esa posición se selecciona; de lo contrario, se selecciona el valor de `y`. En este caso, `condition` es la máscara creada en el paso 2, `x` es el resultado de `tf.argmax()` en el paso 3 (es decir, el índice del token predicho), e `y` es la secuencia de texto actual (`phrase`). Esto significa que si una posición en la máscara es `True`, se selecciona el token predicho en esa posición; de lo contrario, se mantiene el token actual en esa posición.\n\n   * Al final de esta operación, la variable `phrase` se ha actualizado con el token predicho en la posición correspondiente, lo que permite que el bucle itere y agregue el siguiente token en la siguiente posición de la secuencia de texto. De esta manera, la secuencia de texto se va construyendo paso a paso hasta completarla con `MAX_PHRASE_LENGTH` tokens, y así se obtiene la secuencia de texto generada por el modelo de lenguaje.\n\n7. Una vez finalizado el bucle, el tensor `phrase` se comprime para eliminar la dimensión de lote agregada anteriormente, ya que solo estamos generando una secuencia de texto para un solo ejemplo.\n\n8. Los valores numéricos en `phrase` se convierten en representación \"one-hot\" utilizando `tf.one_hot()`. Esta representación \"one-hot\" es útil para obtener las etiquetas discretas (tokens) en formato numérico.\n\n9. Finalmente, la función devuelve un diccionario con la clave \"outputs\" y el tensor de salida resultante. Este tensor contiene la secuencia de texto generada en formato \"one-hot\" con forma `[MAX_PHRASE_LENGTH, N_UNIQUE_CHARACTERS]`.\n\nEn resumen, la función `predict_phrase` toma una secuencia de frames de entrada y utiliza el modelo de lenguaje para predecir la siguiente palabra en la secuencia de texto generada. Luego, itera para predecir todas las palabras de la secuencia. La función devuelve la secuencia de texto generada en formato \"one-hot\" para representar los tokens numéricamente.","metadata":{}},{"cell_type":"code","source":"print(tf.fill([1,MAX_PHRASE_LENGTH], PAD_TOKEN)) # MAX_PHRASE_LENGTH = 32\nprint(tf.cast(tf.fill([1,MAX_PHRASE_LENGTH], PAD_TOKEN), tf.int8))","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:41:15.68755Z","iopub.execute_input":"2023-07-31T07:41:15.687901Z","iopub.status.idle":"2023-07-31T07:41:15.709961Z","shell.execute_reply.started":"2023-07-31T07:41:15.687867Z","shell.execute_reply":"2023-07-31T07:41:15.708954Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"@tf.function()\ndef predict_phrase(frames):\n    # Add Batch Dimension\n    frames = tf.expand_dims(frames, axis=0)\n    # Start Phrase\n    phrase = tf.fill([1,MAX_PHRASE_LENGTH], PAD_TOKEN)\n\n    for idx in tf.range(MAX_PHRASE_LENGTH):\n        # Cast phrase to int8\n        phrase = tf.cast(phrase, tf.int8)\n        # Predict Next Token\n        outputs = model({\n            'frames': frames,\n            'phrase': phrase,\n        })\n\n        # Add predicted token to input phrase\n        phrase = tf.cast(phrase, tf.int32)\n        phrase = tf.where( # where its True search for max prob, if its False keep Pad token\n            tf.range(MAX_PHRASE_LENGTH) < idx + 1, # create a mask of Trues and Falses\n            tf.argmax(outputs, axis=2, output_type=tf.int32), # search for the max probability\n            phrase,\n        )\n\n    # Squeeze outputs\n    outputs = tf.squeeze(phrase, axis=0) # drop first dimension\n    outputs = tf.one_hot(outputs, N_UNIQUE_CHARACTERS) # one-hot encoding of the numbers\n\n    # Return a dictionary with the output tensor\n    return outputs","metadata":{"papermill":{"duration":0.068395,"end_time":"2023-07-02T14:59:43.392955","exception":false,"start_time":"2023-07-02T14:59:43.32456","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:15.711478Z","iopub.execute_input":"2023-07-31T07:41:15.711814Z","iopub.status.idle":"2023-07-31T07:41:15.721046Z","shell.execute_reply.started":"2023-07-31T07:41:15.711782Z","shell.execute_reply":"2023-07-31T07:41:15.719972Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Levenstein Distance Train\n\nCon estas dos funciones, creamos una última función, llamada `get_ld_train`, que se utiliza para calcular la distancia entre las frases real y la frase predicha por el modelo en el conjunto de entrenamiento.\n\nLa función toma como entrada los datos de entrenamiento `X_train` y las etiquetas reales `y_train`, que contienen secuencias de 'frames' y 'phrases' representadas por índices enteros, respectivamente.\n\nCreamos una lista vacía llamada `LD_TRAIN` que se utilizará para guardar las distancias de Levenshtein de cada par de frases reales y predichas.\n\nLuego, la función itera a través de los datos de entrenamiento utilizando un bucle `for`. Para cada par de 'frames' y 'phrase', realiza lo siguiente:\n\n1. Utiliza la función `predict_phrase` para predecir la frase a partir de los frames de entrada. La función `predict_phrase` toma los frames como entrada y devuelve la secuencia predicha de caracteres en formato \"one-hot\". Posteriormente, utiliza la función `outputs2phrase` para convertir la secuencia predicha de caracteres a una cadena legible.\n\n2. Convierte la frase real de índices enteros a una cadena legible utilizando la función `outputs2phrase`.\n\n3. Calcula la distancia de Levenshtein entre la frase real y la frase predicha utilizando la función `levenshtein`. La distancia de Levenshtein es una medida de la diferencia entre dos cadenas, que se calcula contando el número mínimo de operaciones (inserciones, eliminaciones o sustituciones de caracteres) requeridas para transformar una cadena en la otra.\n\n4. Mete la frase real, la frase predicha y la distancia de Levenshtein en la lista `LD_TRAIN`.\n\nDespués de completar el bucle, la función convierte la lista `LD_TRAIN` en un DataFrame de pandas llamado `LD_TRAIN_DF` y lo devuelve como resultado.","metadata":{"papermill":{"duration":0.056358,"end_time":"2023-07-02T14:59:43.504839","exception":false,"start_time":"2023-07-02T14:59:43.448481","status":"completed"},"tags":[]}},{"cell_type":"code","source":"print(f'Shape X_train: {X_train.shape}')\nprint(f'Shape y_train: {y_train.shape}')","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:41:15.722491Z","iopub.execute_input":"2023-07-31T07:41:15.723042Z","iopub.status.idle":"2023-07-31T07:41:15.737401Z","shell.execute_reply.started":"2023-07-31T07:41:15.723009Z","shell.execute_reply":"2023-07-31T07:41:15.736371Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"`zip(tqdm(X_train, total=N)` en cada iteración del bucle, obtiene un par de elementos, uno de X_train y otro de tqdm. Por eso obtenemos 3 elementos en el enumerate, el índice con el tqdm, X_train e y_train","metadata":{}},{"cell_type":"code","source":"i = 0\nN = 100 if IS_INTERACTIVE else 1000\nfor idx, (frames, phrase_true) in enumerate(zip(tqdm(X_train, total=N), y_train)):\n    print(idx)\n    print(frames)\n    print(phrase_true)\n    i+=1\n    if i == 5:\n        break","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:41:15.738996Z","iopub.execute_input":"2023-07-31T07:41:15.739361Z","iopub.status.idle":"2023-07-31T07:41:15.772357Z","shell.execute_reply.started":"2023-07-31T07:41:15.739329Z","shell.execute_reply":"2023-07-31T07:41:15.771419Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Compute Levenstein Distances\ndef get_ld_train():\n    N = 100 if IS_INTERACTIVE else 1000\n    LD_TRAIN = []\n    for idx, (frames, phrase_true) in enumerate(zip(tqdm(X_train, total=N), y_train)):\n        # Predict Phrase and Convert to String\n        phrase_pred = predict_phrase(frames).numpy()\n        phrase_pred = outputs2phrase(phrase_pred)\n        # True Phrase Ordinal to String\n        phrase_true = outputs2phrase(phrase_true)\n        # Add Levenstein Distance\n        LD_TRAIN.append({\n            'phrase_true': phrase_true,\n            'phrase_pred': phrase_pred,\n            'levenshtein_distance': levenshtein(phrase_pred, phrase_true),\n        })\n        # Take subset in interactive mode\n        if idx == N:\n            break\n            \n    # Convert to DataFrame\n    LD_TRAIN_DF = pd.DataFrame(LD_TRAIN)\n    \n    return LD_TRAIN_DF","metadata":{"papermill":{"duration":0.071676,"end_time":"2023-07-02T14:59:43.633777","exception":false,"start_time":"2023-07-02T14:59:43.562101","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:15.773621Z","iopub.execute_input":"2023-07-31T07:41:15.774391Z","iopub.status.idle":"2023-07-31T07:41:15.783203Z","shell.execute_reply.started":"2023-07-31T07:41:15.774356Z","shell.execute_reply":"2023-07-31T07:41:15.782063Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"LD_TRAIN_DF = get_ld_train()\n\n# add column to see the length of the true phrase\nLD_TRAIN_DF['len_char'] = LD_TRAIN_DF['phrase_true'].apply(lambda x: len(x))\n\n# Display Errors\ndisplay(LD_TRAIN_DF.head(30))","metadata":{"papermill":{"duration":126.418133,"end_time":"2023-07-02T15:01:50.108598","exception":false,"start_time":"2023-07-02T14:59:43.690465","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:15.784858Z","iopub.execute_input":"2023-07-31T07:41:15.785248Z","iopub.status.idle":"2023-07-31T07:41:28.532514Z","shell.execute_reply.started":"2023-07-31T07:41:15.785214Z","shell.execute_reply":"2023-07-31T07:41:28.531541Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ahora, calculamos la distribución de las distancias de Levenshtein en el conjunto de entrenamiento y creamos un gráfico de barras para visualizar cómo se distribuyen estas distancias en el conjunto de datos.","metadata":{}},{"cell_type":"code","source":"# Value Counts\nLD_TRAIN_VC = dict([(i, 0) for i in range(LD_TRAIN_DF['levenshtein_distance'].max()+1)])\nfor ld in LD_TRAIN_DF['levenshtein_distance']:\n    LD_TRAIN_VC[ld] += 1\n\nplt.figure(figsize=(15,8))\npd.Series(LD_TRAIN_VC).plot(kind='bar', width=1)\nplt.title(f'Train Levenstein Distance Distribution | Mean: {LD_TRAIN_DF.levenshtein_distance.mean():.4f}')\nplt.xlabel('Levenstein Distance')\nplt.ylabel('Sample Count')\nplt.xlim(-0.50, LD_TRAIN_DF.levenshtein_distance.max()+0.50)\nplt.grid(axis='y')\nplt.savefig('temp.png')\nplt.show()","metadata":{"papermill":{"duration":0.724484,"end_time":"2023-07-02T15:01:50.890212","exception":false,"start_time":"2023-07-02T15:01:50.165728","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:28.534319Z","iopub.execute_input":"2023-07-31T07:41:28.535Z","iopub.status.idle":"2023-07-31T07:41:29.235179Z","shell.execute_reply.started":"2023-07-31T07:41:28.534963Z","shell.execute_reply":"2023-07-31T07:41:29.234241Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Levenstein Distance Evaluation\n\nHacemos lo mismo para el conjunto de validación:","metadata":{"papermill":{"duration":0.057603,"end_time":"2023-07-02T15:01:51.005493","exception":false,"start_time":"2023-07-02T15:01:50.94789","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Compute Levenstein Distances\ndef get_ld_val():\n    N = 100 if IS_INTERACTIVE else 1000\n    LD_VAL = []\n    for idx, (frames, phrase_true) in enumerate(zip(tqdm(X_val, total=N), y_val)):\n        # Predict Phrase and Convert to String\n        phrase_pred = predict_phrase(frames).numpy()\n        phrase_pred = outputs2phrase(phrase_pred)\n        # True Phrase Ordinal to String\n        phrase_true = outputs2phrase(phrase_true)\n        # Add Levenstein Distance\n        LD_VAL.append({\n            'phrase_true': phrase_true,\n            'phrase_pred': phrase_pred,\n            'levenshtein_distance': levenshtein(phrase_pred, phrase_true),\n        })\n        # Take subset in interactive mode\n        if idx == N:\n            break\n            \n    # Convert to DataFrame\n    LD_VAL_DF = pd.DataFrame(LD_VAL)\n    \n    return LD_VAL_DF","metadata":{"papermill":{"duration":0.068889,"end_time":"2023-07-02T15:01:51.131182","exception":false,"start_time":"2023-07-02T15:01:51.062293","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:29.236555Z","iopub.execute_input":"2023-07-31T07:41:29.239755Z","iopub.status.idle":"2023-07-31T07:41:29.246528Z","shell.execute_reply.started":"2023-07-31T07:41:29.239725Z","shell.execute_reply":"2023-07-31T07:41:29.245431Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if USE_VAL:\n    LD_VAL_DF = get_ld_val()\n\n    # Display Errors\n    display(LD_VAL_DF.head(30))","metadata":{"papermill":{"duration":0.064943,"end_time":"2023-07-02T15:01:51.25391","exception":false,"start_time":"2023-07-02T15:01:51.188967","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:29.247982Z","iopub.execute_input":"2023-07-31T07:41:29.248342Z","iopub.status.idle":"2023-07-31T07:41:29.263708Z","shell.execute_reply.started":"2023-07-31T07:41:29.248308Z","shell.execute_reply":"2023-07-31T07:41:29.262669Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Value Counts\nif USE_VAL:\n    LD_VAL_VC = dict([(i, 0) for i in range(LD_VAL_DF['levenshtein_distance'].max()+1)])\n    for ld in LD_VAL_DF['levenshtein_distance']:\n        LD_VAL_VC[ld] += 1\n\n    plt.figure(figsize=(15,8))\n    pd.Series(LD_VAL_VC).plot(kind='bar', width=1)\n    plt.title(f'Validation Levenstein Distance Distribution | Mean: {LD_VAL_DF.levenshtein_distance.mean():.4f}')\n    plt.xlabel('Levenstein Distance')\n    plt.ylabel('Sample Count')\n    plt.xlim(0-0.50, LD_VAL_DF.levenshtein_distance.max()+0.50)\n    plt.grid(axis='y')\n    plt.savefig('temp.png')\n    plt.show()","metadata":{"papermill":{"duration":0.068365,"end_time":"2023-07-02T15:01:51.379464","exception":false,"start_time":"2023-07-02T15:01:51.311099","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:29.265053Z","iopub.execute_input":"2023-07-31T07:41:29.26549Z","iopub.status.idle":"2023-07-31T07:41:29.275669Z","shell.execute_reply.started":"2023-07-31T07:41:29.265456Z","shell.execute_reply":"2023-07-31T07:41:29.274733Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Training History\n\nUna vez entrenado el modelo y creado las predicciones de nuestros datos, se define una función llamada `plot_history_metric` para graficar la evolución de las distintas métricas usadas en el entrenamiento.\n\nVeamos paso a paso qué hace cada parte del código:\n\n- `if not TRAIN_MODEL: return`: Esta línea verifica si la variable `TRAIN_MODEL` es `True` o `False`. Si es `False`, la función no realizará ninguna acción y se detendrá aquí con el `return`. \n\n- `values = history.history[metric]`: Se obtienen los valores de la métrica especificada (`metric`) del historial del entrenamiento del modelo, que se almacena en el diccionario `history.history`.\n\n- `N_EPOCHS = len(values)`: Se obtiene el número total de épocas de entrenamiento al contar la longitud de los valores de la métrica.\n\n- `val = 'val' in ''.join(history.history.keys())`: Se verifica si la palabra 'val' se encuentra en las palabras clave del historial del modelo (si usamos validación). Si es así, se establece `val=True`, de lo contrario, se establece en `False`.\n\n- `if N_EPOCHS <= 20: x = np.arange(1, N_EPOCHS + 1)`: Si el número de épocas es menor o igual a 20, se crea un arange `x` con valores que van desde 1 hasta el número total de épocas.\n\n- `else: x = [1, 5] + [10 + 5 * idx for idx in range((N_EPOCHS - 10) // 5 + 1)]`: Si el número de épocas es mayor a 20, se crea una lista `x` con valores específicos que están espaciados para que el gráfico muestre claramente la evolución de la métrica.\n\n- `x_ticks = np.arange(1, N_EPOCHS+1)`: Se crea otro arange `x_ticks` que contiene los valores desde 1 hasta el número total de épocas. Este se utiliza para definir las etiquetas del eje x en el gráfico.\n\n- Si `val=True`:\n    - `val_values = history.history[f'val_{metric}']`: Se obtienen los valores de la métrica de validación específica (`val_{metric}`) del historial del modelo.\n    - `val_argmin = f_best(val_values)`: Se encuentra el índice de la métrica de validación que tiene el mejor valor, utilizando la función `f_best` que se especifica como argumento (por defecto es `np.argmax`, lo que encuentra el índice con el valor máximo).\n    - `plt.plot(x_ticks, val_values, label=f'val')`: Se traza la evolución de la métrica de validación en el gráfico con la etiqueta 'val'.\n\n- En caso de que `val=False`:\n    - `plt.plot(x_ticks, values, label=f'train')`: Se traza la evolución de la métrica de entrenamiento en el gráfico con la etiqueta 'train'.\n    - `argmin = f_best(values)`: Se encuentra el índice de la métrica de entrenamiento que tiene el mejor valor, utilizando la función `f_best` que se especifica como argumento (por defecto es `np.argmax`, lo que encuentra el índice con el valor máximo).\n    - `plt.scatter(argmin + 1, values[argmin], color='red', s=75, marker='o', label=f'train_best')`: Se agrega un punto rojo en el gráfico que indica la mejor métrica de entrenamiento y su valor correspondiente.\n\n- Si hay métricas de validación (`val`):\n    - `plt.scatter(val_argmin + 1, val_values[val_argmin], color='purple', s=75, marker='o', label=f'val_best')`: Si hay métricas de validación, se agrega un punto morado en el gráfico que indica la mejor métrica de validación y su valor correspondiente.","metadata":{"papermill":{"duration":0.056463,"end_time":"2023-07-02T15:01:51.49247","exception":false,"start_time":"2023-07-02T15:01:51.436007","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def plot_history_metric(metric, f_best=np.argmax, ylim=None, yscale=None, yticks=None):\n    # Only plot when training\n    if not TRAIN_MODEL:\n        return\n    \n    plt.figure(figsize=(20, 10))\n    \n    values = history.history[metric]\n    N_EPOCHS = len(values)\n    val = 'val' in ''.join(history.history.keys())\n    # Epoch Ticks\n    if N_EPOCHS <= 20:\n        x = np.arange(1, N_EPOCHS + 1)\n    else:\n        x = [1, 5] + [10 + 5 * idx for idx in range((N_EPOCHS - 10) // 5 + 1)]\n\n    x_ticks = np.arange(1, N_EPOCHS+1)\n\n    # Validation\n    if val:\n        val_values = history.history[f'val_{metric}']\n        val_argmin = f_best(val_values)\n        plt.plot(x_ticks, val_values, label=f'val')\n\n    # summarize history for accuracy\n    plt.plot(x_ticks, values, label=f'train')\n    argmin = f_best(values)\n    plt.scatter(argmin + 1, values[argmin], color='red', s=75, marker='o', label=f'train_best')\n    if val:\n        plt.scatter(val_argmin + 1, val_values[val_argmin], color='purple', s=75, marker='o', label=f'val_best')\n\n    plt.title(f'Model {metric}', fontsize=24, pad=10)\n    plt.ylabel(metric, fontsize=20, labelpad=10)\n\n    if ylim:\n        plt.ylim(ylim)\n\n    if yscale is not None:\n        plt.yscale(yscale)\n        \n    if yticks is not None:\n        plt.yticks(yticks, fontsize=16)\n\n    plt.xlabel('epoch', fontsize=20, labelpad=10)        \n    plt.tick_params(axis='x', labelsize=8)\n    plt.xticks(x, fontsize=16) # set tick step to 1 and let x axis start at 1\n    plt.yticks(fontsize=16)\n    \n    plt.legend(prop={'size': 10})\n    plt.grid()\n    plt.show()","metadata":{"papermill":{"duration":0.072449,"end_time":"2023-07-02T15:01:51.621731","exception":false,"start_time":"2023-07-02T15:01:51.549282","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:29.276922Z","iopub.execute_input":"2023-07-31T07:41:29.277259Z","iopub.status.idle":"2023-07-31T07:41:29.292958Z","shell.execute_reply.started":"2023-07-31T07:41:29.277227Z","shell.execute_reply":"2023-07-31T07:41:29.291986Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"history.history.keys() # there's not 'val'","metadata":{"execution":{"iopub.status.busy":"2023-07-31T07:41:29.29438Z","iopub.execute_input":"2023-07-31T07:41:29.294718Z","iopub.status.idle":"2023-07-31T07:41:29.308899Z","shell.execute_reply.started":"2023-07-31T07:41:29.294685Z","shell.execute_reply":"2023-07-31T07:41:29.30792Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_history_metric('loss', f_best=np.argmin)","metadata":{"papermill":{"duration":0.534279,"end_time":"2023-07-02T15:01:52.213146","exception":false,"start_time":"2023-07-02T15:01:51.678867","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:29.310102Z","iopub.execute_input":"2023-07-31T07:41:29.310649Z","iopub.status.idle":"2023-07-31T07:41:29.715124Z","shell.execute_reply.started":"2023-07-31T07:41:29.310616Z","shell.execute_reply":"2023-07-31T07:41:29.714132Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_history_metric('top1acc', ylim=[0,1], yticks=np.arange(0.0, 1.1, 0.1))","metadata":{"papermill":{"duration":0.542072,"end_time":"2023-07-02T15:01:52.812958","exception":false,"start_time":"2023-07-02T15:01:52.270886","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:29.716736Z","iopub.execute_input":"2023-07-31T07:41:29.717129Z","iopub.status.idle":"2023-07-31T07:41:30.090169Z","shell.execute_reply.started":"2023-07-31T07:41:29.717093Z","shell.execute_reply":"2023-07-31T07:41:30.089211Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_history_metric('top5acc', ylim=[0,1], yticks=np.arange(0.0, 1.1, 0.1))","metadata":{"papermill":{"duration":0.542339,"end_time":"2023-07-02T15:01:53.417063","exception":false,"start_time":"2023-07-02T15:01:52.874724","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:30.09182Z","iopub.execute_input":"2023-07-31T07:41:30.092483Z","iopub.status.idle":"2023-07-31T07:41:30.460144Z","shell.execute_reply.started":"2023-07-31T07:41:30.092446Z","shell.execute_reply":"2023-07-31T07:41:30.459207Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Inference\n\nEl siguiente código define la clase `TFLiteModel`, que representa un modelo de TensorFlow Lite (TFLite) construido a partir del modelo previamente entrenado. Esto es es útil cuando se quiere desplegar el modelo en dispositivos con recursos limitados, como teléfonos móviles, dispositivos IoT o sistemas embebidos. TFLite es una versión optimizada de TensorFlow diseñada para inferencias rápidas y eficientes en dispositivos con capacidad computacional limitada.\n\n### Paso a paso:\n\n1. `TFLiteModel(tf.Module)`: Se define una clase llamada `TFLiteModel` que hereda de `tf.Module`, indicando que es una entidad entrenable en TensorFlow.\n\n2. `def __init__(self, model)`: El método `__init__` es el constructor de la clase y se ejecuta automáticamente al crear una instancia de `TFLiteModel`. Recibe el argumento `model`, que es el modelo previamente entrenado que se utilizará como base para el modelo TFLite.\n\n3. `self.preprocess_layer = preprocess_layer`: Se asigna a `self.preprocess_layer` la capa de preprocesamiento que se utiliza para procesar los datos de entrada antes de pasarlos al modelo base.\n\n4. `self.model = model`: Se asigna a `self.model` el modelo previamente entrenado que se utilizará como base para el modelo TFLite.\n\n5. `@tf.function(jit_compile=True)`: Los siguientes métodos están decorados con `@tf.function`, lo que indica que serán compilados por TensorFlow para un rendimiento más rápido.\n\n6. `def encoder(self, x, frames_inp)`: Este método implementa la parte del modelo correspondiente al encoder. De igual forma que al construir el modelo, el encoder toma dos argumentos; `x`, que representa los datos de entrada, y `frames_inp`, que es la forma de los datos. En estaó funcin se hace embedding y luego encoding.\n\n7. `def decoder(self, x, phrase_inp)`: Este método implementa la parte del modelo correspondiente al decoder. De igual forma que al construir el modelo, el decoder toma dos argumentos; `x`, que representa la salida del codificador, y `phrase_inp`, que es la forma de los datos tipo \"phrase\".\n\n8. `@tf.function(input_signature=[tf.TensorSpec(shape=[None, N_COLS0], dtype=tf.float32, name='inputs')])`: El método `__call__` se define como una función TensorFlow que se utilizará para realizar inferencias con el modelo TFLite. Se especifica la forma y tipo de los datos de entrada utilizando `tf.TensorSpec`.\n\n9. `N_INPUT_FRAMES = tf.shape(inputs)[0]`: Se obtiene el número de filas de la entrada `inputs`, que representa el número de frames.\n\n10. `frames_inp = self.preprocess_layer(inputs)`: Se procesan los datos de entrada utilizando la capa de preprocesamiento `preprocess_layer`.\n\n11. `frames_inp = tf.expand_dims(frames_inp, axis=0)`: Se agrega una dimensión de lote (batch dimension) a `frames_inp` para que coincida con la entrada del modelo original.\n\n12. `encoding = self.encoder(frames_inp, frames_inp)`: Se obtiene la representación codificada de los datos de entrada utilizando el método `encoder` definido anteriormente.\n\n13. `phrase = tf.fill([1,MAX_PHRASE_LENGTH], PAD_TOKEN)`: De igual forma que hicimos anteriormente, se crea un tensor de forma `[1, MAX_PHRASE_LENGTH]` lleno con el valor `PAD_TOKEN`. Este tensor representa la frase de salida que se irá construyendo durante la inferencia.\n\n14. `for idx in tf.range(MAX_PHRASE_LENGTH)`: Se itera sobre el rango de índices de la longitud máxima de la frase (`MAX_PHRASE_LENGTH=32`).\n\n15. `phrase = tf.cast(phrase, tf.int8)`: Se realiza un casting del tensor `phrase` a tipo `int8`.\n\n16. `outputs = tf.cond(stop, ...)`: Se utiliza una condicional de TensorFlow (`tf.cond`) para determinar si se debe detener la generación de la frase. Si `stop=True` , lo que significa que se predijo el token de final de oración (`END_TOKEN`), se devuelve el tensor `phrase` convertido a one-hot encoding y se detiene la generación de la frase. Si `stop=False`, se continúa generando la siguiente palabra de la frase utilizando el método `decoder`.\n\n17. `phrase = tf.where(tf.range(MAX_PHRASE_LENGTH) < idx + 1, ...)`: Se actualiza el tensor `phrase` reemplazando el token de relleno (`PAD_TOKEN`) con la palabra predicha hasta el índice actual (`idx`) en la generación de la frase.\n\n18. `predicted_token = phrase[0,idx]`: Se obtiene el token predicho en la posición `idx`.\n\n19. `if not stop: stop = predicted_token == END_TOKEN`: Se actualiza la variable `stop` verificando si el token predicho es igual al token de final de oración (`END_TOKEN`). Si es así, se establece `stop` en verdadero, lo que significa que la generación de la frase debe detenerse.\n\n20. `outputs = tf.squeeze(phrase, axis=0)`: Se elimina la dimensión de lote agregada anteriormente para obtener la frase completa generada.\n\n21. `outputs = tf.one_hot(outputs, N_UNIQUE_CHARACTERS)`: Se convierte la frase generada en one-hot encoding.\n\n22. Finalmente, el método `__call__` devuelve un diccionario con la clave `'outputs'` que contiene la frase generada.\n\nDespués de definir la clase `TFLiteModel`, se crea una instancia de la misma llamada `tflite_keras_model` y se realiza una demostración de inferencia utilizando datos de entrada `demo_raw_data`. La inferencia se realiza llamando al método `__call__` de `tflite_keras_model` y se muestra la frase generada, la frase verdadera y los datos de entrada en formato de tensor.","metadata":{"papermill":{"duration":0.059194,"end_time":"2023-07-02T15:01:53.537697","exception":false,"start_time":"2023-07-02T15:01:53.478503","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Model Layer Names\nfor l in model.layers:\n    print(l.name)","metadata":{"papermill":{"duration":0.070811,"end_time":"2023-07-02T15:01:53.667988","exception":false,"start_time":"2023-07-02T15:01:53.597177","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:30.470937Z","iopub.execute_input":"2023-07-31T07:41:30.471509Z","iopub.status.idle":"2023-07-31T07:41:30.477829Z","shell.execute_reply.started":"2023-07-31T07:41:30.471471Z","shell.execute_reply":"2023-07-31T07:41:30.47684Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# TFLite model for submission\nclass TFLiteModel(tf.Module):\n    def __init__(self, model):\n        super(TFLiteModel, self).__init__()\n\n        # Load the feature generation and main models\n        self.preprocess_layer = preprocess_layer\n        self.model = model\n    \n    @tf.function(jit_compile=True)\n    def encoder(self, x, frames_inp):\n        x = self.model.get_layer('embedding')(x)\n        x = self.model.get_layer('encoder')(x, frames_inp)\n        \n        return x\n        \n    @tf.function(jit_compile=True)\n    def decoder(self, x, phrase_inp, frames_inp):\n        x = self.model.get_layer('decoder')(x, phrase_inp, frames_inp)\n        x = self.model.get_layer('classifier')(x)\n        \n        return x\n    \n    @tf.function(input_signature=[tf.TensorSpec(shape=[None, N_COLS0], dtype=tf.float32, name='inputs')])\n    def __call__(self, inputs):\n        # Number Of Input Frames\n        N_INPUT_FRAMES = tf.shape(inputs)[0]\n        # Preprocess Data\n        frames_inp = self.preprocess_layer(inputs)        \n        # Add Batch Dimension\n        frames_inp = tf.expand_dims(frames_inp, axis=0)\n        # Get Encoding\n        encoding = self.encoder(frames_inp, frames_inp)\n        # Make Prediction\n        phrase = tf.fill([1,MAX_PHRASE_LENGTH], PAD_TOKEN)\n        # Predict One Token At A Time\n        stop = False\n        for idx in tf.range(MAX_PHRASE_LENGTH):\n            # Cast phrase to int8\n            phrase = tf.cast(phrase, tf.int8)\n            # If EOS token is predicted, stop predicting\n            outputs = tf.cond(\n                stop,\n                lambda: tf.one_hot(tf.cast(phrase, tf.int32), N_UNIQUE_CHARACTERS),\n                lambda: self.decoder(encoding, phrase, frames_inp)\n            )\n            # Add predicted token to input phrase\n            phrase = tf.cast(phrase, tf.int32)\n            # Replcae PAD token with predicted token up to idx\n            phrase = tf.where(\n                tf.range(MAX_PHRASE_LENGTH) < idx + 1,\n                tf.argmax(outputs, axis=2, output_type=tf.int32),\n                phrase,\n            )\n            # Predicted Token\n            predicted_token = phrase[0,idx]\n            # If EOS (End Of Sentence) token is predicted stop\n            if not stop:\n                stop = predicted_token == END_TOKEN\n            \n        # Squeeze outputs\n        outputs = tf.squeeze(phrase, axis=0)\n        outputs = tf.one_hot(outputs, N_UNIQUE_CHARACTERS)\n            \n        # Return a dictionary with the output tensor\n        return {'outputs': outputs }\n\n# Define TF Lite Model\ntflite_keras_model = TFLiteModel(model)\n\n# Sanity Check\n# demo_sequence_id = 1816796431\ndemo_sequence_id = example_parquet_df.index.unique()[0]\ndemo_raw_data = example_parquet_df.loc[demo_sequence_id, COLUMNS0].values\ndemo_phrase_true = train_sequence_id.loc[demo_sequence_id, 'phrase']\nprint(f'demo_raw_data shape: {demo_raw_data.shape}, dtype: {demo_raw_data.dtype}')\ndemo_output = tflite_keras_model(demo_raw_data)['outputs'].numpy()\nprint(f'demo_output shape: {demo_output.shape}, dtype: {demo_output.dtype}')\nprint(f'demo_outputs phrase decoded: {outputs2phrase(demo_output)}')\nprint(f'phrase true: {demo_phrase_true}')","metadata":{"papermill":{"duration":6.507954,"end_time":"2023-07-02T15:02:00.234987","exception":false,"start_time":"2023-07-02T15:01:53.727033","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:30.479404Z","iopub.execute_input":"2023-07-31T07:41:30.47977Z","iopub.status.idle":"2023-07-31T07:41:36.49979Z","shell.execute_reply.started":"2023-07-31T07:41:30.479736Z","shell.execute_reply":"2023-07-31T07:41:36.49875Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create Model Converter\nkeras_model_converter = tf.lite.TFLiteConverter.from_keras_model(tflite_keras_model)\n# Convert Model\ntflite_model = keras_model_converter.convert()\n# Write Model\nwith open('/kaggle/working/model.tflite', 'wb') as f:\n    f.write(tflite_model)","metadata":{"papermill":{"duration":63.286967,"end_time":"2023-07-02T15:03:03.582231","exception":false,"start_time":"2023-07-02T15:02:00.295264","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:41:36.501727Z","iopub.execute_input":"2023-07-31T07:41:36.502015Z","iopub.status.idle":"2023-07-31T07:42:48.375945Z","shell.execute_reply.started":"2023-07-31T07:41:36.501989Z","shell.execute_reply":"2023-07-31T07:42:48.374764Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Add selected_columns json to only select specific columns from input frames\nwith open('inference_args.json', 'w') as f:\n     json.dump({ 'selected_columns': COLUMNS0.tolist() }, f)","metadata":{"papermill":{"duration":0.070965,"end_time":"2023-07-02T15:03:03.713519","exception":false,"start_time":"2023-07-02T15:03:03.642554","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:42:48.377796Z","iopub.execute_input":"2023-07-31T07:42:48.378401Z","iopub.status.idle":"2023-07-31T07:42:48.384524Z","shell.execute_reply.started":"2023-07-31T07:42:48.378363Z","shell.execute_reply":"2023-07-31T07:42:48.383332Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Zip Model\n!zip submission.zip /kaggle/working/model.tflite /kaggle/working/inference_args.json","metadata":{"papermill":{"duration":1.931415,"end_time":"2023-07-02T15:03:05.70449","exception":false,"start_time":"2023-07-02T15:03:03.773075","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-31T07:42:48.386304Z","iopub.execute_input":"2023-07-31T07:42:48.38672Z","iopub.status.idle":"2023-07-31T07:42:50.624291Z","shell.execute_reply.started":"2023-07-31T07:42:48.386686Z","shell.execute_reply":"2023-07-31T07:42:50.623065Z"},"trusted":true},"execution_count":null,"outputs":[]}]}