{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":52950,"databundleVersionId":5973250}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 🏗️ Update: Try new architectures (Bi-LSTM)\n\n### Changes in this version:\n- **Replaced EmbeddedRNN → EmbeddedLSTM (Bidirectional)**\n  - Layers: 2\n  - Hidden size: 256\n  - Dropout: 0.3\n\n## ✋🏽 Update: Landmarks Normalization\n\n### Changes in this version:\n- **Integrate: Landmarks position normalization**\n- (https://www.kaggle.com/code/lejuin/fingerspelling-data-prep#Center-hand)\n\n## 📈 Update: W&B\n\n### Changes in this version:\n- **Integrate: Metrics monitoring in W&B team workspace**\n- (https://wandb.ai/inaki-rodriguez-reyes-upc-universidad-peruana-de-ciencia/asl-fingerspelling-previews)\n\n## 🔡 Update: Words\n\n### Changes in this version:\n- **Integrate: Filter only words with a-z**\n- (https://www.kaggle.com/code/pauerv/notebook136616d653-v12-overfit)\n- **Reduced dataset size**\n  - Train: 6564\n  - Val: 863\n  - Test: 920","metadata":{}},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2026-03-01T18:36:53.867613Z","iopub.execute_input":"2026-03-01T18:36:53.868192Z","iopub.status.idle":"2026-03-01T18:36:54.025215Z","shell.execute_reply.started":"2026-03-01T18:36:53.868164Z","shell.execute_reply":"2026-03-01T18:36:54.02443Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Install and configure W&B on this notebook\n!pip install wandb -q","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:54.026817Z","iopub.execute_input":"2026-03-01T18:36:54.027476Z","iopub.status.idle":"2026-03-01T18:36:57.250214Z","shell.execute_reply.started":"2026-03-01T18:36:54.027448Z","shell.execute_reply":"2026-03-01T18:36:57.249354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom kaggle_secrets import UserSecretsClient\nimport wandb\n\nuser_secrets = UserSecretsClient()\nsecret_value = user_secrets.get_secret(\"wandb\")\nos.environ[\"WANDB_API_KEY\"] = secret_value\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.251392Z","iopub.execute_input":"2026-03-01T18:36:57.251647Z","iopub.status.idle":"2026-03-01T18:36:57.333803Z","shell.execute_reply.started":"2026-03-01T18:36:57.251619Z","shell.execute_reply":"2026-03-01T18:36:57.333248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\n\n# Lectura de parquets \nimport pyarrow.parquet as pq\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\n\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.334599Z","iopub.execute_input":"2026-03-01T18:36:57.334883Z","iopub.status.idle":"2026-03-01T18:36:57.341056Z","shell.execute_reply.started":"2026-03-01T18:36:57.334846Z","shell.execute_reply":"2026-03-01T18:36:57.340448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_CSV_PATH = \"/kaggle/input/asl-fingerspelling/train.csv\"\n\ntrain_df = pd.read_csv(TRAIN_CSV_PATH)\n\nprint(\"Total muestras:\", len(train_df))\ntrain_df.head()\n\n## para que entendamos que hay dentro de los parquets, conjunto de líneas que representan un frame identificado a la vedz por un sequence_id.\n## como veremos en cada línea del parquet hay muchas coordenadas, de cara, mano izqueirda, mano derecha, etc, a nosotros solo nos va a interesar\n#coordenadas de la mano derecha que supuestamente es la que va a representar el lenguaje de signos\n\n## el dataset se divide en parquets por temas de eficiencia y porque sería poco eficiente almacenar todo en un solo parquet, las consultas \n## a las diferentes secuencias serían muy lentas\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.342752Z","iopub.execute_input":"2026-03-01T18:36:57.343019Z","iopub.status.idle":"2026-03-01T18:36:57.441523Z","shell.execute_reply.started":"2026-03-01T18:36:57.342993Z","shell.execute_reply":"2026-03-01T18:36:57.44089Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Vamos a hacer una función que filtre el \"phrase\" del dataset para que recibamos una única palabra i mediante una expresión regultar\n#que nos saque solo palabras que contengas letras del abecedario, sin números ni carácteres raros.\n\nimport re\n\ndef is_single_word(word):\n    return bool(re.fullmatch(r\"[A-Za-z]+\", word))\n\ntrain_df[\"is_single_word\"] = train_df[\"phrase\"].apply(is_single_word)\n\nsingle_word_df = train_df[train_df[\"is_single_word\"]].copy()\n\nprint(\"Total muestras:\", len(train_df))\nprint(\"Solo palabras:\", len(single_word_df))\nprint(\"Porcentaje:\", len(single_word_df) / len(train_df) * 100)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.442451Z","iopub.execute_input":"2026-03-01T18:36:57.442777Z","iopub.status.idle":"2026-03-01T18:36:57.504018Z","shell.execute_reply.started":"2026-03-01T18:36:57.442754Z","shell.execute_reply":"2026-03-01T18:36:57.503418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#No hay suficientes palabras sueltas para considerar únicament las muestras de una palabra. Como vemos, una única palabra son solo el 1% del dataset\n# y seguramente de estas palabras sueltas algunas contendran caràcteres especiales y números que queremos excluir, por lo que no usaremos este método\n\n#Como lo que queremos es detectar letra a letra, creemos que no es crítico\n\ntrain_df[\"phrase\"].head(20)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.50501Z","iopub.execute_input":"2026-03-01T18:36:57.505566Z","iopub.status.idle":"2026-03-01T18:36:57.511168Z","shell.execute_reply.started":"2026-03-01T18:36:57.50554Z","shell.execute_reply":"2026-03-01T18:36:57.510369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Como vemos, aparecen muchos carácteres especiales o números, deberíamos limpiar estas entradas.\n\nfrom collections import Counter\nimport string\n\nletters = set(string.ascii_lowercase)\n\nletter_counter = Counter()\n\nfor phrase in train_df[\"phrase\"].astype(str):\n    for ch in phrase.lower():\n        if ch in letters:\n            letter_counter[ch] += 1\n\nletter_counter","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.51219Z","iopub.execute_input":"2026-03-01T18:36:57.512398Z","iopub.status.idle":"2026-03-01T18:36:57.747671Z","shell.execute_reply.started":"2026-03-01T18:36:57.51238Z","shell.execute_reply":"2026-03-01T18:36:57.746938Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(letter_counter), letter_counter.most_common()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.748731Z","iopub.execute_input":"2026-03-01T18:36:57.749005Z","iopub.status.idle":"2026-03-01T18:36:57.754343Z","shell.execute_reply.started":"2026-03-01T18:36:57.748974Z","shell.execute_reply":"2026-03-01T18:36:57.753772Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Podemos ver aquí que tenemos muestras que incluyen las 26 letras del abecedario y con bastantes apariciones, por ejemplo la letra e aparece en 71986 veces\n# o la que menos, la q con 1114.\nimport re\n\ndef is_clean_phrase(phrase):\n    return bool(re.fullmatch(r\"[A-Za-z ]+\", phrase))\n\nclean_df = train_df[train_df[\"phrase\"].apply(is_clean_phrase)].copy()\n\nprint(\"Muestras limpias:\", len(clean_df))\nprint(\"Porcentaje:\", len(clean_df) / len(train_df) * 100)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.755329Z","iopub.execute_input":"2026-03-01T18:36:57.7559Z","iopub.status.idle":"2026-03-01T18:36:57.817567Z","shell.execute_reply.started":"2026-03-01T18:36:57.755877Z","shell.execute_reply":"2026-03-01T18:36:57.817024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"clean_df.iloc[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.818346Z","iopub.execute_input":"2026-03-01T18:36:57.818609Z","iopub.status.idle":"2026-03-01T18:36:57.824393Z","shell.execute_reply.started":"2026-03-01T18:36:57.818573Z","shell.execute_reply":"2026-03-01T18:36:57.823617Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# division por participantes, trian, evalu i test no deberían compartir los mismos participantes\nparticipants = clean_df[\"participant_id\"].unique()\nnp.random.shuffle(participants)\n\nn = len(participants)\n\ntrain_ids = participants[:int(0.8 * n)]\nval_ids   = participants[int(0.8 * n):int(0.9 * n)]\ntest_ids  = participants[int(0.9 * n):]\n\ntrain_df_split = clean_df[clean_df[\"participant_id\"].isin(train_ids)]\nval_df_split   = clean_df[clean_df[\"participant_id\"].isin(val_ids)]\ntest_df_split  = clean_df[clean_df[\"participant_id\"].isin(test_ids)]\n\nprint(\"Train:\", len(train_df_split))\nprint(\"Val:  \", len(val_df_split))\nprint(\"Test: \", len(test_df_split))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.825333Z","iopub.execute_input":"2026-03-01T18:36:57.825825Z","iopub.status.idle":"2026-03-01T18:36:57.842012Z","shell.execute_reply.started":"2026-03-01T18:36:57.825794Z","shell.execute_reply":"2026-03-01T18:36:57.841407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"VOCAB_PATH = \"/kaggle/input/asl-fingerspelling/character_to_prediction_index.json\"\n\nwith open(VOCAB_PATH) as f:\n    original_letter_to_int = json.load(f)\n\nprint(\"Tamaño vocabulario original:\", len(original_letter_to_int))\nprint(list(original_letter_to_int.items()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.842873Z","iopub.execute_input":"2026-03-01T18:36:57.843196Z","iopub.status.idle":"2026-03-01T18:36:57.855339Z","shell.execute_reply.started":"2026-03-01T18:36:57.843173Z","shell.execute_reply":"2026-03-01T18:36:57.854733Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Añadimos el blank para que CTC pueda detectar cuando hay un cambio de letra\nletter_to_int = {}\n\nletter_to_int[\"<blank>\"] = 0\n\n# Desplazamos el resto +1\nfor char, idx in original_letter_to_int.items():\n    letter_to_int[char] = idx + 1\n\nint_to_letter = {v: k for k, v in letter_to_int.items()}\n\nprint(\"Tamaño vocabulario CTC:\", len(letter_to_int))\nprint(list(letter_to_int.items()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.85837Z","iopub.execute_input":"2026-03-01T18:36:57.85859Z","iopub.status.idle":"2026-03-01T18:36:57.867037Z","shell.execute_reply.started":"2026-03-01T18:36:57.85857Z","shell.execute_reply":"2026-03-01T18:36:57.866476Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Nuevo vocabulario filtrado por las letras que vamos a procesar al principio, que no contendran números ni carácteres especiales, por lo que este\n#se reduce a 27 i el Blank, que es lo que el ctc detecta como frames donde no pasa nada, pausas entre letras, transiciones, ruido...\n\nimport string\n\n# letras permitidas\nletters = list(string.ascii_lowercase)\n\n# vocabulario CTC\nletter_to_int = {\"<blank>\": 0}\n\nfor i, ch in enumerate(letters):\n    letter_to_int[ch] = i + 1\n\nint_to_letter = {v: k for k, v in letter_to_int.items()}\n\nprint(\"Tamaño vocabulario:\", len(letter_to_int))\nprint(letter_to_int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.867749Z","iopub.execute_input":"2026-03-01T18:36:57.868016Z","iopub.status.idle":"2026-03-01T18:36:57.888461Z","shell.execute_reply.started":"2026-03-01T18:36:57.867986Z","shell.execute_reply":"2026-03-01T18:36:57.887655Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Encode target","metadata":{}},{"cell_type":"code","source":"def encode_phrase(phrase, letter_to_int):\n    return [letter_to_int[c] for c in phrase if c in letter_to_int]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.88926Z","iopub.execute_input":"2026-03-01T18:36:57.889541Z","iopub.status.idle":"2026-03-01T18:36:57.901748Z","shell.execute_reply.started":"2026-03-01T18:36:57.889516Z","shell.execute_reply":"2026-03-01T18:36:57.9011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Añadir frases codificadas\ntrain_df_split = train_df_split.copy()\nval_df_split   = val_df_split.copy()\n\ntrain_df_split[\"encoded\"] = train_df_split[\"phrase\"].apply(\n    lambda x: encode_phrase(x, letter_to_int)\n)\n\nval_df_split[\"encoded\"] = val_df_split[\"phrase\"].apply(\n    lambda x: encode_phrase(x, letter_to_int)\n)\n\ntrain_df_split[[\"phrase\", \"encoded\"]].head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.902772Z","iopub.execute_input":"2026-03-01T18:36:57.903439Z","iopub.status.idle":"2026-03-01T18:36:57.933806Z","shell.execute_reply.started":"2026-03-01T18:36:57.903406Z","shell.execute_reply":"2026-03-01T18:36:57.933218Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Normalize inputs","metadata":{}},{"cell_type":"code","source":"MAX_FRAMES = 160\n#EXPERIMENT_2\n#MAX_FRAMES=300\nRIGHT_HAND_COLS = None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.934874Z","iopub.execute_input":"2026-03-01T18:36:57.935265Z","iopub.status.idle":"2026-03-01T18:36:57.946575Z","shell.execute_reply.started":"2026-03-01T18:36:57.935241Z","shell.execute_reply":"2026-03-01T18:36:57.946033Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def read_right_hand_sequence_df(file_id, sequence_id):\n    global RIGHT_HAND_COLS\n\n    path = f\"/kaggle/input/asl-fingerspelling/train_landmarks/{file_id}.parquet\"\n    pq_file = pq.ParquetFile(path)\n\n    # Detectamos columnas una sola vez\n    if RIGHT_HAND_COLS is None:\n        RIGHT_HAND_COLS = [c for c in pq_file.schema.names if \"right_hand\" in c]\n\n    table = pq.read_table(\n        path,\n        filters=[(\"sequence_id\", \"=\", sequence_id)],\n        columns=RIGHT_HAND_COLS\n    )\n\n    X = table.to_pandas()\n    return X","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.947468Z","iopub.execute_input":"2026-03-01T18:36:57.947702Z","iopub.status.idle":"2026-03-01T18:36:57.960416Z","shell.execute_reply.started":"2026-03-01T18:36:57.947682Z","shell.execute_reply":"2026-03-01T18:36:57.96Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def read_right_hand_sequence_array(file_id, sequence_id):\n    return read_right_hand_sequence_df(file_id, sequence_id).values.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.961018Z","iopub.execute_input":"2026-03-01T18:36:57.961202Z","iopub.status.idle":"2026-03-01T18:36:57.979658Z","shell.execute_reply.started":"2026-03-01T18:36:57.961184Z","shell.execute_reply":"2026-03-01T18:36:57.979154Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Center hand","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.980513Z","iopub.execute_input":"2026-03-01T18:36:57.980926Z","iopub.status.idle":"2026-03-01T18:36:57.994221Z","shell.execute_reply.started":"2026-03-01T18:36:57.980895Z","shell.execute_reply":"2026-03-01T18:36:57.993689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def center_wrist(pdf):\n    tmp = pdf.copy()\n    for i in range (0,21):\n        tmp[f\"x_right_hand_{i}\"] -= pdf[\"x_right_hand_0\"]\n        tmp[f\"y_right_hand_{i}\"] -= pdf[\"y_right_hand_0\"]\n        tmp[f\"z_right_hand_{i}\"] -= pdf[\"z_right_hand_0\"]\n    return tmp","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:57.994946Z","iopub.execute_input":"2026-03-01T18:36:57.99544Z","iopub.status.idle":"2026-03-01T18:36:58.009929Z","shell.execute_reply.started":"2026-03-01T18:36:57.995419Z","shell.execute_reply":"2026-03-01T18:36:58.009383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def normalize_to_box(pdf):\n    x_min = pdf[[f\"x_right_hand_{i}\" for i in range (0,21)]].min().min()\n    x_max = pdf[[f\"x_right_hand_{i}\" for i in range (0,21)]].max().max()\n    y_min = pdf[[f\"y_right_hand_{i}\" for i in range (0,21)]].min().min()\n    y_max = pdf[[f\"y_right_hand_{i}\" for i in range (0,21)]].max().max()\n    \n    ratio = min(1/(x_max-x_min), 1/(y_max-y_min))\n    tmp = pdf.copy()\n    for i in range (0,21):\n        tmp[f\"x_right_hand_{i}\"] -= x_min\n        tmp[f\"x_right_hand_{i}\"] *= ratio\n        tmp[f\"y_right_hand_{i}\"] -= y_min\n        tmp[f\"y_right_hand_{i}\"] *= ratio\n        tmp[f\"z_right_hand_{i}\"] *= ratio\n    return tmp","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:58.010754Z","iopub.execute_input":"2026-03-01T18:36:58.011063Z","iopub.status.idle":"2026-03-01T18:36:58.024671Z","shell.execute_reply.started":"2026-03-01T18:36:58.011035Z","shell.execute_reply":"2026-03-01T18:36:58.02416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"row = train_df_split.iloc[20]\nX_pdf = read_right_hand_sequence_df(row[\"file_id\"], row[\"sequence_id\"])\nX_pdf","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:58.025362Z","iopub.execute_input":"2026-03-01T18:36:58.025598Z","iopub.status.idle":"2026-03-01T18:36:58.171522Z","shell.execute_reply.started":"2026-03-01T18:36:58.025567Z","shell.execute_reply":"2026-03-01T18:36:58.170797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# distance between hand landmark 0 and 9 should be normalized, doesn't seem to be the case here, ToDo ?\n#(cf. https://storage.googleapis.com/mediapipe-assets/Model%20Card%20Hand%20Tracking%20(Lite_Full)%20with%20Fairness%20Oct%202021.pdf)\n\ndef dist(df, i,j):\n    return np.linalg.norm(df[[f\"{ax}_right_hand_{i}\" for ax in ['x', 'y', 'z']]].values - df[[f\"{ax}_right_hand_{j}\" for ax in ['x', 'y', 'z']]].values, axis=1)\n\n#X_pd[~X_pd.x_right_hand_0.isna()][['x_right_hand_0', 'y_right_hand_0', 'z_right_hand_0']]\nsns.displot(dist(X_pdf[~X_pdf['x_right_hand_0'].isna()], 0, 9))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:58.172469Z","iopub.execute_input":"2026-03-01T18:36:58.172706Z","iopub.status.idle":"2026-03-01T18:36:58.363087Z","shell.execute_reply.started":"2026-03-01T18:36:58.172685Z","shell.execute_reply":"2026-03-01T18:36:58.362306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X_centered = center_wrist(X_pdf)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:58.364042Z","iopub.execute_input":"2026-03-01T18:36:58.36433Z","iopub.status.idle":"2026-03-01T18:36:58.386283Z","shell.execute_reply.started":"2026-03-01T18:36:58.364297Z","shell.execute_reply":"2026-03-01T18:36:58.385659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.displot(X_pdf[[f\"x_right_hand_{i}\" for i in [4, 8, 12, 16, 20]]], kind='kde')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:58.38729Z","iopub.execute_input":"2026-03-01T18:36:58.387572Z","iopub.status.idle":"2026-03-01T18:36:58.80146Z","shell.execute_reply.started":"2026-03-01T18:36:58.38755Z","shell.execute_reply":"2026-03-01T18:36:58.800836Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.displot(X_centered[[f\"x_right_hand_{i}\" for i in [4, 8, 12, 16, 20]]], kind='kde')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:58.802351Z","iopub.execute_input":"2026-03-01T18:36:58.80257Z","iopub.status.idle":"2026-03-01T18:36:59.154488Z","shell.execute_reply.started":"2026-03-01T18:36:58.80255Z","shell.execute_reply":"2026-03-01T18:36:59.153813Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Visualization","metadata":{}},{"cell_type":"code","source":"!pip install mediapipe","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:36:59.155369Z","iopub.execute_input":"2026-03-01T18:36:59.155672Z","iopub.status.idle":"2026-03-01T18:37:02.451378Z","shell.execute_reply.started":"2026-03-01T18:36:59.155641Z","shell.execute_reply":"2026-03-01T18:37:02.450471Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_hands(seq_df):\n    import mediapipe as mp\n    from mediapipe.tasks.python.components.containers.landmark import NormalizedLandmark, Landmark\n    #import numpy as np\n    \n    mp_hands = mp.tasks.vision.HandLandmarksConnections\n    mp_drawing = mp.tasks.vision.drawing_utils\n    mp_drawing_styles = mp.tasks.vision.drawing_styles\n    \n    images = []\n    all_hand_landmarks = []\n    for seq_idx in range(len(seq_df)):\n        x_pose = seq_df.iloc[seq_idx].filter(regex=\"x_right_hand.*\").values\n        y_pose = seq_df.iloc[seq_idx].filter(regex=\"y_right_hand.*\").values\n        z_pose = seq_df.iloc[seq_idx].filter(regex=\"z_right_hand.*\").values\n\n        right_hand_landmarks = [NormalizedLandmark(x, y, z) for x, y, z in zip(x_pose, y_pose, z_pose)]\n\n        right_hand_image = np.zeros((900, 600, 3), dtype=\"int8\")\n\n        mp_drawing.draw_landmarks(\n              right_hand_image,\n              right_hand_landmarks,\n              mp_hands.HAND_CONNECTIONS,\n              mp_drawing_styles.get_default_hand_landmarks_style(),\n              mp_drawing_styles.get_default_hand_connections_style())\n        \n        x_pose = seq_df.iloc[seq_idx].filter(regex=\"x_left_hand.*\").values\n        y_pose = seq_df.iloc[seq_idx].filter(regex=\"y_left_hand.*\").values\n        z_pose = seq_df.iloc[seq_idx].filter(regex=\"z_left_hand.*\").values\n\n        left_hand_landmarks = [NormalizedLandmark(x, y, z) for x, y, z in zip(x_pose, y_pose, z_pose)]\n        \n        left_hand_image = np.zeros((900, 600, 3), dtype=\"int8\")\n\n        mp_drawing.draw_landmarks(\n                left_hand_image,\n                left_hand_landmarks,\n                mp_hands.HAND_CONNECTIONS,\n                mp_drawing_styles.get_default_hand_landmarks_style(),\n                mp_drawing_styles.get_default_hand_connections_style())\n        \n        images.append([right_hand_image.astype(np.uint8), left_hand_image.astype(np.uint8)])\n        all_hand_landmarks.append([right_hand_landmarks, left_hand_landmarks])\n    return images, all_hand_landmarks","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:02.45282Z","iopub.execute_input":"2026-03-01T18:37:02.453465Z","iopub.status.idle":"2026-03-01T18:37:02.462425Z","shell.execute_reply.started":"2026-03-01T18:37:02.453432Z","shell.execute_reply":"2026-03-01T18:37:02.461892Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from matplotlib import animation, rc\nrc('animation', html='jshtml')\n\ndef create_animation(images):\n    fig = plt.figure(figsize=(2, 3))\n    ax = plt.Axes(fig, [0., 0., 1., 1.])\n    ax.set_axis_off()\n    fig.add_axes(ax)\n    im=ax.imshow(images[0], cmap=\"gray\")\n    plt.close(fig)\n    \n    def animate_func(i):\n        im.set_array(images[i])\n        return [im]\n\n    return animation.FuncAnimation(fig, animate_func, frames=len(images), interval=1000/10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:02.463209Z","iopub.execute_input":"2026-03-01T18:37:02.46347Z","iopub.status.idle":"2026-03-01T18:37:02.481515Z","shell.execute_reply.started":"2026-03-01T18:37:02.463436Z","shell.execute_reply":"2026-03-01T18:37:02.481013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"hand_images, hand_landmarks = get_hands(X_pdf)\ncreate_animation(np.array(hand_images)[:, 0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:02.482373Z","iopub.execute_input":"2026-03-01T18:37:02.482739Z","iopub.status.idle":"2026-03-01T18:37:07.597309Z","shell.execute_reply.started":"2026-03-01T18:37:02.482699Z","shell.execute_reply":"2026-03-01T18:37:07.596415Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"hand_images, hand_landmarks = get_hands(normalize_to_box(X_pdf))\ncreate_animation(np.array(hand_images)[:, 0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:07.598624Z","iopub.execute_input":"2026-03-01T18:37:07.598972Z","iopub.status.idle":"2026-03-01T18:37:12.891754Z","shell.execute_reply.started":"2026-03-01T18:37:07.598925Z","shell.execute_reply":"2026-03-01T18:37:12.891001Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"hand_images, hand_landmarks = get_hands(normalize_to_box(X_centered))\ncreate_animation(np.array(hand_images)[:, 0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:12.893115Z","iopub.execute_input":"2026-03-01T18:37:12.893707Z","iopub.status.idle":"2026-03-01T18:37:18.039159Z","shell.execute_reply.started":"2026-03-01T18:37:12.893665Z","shell.execute_reply":"2026-03-01T18:37:18.03831Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Normalize sequence length","metadata":{}},{"cell_type":"code","source":"def normalize_frames(X, max_frames=MAX_FRAMES):\n    T, D = X.shape\n\n    if T > max_frames:\n        return X[:max_frames]\n\n    if T < max_frames:\n        pad = np.zeros((max_frames - T, D), dtype=np.float32)\n        return np.vstack([X, pad])\n\n    return X","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.04011Z","iopub.execute_input":"2026-03-01T18:37:18.040338Z","iopub.status.idle":"2026-03-01T18:37:18.045473Z","shell.execute_reply.started":"2026-03-01T18:37:18.040316Z","shell.execute_reply":"2026-03-01T18:37:18.044748Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#row = train_df_split.iloc[20]\n\n#X = read_right_hand_sequence(row[\"file_id\"], row[\"sequence_id\"])\nX = X_centered.values.astype(np.float32)\nXn = normalize_frames(X)\n\nprint(\"Original:\", X.shape)\nprint(\"Normalizado:\", Xn.shape)\nprint(Xn[:2])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.046341Z","iopub.execute_input":"2026-03-01T18:37:18.046661Z","iopub.status.idle":"2026-03-01T18:37:18.065614Z","shell.execute_reply.started":"2026-03-01T18:37:18.04663Z","shell.execute_reply":"2026-03-01T18:37:18.06487Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training","metadata":{}},{"cell_type":"code","source":"row = train_df_split.iloc[0]\nfile_id, sequence_id = row[\"file_id\"], row[\"sequence_id\"]\n\npath = f\"/kaggle/input/asl-fingerspelling/train_landmarks/{file_id}.parquet\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.066703Z","iopub.execute_input":"2026-03-01T18:37:18.06699Z","iopub.status.idle":"2026-03-01T18:37:18.078771Z","shell.execute_reply.started":"2026-03-01T18:37:18.066937Z","shell.execute_reply":"2026-03-01T18:37:18.078259Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pq_file = pq.ParquetFile(path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.079557Z","iopub.execute_input":"2026-03-01T18:37:18.079844Z","iopub.status.idle":"2026-03-01T18:37:18.112656Z","shell.execute_reply.started":"2026-03-01T18:37:18.079811Z","shell.execute_reply":"2026-03-01T18:37:18.112124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def count_valid_frames(X):\n    # Un frame es válido si NO todo es NaN\n    return np.sum(~np.all(np.isnan(X), axis=1))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.113571Z","iopub.execute_input":"2026-03-01T18:37:18.113874Z","iopub.status.idle":"2026-03-01T18:37:18.117628Z","shell.execute_reply.started":"2026-03-01T18:37:18.113844Z","shell.execute_reply":"2026-03-01T18:37:18.116886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ASLRightHandDataset(Dataset):\n    def __init__(self, df, max_frames=160):\n        self.df = df.reset_index(drop=True)\n        self.max_frames = max_frames\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        X_raw = read_right_hand_sequence_df(\n            row[\"file_id\"],\n            row[\"sequence_id\"]\n        )  \n        X_centered = center_wrist(X_raw)\n    \n        input_len = count_valid_frames(X_centered)\n    \n        Y = torch.tensor(row[\"encoded\"], dtype=torch.long)\n        target_len = len(Y)\n\n        if input_len < target_len:\n            return None\n\n\n        X = normalize_frames(X_centered, self.max_frames)\n    \n        X = np.nan_to_num(X, nan=0.0)\n    \n        X = torch.tensor(X, dtype=torch.float32)\n    \n        input_len = min(input_len, self.max_frames)\n    \n        return X, Y, input_len, target_len\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.118507Z","iopub.execute_input":"2026-03-01T18:37:18.118762Z","iopub.status.idle":"2026-03-01T18:37:18.130745Z","shell.execute_reply.started":"2026-03-01T18:37:18.118733Z","shell.execute_reply":"2026-03-01T18:37:18.130079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def collate_fn(batch):\n    # Quitamos ejemplos inválidos (None)\n    batch = [b for b in batch if b is not None]\n\n    # Si todo el batch era inválido, devolvemos None\n    if len(batch) == 0:\n        return None\n\n    Xs = []\n    Ys = []\n    in_lens = []\n    tar_lens = []\n\n    for X, Y, in_len, tar_len in batch:\n        Xs.append(X)\n        Ys.append(Y)\n        in_lens.append(in_len)\n        tar_lens.append(tar_len)\n\n    Xs = torch.stack(Xs)\n    Ys = torch.cat(Ys)\n    in_lens = torch.tensor(in_lens, dtype=torch.long)\n    tar_lens = torch.tensor(tar_lens, dtype=torch.long)\n\n    return Xs, Ys, in_lens, tar_lens\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.135217Z","iopub.execute_input":"2026-03-01T18:37:18.135478Z","iopub.status.idle":"2026-03-01T18:37:18.144285Z","shell.execute_reply.started":"2026-03-01T18:37:18.135458Z","shell.execute_reply":"2026-03-01T18:37:18.143828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df_tiny = train_df_split.sample(200, random_state=0)\ntrain_dataset_tiny = ASLRightHandDataset(train_df_tiny)\n\nprint(\"INFO: Creating data loader with TINY size (200)\")\n\ntrain_loader_tiny = DataLoader(\n    train_dataset_tiny,\n    batch_size=4,\n    shuffle=True,\n    collate_fn=collate_fn,\n    num_workers=0\n)\n\nprint(\"INFO: Checking sizes\")\n\nX, Y, in_len, tar_len = next(iter(train_loader_tiny))\n\nprint(\"X:\", X.shape)          # (B, 160, 63)\nprint(\"Y:\", Y.shape)          # (sum of target lengths)\nprint(\"in_len:\", in_len)\nprint(\"tar_len:\", tar_len)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.14533Z","iopub.execute_input":"2026-03-01T18:37:18.145541Z","iopub.status.idle":"2026-03-01T18:37:18.670925Z","shell.execute_reply.started":"2026-03-01T18:37:18.145521Z","shell.execute_reply":"2026-03-01T18:37:18.670284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class EmbeddedLSTM(nn.Module):\n    def __init__(self, input_size, embed_size, hidden_size, num_classes, num_layers=2, dropout=0.3):\n        super().__init__()\n\n        self.embedding = nn.Linear(input_size, embed_size)\n        self.relu = nn.ReLU()\n        self.dropout_emb = nn.Dropout(dropout)\n\n        self.lstm = nn.LSTM(\n            input_size=embed_size,\n            hidden_size=hidden_size,\n            num_layers=num_layers,\n            batch_first=True,\n            bidirectional=True,\n            dropout=dropout if num_layers > 1 else 0\n            #EXPERIMENT_1\n            #dropout=0\n        )\n\n        # Bidirectional -> x2 hidden size\n        self.classifier = nn.Linear(hidden_size * 2, num_classes)\n        self.log_softmax = nn.LogSoftmax(dim=-1)\n\n    def forward(self, x):\n        x = self.embedding(x)\n        x = self.relu(x)\n        x = self.dropout_emb(x)\n        out, _ = self.lstm(x)       # (B, T, H*2)\n        out = self.classifier(out)  # (B, T, C)\n        out = self.log_softmax(out)\n        out = out.permute(1, 0, 2)  # (T, B, C) para CTC\n\n        return out\nprint(\"INFO: EmbeddedLSTM defined (bidirectional + dropout)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.671845Z","iopub.execute_input":"2026-03-01T18:37:18.672152Z","iopub.status.idle":"2026-03-01T18:37:18.679536Z","shell.execute_reply.started":"2026-03-01T18:37:18.672128Z","shell.execute_reply":"2026-03-01T18:37:18.678669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_classes = len(letter_to_int)\n\nmodel = EmbeddedLSTM(\n    input_size=63,         \n    embed_size=128,         \n    hidden_size=256,  # x2 hidden size\n    num_classes=len(letter_to_int),\n    num_layers=2,\n    dropout=0.3   # 0.3 dropout\n).to(DEVICE)\n\nprint(f\"Model: EmbeddedLSTM (Bidirectional) / Parameters: {sum(p.numel() for p in model.parameters()):,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.680614Z","iopub.execute_input":"2026-03-01T18:37:18.680941Z","iopub.status.idle":"2026-03-01T18:37:18.718138Z","shell.execute_reply.started":"2026-03-01T18:37:18.68091Z","shell.execute_reply":"2026-03-01T18:37:18.717526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\nwith torch.no_grad():\n    outputs = model(X.to(DEVICE))\n\nprint(outputs.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.718938Z","iopub.execute_input":"2026-03-01T18:37:18.719303Z","iopub.status.idle":"2026-03-01T18:37:18.7316Z","shell.execute_reply.started":"2026-03-01T18:37:18.719282Z","shell.execute_reply":"2026-03-01T18:37:18.731014Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.CTCLoss(\n    blank=letter_to_int[\"<blank>\"],\n    zero_infinity=True,\n    reduction='mean'  # ensure proper reduction\n)\n\nloss = criterion(\n    outputs,    # (T, B, C)\n    Y.to(DEVICE),\n    in_len,\n    tar_len\n)\n\nprint(\"CTC loss:\", loss.item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.732372Z","iopub.execute_input":"2026-03-01T18:37:18.732578Z","iopub.status.idle":"2026-03-01T18:37:18.741103Z","shell.execute_reply.started":"2026-03-01T18:37:18.732541Z","shell.execute_reply":"2026-03-01T18:37:18.740411Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    total_loss = 0.0\n    num_batches = 0\n\n    for batch in loader:\n\n        if batch is None:\n            continue\n\n        X, Y, input_lens, target_lens = batch\n\n        X = X.to(device)\n        Y = Y.to(device)\n        input_lens = input_lens.to(device)\n        target_lens = target_lens.to(device)\n\n        optimizer.zero_grad()\n\n        outputs = model(X)  # (T, B, C)\n\n        loss = criterion(\n            outputs,\n            Y,\n            input_lens,\n            target_lens\n        )\n\n        loss.backward()\n\n\n        optimizer.step()\n\n        total_loss += loss.item()\n        num_batches += 1\n\n    avg_loss = total_loss / num_batches\n\n    return avg_loss\nprint(\"INFO: train_one_epoch method defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.742085Z","iopub.execute_input":"2026-03-01T18:37:18.742738Z","iopub.status.idle":"2026-03-01T18:37:18.748632Z","shell.execute_reply.started":"2026-03-01T18:37:18.742713Z","shell.execute_reply":"2026-03-01T18:37:18.747864Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nstart_time = time.time()\n\nnum_epochs = 10\n\ntrain_losses = []\ngrad_norms = []\nlrate = 1e-3\noptimizer = optim.Adam(model.parameters(), lr=lrate)\n\nprint(\"=\"*60)\nprint(f\"INITIAL TRAINING\")\nprint(\"=\"*60)\n\n# Start a new wandb run to track this training script\nrun = wandb.init(\n    # Team workspace\n    entity=\"inaki-rodriguez-reyes-upc-universidad-peruana-de-ciencia\",\n    # Preview project\n    project=\"asl-fingerspelling-previews\",\n    \n    # Track hyperparameters and run metadata.\n    config={\n        \"name\": \"INITIAL TRAINING (EmbeddedLSTM)\",\n        \"notebook\": \"notebook136616d653_v12_lstm\",\n        \"version\": \"v1\",\n        \"learning_rate\": lrate,\n        \"architecture\": \"EmbeddedLSTM(Bidirectional)\",\n        \"dataset\": \"Google-ASL\",\n        \"datasize\": \"TINY(200)\",\n        #\"variante\": \"DROPOUT=0\", #EXPERIMENT_1\n        #\"variante\": \"MAX_FRAMES=300\", #EXPERIMENT_2\n        #\"variante\": \"EPOCHS=20\", #EXPERIMENT_3\n        \"epochs\": num_epochs,\n    },\n)\n\n\nprint(f\"[Very first training with TINY / lr={lrate}]\")\nfor epoch in range(num_epochs):\n    int_time = time.time()\n    loss = train_one_epoch(\n        model,\n        train_loader_tiny,\n        optimizer,\n        criterion,\n        DEVICE\n    )\n    train_losses.append(loss)\n    print(f\"Epoch {epoch+1} -> LOSS = {loss:.2f}\")\n    run.log({\"loss\": loss})\n    end_time = time.time()\n    print(f\"Elapsed time: {end_time - int_time:.2f} secs\")\n\nrun.finish()\n\nfinal_time = time.time()\nprint(\"-\"*60)\nprint(f\"TOTAL time: {(final_time - start_time)/60:.2f} mins\")\nprint(\"-\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:37:18.749583Z","iopub.execute_input":"2026-03-01T18:37:18.74993Z","iopub.status.idle":"2026-03-01T18:45:59.675524Z","shell.execute_reply.started":"2026-03-01T18:37:18.749896Z","shell.execute_reply":"2026-03-01T18:45:59.674823Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(6,4))\nplt.plot(train_losses, marker=\"o\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.title(f\"Training Loss over {num_epochs} Epochs / lr={lrate}\")\nplt.grid(True)\nplt.show()\n\nprint(\"-\"*60)\nprint(f\"LOSS TREND\")\nprint(f\"First loss: {train_losses[0]:.2f}\")\nprint(f\"Last loss: {train_losses[-1]:.2f}\")\nprint(f\"Loss decreased: {train_losses[0] - train_losses[-1]:.2f}\")\nprint(\"-\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:45:59.676578Z","iopub.execute_input":"2026-03-01T18:45:59.677069Z","iopub.status.idle":"2026-03-01T18:45:59.821062Z","shell.execute_reply.started":"2026-03-01T18:45:59.677041Z","shell.execute_reply":"2026-03-01T18:45:59.820435Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"INFO: [END_OF_NOTEBOOK]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-01T18:45:59.82181Z","iopub.execute_input":"2026-03-01T18:45:59.822098Z","iopub.status.idle":"2026-03-01T18:45:59.827091Z","shell.execute_reply.started":"2026-03-01T18:45:59.822067Z","shell.execute_reply":"2026-03-01T18:45:59.826184Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}