{"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":[{"sourceId":46105,"databundleVersionId":5087314,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import tensorflow as tf\nimport numpy as np\nimport pandas as pd\nimport math\n\n# --- Hyperparameters ---\nINPUT_SIZE = 64\nN_COLS = 66\nN_DIMS = 3\nN_EPOCHS = 100\nBATCH_SIZE = 256\nNUM_CLASSES = 250\n\n# Indices for landmarks\nLIPS_START = 0\nLEFT_HAND_START = 40\nRIGHT_HAND_START = 61\nPOSE_START = 61\n\n# Transformer Architecture Params\nLIPS_UNITS = 384\nHANDS_UNITS = 384\nPOSE_UNITS = 384\n\n# --- CHANGED: REDUCED CAPACITY ---\nUNITS = 256  # Was 512. Smaller units force generalization.\n# ---------------------------------\n\nNUM_BLOCKS = 2\nMLP_RATIO = 2\n\n# Keep the high dropout from previous attempt\nMLP_DROPOUT_RATIO = 0.50\nCLASSIFIER_DROPOUT_RATIO = 0.40\n\n# Initialize random weights\nINIT_HE_UNIFORM = tf.keras.initializers.he_uniform\nINIT_GLOROT_UNIFORM = tf.keras.initializers.glorot_uniform\nINIT_ZEROS = tf.keras.initializers.constant(0.0)\nGELU = tf.keras.activations.gelu","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:02:15.931058Z","iopub.execute_input":"2026-01-09T03:02:15.931289Z","iopub.status.idle":"2026-01-09T03:02:30.931319Z","shell.execute_reply.started":"2026-01-09T03:02:15.931250Z","shell.execute_reply":"2026-01-09T03:02:30.930725Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Custom Layers ---\n\ndef scaled_dot_product(q, k, v, softmax, attention_mask):\n    # Calculates Q . K(transpose)\n    qkt = tf.matmul(q, k, transpose_b=True)\n    # Calculates scaling factor\n    dk = tf.math.sqrt(tf.cast(q.shape[-1], dtype=tf.float32))\n    scaled_qkt = qkt / dk\n    softmax = softmax(scaled_qkt, mask=attention_mask)\n    z = tf.matmul(softmax, v)\n    return z\n\nclass MultiHeadAttention(tf.keras.layers.Layer):\n    def __init__(self, d_model, num_of_heads):\n        super(MultiHeadAttention, self).__init__()\n        self.d_model = d_model\n        self.num_of_heads = num_of_heads\n        self.depth = d_model // num_of_heads\n        self.wq = [tf.keras.layers.Dense(self.depth) for i in range(num_of_heads)]\n        self.wk = [tf.keras.layers.Dense(self.depth) for i in range(num_of_heads)]\n        self.wv = [tf.keras.layers.Dense(self.depth) for i in range(num_of_heads)]\n        self.wo = tf.keras.layers.Dense(d_model)\n        self.softmax = tf.keras.layers.Softmax()\n\n    def call(self, x, attention_mask):\n        multi_attn = []\n        for i in range(self.num_of_heads):\n            Q = self.wq[i](x)\n            K = self.wk[i](x)\n            V = self.wv[i](x)\n            multi_attn.append(scaled_dot_product(Q, K, V, self.softmax, attention_mask))\n        multi_head = tf.concat(multi_attn, axis=-1)\n        multi_head_attention = self.wo(multi_head)\n        return multi_head_attention\n\nclass Transformer(tf.keras.Model):\n    def __init__(self, num_blocks):\n        super(Transformer, self).__init__(name='transformer')\n        self.num_blocks = num_blocks\n\n    def build(self, input_shape):\n        self.mhas = []\n        self.mlps = []\n        for i in range(self.num_blocks):\n            self.mhas.append(MultiHeadAttention(UNITS, 8))\n            self.mlps.append(tf.keras.Sequential([\n                tf.keras.layers.Dense(UNITS * MLP_RATIO, activation=GELU, kernel_initializer=INIT_GLOROT_UNIFORM),\n                tf.keras.layers.Dropout(MLP_DROPOUT_RATIO),\n                tf.keras.layers.Dense(UNITS, kernel_initializer=INIT_HE_UNIFORM),\n            ]))\n\n    def call(self, x, attention_mask):\n        for mha, mlp in zip(self.mhas, self.mlps):\n            x = x + mha(x, attention_mask)\n            x = x + mlp(x)\n        return x\n\nclass LandmarkEmbedding(tf.keras.Model):\n    def __init__(self, units, name):\n        super(LandmarkEmbedding, self).__init__(name=f'{name}_embedding')\n        self.units = units\n\n    def build(self, input_shape):\n        self.empty_embedding = self.add_weight(\n            name=f'{self.name}_empty_embedding',\n            shape=[self.units],\n            initializer=INIT_ZEROS,\n        )\n        self.dense = tf.keras.Sequential([\n            tf.keras.layers.Dense(self.units, name=f'{self.name}_dense_1', use_bias=False, kernel_initializer=INIT_GLOROT_UNIFORM),\n            tf.keras.layers.Activation(GELU),\n            tf.keras.layers.Dense(self.units, name=f'{self.name}_dense_2', use_bias=False, kernel_initializer=INIT_HE_UNIFORM),\n        ], name=f'{self.name}_dense')\n\n    def call(self, x):\n        return tf.where(\n            tf.reduce_sum(x, axis=2, keepdims=True) == 0,\n            self.empty_embedding,\n            self.dense(x),\n        )\n\nclass Embedding(tf.keras.Model):\n    def __init__(self):\n        super(Embedding, self).__init__()\n\n    def build(self, input_shape):\n        self.positional_embedding = tf.keras.layers.Embedding(INPUT_SIZE + 1, UNITS, embeddings_initializer=INIT_ZEROS)\n        self.lips_embedding = LandmarkEmbedding(LIPS_UNITS, 'lips')\n        self.left_hand_embedding = LandmarkEmbedding(HANDS_UNITS, 'left_hand')\n        self.pose_embedding = LandmarkEmbedding(POSE_UNITS, 'pose')\n        self.landmark_weights = tf.Variable(tf.zeros([3], dtype=tf.float32), name='landmark_weights')\n        self.fc = tf.keras.Sequential([\n            tf.keras.layers.Dense(UNITS, name='fully_connected_1', use_bias=False, kernel_initializer=INIT_GLOROT_UNIFORM),\n            tf.keras.layers.Activation(GELU),\n            tf.keras.layers.Dense(UNITS, name='fully_connected_2', use_bias=False, kernel_initializer=INIT_HE_UNIFORM),\n        ], name='fc')\n\n    def call(self, lips0, left_hand0, pose0, non_empty_frame_idxs, training=False):\n        lips_embedding = self.lips_embedding(lips0)\n        left_hand_embedding = self.left_hand_embedding(left_hand0)\n        pose_embedding = self.pose_embedding(pose0)\n        \n        x = tf.stack((lips_embedding, left_hand_embedding, pose_embedding), axis=3)\n        x = x * tf.nn.softmax(self.landmark_weights)\n        x = tf.reduce_sum(x, axis=3)\n        \n        x = self.fc(x)\n        \n        max_frame_idxs = tf.clip_by_value(\n            tf.reduce_max(non_empty_frame_idxs, axis=1, keepdims=True),\n            1,\n            np.inf,\n        )\n        normalised_non_empty_frame_idxs = tf.where(\n            tf.math.equal(non_empty_frame_idxs, -1.0),\n            INPUT_SIZE,\n            tf.cast(non_empty_frame_idxs / max_frame_idxs * INPUT_SIZE, tf.int32),\n        )\n        x = x + self.positional_embedding(normalised_non_empty_frame_idxs)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:02:30.932647Z","iopub.execute_input":"2026-01-09T03:02:30.933099Z","iopub.status.idle":"2026-01-09T03:02:30.950624Z","shell.execute_reply.started":"2026-01-09T03:02:30.933076Z","shell.execute_reply":"2026-01-09T03:02:30.949959Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def scce_with_ls(y_true, y_pred):\n    # Categorical Crossentropy with native label smoothing support\n    y_true = tf.cast(y_true, tf.int32)\n    y_true = tf.one_hot(y_true, NUM_CLASSES, axis=1)\n    y_true = tf.squeeze(y_true, axis=2)\n    return tf.keras.losses.categorical_crossentropy(y_true, y_pred, label_smoothing=0.25)\n\ndef scce_with_ls(y_true, y_pred):\n    y_true = tf.cast(y_true, tf.int32)\n    y_true = tf.one_hot(y_true, NUM_CLASSES, axis=1)\n    y_true = tf.squeeze(y_true, axis=2)\n    return tf.keras.losses.categorical_crossentropy(y_true, y_pred, label_smoothing=0.25)\ndef scce_with_ls(y_true, y_pred):\n    y_true = tf.cast(y_true, tf.int32)\n    y_true = tf.one_hot(y_true, NUM_CLASSES, axis=1)\n    y_true = tf.squeeze(y_true, axis=2)\n    return tf.keras.losses.categorical_crossentropy(y_true, y_pred, label_smoothing=0.25)\n\ndef get_model():\n    # 1. Inputs\n    frames = tf.keras.layers.Input([INPUT_SIZE, N_COLS, N_DIMS], dtype=tf.float32, name='frames')\n    non_empty_frame_idxs = tf.keras.layers.Input([INPUT_SIZE], dtype=tf.float32, name='non_empty_frame_idxs')\n\n    # 2. Masking\n    def make_mask(args):\n        idxs = args[0]\n        mask0 = tf.cast(tf.math.not_equal(idxs, -1), tf.float32)\n        mask0 = tf.expand_dims(mask0, axis=2)\n        mask = tf.where(\n            (tf.random.uniform(tf.shape(mask0)) > 0.25) & tf.math.not_equal(mask0, 0.0),\n            1.0,\n            0.0,\n        )\n        return mask\n\n    mask = tf.keras.layers.Lambda(make_mask, output_shape=(INPUT_SIZE, 1), name='masking_layer')([non_empty_frame_idxs])\n\n    # 3. Slicing\n    def slice_data(args):\n        x = args[0]\n        x = tf.slice(x, [0, 0, 0, 0], [-1, INPUT_SIZE, N_COLS, 2])\n        lips = tf.slice(x, [0, 0, LIPS_START, 0], [-1, INPUT_SIZE, 40, 2])\n        left_hand = tf.slice(x, [0, 0, 40, 0], [-1, INPUT_SIZE, 21, 2])\n        pose = tf.slice(x, [0, 0, 61, 0], [-1, INPUT_SIZE, 5, 2])\n        lips = tf.reshape(lips, [-1, INPUT_SIZE, 40 * 2])\n        left_hand = tf.reshape(left_hand, [-1, INPUT_SIZE, 21 * 2])\n        pose = tf.reshape(pose, [-1, INPUT_SIZE, 5 * 2])\n        return lips, left_hand, pose\n\n    lips, left_hand, pose = tf.keras.layers.Lambda(slice_data, name='slicing_layer')([frames])\n\n    # 4. Embedding\n    x = Embedding()(lips, left_hand, pose, non_empty_frame_idxs)\n    \n    # --- CHANGED: INCREASED NOISE ---\n    # Increased from 0.05 to 0.15. This makes training much harder.\n    x = tf.keras.layers.GaussianNoise(0.30)(x)\n    # --------------------------------\n    \n    x = Transformer(NUM_BLOCKS)(x, mask)\n    \n    # 6. Global Pooling\n    def global_pool(args):\n        x, m = args\n        return tf.reduce_sum(x * m, axis=1) / (tf.reduce_sum(m, axis=1) + 1e-9)\n\n    x = tf.keras.layers.Lambda(global_pool, name='global_pooling')([x, mask])\n    \n    # 7. Classifier\n    x = tf.keras.layers.Dropout(CLASSIFIER_DROPOUT_RATIO)(x)\n    outputs = tf.keras.layers.Dense(NUM_CLASSES, activation='softmax', kernel_initializer=INIT_GLOROT_UNIFORM)(x)\n\n    # 8. Compile\n    model = tf.keras.models.Model(inputs=[frames, non_empty_frame_idxs], outputs=outputs)\n    \n    optimizer = tf.keras.optimizers.AdamW(learning_rate=1e-3, weight_decay=1e-5, clipnorm=1.0)\n    \n    model.compile(loss=scce_with_ls, optimizer=optimizer, metrics=[\n        tf.keras.metrics.SparseCategoricalAccuracy(name='acc'),\n        tf.keras.metrics.SparseTopKCategoricalAccuracy(k=5, name='top_5_acc'),\n        tf.keras.metrics.SparseTopKCategoricalAccuracy(k=10, name='top_10_acc'),\n    ])\n    \n    return model\n\nmodel = get_model()\nmodel.summary()\n\n\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:13:44.851590Z","iopub.execute_input":"2026-01-09T03:13:44.852448Z","iopub.status.idle":"2026-01-09T03:13:46.329981Z","shell.execute_reply.started":"2026-01-09T03:13:44.852414Z","shell.execute_reply":"2026-01-09T03:13:46.329348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nfrom tqdm.notebook import tqdm\n\n# --- 1. Landmark Indices Configuration ---\n# These specific indices map the 543 MediaPipe landmarks to the 66 used by the model.\nLIPS_IDXS0 = np.array([\n        61, 185, 40, 39, 37, 0, 267, 269, 270, 409,\n        291, 146, 91, 181, 84, 17, 314, 405, 321, 375,\n        78, 191, 80, 81, 82, 13, 312, 311, 310, 415,\n        95, 88, 178, 87, 14, 317, 402, 318, 324, 308,\n    ])\n# MediaPipe Indices for hands and pose\nLEFT_HAND_IDXS0 = np.arange(468, 489)\nRIGHT_HAND_IDXS0 = np.arange(522, 543)\nLEFT_POSE_IDXS0 = np.array([502, 504, 506, 508, 510])\nRIGHT_POSE_IDXS0 = np.array([503, 505, 507, 509, 511])\n\n# Define the full sets for Dominant Hand Selection\n# If Left hand is dominant: Lips + Left Hand + Left Pose\nLANDMARK_IDXS_LEFT_DOMINANT0 = np.concatenate((LIPS_IDXS0, LEFT_HAND_IDXS0, LEFT_POSE_IDXS0))\n# If Right hand is dominant: Lips + Right Hand + Right Pose\nLANDMARK_IDXS_RIGHT_DOMINANT0 = np.concatenate((LIPS_IDXS0, RIGHT_HAND_IDXS0, RIGHT_POSE_IDXS0))\n\n# --- 2. Preprocessing Function ---\ndef load_and_process_sample(parquet_path, input_size=64):\n    full_path = f\"/kaggle/input/asl-signs/{parquet_path}\"\n    try:\n        # 1. Load Data\n        df = pd.read_parquet(full_path)\n    except FileNotFoundError:\n        return None, None\n\n    # FIX: Select only x, y, z columns\n    # The dataframe has 543 rows per frame\n    n_frames = int(len(df) / 543)\n    \n    # Extract only coordinates and reshape\n    data = df[['x', 'y', 'z']].values\n    data = data.reshape(n_frames, 543, 3)\n    \n    # 2. Determine Dominant Hand\n    # (Same logic as before)\n    left_hand_sum = np.sum(np.isnan(data[:, LEFT_HAND_IDXS0, :]))\n    right_hand_sum = np.sum(np.isnan(data[:, RIGHT_HAND_IDXS0, :]))\n    \n    if left_hand_sum < right_hand_sum:\n        # Left Hand Dominant\n        data = data[:, LANDMARK_IDXS_LEFT_DOMINANT0, :]\n    else:\n        # Right Hand Dominant -> Mirror\n        data = data[:, LANDMARK_IDXS_RIGHT_DOMINANT0, :]\n        data[:, :, 0] = -data[:, :, 0] # Flip X\n\n    # 3. Handle NaNs\n    data = np.nan_to_num(data)\n\n    # 4. Resize to fixed frame count (Input Size = 64)\n    if n_frames != input_size:\n        # Linear interpolation\n        x = np.linspace(0, n_frames - 1, n_frames)\n        x_new = np.linspace(0, n_frames - 1, input_size)\n        \n        data_resized = np.zeros((input_size, data.shape[1], data.shape[2]), dtype=np.float32)\n        for i in range(data.shape[1]):\n            for j in range(3):\n                data_resized[:, i, j] = np.interp(x_new, x, data[:, i, j])\n        data = data_resized\n    \n    # Create mask (all ones since we interpolated)\n    non_empty_frame_idxs = np.arange(input_size, dtype=np.float32)\n    \n    return data.astype(np.float32), non_empty_frame_idxs\n\n# --- 3. Main Data Loading Loop ---\n\n# Load Label Map\ntrain = pd.read_csv(\"/kaggle/input/asl-signs/train.csv\")\n# Create integer labels\ntrain['label_code'] = train['sign'].astype('category').cat.codes\nlabel_map = dict(enumerate(train['sign'].astype('category').cat.categories))\n\n# CONFIG: Load a subset for testing? (Set to None for full training)\n# Loading 90k files takes time. Try 5000 first to check memory.\nN_SAMPLES = 20000 \ntrain_subset = train.iloc[:N_SAMPLES]\n\nprint(f\"Loading {len(train_subset)} samples...\")\n\nX_frames = []\nX_idxs = []\ny_labels = []\n\nfor index, row in tqdm(train_subset.iterrows(), total=len(train_subset)):\n    frames, idxs = load_and_process_sample(row['path'])\n    if frames is not None:\n        X_frames.append(frames)\n        X_idxs.append(idxs)\n        y_labels.append(row['label_code'])\n\n# Convert to Numpy Arrays\nX_frames = np.array(X_frames)\nX_idxs = np.array(X_idxs)\ny_train = np.array(y_labels)\n\nprint(\"Data Loaded!\")\nprint(f\"X_frames shape: {X_frames.shape}\") # Should be (N, 64, 66, 3)\nprint(f\"y_train shape: {y_train.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:13:51.202877Z","iopub.execute_input":"2026-01-09T03:13:51.203506Z","iopub.status.idle":"2026-01-09T03:20:49.144380Z","shell.execute_reply.started":"2026-01-09T03:13:51.203474Z","shell.execute_reply":"2026-01-09T03:20:49.143554Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import math\nimport matplotlib.pyplot as plt\n\n# --- 1. Corrected Loss Function ---\ndef scce_with_ls(y_true, y_pred):\n    # Ensure labels are integers\n    y_true = tf.cast(y_true, tf.int32)\n    \n    # Robust Fix: Force input to be 1D (Batch_Size,) before one_hot\n    # This handles both (Batch_Size, 1) and (Batch_Size,) inputs safely.\n    y_true = tf.reshape(y_true, [-1]) \n    \n    # One Hot Encode -> Result shape: (Batch_Size, NUM_CLASSES)\n    y_true = tf.one_hot(y_true, NUM_CLASSES, axis=1)\n    \n    # Calculate Loss\n    return tf.keras.losses.categorical_crossentropy(y_true, y_pred, label_smoothing=0.25)\n\n# --- 2. Re-Compile Model with New Loss ---\n# We must re-compile to apply the fixed loss function\noptimizer = tf.keras.optimizers.AdamW(learning_rate=1e-3, weight_decay=1e-5, clipnorm=1.0)\n\nmodel.compile(loss=scce_with_ls, optimizer=optimizer, metrics=[\n    tf.keras.metrics.SparseCategoricalAccuracy(name='acc'),\n    tf.keras.metrics.SparseTopKCategoricalAccuracy(k=5, name='top_5_acc'),\n    tf.keras.metrics.SparseTopKCategoricalAccuracy(k=10, name='top_10_acc'),\n])\n\n# --- 3. Callbacks ---\ndef lrfn(current_step, num_warmup_steps, lr_max, num_cycles=0.50, num_training_steps=N_EPOCHS):\n    if current_step < num_warmup_steps:\n        return lr_max * 0.10 ** (num_warmup_steps - current_step)\n    else:\n        progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))\n        return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))) * lr_max\n\nlr_callback = tf.keras.callbacks.LearningRateScheduler(\n    lambda step: lrfn(step, num_warmup_steps=0, lr_max=1e-3, num_training_steps=N_EPOCHS)\n)\n\nearly_stopping = tf.keras.callbacks.EarlyStopping(\n    monitor='val_loss', patience=10, restore_best_weights=True, verbose=1\n)\n\n# --- 4. Start Training ---\nprint(\"Starting training...\")\nhistory = model.fit(\n    x=[X_frames_train, X_idxs_train],\n    y=y_train_split,\n    epochs=N_EPOCHS,\n    batch_size=BATCH_SIZE, \n    validation_data=([X_frames_val, X_idxs_val], y_val_split),\n    callbacks=[lr_callback, early_stopping],\n    verbose=1\n)\n\n# --- 5. Save & Plot ---\nmodel.save(\"asl_model.keras\")\nprint(\"✅ Training Complete & Model Saved to asl_model.keras\")\n\nplt.figure(figsize=(12, 5))\nplt.subplot(1, 2, 1)\nplt.plot(history.history['loss'], label='Train Loss')\nplt.plot(history.history['val_loss'], label='Val Loss')\nplt.title('Loss')\nplt.legend()\nplt.subplot(1, 2, 2)\nplt.plot(history.history['acc'], label='Train Acc')\nplt.plot(history.history['val_acc'], label='Val Acc')\nplt.title('Accuracy')\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:22:00.302236Z","iopub.execute_input":"2026-01-09T03:22:00.302886Z","iopub.status.idle":"2026-01-09T03:34:16.438317Z","shell.execute_reply.started":"2026-01-09T03:22:00.302857Z","shell.execute_reply":"2026-01-09T03:34:16.437549Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\n# --- CONFIGURATION ---\nN_EVAL_SAMPLES = 100  # Number of samples to test\n\n# 1. Select random samples from the validation set\n# We use the indices we created during the train_test_split\n# Ensure N_EVAL_SAMPLES isn't larger than the validation set\nn_val = len(X_frames_val)\nif N_EVAL_SAMPLES > n_val:\n    print(f\"Requested {N_EVAL_SAMPLES} samples, but only {n_val} available. Testing on all.\")\n    N_EVAL_SAMPLES = n_val\n\n# Randomly choose indices\nrandom_indices = np.random.choice(n_val, size=N_EVAL_SAMPLES, replace=False)\n\n# 2. Prepare batches for prediction\neval_frames = X_frames_val[random_indices]\neval_idxs = X_idxs_val[random_indices]\neval_labels_idx = y_val_split[random_indices]\n\nprint(f\"Predicting on {N_EVAL_SAMPLES} samples...\")\n\n# 3. Predict\n# Returns probabilities: shape (N_SAMPLES, NUM_CLASSES)\npredictions_prob = model.predict([eval_frames, eval_idxs], verbose=1)\n\n# 4. Decode and Compare\ncorrect_count = 0\n\nprint(f\"\\n{'IDX':<6} | {'TRUE LABEL':<20} | {'PREDICTED LABEL':<20} | {'CONF':<6} | {'RESULT'}\")\nprint(\"-\" * 75)\n\nfor i in range(N_EVAL_SAMPLES):\n    true_code = eval_labels_idx[i]\n    pred_code = np.argmax(predictions_prob[i])\n    confidence = np.max(predictions_prob[i])\n    \n    # Map integer code back to string label\n    true_label = label_map[true_code]\n    pred_label = label_map[pred_code]\n    \n    is_correct = (true_code == pred_code)\n    if is_correct:\n        correct_count += 1\n        status = \"✅\"\n    else:\n        status = \"❌\"\n        \n    # Print row\n    print(f\"{random_indices[i]:<6} | {true_label:<20} | {pred_label:<20} | {confidence:.0%} | {status}\")\n\n# 5. Final Stats\naccuracy = correct_count / N_EVAL_SAMPLES\nprint(\"-\" * 75)\nprint(f\"Final Evaluation Accuracy: {accuracy:.2%}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:35:16.645158Z","iopub.execute_input":"2026-01-09T03:35:16.645464Z","iopub.status.idle":"2026-01-09T03:35:16.774632Z","shell.execute_reply.started":"2026-01-09T03:35:16.645437Z","shell.execute_reply":"2026-01-09T03:35:16.773883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\n# Split the data into Training (80%) and Validation (20%)\nX_frames_train, X_frames_val, X_idxs_train, X_idxs_val, y_train_split, y_val_split = train_test_split(\n    X_frames, \n    X_idxs, \n    y_train, \n    test_size=0.2, \n    random_state=42,\n    stratify=y_train  # Ensures all classes are represented in validation\n)\n\nprint(f\"Training Set:   {X_frames_train.shape}\")\nprint(f\"Validation Set: {X_frames_val.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T03:21:51.481477Z","iopub.execute_input":"2026-01-09T03:21:51.482296Z","iopub.status.idle":"2026-01-09T03:21:51.799829Z","shell.execute_reply.started":"2026-01-09T03:21:51.482263Z","shell.execute_reply":"2026-01-09T03:21:51.799183Z"}},"outputs":[],"execution_count":null}]}