{"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":"markdown","source":"### <p style=\"font-family:JetBrains Mono; font-weight:normal; letter-spacing: 2px; color:#000000; font-size:140%; text-align:center;padding: 0px; border-bottom: 3px solid #000000\">WELCOME TO Google - American Sign Language Fingerspelling Recognition</p>\n\n**AUTHOR - SUJAY KAPADNIS**\n\n**DATE - 12/05/2023**\n\n## Description\n### Goal of the Competition\nThe goal of this competition is to **detect** and **translate** American Sign Language **(ASL)** fingerspelling into **text**. You will create a model trained on the largest dataset of its kind, released specifically for this competition. The data includes more than **three million fingerspelled characters** produced by **over 100 Deaf signers** captured via the selfie camera of a smartphone with a variety of backgrounds and lighting conditions.\n\n![](https://cdn.pixabay.com/photo/2012/04/24/17/00/hand-40446_960_720.png)\n\n## Context\n**Fingerspelling** uses **hand shapes** that represent **individual letters** *to* convey **words**. While fingerspelling is only a part of ASL, it is often **used for communicating names, addresses, phone numbers, and other information commonly entered on a mobile phone**. Many Deaf smartphone users can **fingerspell** words **faster** than they can type on **mobile keyboards**. In fact, ASL **fingerspelling** can be **substantially faster** than typing on a smartphone’s virtual keyboard (57 words/minute average versus 36 words/minute US average). **But** **sign language recognition AI for text** entry lags far behind voice-to-text or even gesture-based typing, as robust datasets didn't previously exist.\nTechnology that understands sign language fits squarely within Google's mission to organize the world's information and make it universally accessible and useful. Google’s AI principles also support this idea and encourage Google to make products that empower people, widely benefit current and future generations, and work for the common good. This collaboration between Google and the Deaf Professional Arts Network will explore AI solutions that can be scaled globally (such as other sign languages), and support individual user experience needs while interacting with products.\nBesides convenient text entry for web search, map directions, and texting, there is potential for an **app** that **can** then **translate this input using sign language-to-speech technology to speak the words**. Such an app would enable the Deaf and Hard of Hearing community to communicate with hearing non-signers more quickly and smoothly.\n\n## Evaluation Metric\nThe evaluation metric for this contest is the normalized total [levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). Let the **total number of characters in the labels be N** and the **total levenshtein distance be D**. The **metric** equals `(N - D) / N\n`*levenshtein distance - between two words is the minimum number of single-character edits (insertions, deletions or substitutions) required to change one word into the other*\n\nLets try to understand it with the example\nFor example, the Levenshtein distance between \"kitten\" and \"sitting\" is 3, since the following 3 edits change one into the other, and there is no way to do it with fewer than 3 edits:\n\n- kitten → sitten (substitution of \"s\" for \"k\"),\n- sitten → sittin (substitution of \"i\" for \"e\"),\n- sittin → sitting (insertion of \"g\" at the end).[source: Wiki]\n\n## Dataset Summary\nThe ASL Fingerspelling Recognition Corpus (version 1.0) is a collection of hand and facial landmarks generated by **Mediapipe version 0.9.0.1** on videos of phrases, addresses, phone numbers, and urls fingerspelling by over 100 Deaf signers.\n\n## Files\n**[train/supplemental_metadata].csv**\n\n`path` - The path to the landmark file.\n\n`file_id` - A unique identifier for the data file.\n\n`participant_id` - A unique identifier for the data contributor.\n\n`sequence_id` - A unique identifier for the landmark sequence. Each data file may contain many sequences.\n\n`phrase` - The labels for the landmark sequence. The train and test datasets contain randomly generated addresses, phone numbers, and urls derived from components of real addresses/phone numbers/urls. Any overlap with real addresses, phone numbers, or urls is purely accidental. The supplemental dataset consists of fingerspelled sentences. Note that some of the urls include adult content. The intent of this competition is to support the Deaf and Hard of Hearing community in engaging with technology on an equal footing with other adults.\n\n**character_to_prediction_index.json**\n\n**[train/supplemental]_landmarks**/ The landmark data. The landmarks were extracted from raw videos with the MediaPipe holistic model. Not all of the frames necessarily had visible hands or hands that could be detected by the model.\nThe landmark files contain the same data as in the ASL Signs competition (minus the row ID column) but reshaped into a wide format. This allows you to take advantage of the Parquet format to entirely skip loading landmarks that you aren't using.\n\n`sequence_id` - A unique identifier for the landmark sequence. Most landmark files contain 1,000 sequences. The sequence ID is used as the dataframe index.\n\n`frame` - The frame number within a landmark sequence.\n\n`[x/y/z]_[type]_[landmark_index]` - There are now 1,629 spatial coordinate columns for the x, y and z coordinates for each of the 543 landmarks. The type of landmark is one of `['face', 'left_hand', 'pose', 'right_hand']`. Details of the hand landmark locations can be found here. The spatial coordinates have already been normalized by MediaPipe. Note that the MediaPipe model is not fully trained to predict depth so you may wish to ignore the z values. The landmarks have been converted to float32.\n\n\n### This is work in Progress, if you find this helpful kindly upvote","metadata":{"papermill":{"duration":0.011905,"end_time":"2023-04-24T10:43:58.469766","exception":false,"start_time":"2023-04-24T10:43:58.457861","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"### <p style=\"font-family:JetBrains Mono; font-weight:normal; letter-spacing: 2px; color:#000000; font-size:140%; text-align:center;padding: 0px; border-bottom: 3px solid #000000\">Who is this notebook for</p>\n\nThis notebook can be very helpful for the beginners, it contains\n- Deep Learning concepts\n- NLP concepts\n- Tensorflow apis understanding\n- Transformers\n\nIt was quite a tough task for me too, I have analzed the codes line by line and written comments for understanding everywhere, also how the inbuilt functions of tensorflow(the ones that we do not commonly use) are shown in relevant cells. \n\nIt was a genuine try for help, help for myself and others, I hope you find this notebook helpful and start your journey for kaggle's featured competition from this notebook.\n\nI'll Leave you with the code, Enjoy:)","metadata":{}},{"cell_type":"markdown","source":"### <p style=\"font-family:JetBrains Mono; font-weight:normal; letter-spacing: 2px; color:#000000; font-size:140%; text-align:center;padding: 0px; border-bottom: 3px solid #000000\">LOAD YOUR DEPENDENCIES</p>","metadata":{"papermill":{"duration":0.010273,"end_time":"2023-04-24T10:43:58.490826","exception":false,"start_time":"2023-04-24T10:43:58.480553","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# # install nb_black for autoformating\n# !pip install nb_black --quiet\n# %load_ext lab_black","metadata":{"papermill":{"duration":15.177598,"end_time":"2023-04-24T10:44:13.679161","exception":false,"start_time":"2023-04-24T10:43:58.501563","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-07-14T18:39:01.471384Z","iopub.execute_input":"2023-07-14T18:39:01.471836Z","iopub.status.idle":"2023-07-14T18:39:01.478581Z","shell.execute_reply.started":"2023-07-14T18:39:01.47178Z","shell.execute_reply":"2023-07-14T18:39:01.47735Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"!pip install mediapipe --quiet","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:01.481883Z","iopub.execute_input":"2023-07-14T18:39:01.482463Z","iopub.status.idle":"2023-07-14T18:39:15.877125Z","shell.execute_reply.started":"2023-07-14T18:39:01.48243Z","shell.execute_reply":"2023-07-14T18:39:15.875947Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib as mpl\nfrom matplotlib import animation, rc\nimport matplotlib.pyplot as plt\nimport json\nimport mediapipe as mp\nfrom mediapipe.framework.formats import landmark_pb2\nimport plotly.graph_objs as go\n\nfrom colorama import Style, Fore\n\nimport os\nimport shutil\nimport pyarrow.parquet as pq\nimport tensorflow as tf\nimport json\nimport random\n\nfrom skimage.transform import resize\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom tqdm.notebook import tqdm\nfrom matplotlib import animation, rc\n\nblk = Style.BRIGHT + Fore.BLACK\nred = Style.BRIGHT + Fore.RED\nblu = Style.BRIGHT + Fore.BLUE\ncyan = Style.BRIGHT + Fore.CYAN\ngreen = Style.BRIGHT + Fore.GREEN\nres = Style.RESET_ALL","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:15.879842Z","iopub.execute_input":"2023-07-14T18:39:15.881567Z","iopub.status.idle":"2023-07-14T18:39:23.996832Z","shell.execute_reply.started":"2023-07-14T18:39:15.881521Z","shell.execute_reply":"2023-07-14T18:39:23.995879Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Load data\nBASE_DIR = \"/kaggle/input/asl-fingerspelling/\"\ntrain_metadata_df = pd.read_csv(f\"{BASE_DIR}train.csv\")\nsupp_metadata_df = pd.read_csv(f\"{BASE_DIR}supplemental_metadata.csv\")\nchar_to_pred_idx = json.load(open(f\"{BASE_DIR}character_to_prediction_index.json\"))","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:23.998134Z","iopub.execute_input":"2023-07-14T18:39:23.998833Z","iopub.status.idle":"2023-07-14T18:39:24.263323Z","shell.execute_reply.started":"2023-07-14T18:39:23.998797Z","shell.execute_reply":"2023-07-14T18:39:24.26231Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Load the training data\nprint(train_metadata_df.shape)\nprint(train_metadata_df['participant_id'].nunique())\ntrain_metadata_df.head()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:24.265967Z","iopub.execute_input":"2023-07-14T18:39:24.266339Z","iopub.status.idle":"2023-07-14T18:39:24.291132Z","shell.execute_reply.started":"2023-07-14T18:39:24.266305Z","shell.execute_reply":"2023-07-14T18:39:24.290248Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Load the supplemental data\nprint(supp_metadata_df.shape)\nprint(supp_metadata_df['participant_id'].nunique())\nsupp_metadata_df.head()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:24.292596Z","iopub.execute_input":"2023-07-14T18:39:24.293195Z","iopub.status.idle":"2023-07-14T18:39:24.307294Z","shell.execute_reply.started":"2023-07-14T18:39:24.293162Z","shell.execute_reply":"2023-07-14T18:39:24.306266Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(8, 8))\ntrain_metadata_df[\"phrase\"].value_counts().head(20).sort_values(ascending=True).plot(\n    kind=\"barh\", ax=ax, title=\"Top 20 phrases\"\n)\nax.set_xlabel(\"Number of Training Examples\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:24.309118Z","iopub.execute_input":"2023-07-14T18:39:24.309449Z","iopub.status.idle":"2023-07-14T18:39:24.74697Z","shell.execute_reply.started":"2023-07-14T18:39:24.309419Z","shell.execute_reply":"2023-07-14T18:39:24.746058Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<span style=\"color:green\">surprise az was the phrase the occured the most followed by yonkers new york and so on</span>","metadata":{}},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(8,8))\ntrain_metadata_df[\"phrase\"].value_counts().tail(10).sort_values(ascending = True).plot(\n    kind=\"barh\", ax=ax, title=\"Bottom 10 Phrases\"\n)\nax.set_xlabel(\"Number of Training Examples\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:24.74808Z","iopub.execute_input":"2023-07-14T18:39:24.748406Z","iopub.status.idle":"2023-07-14T18:39:25.039772Z","shell.execute_reply.started":"2023-07-14T18:39:24.748373Z","shell.execute_reply":"2023-07-14T18:39:25.038901Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"supp_metadata_df.query(\"phrase == 'surprise az'\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:25.041166Z","iopub.execute_input":"2023-07-14T18:39:25.042237Z","iopub.status.idle":"2023-07-14T18:39:25.059998Z","shell.execute_reply.started":"2023-07-14T18:39:25.0422Z","shell.execute_reply":"2023-07-14T18:39:25.059145Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(12, 12))\nsupp_metadata_df[\"phrase\"].value_counts().head(50).sort_values(ascending=True).plot(\n    kind=\"barh\", ax=ax, title=\"Top 50 phrases\"\n)\nax.set_xlabel(\"Number of Training Examples\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:25.061357Z","iopub.execute_input":"2023-07-14T18:39:25.061762Z","iopub.status.idle":"2023-07-14T18:39:25.991565Z","shell.execute_reply.started":"2023-07-14T18:39:25.061729Z","shell.execute_reply":"2023-07-14T18:39:25.990656Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### In the training data provided to us, let us see how many instances are there for surprise az phrase","metadata":{}},{"cell_type":"code","source":"for col in train_metadata_df.columns:\n    print(col,end = '\\t')\n    print(pd.merge(train_metadata_df,supp_metadata_df, on = [col]).shape)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:25.996281Z","iopub.execute_input":"2023-07-14T18:39:25.996948Z","iopub.status.idle":"2023-07-14T18:39:30.471553Z","shell.execute_reply.started":"2023-07-14T18:39:25.99691Z","shell.execute_reply":"2023-07-14T18:39:30.470596Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div class=\"alert alert-block alert-info\">\n<b>There are no common file id, Sequence_id and phrase in metadata and supplemental data</div>","metadata":{}},{"cell_type":"markdown","source":"### Workflow\n- For surprise az phras we had 17 unique sequence id\n- Choose a path corresponding any sequence id --------------------------------------------**surprise**\n- load a parquet file into a dataframe corresponding to that path (Rem - still surprise az) ------------**surprised**\n- in this file again there are 999 unique seq ids, select any at random --------------------**selected_id**\n- query a dataframe for that particular selected id ------------------------------**selected_id_df**\n","metadata":{}},{"cell_type":"code","source":"surprise = train_metadata_df.query('phrase == \"surprise az\"')[\"path\"].values[0]\nsurprised = pd.read_parquet(f\"{BASE_DIR}{surprise}\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:30.472833Z","iopub.execute_input":"2023-07-14T18:39:30.473912Z","iopub.status.idle":"2023-07-14T18:39:49.592216Z","shell.execute_reply.started":"2023-07-14T18:39:30.473873Z","shell.execute_reply":"2023-07-14T18:39:49.591399Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"surprised","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.593509Z","iopub.execute_input":"2023-07-14T18:39:49.594234Z","iopub.status.idle":"2023-07-14T18:39:49.642038Z","shell.execute_reply.started":"2023-07-14T18:39:49.594195Z","shell.execute_reply":"2023-07-14T18:39:49.639898Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"surprised.index.nunique()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.643403Z","iopub.execute_input":"2023-07-14T18:39:49.64438Z","iopub.status.idle":"2023-07-14T18:39:49.788204Z","shell.execute_reply.started":"2023-07-14T18:39:49.644344Z","shell.execute_reply":"2023-07-14T18:39:49.787107Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div class=\"alert alert-block alert-info\">\n<b>There are total of nearly 1000 unique sequence Ids for first parquet file of surprised phrase</div>","metadata":{}},{"cell_type":"code","source":"selected_id = surprised.index.values[10]\nprint(selected_id)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.789532Z","iopub.execute_input":"2023-07-14T18:39:49.79264Z","iopub.status.idle":"2023-07-14T18:39:49.797876Z","shell.execute_reply.started":"2023-07-14T18:39:49.792593Z","shell.execute_reply":"2023-07-14T18:39:49.796925Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**let's see how many instances are there for this sequence id**","metadata":{}},{"cell_type":"code","source":"selected_id_df = surprised[surprised.index == selected_id]\nselected_id_df","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.799345Z","iopub.execute_input":"2023-07-14T18:39:49.799955Z","iopub.status.idle":"2023-07-14T18:39:49.835811Z","shell.execute_reply.started":"2023-07-14T18:39:49.799919Z","shell.execute_reply":"2023-07-14T18:39:49.834945Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div class=\"alert alert-block alert-info\">\n<b>There are total of 186 instances for the selected sequence Id</div>","metadata":{}},{"cell_type":"markdown","source":"### Create a function which will seperate all the columns for loaded parquet file along three axes - x, y, z\n**Pseudo Code**\n\nfunction:\n\n    x = list(col if col start with x e.g. x_face_0 will be added)\n    \n    y = list(col if col start with y e.g. y_face_0 will be added\n    \n    ...same for z\n    return it","metadata":{}},{"cell_type":"code","source":"def x_y_z(columns):\n    x = [col for col in columns if col.startswith(\"x\")]\n    y = [col for col in columns if col.startswith(\"y\")]\n    z = [col for col in columns if col.startswith(\"z\")]\n    return x, y, z","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.837304Z","iopub.execute_input":"2023-07-14T18:39:49.837639Z","iopub.status.idle":"2023-07-14T18:39:49.843526Z","shell.execute_reply.started":"2023-07-14T18:39:49.837607Z","shell.execute_reply":"2023-07-14T18:39:49.842047Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Creating a function which will store types of landmarks present for that id\n- code for this function is pretty straighforward to understand","metadata":{}},{"cell_type":"code","source":"def type_of_landmark(example_landmark):\n    body_parts = set()\n    for col in example_landmark.columns:\n        parts = col.split(\"_\")\n        if len(parts) >= 2:\n            if parts[1] == \"right\":\n                body_parts.add(\"right_hand\")\n            elif parts[1] == \"left\":\n                body_parts.add(\"left_hand\")\n            else:\n                body_parts.add(parts[1])\n    return body_parts","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.845044Z","iopub.execute_input":"2023-07-14T18:39:49.845423Z","iopub.status.idle":"2023-07-14T18:39:49.85512Z","shell.execute_reply.started":"2023-07-14T18:39:49.845392Z","shell.execute_reply":"2023-07-14T18:39:49.854096Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# how many frames are present for this id's dataframe\nunique_frames = selected_id_df[\"frame\"].nunique()\n# type of landmarks/body parts = [face/pose/right hand/left hand]\ntype_landmark_train = type_of_landmark(selected_id_df)\n\n# seperate columns according to landmarks\nface_train = [col for col in selected_id_df.columns if \"face\" in col]\nright_hand_train = [col for col in selected_id_df.columns if \"right_hand\" in col]\nleft_hand_train = [col for col in selected_id_df.columns if \"left_hand\" in col]\npose_train = [col for col in selected_id_df.columns if \"pose\" in col]\n\n# use the function created earlier to distribute along axes\nx_face_train, y_face_train, z_face_train = x_y_z(face_train)\nx_right_hand, y_right_hand, z_right_hand = x_y_z(right_hand_train)\nx_left_hand, y_left_hand, z_left_hand = x_y_z(left_hand_train)\nx_pose, y_pose, z_pose = x_y_z(pose_train)\n\n# Summary\nprint(f'{cyan}{\"*\"*30} Training Data {\"*\"*30}')\nprint(f\"{blk}Selected Sequence ID: {red}{selected_id}\")\nprint(f\"{blk}Unique Frames: {red} {unique_frames}\")\nprint(\n    f\"{blk}Selected example has total {red}{len(type_landmark_train)}{blk} landmards and they are{red}{type_landmark_train}\"\n)\nprint(f\"{blk}{red}\")\n\nprint(f'{green}{\"*\"*20}FACE{\"*\"*20}')\nprint(f\"{blk}Total Keypoints: {red}{len(face_train)}\")\nprint(f\"{blk}Keypoints in X: {red}{len(x_face_train)}\")\nprint(f\"{blk}Keypoints in Y: {red}{len(y_face_train)}\")\nprint(f\"{blk}Keypoints in Z: {red}{len(z_face_train)}\")\n\nprint(f'{green}{\"*\"*20}RIGHT HAND{\"*\"*20}')\nprint(f\"{blk}Total Keypoints: {red}{len(right_hand_train)}\")\nprint(f\"{blk}Keypoints in X: {red}{len(x_right_hand)}\")\nprint(f\"{blk}Keypoints in Y: {red}{len(y_right_hand)}\")\nprint(f\"{blk}Keypoints in Z: {red}{len(z_right_hand)}\")\n\nprint(f'{green}{\"*\"*20}LEFT HAND{\"*\"*20}')\nprint(f\"{blk}Total Keypoints: {red}{len(left_hand_train)}\")\nprint(f\"{blk}Keypoints in X: {red}{len(x_left_hand)}\")\nprint(f\"{blk}Keypoints in Y: {red}{len(y_left_hand)}\")\nprint(f\"{blk}Keypoints in Z: {red}{len(z_left_hand)}\")\n\nprint(f'{green}{\"*\"*20}POSE{\"*\"*20}')\nprint(f\"{blk}Total Keypoints: {red}{len(pose_train)}\")\nprint(f\"{blk}Keypoints in X: {red}{len(x_pose)}\")\nprint(f\"{blk}Keypoints in Y: {red}{len(y_pose)}\")\nprint(f\"{blk}Keypoints in Z: {red}{len(z_pose)}\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.856547Z","iopub.execute_input":"2023-07-14T18:39:49.856955Z","iopub.status.idle":"2023-07-14T18:39:49.878828Z","shell.execute_reply.started":"2023-07-14T18:39:49.856923Z","shell.execute_reply":"2023-07-14T18:39:49.877526Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"mp_hands = mp.solutions.hands","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.88008Z","iopub.execute_input":"2023-07-14T18:39:49.880827Z","iopub.status.idle":"2023-07-14T18:39:49.89265Z","shell.execute_reply.started":"2023-07-14T18:39:49.880793Z","shell.execute_reply":"2023-07-14T18:39:49.891723Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"selected_id_df.query(\"sequence_id == @selected_id and frame == 12\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:49.898233Z","iopub.execute_input":"2023-07-14T18:39:49.898489Z","iopub.status.idle":"2023-07-14T18:39:50.005229Z","shell.execute_reply.started":"2023-07-14T18:39:49.898466Z","shell.execute_reply":"2023-07-14T18:39:50.00393Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[x_face_train]","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.006635Z","iopub.execute_input":"2023-07-14T18:39:50.006976Z","iopub.status.idle":"2023-07-14T18:39:50.072024Z","shell.execute_reply.started":"2023-07-14T18:39:50.006943Z","shell.execute_reply":"2023-07-14T18:39:50.070993Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[x_face_train].iloc[\n    0\n].values","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.073275Z","iopub.execute_input":"2023-07-14T18:39:50.074088Z","iopub.status.idle":"2023-07-14T18:39:50.136814Z","shell.execute_reply.started":"2023-07-14T18:39:50.074051Z","shell.execute_reply":"2023-07-14T18:39:50.13592Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"len(\n    selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[x_face_train]\n    .iloc[0]\n    .values\n)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.138082Z","iopub.execute_input":"2023-07-14T18:39:50.138427Z","iopub.status.idle":"2023-07-14T18:39:50.186612Z","shell.execute_reply.started":"2023-07-14T18:39:50.138394Z","shell.execute_reply":"2023-07-14T18:39:50.185672Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# [\n#     int(col.split(\"_\")[-1])\n#     for col in selected_id_df.query(\"sequence_id == @selected_id and frame == 12\")[\n#         face_train\n#     ].columns\n# ]\nlen(\n    [\n        int(col.split(\"_\")[-1])\n        for col in selected_id_df.query(\"sequence_id == @selected_id and frame == 12\")[\n            face_train\n        ].columns\n    ]\n)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.188185Z","iopub.execute_input":"2023-07-14T18:39:50.188602Z","iopub.status.idle":"2023-07-14T18:39:50.249104Z","shell.execute_reply.started":"2023-07-14T18:39:50.188566Z","shell.execute_reply":"2023-07-14T18:39:50.248182Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(len(selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[x_face_train].iloc[0].values)+\n      len(selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[y_face_train].iloc[0].values)+\n      len(selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[z_face_train].iloc[0].values))\n\nprint(len(selected_id_df.query(\"sequence_id == @selected_id and frame == 2\")[face_train].iloc[0].values))","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.25237Z","iopub.execute_input":"2023-07-14T18:39:50.252645Z","iopub.status.idle":"2023-07-14T18:39:50.421293Z","shell.execute_reply.started":"2023-07-14T18:39:50.252621Z","shell.execute_reply":"2023-07-14T18:39:50.420318Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def data_plot(seq, frame, x_col, y_col, z_col, df):\n    x = df.query(\"sequence_id == @seq and frame == @frame\")[x_col].iloc[0].values\n    y = df.query(\"sequence_id == @seq and frame == @frame\")[y_col].iloc[0].values\n    z = df.query(\"sequence_id == @seq and frame == @frame\")[z_col].iloc[0].values\n\n    landmark_idx = [\n        int(col.split(\"_\")[-1])\n        for col in df.query(\"sequence_id == @seq and frame == @frame\")[x_col].columns\n    ]\n\n    dataframe = pd.DataFrame({\"x\": x, \"y\": y, \"z\": z, \"landmark_idx\": landmark_idx})\n\n    return dataframe","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.422533Z","iopub.execute_input":"2023-07-14T18:39:50.423603Z","iopub.status.idle":"2023-07-14T18:39:50.430599Z","shell.execute_reply.started":"2023-07-14T18:39:50.423566Z","shell.execute_reply":"2023-07-14T18:39:50.429515Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"frame = 3\nleft_hand_train = data_plot(\n    selected_id, frame, x_left_hand, y_left_hand, z_left_hand, selected_id_df\n)\nright_hand_train = data_plot(\n    selected_id, frame, x_right_hand, y_right_hand, z_right_hand, selected_id_df\n)\nface_train = data_plot(\n    selected_id, frame, x_face_train, y_face_train, z_face_train, selected_id_df\n)\npose_train = data_plot(selected_id, frame, x_pose, y_pose, z_pose, selected_id_df)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:50.432222Z","iopub.execute_input":"2023-07-14T18:39:50.432625Z","iopub.status.idle":"2023-07-14T18:39:51.053156Z","shell.execute_reply.started":"2023-07-14T18:39:50.432591Z","shell.execute_reply":"2023-07-14T18:39:51.052185Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(5, 5))\nax.scatter(right_hand_train[\"x\"], right_hand_train[\"y\"])\nax.scatter(left_hand_train[\"x\"], left_hand_train[\"y\"])\nax.scatter(face_train[\"x\"], face_train[\"y\"])\nax.scatter(pose_train[\"x\"], pose_train[\"y\"])\n\n\nfor connection in mp_hands.HAND_CONNECTIONS:\n    point_a = connection[0]\n    point_b = connection[1]\n    x1, y1 = right_hand_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x2, y2 = right_hand_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x1, x2], [y1, y2], color=\"red\")\n    x3, y3 = left_hand_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x4, y4 = left_hand_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x3, x4], [y3, y4], color=\"red\")\n    x5, y5 = face_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x6, y6 = face_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x5, x6], [y5, y6], color=\"blue\")\n    x7, y7 = pose_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x8, y8 = pose_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x7, x8], [y7, y8], color=\"blue\")\n\nax.set_title(\"Surprised\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:51.061369Z","iopub.execute_input":"2023-07-14T18:39:51.061689Z","iopub.status.idle":"2023-07-14T18:39:51.765541Z","shell.execute_reply.started":"2023-07-14T18:39:51.061661Z","shell.execute_reply":"2023-07-14T18:39:51.764626Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(5, 5))\nax.scatter(right_hand_train[\"x\"], right_hand_train[\"y\"])\nax.scatter(left_hand_train[\"x\"], left_hand_train[\"y\"])\n# ax.scatter(face_train[\"x\"], face_train[\"y\"])\n# ax.scatter(pose_train[\"x\"], pose_train[\"y\"])\n\n\nfor connection in mp_hands.HAND_CONNECTIONS:\n    point_a = connection[0]\n    point_b = connection[1]\n    x1, y1 = right_hand_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x2, y2 = right_hand_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x1, x2], [y1, y2], color=\"red\")\n    x3, y3 = left_hand_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x4, y4 = left_hand_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x3, x4], [y3, y4], color=\"red\")\n#     x5, y5 = face_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n#     x6, y6 = face_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n#     plt.plot([x5, x6], [y5, y6], color=\"blue\")\n#     x7, y7 = pose_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n#     x8, y8 = pose_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n#     plt.plot([x7, x8], [y7, y8], color=\"blue\")\n\nax.set_title(\"Surprised\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:51.766755Z","iopub.execute_input":"2023-07-14T18:39:51.768235Z","iopub.status.idle":"2023-07-14T18:39:52.264776Z","shell.execute_reply.started":"2023-07-14T18:39:51.768198Z","shell.execute_reply":"2023-07-14T18:39:52.263905Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(5, 5))\n# ax.scatter(right_hand_train[\"x\"], right_hand_train[\"y\"])\n# ax.scatter(left_hand_train[\"x\"], left_hand_train[\"y\"])\nax.scatter(face_train[\"x\"], face_train[\"y\"])\nax.scatter(pose_train[\"x\"], pose_train[\"y\"])\n\n\nfor connection in mp_hands.HAND_CONNECTIONS:\n    point_a = connection[0]\n    point_b = connection[1]\n    #     x1, y1 = right_hand_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    #     x2, y2 = right_hand_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    #     plt.plot([x1, x2], [y1, y2], color=\"red\")\n    #     x3, y3 = left_hand_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    #     x4, y4 = left_hand_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    #     plt.plot([x3, x4], [y3, y4], color=\"red\")\n    x5, y5 = face_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x6, y6 = face_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x5, x6], [y5, y6], color=\"blue\")\n    x7, y7 = pose_train.query(\"landmark_idx == @point_a\")[[\"x\", \"y\"]].values[0]\n    x8, y8 = pose_train.query(\"landmark_idx == @point_b\")[[\"x\", \"y\"]].values[0]\n    plt.plot([x7, x8], [y7, y8], color=\"blue\")\n\nax.set_title(\"Surprised\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:52.266441Z","iopub.execute_input":"2023-07-14T18:39:52.267125Z","iopub.status.idle":"2023-07-14T18:39:52.746768Z","shell.execute_reply.started":"2023-07-14T18:39:52.267088Z","shell.execute_reply":"2023-07-14T18:39:52.745895Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(x_face_train)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:52.748411Z","iopub.execute_input":"2023-07-14T18:39:52.749095Z","iopub.status.idle":"2023-07-14T18:39:52.754122Z","shell.execute_reply.started":"2023-07-14T18:39:52.749059Z","shell.execute_reply":"2023-07-14T18:39:52.753154Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# <p style=\"font-family:JetBrains Mono; font-weight:normal; letter-spacing: 2px; color:#000000; font-size:140%; text-align:center;padding: 0px; border-bottom: 3px solid #000000\">Attention Please</p>\n\nI have analyzed other notebooks that have been made public as of today(15-5-23). They are very good notebooks but due to absence of comments I have a little hard time understanding some code. So I am here making that code easy using comments. kindly upvote the original content as well.\n\nThanks","metadata":{}},{"cell_type":"code","source":"# helper functions\ndef map_new_to_old_style(sequence):\n    # there are 4 types of landmarks [face,pose,right_hand,left_hand]\n    types = []# list where we'll store landmarks\n    landmark_indexes = []\n    for column in list(sequence.columns)[1:544]:\n        # first column is frame which is not a landmark so counter starts from 1\n        # why 544 - because we are given that there are now 1,629 spatial coordinate columns for the x, y and z coordinates for each of the 543 landmarks.\n        parts = column.split(\"_\")\n        if len(parts) == 4:\n            # for x_left_hand_1 - there will be 4 parts and for x_pose_1 there will be 3 only\n            types.append(parts[1] + \"_\" + parts[2])\n        else:\n            types.append(parts[1])\n\n        landmark_indexes.append(int(parts[-1]))\n\n    data = {\"frame\": [], \"type\": [], \"landmark_index\": [], \"x\": [], \"y\": [], \"z\": []}\n\n    for index, row in sequence.iterrows():\n        data[\"frame\"] += [int(row.frame)] * 543\n        #       frame has values from 1,2,3....\n        # 1. Firstly we are converting them into integers - 1\n        # 2. Then we are making it a list - [1]\n        # 3. Multiplying by 543 makes the replicates the same element(1 from our example) 543 times for all the landmarks present\n        data[\"type\"] += types\n        data[\"landmark_index\"] += landmark_indexes\n\n        for _type, landmark_index in zip(types, landmark_indexes):\n            data[\"x\"].append(row[f\"x_{_type}_{landmark_index}\"])\n            data[\"y\"].append(row[f\"y_{_type}_{landmark_index}\"])\n            data[\"z\"].append(row[f\"z_{_type}_{landmark_index}\"])\n\n    return pd.DataFrame.from_dict(data)\n\n\ndef assign_colors(row):\n    if row == \"face\":\n        return \"red\"\n    elif \"hand\" in row:\n        return \"blue\"\n    else:\n        return \"green\"\n\n\n# specifies the plotting order\ndef assign_order(row):\n    if row.type == \"face\":\n        return row.landmark_index + 101\n    elif row.type == \"pose\":\n        return row.landmark_index + 30\n    elif row.type == \"left_hand\":\n        return row.landmark_index + 80\n    else:\n        return row.landmark_index\n\n\n# A function to visualize the landmarks in 2d\ndef visualise2d_landmarks(parquet_df, title=\"\"):\n    # we first define a list of landmark connections, which specify which landmarks are connected by a line.\n\n    # face landmarks are not connected by lines, you can also see that we have added 101 to face landmark indexs and in connections all values are below 100\n\n    connections = [  \n        [0, 1, 2, 3, 4,],\n        [0, 5, 6, 7, 8],\n        [0, 9, 10, 11, 12],\n        [0, 13, 14, 15, 16],\n        [0, 17, 18, 19, 20],\n\n        \n        [38, 36, 35, 34, 30, 31, 32, 33, 37],\n        [40, 39],\n        [52, 46, 50, 48, 46, 44, 42, 41, 43, 45, 47, 49, 45, 51],\n        [42, 54, 56, 58, 60, 62, 58],\n        [41, 53, 55, 57, 59, 61, 57],\n        [54, 53],\n\n        \n        [80, 81, 82, 83, 84, ],\n        [80, 85, 86, 87, 88],\n        [80, 89, 90, 91, 92],\n        [80, 93, 94, 95, 96],\n        [80, 97, 98, 99, 100], ]\n\n    parquet_df = map_new_to_old_style(parquet_df)\n    frames = sorted(set(parquet_df.frame))\n    first_frame = min(frames)\n    parquet_df[\"color\"] = parquet_df.type.apply(lambda row: assign_colors(row))\n    parquet_df[\"plot_order\"] = parquet_df.apply(lambda row: assign_order(row), axis=1)\n    first_frame_df = parquet_df[parquet_df.frame == first_frame].copy()\n    first_frame_df = first_frame_df.sort_values([\"plot_order\"]).set_index(\"plot_order\")\n\n    frames_l = []\n    for frame in frames:\n        filtered_df = parquet_df[parquet_df.frame == frame].copy()\n        filtered_df = filtered_df.sort_values([\"plot_order\"]).set_index(\"plot_order\")\n        traces = [\n            go.Scatter(\n                x=filtered_df[\"x\"],\n                y=filtered_df[\"y\"],\n                mode=\"markers\",\n                marker=dict(color=filtered_df.color, size=9),\n            )\n        ]\n\n        for i, seg in enumerate(connections):\n            trace = go.Scatter(\n                x=filtered_df.loc[seg][\"x\"],\n                y=filtered_df.loc[seg][\"y\"],\n                mode=\"lines\",\n            )\n            traces.append(trace)\n        frame_data = go.Frame(data=traces, traces=[i for i in range(17)])\n        frames_l.append(frame_data)\n\n    traces = [\n        go.Scatter(\n            x=first_frame_df[\"x\"],\n            y=first_frame_df[\"y\"],\n            mode=\"markers\",\n            marker=dict(color=first_frame_df.color, size=9),\n        )\n    ]\n    for i, seg in enumerate(connections):\n        trace = go.Scatter(\n            x=first_frame_df.loc[seg][\"x\"],\n            y=first_frame_df.loc[seg][\"y\"],\n            mode=\"lines\",\n            line=dict(color=\"black\", width=2),\n        )\n        traces.append(trace)\n    fig = go.Figure(data=traces, frames=frames_l)\n\n    fig.update_layout(\n        width=500,\n        height=800,\n        scene={\n            \"aspectmode\": \"data\",\n        },\n        updatemenus=[\n            {\n                \"buttons\": [\n                    {\n                        \"args\": [\n                            None,\n                            {\n                                \"frame\": {\"duration\": 100, \"redraw\": True},\n                                \"fromcurrent\": True,\n                                \"transition\": {\"duration\": 0},\n                            },\n                        ],\n                        \"label\": \"&#9654;\",\n                        \"method\": \"animate\",\n                    },\n                    {\n                        \"args\": [\n                            [None],\n                            {\n                                \"frame\": {\"duration\": 0, \"redraw\": False},\n                                \"mode\": \"immediate\",\n                                \"transition\": {\"duration\": 0},\n                            },\n                        ],\n                        \"label\": \"&#9612;&#9612;\",\n                        \"method\": \"animate\",\n                    },\n                ],\n                \"direction\": \"left\",\n                \"pad\": {\"r\": 100, \"t\": 100},\n                \"font\": {\"size\": 20},\n                \"type\": \"buttons\",\n                \"x\": 0.1,\n                \"y\": 0,\n            }\n        ],\n    )\n    camera = dict(up=dict(x=0, y=-1, z=0), eye=dict(x=0, y=0, z=2.5))\n    fig.update_layout(title_text=title, title_x=0.5)\n    fig.update_layout(scene_camera=camera, showlegend=False)\n    fig.update_layout(\n        xaxis=dict(visible=False),\n        yaxis=dict(visible=False),\n    )\n    fig.update_yaxes(autorange=\"reversed\")\n\n    fig.show()\n\n\ndef get_phrase(df, file_id, sequence_id):\n    return df[\n        np.logical_and(df.file_id == file_id, df.sequence_id == sequence_id)\n    ].phrase.iloc[0]","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:52.755642Z","iopub.execute_input":"2023-07-14T18:39:52.755997Z","iopub.status.idle":"2023-07-14T18:39:52.78886Z","shell.execute_reply.started":"2023-07-14T18:39:52.755951Z","shell.execute_reply":"2023-07-14T18:39:52.788241Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"file_id = 474255203\nsequence_id = 53122870\n\nsurprise = f\"/kaggle/input/asl-fingerspelling/train_landmarks/{file_id}.parquet\"\nsurprised = pd.read_parquet(surprise)\n\nsequence = surprised[surprised.index == sequence_id]\nsequence","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:39:52.790206Z","iopub.execute_input":"2023-07-14T18:39:52.790809Z","iopub.status.idle":"2023-07-14T18:40:09.26095Z","shell.execute_reply.started":"2023-07-14T18:39:52.790776Z","shell.execute_reply":"2023-07-14T18:40:09.259972Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"get_phrase(train_metadata_df, file_id, sequence_id)\nvisualise2d_landmarks(sequence, \"Surprised\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:09.262412Z","iopub.execute_input":"2023-07-14T18:40:09.262744Z","iopub.status.idle":"2023-07-14T18:40:24.326696Z","shell.execute_reply.started":"2023-07-14T18:40:09.262711Z","shell.execute_reply":"2023-07-14T18:40:24.325864Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Function create animation from images.\n\nmpl.rcParams['animation.embed_limit'] = 2**128\n'''\nanimation.embed_limit determines the maximum size limit for embedding animations in output files or displays.\nIn this case, the value 2**128 is assigned, which is a very large value to ensure that there are no limitations on embedding animations.\n'''\n\nmpl.rcParams['savefig.pad_inches'] = 0\n'''\nsavefig.pad_inches determines the amount of padding (in inches) to be added around the saved figures.\nIn this case, the value 0 is assigned, meaning no padding will be added around the saved figures.'''\n\nrc('animation', html='jshtml')\n'''animation.html determines the HTML representation for displaying animations.\nIn this case, the value 'jshtml' is assigned, which indicates that JavaScript-based HTML animations will be used.\n'''\n\ndef create_animation(images):\n    fig = plt.figure(figsize=(6, 9))\n    ax = plt.Axes(fig, [0., 0., 1., 1.])#The [0., 0., 1., 1.] argument specifies the position and size of the Axes object, where [left, bottom, width, height] are normalized coordinates ranging from 0 to 1.'''\n    ax.set_axis_off()\n    fig.add_axes(ax)\n    im=ax.imshow(images[0], cmap=\"gray\")# assigns the first image from the images list to the im object.'''\n    plt.close(fig)\n    \n    def animate_func(i): #will be called for each frame of the animation.'''\n        im.set_array(images[i])\n        return [im]\n\n    return animation.FuncAnimation(fig, animate_func, frames=len(images), interval=1000/10)\n\n\n'''FuncAnimation class constructs an animation by repeatedly calling the animate_func function for each frame\nfig is the figure object to be animated, \nanimate_func is the animation function,\nframes specifies the number of frames (equal to the length of the images list), and interval sets the delay between frames in milliseconds.'''","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:24.328232Z","iopub.execute_input":"2023-07-14T18:40:24.328805Z","iopub.status.idle":"2023-07-14T18:40:24.343546Z","shell.execute_reply.started":"2023-07-14T18:40:24.328771Z","shell.execute_reply":"2023-07-14T18:40:24.342172Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Extract the landmark data and convert it to an image using medipipe library.\n# This function extracts the data for both hands.\n\nmp_pose = mp.solutions.pose\nmp_hands = mp.solutions.hands\nmp_drawing = mp.solutions.drawing_utils \nmp_drawing_styles = mp.solutions.drawing_styles\n\ndef get_hands(seq_df):\n    images = []\n    all_hand_landmarks = []\n    for seq_idx in range(len(seq_df)):\n        img, hand_land = list(),list()\n        for side in ['right','left']:\n            x_hand = seq_df.iloc[seq_idx].filter(regex=f\"x_{side}_hand.*\").values\n            y_hand = seq_df.iloc[seq_idx].filter(regex=f\"y_{side}_hand.*\").values\n            z_hand = seq_df.iloc[seq_idx].filter(regex=f\"z_{side}_hand.*\").values\n            hand_image = f'{side}_hand_image'\n            hand_image = np.zeros((600, 600, 3))\n            landmarks = f'{side}_hand_landmarks'\n            landmarks = landmark_pb2.NormalizedLandmarkList()\n\n            for x, y, z in zip(x_hand, y_hand, z_hand):\n                landmarks.landmark.add(x=x, y=y, z=z)\n\n            mp_drawing.draw_landmarks(\n                    hand_image,\n                    landmarks,\n                    mp_hands.HAND_CONNECTIONS,\n                    landmark_drawing_spec=mp_drawing_styles.get_default_hand_landmarks_style())\n            img.append(hand_image.astype(np.uint8))\n            hand_land.append(landmarks)\n        images.append(img)\n        all_hand_landmarks.append(hand_land)\n\n    return images, all_hand_landmarks","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:24.345094Z","iopub.execute_input":"2023-07-14T18:40:24.345671Z","iopub.status.idle":"2023-07-14T18:40:24.364399Z","shell.execute_reply.started":"2023-07-14T18:40:24.345639Z","shell.execute_reply":"2023-07-14T18:40:24.363285Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Get the images created using mediapipe apis\nhand_images, hand_landmarks = get_hands(sequence)\n# Fetch and show the data for right hand\ncreate_animation(np.array(hand_images)[:, 0])","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:24.366587Z","iopub.execute_input":"2023-07-14T18:40:24.367279Z","iopub.status.idle":"2023-07-14T18:40:34.035525Z","shell.execute_reply.started":"2023-07-14T18:40:24.367245Z","shell.execute_reply":"2023-07-14T18:40:34.03306Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## points to consider\n- Up untill now we were taking file path and phrase from training data and then get the landmarks data using file path.\n- We will gather data such that we don't have to juggle between these dataframes and have a single dataframe only.\n- We will store the data in TFRecord format becuase it stores the data efficiently. More on this [here](https://www.tensorflow.org/tutorials/load_data/tfrecord)\n\n- Also This is only fingerspelling challenge and it focuses on hand movements. So what we need is to focus on those corresponding landmarks only. Refer below. [Source - Mediapipe]","metadata":{}},{"cell_type":"markdown","source":"![](data:image/png;base64,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)","metadata":{}},{"cell_type":"code","source":"# Pose coordinates for hand movement.\nLPOSE = [13, 15, 17, 19, 21]\nRPOSE = [14, 16, 18, 20, 22]\nPOSE = LPOSE + RPOSE","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:34.036973Z","iopub.execute_input":"2023-07-14T18:40:34.03797Z","iopub.status.idle":"2023-07-14T18:40:34.04297Z","shell.execute_reply.started":"2023-07-14T18:40:34.037933Z","shell.execute_reply":"2023-07-14T18:40:34.042223Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"X = [f'x_right_hand_{i}' for i in range(21)] + [f'x_left_hand_{i}' for i in range(21)] + [f'x_pose_{i}' for i in POSE] #20+20+12 = 52\nY = [f'y_right_hand_{i}' for i in range(21)] + [f'y_left_hand_{i}' for i in range(21)] + [f'y_pose_{i}' for i in POSE] #20+20+12 = 52\nZ = [f'z_right_hand_{i}' for i in range(21)] + [f'z_left_hand_{i}' for i in range(21)] + [f'z_pose_{i}' for i in POSE] #20+20+12 = 52\n\nFEATURE_COLUMNS = X + Y + Z","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:34.044353Z","iopub.execute_input":"2023-07-14T18:40:34.044883Z","iopub.status.idle":"2023-07-14T18:40:34.056451Z","shell.execute_reply.started":"2023-07-14T18:40:34.044848Z","shell.execute_reply":"2023-07-14T18:40:34.055466Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"X_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if \"x_\" in col]\nY_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if \"y_\" in col]\nZ_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if \"z_\" in col]\n\nRHAND_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if \"right\" in col]\nLHAND_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if  \"left\" in col]\nRPOSE_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if  \"pose\" in col and int(col[-2:]) in RPOSE]\nLPOSE_IDX = [i for i, col in enumerate(FEATURE_COLUMNS)  if  \"pose\" in col and int(col[-2:]) in LPOSE]","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:34.057938Z","iopub.execute_input":"2023-07-14T18:40:34.058458Z","iopub.status.idle":"2023-07-14T18:40:34.071958Z","shell.execute_reply.started":"2023-07-14T18:40:34.058423Z","shell.execute_reply":"2023-07-14T18:40:34.070928Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Preprocess and write the dataset as TFRecords\nUsing the extracted landmarks and phrases let us create new dataset files and write them as TFRecords.","metadata":{}},{"cell_type":"code","source":"from tqdm.notebook import tqdm\nimport os\nimport shutil\nimport tensorflow as tf\n# Set length of frames to 128\nFRAME_LEN = 128\n\n# Create directory to store the new data\nif not os.path.isdir(\"preprocessed\"):\n    os.mkdir(\"preprocessed\")\nelse:\n    shutil.rmtree(\"preprocessed\")\n    os.mkdir(\"preprocessed\")\n\n# Loop through each file_id\nfor file_id in tqdm(train_metadata_df.file_id.unique()):\n    # Parquet file name\n    pq_file = f\"{BASE_DIR}/train_landmarks/{file_id}.parquet\"\n    # Filter train.csv and fetch entries only for the relevant file_id\n    file_df = train_metadata_df.loc[train_metadata_df[\"file_id\"] == file_id]\n    # Fetch the parquet file\n    parquet_df = pd.read_parquet(f\"{BASE_DIR}train_landmarks/{str(file_id)}.parquet\",\n                              columns=['sequence_id'] + FEATURE_COLUMNS)\n    # File name for the updated data\n    tf_file = f\"preprocessed/{file_id}.tfrecord\"\n    parquet_numpy = parquet_df.to_numpy()\n    # Initialize the pointer to write the output of \n    # each `for loop` below as a sequence into the file.\n    with tf.io.TFRecordWriter(tf_file) as file_writer:\n        # Loop through each sequence in file.\n        for seq_id, phrase in zip(file_df.sequence_id, file_df.phrase):\n            # Fetch sequence data\n            frames = parquet_numpy[parquet_df.index == seq_id]\n            \n            # Calculate the number of NaN values in each hand landmark\n            r_nonan = np.sum(np.sum(np.isnan(frames[:, RHAND_IDX]), axis = 1) == 0)\n            l_nonan = np.sum(np.sum(np.isnan(frames[:, LHAND_IDX]), axis = 1) == 0)\n            no_nan = max(r_nonan, l_nonan)\n            \n            if 2*len(phrase)<no_nan:\n                features = {FEATURE_COLUMNS[i]: tf.train.Feature(\n                    float_list=tf.train.FloatList(value=frames[:, i])) for i in range(len(FEATURE_COLUMNS))}\n                features[\"phrase\"] = tf.train.Feature(bytes_list=tf.train.BytesList(value=[bytes(phrase, 'utf-8')]))\n                record_bytes = tf.train.Example(features=tf.train.Features(feature=features)).SerializeToString()\n                file_writer.write(record_bytes)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:40:34.07368Z","iopub.execute_input":"2023-07-14T18:40:34.074521Z","iopub.status.idle":"2023-07-14T18:51:44.822434Z","shell.execute_reply.started":"2023-07-14T18:40:34.074495Z","shell.execute_reply":"2023-07-14T18:51:44.821461Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Get the saved TFRecord files into a list","metadata":{}},{"cell_type":"code","source":"tf_records = train_metadata_df.file_id.map(lambda x: f'/kaggle/working/preprocessed/{x}.tfrecord').unique()\nprint(f\"List of {len(tf_records)} TFRecord files.\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.824005Z","iopub.execute_input":"2023-07-14T18:51:44.824595Z","iopub.status.idle":"2023-07-14T18:51:44.881648Z","shell.execute_reply.started":"2023-07-14T18:51:44.824558Z","shell.execute_reply":"2023-07-14T18:51:44.880007Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Load character_to_prediction json file\nThis json file contains a character and its value.\nWe will add three new characters, \"<\" and \">\" to mark the start and end of each phrase, and \"P\" for padding.","metadata":{}},{"cell_type":"code","source":"with open (f\"{BASE_DIR}/character_to_prediction_index.json\", \"r\") as f:\n    char_to_num = json.load(f)\n\n# Add pad_token, start pointer and end pointer to the dict\npad_token = 'P'\nstart_token = '<'\nend_token = '>'\npad_token_idx = 59\nstart_token_idx = 60\nend_token_idx = 61\n\nchar_to_num[pad_token] = pad_token_idx\nchar_to_num[start_token] = start_token_idx\nchar_to_num[end_token] = end_token_idx\nnum_to_char = {j:i for i,j in char_to_num.items()}","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.883016Z","iopub.execute_input":"2023-07-14T18:51:44.883462Z","iopub.status.idle":"2023-07-14T18:51:44.893004Z","shell.execute_reply.started":"2023-07-14T18:51:44.883425Z","shell.execute_reply":"2023-07-14T18:51:44.891967Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Reference: https://www.kaggle.com/code/irohith/aslfr-transformer/notebook\n\n# Function to resize and add padding.\ndef resize_pad(x):\n#     check whether the tensor length is less than the FRAME_LEN, if it is - Padding is required, else resizing is needed\n    if tf.shape(x)[0] < FRAME_LEN:\n#         padding is being applied to only first dimensions and remaining dimensions are left unchanged\n        x = tf.pad(x, ([[0, FRAME_LEN-tf.shape(x)[0]], [0, 0], [0, 0]]))\n    else:\n        x = tf.image.resize(x, (FRAME_LEN, tf.shape(x)[1]))\n    return x","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.894312Z","iopub.execute_input":"2023-07-14T18:51:44.894731Z","iopub.status.idle":"2023-07-14T18:51:44.902926Z","shell.execute_reply.started":"2023-07-14T18:51:44.894697Z","shell.execute_reply":"2023-07-14T18:51:44.90202Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### working of tf.reduce_any \n```import tensorflow as tf\ncheck whether we have at least one true value or not, yes -> true else false\nx = tf.constant([[True, False, True],             #True\n                 [False, False, False],           #False\n                 [True, True, False]])            #True\n\nresult = tf.reduce_any(x, axis=1)\n\n>> result - [True, False, True]\n```\n","metadata":{}},{"cell_type":"code","source":"# import tensorflow as tf\n\n# # Example tensor\n# x = tf.constant([[1, 2, 3],\n#                  [4, 5, 6]])\n# print(x)\n# print(x.shape)\n# # Add a new dimension using tf.newaxis\n# new_dim_tensor = x[..., tf.newaxis]\n# print(new_dim_tensor.shape)\n# print(new_dim_tensor)\n# # Output: (2, 3, 1)\n\n# # Reshape the tensor using ...\n# reshaped_tensor = x[tf.newaxis, ...]\n# print(reshaped_tensor.shape)\n# print(reshaped_tensor)\n# # Output: (1, 2, 3)\n\n# # Perform slicing with ...\n# sliced_tensor = x[..., 0]\n# print(sliced_tensor)\n# # Output: [1, 4]\n\n","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.904393Z","iopub.execute_input":"2023-07-14T18:51:44.904748Z","iopub.status.idle":"2023-07-14T18:51:44.917784Z","shell.execute_reply.started":"2023-07-14T18:51:44.904715Z","shell.execute_reply":"2023-07-14T18:51:44.916676Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Detect the dominant hand from the number of NaN values.\n# Dominant hand will have less NaN values since it is in frame moving.\ndef pre_process(x):\n    #extract specific parts usign their indexes we derived earlier\n    rhand = tf.gather(x, RHAND_IDX, axis=1)\n    lhand = tf.gather(x, LHAND_IDX, axis=1)\n    rpose = tf.gather(x, RPOSE_IDX, axis=1)\n    lpose = tf.gather(x, LPOSE_IDX, axis=1)\n    \n#     tf.math.is_nan - True if null value else False and reduce any will show status along axis1 \n    rnan_idx = tf.reduce_any(tf.math.is_nan(rhand), axis=1)\n    lnan_idx = tf.reduce_any(tf.math.is_nan(lhand), axis=1)\n    \n    rnans = tf.math.count_nonzero(rnan_idx)\n    lnans = tf.math.count_nonzero(lnan_idx)\n    \n    # For dominant hand\n    if rnans > lnans:           # if more null values in right hand - left hand's dominant\n        hand = lhand\n        pose = lpose\n        \n#         hand data and pose data is then divided along x,y and z axis\n        hand_x = hand[:, 0*(len(LHAND_IDX)//3) : 1*(len(LHAND_IDX)//3)]\n        hand_y = hand[:, 1*(len(LHAND_IDX)//3) : 2*(len(LHAND_IDX)//3)]\n        hand_z = hand[:, 2*(len(LHAND_IDX)//3) : 3*(len(LHAND_IDX)//3)]\n        hand = tf.concat([1-hand_x, hand_y, hand_z], axis=1)\n        \n        pose_x = pose[:, 0*(len(LPOSE_IDX)//3) : 1*(len(LPOSE_IDX)//3)]\n        pose_y = pose[:, 1*(len(LPOSE_IDX)//3) : 2*(len(LPOSE_IDX)//3)]\n        pose_z = pose[:, 2*(len(LPOSE_IDX)//3) : 3*(len(LPOSE_IDX)//3)]\n        pose = tf.concat([1-pose_x, pose_y, pose_z], axis=1)\n    else:\n        hand = rhand\n        pose = rpose\n    \n    hand_x = hand[:, 0*(len(LHAND_IDX)//3) : 1*(len(LHAND_IDX)//3)]\n    hand_y = hand[:, 1*(len(LHAND_IDX)//3) : 2*(len(LHAND_IDX)//3)]\n    hand_z = hand[:, 2*(len(LHAND_IDX)//3) : 3*(len(LHAND_IDX)//3)]\n    hand = tf.concat([hand_x[..., tf.newaxis], hand_y[..., tf.newaxis], hand_z[..., tf.newaxis]], axis=-1)\n#     ...(ellipsis) notation represents all dimensions before or after a specified axis. It allows for more flexible indexing and slicing operations.\n    \n    mean = tf.math.reduce_mean(hand, axis=1)[:, tf.newaxis, :]\n    std = tf.math.reduce_std(hand, axis=1)[:, tf.newaxis, :]\n#     normalization of the hand data\n    hand = (hand - mean) / std\n\n    pose_x = pose[:, 0*(len(LPOSE_IDX)//3) : 1*(len(LPOSE_IDX)//3)]\n    pose_y = pose[:, 1*(len(LPOSE_IDX)//3) : 2*(len(LPOSE_IDX)//3)]\n    pose_z = pose[:, 2*(len(LPOSE_IDX)//3) : 3*(len(LPOSE_IDX)//3)]\n    pose = tf.concat([pose_x[..., tf.newaxis], pose_y[..., tf.newaxis], pose_z[..., tf.newaxis]], axis=-1)\n    \n    x = tf.concat([hand, pose], axis=1)\n    x = resize_pad(x)\n    \n#     if there are any null values left, are replaced by zeroes and then final tensor is reshaped into desired shape\n    x = tf.where(tf.math.is_nan(x), tf.zeros_like(x), x)\n    x = tf.reshape(x, (FRAME_LEN, len(LHAND_IDX) + len(LPOSE_IDX)))\n    return x","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.919286Z","iopub.execute_input":"2023-07-14T18:51:44.919619Z","iopub.status.idle":"2023-07-14T18:51:44.939408Z","shell.execute_reply.started":"2023-07-14T18:51:44.919589Z","shell.execute_reply":"2023-07-14T18:51:44.938381Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# earlier we saved the data in TFERecord format, now we will convert it into tensors\ndef decode_fn(record_bytes):\n#     schema - each FEATURE_COLUMNS is a key in the dict and tf.io.VarLenFeature is applied to specify that the feature is variable length with data type tf.float32.\n    schema = {COL: tf.io.VarLenFeature(dtype=tf.float32) for COL in FEATURE_COLUMNS}\n    \n#     similar thing \n    schema[\"phrase\"] = tf.io.FixedLenFeature([], dtype=tf.string)\n    \n#     This function parses the binary record and returns a dictionary of parsed features.\n    features = tf.io.parse_single_example(record_bytes, schema)\n    \n#     retrieve the phrases\n    phrase = features[\"phrase\"]\n    \n#     VarLenFeature - sparse -> hence sparse.to_dense for conversion to dense representation - landmarks is the list of tensors\n    landmarks = ([tf.sparse.to_dense(features[COL]) for COL in FEATURE_COLUMNS])\n    # Transpose to maintain the original shape of landmarks data.\n    landmarks = tf.transpose(landmarks)\n    \n    return landmarks, phrase","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.940777Z","iopub.execute_input":"2023-07-14T18:51:44.941137Z","iopub.status.idle":"2023-07-14T18:51:44.953785Z","shell.execute_reply.started":"2023-07-14T18:51:44.941105Z","shell.execute_reply":"2023-07-14T18:51:44.952864Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# import tensorflow as tf\n\n# FEATURE_COLUMNS = ['column1', 'column2', 'column3']\n\n# # Define the schema using dictionary comprehension\n# schema = {COL: tf.io.VarLenFeature(dtype=tf.float32) for COL in FEATURE_COLUMNS}\n\n# print('Schema')\n# print(schema)\n# schema[\"phrase\"] = tf.io.FixedLenFeature([], dtype=tf.string)\n\n# print('schema[\"phrase\"]')\n# schema['phrase']","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.955137Z","iopub.execute_input":"2023-07-14T18:51:44.956113Z","iopub.status.idle":"2023-07-14T18:51:44.968332Z","shell.execute_reply.started":"2023-07-14T18:51:44.956078Z","shell.execute_reply":"2023-07-14T18:51:44.967371Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# we will create a hash table to map the values from char_to_num dictionary\ntable = tf.lookup.StaticHashTable(\n    initializer=tf.lookup.KeyValueTensorInitializer(\n        keys=list(char_to_num.keys()),\n        values=list(char_to_num.values()),\n    ),\n    default_value=tf.constant(-1),  # if the hash table fails to find the matching key - returns -1\n    name=\"class_weight\"   # name for the hash table\n)\n\ndef convert_fn(landmarks, phrase):\n    # Add start and end pointers to phrase. - - - - in that json file we have added additional tokens\n    phrase = start_token + phrase + end_token\n    phrase = tf.strings.bytes_split(phrase)\n    phrase = table.lookup(phrase)\n    # Vectorize and add padding.\n    phrase = tf.pad(phrase, paddings=[[0, 64 - tf.shape(phrase)[0]]], mode = 'CONSTANT',\n                    constant_values = pad_token_idx)\n#     function converts the phrase into a sequence of numerical values using a lookup table, pads it to a fixed length,\n    # Apply pre_process function to the landmarks.\n    return pre_process(landmarks), phrase","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:44.969887Z","iopub.execute_input":"2023-07-14T18:51:44.970492Z","iopub.status.idle":"2023-07-14T18:51:47.832414Z","shell.execute_reply.started":"2023-07-14T18:51:44.970409Z","shell.execute_reply":"2023-07-14T18:51:47.831424Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Final dataset/s","metadata":{}},{"cell_type":"code","source":"batch_size = 64\n# 80 - 20 split\ntrain_len = int(0.8 * len(tf_records))\n\ntrain_ds = tf.data.TFRecordDataset(tf_records[:train_len]).map(decode_fn).map(convert_fn).batch(batch_size).prefetch(buffer_size=tf.data.AUTOTUNE).cache()\nvalid_ds = tf.data.TFRecordDataset(tf_records[train_len:]).map(decode_fn).map(convert_fn).batch(batch_size).prefetch(buffer_size=tf.data.AUTOTUNE).cache()\n\n# buffer_size - number of elements to prefetch\n# Using tf.data.AUTOTUNE -  optimal buffer size automatically based on available system resources.\n# cache - caches dataset in memory - fast execution","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:47.833759Z","iopub.execute_input":"2023-07-14T18:51:47.834355Z","iopub.status.idle":"2023-07-14T18:51:49.897298Z","shell.execute_reply.started":"2023-07-14T18:51:47.834318Z","shell.execute_reply":"2023-07-14T18:51:49.896309Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Define the Transformer Input Layers","metadata":{}},{"cell_type":"code","source":"class TokenEmbedding(layers.Layer):\n    def __init__(self, num_vocab=1000, maxlen=100, num_hid=64):\n        super().__init__()\n        self.emb = tf.keras.layers.Embedding(num_vocab, num_hid)\n        self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=num_hid)\n\n    def call(self, x):   #x - tokens\n        maxlen = tf.shape(x)[-1]\n#         apply embedding layer - mappint\n        x = self.emb(x)\n        positions = tf.range(start=0, limit=maxlen, delta=1)\n#         position embedding layer - mapping\n        positions = self.pos_emb(positions)\n#     returns combined embeddings\n        return x + positions\n\n# define layers\nclass LandmarkEmbedding(layers.Layer):\n    def __init__(self, num_hid=64, maxlen=100):\n        super().__init__()\n        self.conv1 = tf.keras.layers.Conv1D(\n            num_hid, 11, strides=2, padding=\"same\", activation=\"relu\"\n        )\n        self.conv2 = tf.keras.layers.Conv1D(\n            num_hid, 11, strides=2, padding=\"same\", activation=\"relu\"\n        )\n        self.conv3 = tf.keras.layers.Conv1D(\n            num_hid, 11, strides=2, padding=\"same\", activation=\"relu\"\n        )\n        self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=num_hid)\n\n    def call(self, x):\n        x = self.conv1(x)\n        x = self.conv2(x)\n        return self.conv3(x)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:51:49.899151Z","iopub.execute_input":"2023-07-14T18:51:49.899514Z","iopub.status.idle":"2023-07-14T18:51:49.910776Z","shell.execute_reply.started":"2023-07-14T18:51:49.899481Z","shell.execute_reply":"2023-07-14T18:51:49.909851Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## encoder","metadata":{}},{"cell_type":"code","source":"class TransformerEncoder(layers.Layer):\n    def __init__(self, embed_dim, num_heads, feed_forward_dim, rate=0.1):\n        super().__init__()\n        '''embed_dim - dimensionality of the input and output embeddings.\n        num_heads - number of attention heads to use in the multi-head attention mechanism.\n        feed_forward_dim - dimensionality of the feed-forward layer.\n        rate - dropout rate to apply to the attention and feed-forward layers.'''\n        self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)\n        self.ffn = keras.Sequential(\n            [\n                layers.Dense(feed_forward_dim, activation=\"relu\"),\n                layers.Dense(embed_dim),\n            ]\n        )\n        self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)\n        self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)\n        self.dropout1 = layers.Dropout(rate)\n        self.dropout2 = layers.Dropout(rate)\n\n    def call(self, inputs, training):\n        attn_output = self.att(inputs, inputs)\n        attn_output = self.dropout1(attn_output, training=training)\n        out1 = self.layernorm1(inputs + attn_output)\n        ffn_output = self.ffn(out1)\n        ffn_output = self.dropout2(ffn_output, training=training)\n        return self.layernorm2(out1 + ffn_output)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:58:18.736935Z","iopub.execute_input":"2023-07-14T18:58:18.737673Z","iopub.status.idle":"2023-07-14T18:58:18.748582Z","shell.execute_reply.started":"2023-07-14T18:58:18.737636Z","shell.execute_reply":"2023-07-14T18:58:18.746579Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## decoder","metadata":{}},{"cell_type":"code","source":"# Customized to add `training` variable\n# Reference: https://www.kaggle.com/code/shlomoron/aslfr-a-simple-transformer/notebook\n\nclass TransformerDecoder(layers.Layer):\n    def __init__(self, embed_dim, num_heads, feed_forward_dim, dropout_rate=0.1):\n        super().__init__()\n        self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)\n        self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)\n        self.layernorm3 = layers.LayerNormalization(epsilon=1e-6)\n        self.self_att = layers.MultiHeadAttention(\n            num_heads=num_heads, key_dim=embed_dim\n        )\n        self.enc_att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)\n        self.self_dropout = layers.Dropout(0.5)\n        self.enc_dropout = layers.Dropout(0.1)\n        self.ffn_dropout = layers.Dropout(0.1)\n        self.ffn = keras.Sequential(\n            [\n                layers.Dense(feed_forward_dim, activation=\"relu\"),\n                layers.Dense(embed_dim),\n            ]\n        )\n\n    def causal_attention_mask(self, batch_size, n_dest, n_src, dtype):\n        \"\"\"Masks the upper half of the dot product matrix in self attention.\n\n        This prevents flow of information from future tokens to current token.\n        1's in the lower triangle, counting from the lower right corner.\n        \"\"\"\n        i = tf.range(n_dest)[:, None]\n        j = tf.range(n_src)\n        m = i >= j - n_src + n_dest\n        mask = tf.cast(m, dtype)\n        mask = tf.reshape(mask, [1, n_dest, n_src])\n        mult = tf.concat(\n            [batch_size[..., tf.newaxis], tf.constant([1, 1], dtype=tf.int32)], 0\n        )\n        return tf.tile(mask, mult)\n\n    def call(self, enc_out, target, training):\n        input_shape = tf.shape(target)\n        batch_size = input_shape[0]\n        seq_len = input_shape[1]\n        causal_mask = self.causal_attention_mask(batch_size, seq_len, seq_len, tf.bool)\n        target_att = self.self_att(target, target, attention_mask=causal_mask)\n        target_norm = self.layernorm1(target + self.self_dropout(target_att, training = training))\n        enc_out = self.enc_att(target_norm, enc_out)\n        enc_out_norm = self.layernorm2(self.enc_dropout(enc_out, training = training) + target_norm)\n        ffn_out = self.ffn(enc_out_norm)\n        ffn_out_norm = self.layernorm3(enc_out_norm + self.ffn_dropout(ffn_out, training = training))\n        return ffn_out_norm","metadata":{"execution":{"iopub.status.busy":"2023-07-14T18:58:33.450145Z","iopub.execute_input":"2023-07-14T18:58:33.450513Z","iopub.status.idle":"2023-07-14T18:58:33.466459Z","shell.execute_reply.started":"2023-07-14T18:58:33.450484Z","shell.execute_reply":"2023-07-14T18:58:33.465379Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Transformer\n\nThis model takes landmark coordinates as inputs and predicts a sequence of characters. The target character sequence, which has been shifted to the left is provided as the input to the decoder during training. The decoder employs its own past predictions during inference to forecast the next token.\n\nThe Levenshtein Distance between sequences is used as the accuracy metric since the evaluation metric for this contest is the Normalized Total Levenshtein Distance.\n\n","metadata":{}},{"cell_type":"code","source":"# Customized to add edit_dist metric and training variable.\n# Reference:\n# https://www.kaggle.com/code/irohith/aslfr-transformer/notebook\n# https://www.kaggle.com/code/shlomoron/aslfr-a-simple-transformer/notebook\n\nclass Transformer(keras.Model):\n    def __init__(\n        self,\n        num_hid=64,\n        num_head=2,\n        num_feed_forward=128,\n        source_maxlen=100,\n        target_maxlen=100,\n        num_layers_enc=4,\n        num_layers_dec=1,\n        num_classes=60,\n    ):\n        super().__init__()\n#         create two metric objects, loss_metric and acc_metric, \n#         used to track the loss and edit distance metric during training and evaluation.\n        self.loss_metric = keras.metrics.Mean(name=\"loss\")\n        self.acc_metric = keras.metrics.Mean(name=\"edit_dist\")\n        self.num_layers_enc = num_layers_enc\n        self.num_layers_dec = num_layers_dec\n        self.target_maxlen = target_maxlen\n        self.num_classes = num_classes\n\n#         input landmarks are encoded using Landmark embedding\n#         create instance of LandmarkEmbedding\n        self.enc_input = LandmarkEmbedding(num_hid=num_hid, maxlen=source_maxlen)\n#     output tokens are encoded using token embedding\n#     create TokenEmbedding instance\n        self.dec_input = TokenEmbedding(\n            num_vocab=num_classes, maxlen=target_maxlen, num_hid=num_hid\n        )\n        '''\n        encoder is instance of Sequential class it consists of\n        1. enc_input - instance of LandmarkEmedding\n        2. Multiple instances of TransformedEncoder(Class we defined earlier) - depend on num_layers_enc\n        '''\n        self.encoder = keras.Sequential(\n            [self.enc_input]\n            + [\n                TransformerEncoder(num_hid, num_head, num_feed_forward)\n                for _ in range(num_layers_enc)\n            ]\n        )\n\n        for i in range(num_layers_dec):   # num_layers_dec - TransformerDecoder instances are generated for out transformer\n            setattr(\n                self,\n                f\"dec_layer_{i}\",\n                TransformerDecoder(num_hid, num_head, num_feed_forward),\n            )\n        self.classifier = layers.Dense(num_classes)  # define the final dense layer for output tokens\n\n    def decode(self, enc_out, target, training):\n        y = self.dec_input(target)     #  dec_input is instance of TokenEmbedding class which is being applied to target class\n        for i in range(self.num_layers_dec):     # iterate through length of decoder layer\n            y = getattr(self, f\"dec_layer_{i}\")(enc_out, y, training)\n        return y\n\n    def call(self, inputs, training):\n        source = inputs[0]\n        target = inputs[1]\n        x = self.encoder(source, training)\n        y = self.decode(x, target, training)\n        return self.classifier(y)\n\n    @property\n    def metrics(self):\n        return [self.loss_metric]\n    \n# this function - calculate loss - update variables using gradient descent\n    def train_step(self, batch):\n        \"\"\"Processes one batch inside model.fit().\"\"\"\n        source = batch[0]\n        target = batch[1]\n\n        input_shape = tf.shape(target)\n        batch_size = input_shape[0]\n        \n        dec_input = target[:, :-1]\n        dec_target = target[:, 1:]\n        with tf.GradientTape() as tape:\n            preds = self([source, dec_input])\n            one_hot = tf.one_hot(dec_target, depth=self.num_classes)\n            mask = tf.math.logical_not(tf.math.equal(dec_target, pad_token_idx))\n            loss = self.compiled_loss(one_hot, preds, sample_weight=mask)\n        trainable_vars = self.trainable_variables\n        gradients = tape.gradient(loss, trainable_vars)\n        self.optimizer.apply_gradients(zip(gradients, trainable_vars))\n        # Computes the Levenshtein distance between sequences since the evaluation\n        # metric for this contest is the normalized total levenshtein distance.\n        edit_dist = tf.edit_distance(tf.sparse.from_dense(target), \n                                     tf.sparse.from_dense(tf.cast(tf.argmax(preds, axis=1), tf.int32)))\n        edit_dist = tf.reduce_mean(edit_dist)\n        self.acc_metric.update_state(edit_dist)\n        self.loss_metric.update_state(loss)\n        return {\"loss\": self.loss_metric.result(), \"edit_dist\": self.acc_metric.result()}\n\n    def test_step(self, batch):        \n        source = batch[0]\n        target = batch[1]\n\n        input_shape = tf.shape(target)\n        batch_size = input_shape[0]\n        \n        dec_input = target[:, :-1]\n        dec_target = target[:, 1:]\n        preds = self([source, dec_input])\n        one_hot = tf.one_hot(dec_target, depth=self.num_classes)\n        mask = tf.math.logical_not(tf.math.equal(dec_target, pad_token_idx))\n        loss = self.compiled_loss(one_hot, preds, sample_weight=mask)\n        # Computes the Levenshtein distance between sequences since the evaluation\n        # metric for this contest is the normalized total levenshtein distance.\n        edit_dist = tf.edit_distance(tf.sparse.from_dense(target), \n                                     tf.sparse.from_dense(tf.cast(tf.argmax(preds, axis=1), tf.int32)))\n        edit_dist = tf.reduce_mean(edit_dist)\n        self.acc_metric.update_state(edit_dist)\n        self.loss_metric.update_state(loss)\n        return {\"loss\": self.loss_metric.result(), \"edit_dist\": self.acc_metric.result()}\n\n    def generate(self, source, target_start_token_idx):\n        \"\"\"Performs inference over one batch of inputs using greedy decoding\n        performs inference on one batch of input data using greedy decoding. \n        It takes the source and a target_start_token_idx - generates the output tokens by iteratively predicting the next token..\"\"\"\n        bs = tf.shape(source)[0]\n        enc = self.encoder(source, training = False)\n        dec_input = tf.ones((bs, 1), dtype=tf.int32) * target_start_token_idx\n        dec_logits = []\n        for i in range(self.target_maxlen - 1):\n            dec_out = self.decode(enc, dec_input, training = False)\n            logits = self.classifier(dec_out)\n            logits = tf.argmax(logits, axis=-1, output_type=tf.int32)\n            last_logit = logits[:, -1][..., tf.newaxis]\n            dec_logits.append(last_logit)\n            dec_input = tf.concat([dec_input, last_logit], axis=-1)\n        return dec_input","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:00:25.292847Z","iopub.execute_input":"2023-07-14T19:00:25.293344Z","iopub.status.idle":"2023-07-14T19:00:25.321038Z","shell.execute_reply.started":"2023-07-14T19:00:25.293302Z","shell.execute_reply":"2023-07-14T19:00:25.319914Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class DisplayOutputs(keras.callbacks.Callback):\n    def __init__(\n        self, batch, idx_to_token, target_start_token_idx=60, target_end_token_idx=61\n    ):\n        \"\"\"Displays a batch of outputs after every 4 epoch\n\n        Args:\n            batch: A test batch\n            idx_to_token: A List containing the vocabulary tokens corresponding to their indices\n            target_start_token_idx: A start token index in the target vocabulary\n            target_end_token_idx: An end token index in the target vocabulary\n        \"\"\"\n        self.batch = batch\n        self.target_start_token_idx = target_start_token_idx\n        self.target_end_token_idx = target_end_token_idx\n        self.idx_to_char = idx_to_token\n\n    def on_epoch_end(self, epoch, logs=None):\n        if epoch % 4 != 0:\n            return\n        source = self.batch[0]\n        target = self.batch[1].numpy()\n        bs = tf.shape(source)[0]\n        preds = self.model.generate(source, self.target_start_token_idx)\n        preds = preds.numpy()\n        for i in range(bs):\n            target_text = \"\".join([self.idx_to_char[_] for _ in target[i, :]])\n            prediction = \"\"\n            for idx in preds[i, :]:\n#                 token indices are converted to strings by mapping them to tokens using idx_to_char\n                prediction += self.idx_to_char[idx]\n                if idx == self.target_end_token_idx:\n                    break\n            print(f\"target:     {target_text.replace('-','')}\")\n            print(f\"prediction: {prediction}\\n\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:00:32.668504Z","iopub.execute_input":"2023-07-14T19:00:32.668877Z","iopub.status.idle":"2023-07-14T19:00:32.680925Z","shell.execute_reply.started":"2023-07-14T19:00:32.668847Z","shell.execute_reply":"2023-07-14T19:00:32.679955Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Transformer variables are customized from original keras tutorial to suit this dataset.\n# Reference: https://www.kaggle.com/code/shlomoron/aslfr-a-simple-transformer/notebook\n\n\n# transformer model is initiated\nbatch = next(iter(valid_ds))\n\n# The vocabulary to convert predicted indices into characters\nidx_to_char = list(char_to_num.keys())\ndisplay_cb = DisplayOutputs(\n    batch, idx_to_char, target_start_token_idx=char_to_num['<'], target_end_token_idx=char_to_num['>']\n)  # set the arguments as per vocabulary index for '<' and '>'\n\nmodel = Transformer(\n    num_hid=200,\n    num_head=4,\n    num_feed_forward=400,\n    source_maxlen = FRAME_LEN,\n    target_maxlen=64,\n    num_layers_enc=2,\n    num_layers_dec=1,\n    num_classes=62\n)\nloss_fn = tf.keras.losses.CategoricalCrossentropy(\n    from_logits=True, label_smoothing=0.1,\n)\n\n\noptimizer = keras.optimizers.Adam(0.0001)\nmodel.compile(optimizer=optimizer, loss=loss_fn)\n\nhistory = model.fit(train_ds, validation_data=valid_ds, callbacks=[display_cb], epochs=13)\n","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:00:34.618589Z","iopub.execute_input":"2023-07-14T19:00:34.619621Z","iopub.status.idle":"2023-07-14T19:08:53.268548Z","shell.execute_reply.started":"2023-07-14T19:00:34.619579Z","shell.execute_reply":"2023-07-14T19:08:53.26744Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plt.plot(history.history['loss'])\nplt.plot(history.history['val_loss'])\nplt.legend(['training loss', 'val_loss'])","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:08:53.270668Z","iopub.execute_input":"2023-07-14T19:08:53.271041Z","iopub.status.idle":"2023-07-14T19:08:53.823018Z","shell.execute_reply.started":"2023-07-14T19:08:53.271006Z","shell.execute_reply":"2023-07-14T19:08:53.822119Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## TFLite model","metadata":{}},{"cell_type":"code","source":"class TFLiteModel(tf.Module):\n    def __init__(self, model):\n        super(TFLiteModel, self).__init__()\n        self.target_start_token_idx = start_token_idx\n        self.target_end_token_idx = end_token_idx\n        # Load the feature generation and main models\n        self.model = model\n    \n    @tf.function(input_signature=[tf.TensorSpec(shape=[None, len(FEATURE_COLUMNS)], dtype=tf.float32, name='inputs')])\n    def __call__(self, inputs, training=False):\n        # Preprocess Data\n        x = tf.cast(inputs, tf.float32)\n        x = x[None]\n        x = tf.cond(tf.shape(x)[1] == 0, lambda: tf.zeros((1, 1, len(FEATURE_COLUMNS))), lambda: tf.identity(x))\n        x = x[0]\n        x = pre_process(x)\n        x = x[None]\n        x = self.model.generate(x, self.target_start_token_idx)\n        x = x[0]\n        idx = tf.argmax(tf.cast(tf.equal(x, self.target_end_token_idx), tf.int32))\n        idx = tf.where(tf.math.less(idx, 1), tf.constant(2, dtype=tf.int64), idx)\n        x = x[1:idx]\n        x = tf.one_hot(x, 59)\n        return {'outputs': x}\n    \ntflitemodel_base = TFLiteModel(model)","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:09:08.784107Z","iopub.execute_input":"2023-07-14T19:09:08.784489Z","iopub.status.idle":"2023-07-14T19:09:08.79788Z","shell.execute_reply.started":"2023-07-14T19:09:08.78446Z","shell.execute_reply":"2023-07-14T19:09:08.79653Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# save the weights of model\nmodel.save_weights(\"model.h5\")","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:09:09.819389Z","iopub.execute_input":"2023-07-14T19:09:09.819746Z","iopub.status.idle":"2023-07-14T19:09:09.95037Z","shell.execute_reply.started":"2023-07-14T19:09:09.819717Z","shell.execute_reply":"2023-07-14T19:09:09.949358Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# save the tflite file\n\nkeras_model_converter = tf.lite.TFLiteConverter.from_keras_model(tflitemodel_base)\nkeras_model_converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]#, tf.lite.OpsSet.SELECT_TF_OPS]\ntflite_model = keras_model_converter.convert()\nwith open('/kaggle/working/model.tflite', 'wb') as f:\n    f.write(tflite_model)\n    \ninfargs = {\"selected_columns\" : FEATURE_COLUMNS}\n\nwith open('inference_args.json', \"w\") as json_file:\n    json.dump(infargs, json_file)\n","metadata":{"execution":{"iopub.status.busy":"2023-07-14T19:09:11.558609Z","iopub.execute_input":"2023-07-14T19:09:11.559007Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"!zip submission.zip  './model.tflite' './inference_args.json'","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# uses a TensorFlow Lite interpreter to load a TensorFlow Lite model which does the work\ninterpreter = tf.lite.Interpreter(\"model.tflite\")\n\nREQUIRED_SIGNATURE = \"serving_default\"\nREQUIRED_OUTPUT = \"outputs\"\n\nwith open (\"/kaggle/input/asl-fingerspelling/character_to_prediction_index.json\", \"r\") as f:\n    character_map = json.load(f)\nrev_character_map = {j:i for i,j in character_map.items()}\n\nfound_signatures = list(interpreter.get_signature_list().keys())\n\nif REQUIRED_SIGNATURE not in found_signatures:\n    raise KernelEvalException('Required input signature not found.')\n\nprediction_fn = interpreter.get_signature_runner(\"serving_default\")\noutput = prediction_fn(inputs=batch[0][0])\nprediction_str = \"\".join([rev_character_map.get(s, \"\") for s in np.argmax(output[REQUIRED_OUTPUT], axis=1)])\nprint(prediction_str)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# <p style=\"font-family:JetBrains Mono; font-weight:normal; letter-spacing: 2px; color:#000000; font-size:140%; text-align:center;padding: 0px; border-bottom: 3px solid #000000\">References</p>\n1. [📊 [EDA] ASL - Fingerspelling 2D & 3D Plot 📊](https://www.kaggle.com/code/drmwnnrafi/eda-asl-fingerspelling-2d-3d-plot)\n2. [[EDA] 🤟ASLFR🖖 - Animated visualization📊🧍](https://www.kaggle.com/code/leonidkulyk/eda-aslfr-animated-visualization/notebook)\n3. [ASL Fingerspelling Recognition w/ TensorFlow](https://www.kaggle.com/code/gusthema/asl-fingerspelling-recognition-w-tensorflow/notebook#Google---American-Sign-Language-Fingerspelling-Recognition-with-TensorFlow)","metadata":{}},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}