{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":46105,"databundleVersionId":5087314},{"sourceType":"datasetVersion","sourceId":15990179,"datasetId":10254927,"databundleVersionId":16952429},{"sourceType":"modelInstanceVersion","sourceId":851990,"databundleVersionId":16970200,"modelInstanceId":647609,"modelId":659579},{"sourceType":"modelInstanceVersion","sourceId":829073,"databundleVersionId":16636966,"modelInstanceId":630512,"modelId":642422},{"sourceType":"modelInstanceVersion","sourceId":851075,"databundleVersionId":16957716,"modelInstanceId":646960,"modelId":658938},{"sourceType":"modelInstanceVersion","sourceId":850983,"databundleVersionId":16956398,"modelInstanceId":646902,"modelId":658865},{"sourceType":"kernelVersion","sourceId":315376202}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ASL Sign Language Transformer — Research-Grade Evaluation Pipeline\n\n**Task:** American Sign Language Isolated Sign Recognition (250 classes)  \n**Architecture:** Spatial-Temporal Transformer with Causal Depthwise Convolutions  \n**Scope:** Evaluation only. No training is performed. Pretrained weights are loaded directly.\n\n---\n\n| Section | Description |\n|---|---|\n| 1 | Environment Setup |\n| 2 | Constants, Preprocessing Pipeline, and Custom Layers |\n| 3 | Data Loading and Splitting |\n| 4 | Model Architecture Definition |\n| 5 | Pretrained Model Loading (Multi-Format Detection) |\n| 6 | Model Performance Benchmarking |\n| 7 | Full Evaluation Pipeline |\n| 8 | Advanced Analysis and Diagnostics |\n| 9 | Visualization Suite |\n| 10 | Export Results |\n","metadata":{}},{"cell_type":"markdown","source":"## Section 1: Environment Setup","metadata":{}},{"cell_type":"code","source":"# Standard library\nimport sys\nimport gc\nimport time\nimport math\nimport random\nimport logging\nimport datetime\nimport traceback\nimport json\nfrom pathlib import Path\n\n# Data and visualisation\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.autonotebook import tqdm\n\n# Machine learning\nimport sklearn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, f1_score, classification_report\n\n# Deep learning\nimport tensorflow as tf\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras import layers, metrics, losses, optimizers, mixed_precision\nfrom tensorflow.keras.utils import plot_model\n\nimport os, sys, gc, time, random, glob, warnings, json, pickle\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as ticker\nimport seaborn as sns\nfrom pathlib import Path\nfrom tqdm.autonotebook import tqdm\n\nimport sklearn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score, f1_score,\n    classification_report, confusion_matrix, log_loss,\n    roc_auc_score, average_precision_score,\n    cohen_kappa_score, matthews_corrcoef,\n    top_k_accuracy_score\n)\nfrom sklearn.calibration import calibration_curve\n\nimport tensorflow as tf\nfrom tensorflow.keras.models import load_model\n\nwarnings.filterwarnings(\"ignore\")\n\nSEED = 42\nos.environ[\"PYTHONHASHSEED\"] = str(SEED)\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\ngpus = tf.config.list_physical_devices(\"GPU\")\nif gpus:\n    for gpu in gpus:\n        tf.config.experimental.set_memory_growth(gpu, True)\n\nprint(\"Environment\")\nprint(f\"  TensorFlow : {tf.__version__}\")\nprint(f\"  Python     : {sys.version.split()[0]}\")\nprint(f\"  NumPy      : {np.__version__}\")\nprint(f\"  Pandas     : {pd.__version__}\")\nprint(f\"  Sklearn    : {sklearn.__version__}\")\nprint(f\"  GPUs       : {len(gpus)}\")\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:13:46.011354Z","iopub.execute_input":"2026-04-29T17:13:46.011651Z","iopub.status.idle":"2026-04-29T17:14:08.755628Z","shell.execute_reply.started":"2026-04-29T17:13:46.011616Z","shell.execute_reply":"2026-04-29T17:14:08.754610Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Section 2: Constants, Preprocessing Pipeline, and Custom Layers\n\nThe preprocessing pipeline is preserved exactly from the original training notebook.\nThe custom `Preprocess` layer performs NaN-safe normalization, landmark selection,\nand temporal feature engineering (position, velocity, acceleration) directly\nwithin the TensorFlow graph. All custom layers required for model loading are\ndefined here.\n","metadata":{}},{"cell_type":"code","source":"# ---------------------------------------------------------------------------\n# Data Dimensions\n# ---------------------------------------------------------------------------\nROWS_PER_FRAME = 543\nMAX_LEN        = 384\nCROP_LEN       = MAX_LEN\nNUM_CLASSES    = 250\nPAD            = -100.0\nBATCH_SIZE     = 64\n\n# ---------------------------------------------------------------------------\n# Landmark Index Groups (MediaPipe)\n# ---------------------------------------------------------------------------\nNOSE  = [1, 2, 98, 327]\nLNOSE = [98]\nRNOSE = [327]\n\nLIP = [\n    0, 61, 185, 40, 39, 37, 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]\nLLIP  = [84, 181, 91, 146, 61, 185, 40, 39, 37, 87, 178, 88, 95, 78, 191, 80, 81, 82]\nRLIP  = [314, 405, 321, 375, 291, 409, 270, 269, 267, 317, 402, 318, 324, 308, 415, 310, 311, 312]\n\nPOSE  = [500, 502, 504, 501, 503, 505, 512, 513]\nLPOSE = [513, 505, 503, 501]\nRPOSE = [512, 504, 502, 500]\n\nREYE = [33, 7, 163, 144, 145, 153, 154, 155, 133, 246, 161, 160, 159, 158, 157, 173]\nLEYE = [263, 249, 390, 373, 374, 380, 381, 382, 362, 466, 388, 387, 386, 385, 384, 398]\n\nLHAND = np.arange(468, 489).tolist()\nRHAND = np.arange(522, 543).tolist()\n\nPOINT_LANDMARKS = LIP + LHAND + RHAND + NOSE + REYE + LEYE\nNUM_NODES       = len(POINT_LANDMARKS)\nCHANNELS        = 6 * NUM_NODES\n\nprint(f\"Selected Landmark Nodes : {NUM_NODES}\")\nprint(f\"Feature Channels        : {CHANNELS}  (X, Y) x (pos, vel, acc)\")\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:08.757738Z","iopub.execute_input":"2026-04-29T17:14:08.758428Z","iopub.status.idle":"2026-04-29T17:14:08.769010Z","shell.execute_reply.started":"2026-04-29T17:14:08.758400Z","shell.execute_reply":"2026-04-29T17:14:08.768270Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------------------------------------------------------\n# NaN-Safe Math Utilities\n# ---------------------------------------------------------------------------\ndef tf_nan_mean(x, axis=0, keepdims=False):\n    s = tf.reduce_sum(tf.where(tf.math.is_nan(x), tf.zeros_like(x), x), axis=axis, keepdims=keepdims)\n    n = tf.reduce_sum(tf.where(tf.math.is_nan(x), tf.zeros_like(x), tf.ones_like(x)), axis=axis, keepdims=keepdims)\n    return s / n\n\ndef tf_nan_std(x, center=None, axis=0, keepdims=False):\n    if center is None:\n        center = tf_nan_mean(x, axis=axis, keepdims=True)\n    d = x - center\n    return tf.math.sqrt(tf_nan_mean(d * d, axis=axis, keepdims=keepdims))\n\n# ---------------------------------------------------------------------------\n# Custom Preprocessing Layer (must match training exactly)\n# ---------------------------------------------------------------------------\nclass Preprocess(tf.keras.layers.Layer):\n    def __init__(self, max_len=MAX_LEN, point_landmarks=POINT_LANDMARKS, **kwargs):\n        super().__init__(**kwargs)\n        self.max_len = max_len\n        self.point_landmarks = point_landmarks\n\n    def call(self, inputs):\n        x = inputs[None, ...] if len(inputs.shape) == 3 else inputs\n        mean = tf_nan_mean(tf.gather(x, [17], axis=2), axis=[1, 2], keepdims=True)\n        mean = tf.where(tf.math.is_nan(mean), tf.constant(0.5, x.dtype), mean)\n        x    = tf.gather(x, self.point_landmarks, axis=2)\n        std  = tf_nan_std(x, center=mean, axis=[1, 2], keepdims=True)\n        x    = (x - mean) / std\n        if self.max_len is not None:\n            x = x[:, :self.max_len]\n        length = tf.shape(x)[1]\n        x  = x[..., :2]\n        dx = tf.cond(tf.shape(x)[1] > 1,\n                     lambda: tf.pad(x[:, 1:] - x[:, :-1], [[0,0],[0,1],[0,0],[0,0]]),\n                     lambda: tf.zeros_like(x))\n        dx2 = tf.cond(tf.shape(x)[1] > 2,\n                      lambda: tf.pad(x[:, 2:] - x[:, :-2], [[0,0],[0,2],[0,0],[0,0]]),\n                      lambda: tf.zeros_like(x))\n        x = tf.concat([\n            tf.reshape(x,   (-1, length, 2 * len(self.point_landmarks))),\n            tf.reshape(dx,  (-1, length, 2 * len(self.point_landmarks))),\n            tf.reshape(dx2, (-1, length, 2 * len(self.point_landmarks))),\n        ], axis=-1)\n        x = tf.where(tf.math.is_nan(x), tf.constant(0., x.dtype), x)\n        return x\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\"max_len\": self.max_len, \"point_landmarks\": self.point_landmarks})\n        return config\n\npreprocess_layer = Preprocess(max_len=MAX_LEN, point_landmarks=POINT_LANDMARKS)\nprint(\"Preprocess layer initialized.\")\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:08.770075Z","iopub.execute_input":"2026-04-29T17:14:08.770411Z","iopub.status.idle":"2026-04-29T17:14:08.804266Z","shell.execute_reply.started":"2026-04-29T17:14:08.770389Z","shell.execute_reply":"2026-04-29T17:14:08.803445Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------------------------------------------------------\n# Custom Keras Layers (required for model deserialization)\n# ---------------------------------------------------------------------------\nclass SmartMasking(tf.keras.layers.Layer):\n    def __init__(self, pad_value=-100.0, **kwargs):\n        super().__init__(**kwargs)\n        self.pad_value = pad_value\n        self.supports_masking = True\n\n    def compute_mask(self, inputs, mask=None):\n        is_pad = tf.equal(inputs, self.pad_value)\n        is_nan = tf.math.is_nan(inputs)\n        return tf.logical_not(tf.reduce_all(tf.logical_or(is_pad, is_nan), axis=-1))\n\n    def call(self, inputs):\n        return tf.where(tf.math.is_nan(inputs), tf.zeros_like(inputs), inputs)\n\n    def get_config(self):\n        config = super().get_config()\n        config[\"pad_value\"] = self.pad_value\n        return config\n\n\nclass ECA(tf.keras.layers.Layer):\n    def __init__(self, kernel_size=5, **kwargs):\n        super().__init__(**kwargs)\n        self.supports_masking = True\n        self.kernel_size = kernel_size\n        self.conv = tf.keras.layers.Conv1D(1, kernel_size=kernel_size, padding=\"same\", use_bias=False)\n\n    def call(self, inputs, mask=None):\n        nn = tf.keras.layers.GlobalAveragePooling1D()(inputs, mask=mask)\n        nn = tf.expand_dims(nn, -1)\n        nn = self.conv(nn)\n        nn = tf.squeeze(nn, -1)\n        nn = tf.nn.sigmoid(nn)\n        return inputs * nn[:, None, :]\n\n    def get_config(self):\n        config = super().get_config()\n        config[\"kernel_size\"] = self.kernel_size\n        return config\n\n\nclass LateDropout(tf.keras.layers.Layer):\n    def __init__(self, rate, noise_shape=None, start_step=0, **kwargs):\n        super().__init__(**kwargs)\n        self.supports_masking = True\n        self.rate = rate\n        self.start_step = start_step\n        self.dropout = tf.keras.layers.Dropout(rate, noise_shape=noise_shape)\n\n    def build(self, input_shape):\n        super().build(input_shape)\n        self._train_counter = tf.Variable(0, dtype=\"int64\", trainable=False)\n\n    def call(self, inputs, training=False):\n        x = tf.cond(\n            self._train_counter < self.start_step,\n            lambda: inputs,\n            lambda: self.dropout(inputs, training=training)\n        )\n        if training:\n            self._train_counter.assign_add(1)\n        return x\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\"rate\": self.rate, \"start_step\": self.start_step})\n        return config\n\n\nclass CausalDWConv1D(tf.keras.layers.Layer):\n    def __init__(self, kernel_size=17, dilation_rate=1, use_bias=False,\n                 depthwise_initializer=\"glorot_uniform\", name=\"\", **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.kernel_size = kernel_size\n        self.dilation_rate = dilation_rate\n        self.causal_pad = tf.keras.layers.ZeroPadding1D(\n            (dilation_rate * (kernel_size - 1), 0), name=name + \"_pad\")\n        self.dw_conv = tf.keras.layers.DepthwiseConv1D(\n            kernel_size, strides=1, dilation_rate=dilation_rate, padding=\"valid\",\n            use_bias=use_bias, depthwise_initializer=depthwise_initializer,\n            name=name + \"_dwconv\")\n        self.supports_masking = True\n\n    def call(self, inputs):\n        return self.dw_conv(self.causal_pad(inputs))\n\n    def compute_mask(self, inputs, mask=None):\n        return mask\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\"kernel_size\": self.kernel_size, \"dilation_rate\": self.dilation_rate})\n        return config\n\n\nclass MultiHeadSelfAttention(tf.keras.layers.Layer):\n    def __init__(self, dim=256, num_heads=4, dropout=0, **kwargs):\n        super().__init__(**kwargs)\n        self.dim = dim\n        self.scale = dim ** -0.5\n        self.num_heads = num_heads\n        self.qkv   = tf.keras.layers.Dense(3 * dim, use_bias=False)\n        self.drop1 = tf.keras.layers.Dropout(dropout)\n        self.proj  = tf.keras.layers.Dense(dim, use_bias=False)\n        self.supports_masking = True\n\n    def call(self, inputs, mask=None):\n        B, S = tf.shape(inputs)[0], tf.shape(inputs)[1]\n        head_dim = self.dim // self.num_heads\n        qkv = self.qkv(inputs)\n        qkv = tf.reshape(qkv, (B, S, self.num_heads, 3 * head_dim))\n        qkv = tf.transpose(qkv, (0, 2, 1, 3))\n        q, k, v = tf.split(qkv, 3, axis=-1)\n        attn = tf.matmul(q, k, transpose_b=True) * self.scale\n        if mask is not None:\n            attn += (1.0 - tf.cast(mask[:, None, None, :], attn.dtype)) * -1e9\n        attn = tf.nn.softmax(attn, axis=-1)\n        attn = self.drop1(attn)\n        x = attn @ v\n        x = tf.transpose(x, (0, 2, 1, 3))\n        x = tf.reshape(x, (B, S, self.dim))\n        return self.proj(x)\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\"dim\": self.dim, \"num_heads\": self.num_heads})\n        return config\n\n\nCUSTOM_OBJECTS = {\n    \"Preprocess\": Preprocess,\n    \"SmartMasking\": SmartMasking,\n    \"ECA\": ECA,\n    \"LateDropout\": LateDropout,\n    \"CausalDWConv1D\": CausalDWConv1D,\n    \"MultiHeadSelfAttention\": MultiHeadSelfAttention,\n}\nprint(\"All custom layers registered.\")\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:08.805223Z","iopub.execute_input":"2026-04-29T17:14:08.805483Z","iopub.status.idle":"2026-04-29T17:14:08.827597Z","shell.execute_reply.started":"2026-04-29T17:14:08.805461Z","shell.execute_reply":"2026-04-29T17:14:08.826793Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Section 3: Data Loading and Test Split\n\nThe dataset paths follow the original Kaggle competition structure.\nAdjust `DATA_DIR` if running locally. The same stratified 80/10/10 split\nused during training is reconstructed deterministically using `SEED=42`.\n","metadata":{}},{"cell_type":"code","source":"# ---------------------------------------------------------------------------\n# Paths — adjust DATA_DIR for your environment\n# ---------------------------------------------------------------------------\nDATA_DIR     = Path(\"/kaggle/input/competitions/asl-signs\")\nTRAIN_CSV    = DATA_DIR / \"train.csv\"\nLANDMARK_DIR = DATA_DIR / \"train_landmark_files\"\n\nOUTPUT_DIR = Path(\"./eval_outputs\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n(OUTPUT_DIR / \"plots\").mkdir(exist_ok=True)\n(OUTPUT_DIR / \"reports\").mkdir(exist_ok=True)\n(OUTPUT_DIR / \"predictions\").mkdir(exist_ok=True)\n\nprint(\"Output directory:\", OUTPUT_DIR.resolve())\nprint(\"Train CSV exists:\", TRAIN_CSV.exists())\nprint(\"Landmark dir exists:\", LANDMARK_DIR.exists())\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:08.828685Z","iopub.execute_input":"2026-04-29T17:14:08.828946Z","iopub.status.idle":"2026-04-29T17:14:08.848690Z","shell.execute_reply.started":"2026-04-29T17:14:08.828924Z","shell.execute_reply":"2026-04-29T17:14:08.847863Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(TRAIN_CSV)\n\nif \"label\" not in train_df.columns:\n    sign_list    = sorted(train_df[\"sign\"].unique())\n    sign_to_label = {s: i for i, s in enumerate(sign_list)}\n    label_to_sign = {i: s for s, i in sign_to_label.items()}\n    train_df[\"label\"] = train_df[\"sign\"].map(sign_to_label)\nelse:\n    sign_list     = sorted(train_df[\"sign\"].unique())\n    sign_to_label = {s: i for i, s in enumerate(sign_list)}\n    label_to_sign = {i: s for s, i in sign_to_label.items()}\n\nprint(f\"Total sequences : {len(train_df):,}\")\nprint(f\"Unique signs    : {train_df['sign'].nunique()}\")\nprint(f\"Participants    : {train_df['participant_id'].nunique()}\")\ntrain_df.head()\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:08.849718Z","iopub.execute_input":"2026-04-29T17:14:08.850634Z","iopub.status.idle":"2026-04-29T17:14:09.051411Z","shell.execute_reply.started":"2026-04-29T17:14:08.850609Z","shell.execute_reply":"2026-04-29T17:14:09.050770Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Reconstruct the identical stratified split used during training\n_, temp_df = train_test_split(train_df, test_size=0.20, random_state=SEED, stratify=train_df[\"label\"])\nval_df, test_df = train_test_split(temp_df, test_size=0.50, random_state=SEED, stratify=temp_df[\"label\"])\n\nprint(f\"Test set size   : {len(test_df):,} sequences\")\nprint(f\"Class coverage  : {test_df['label'].nunique()} / {NUM_CLASSES} classes\")\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:09.052338Z","iopub.execute_input":"2026-04-29T17:14:09.052604Z","iopub.status.idle":"2026-04-29T17:14:09.119573Z","shell.execute_reply.started":"2026-04-29T17:14:09.052583Z","shell.execute_reply":"2026-04-29T17:14:09.118866Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------------------------------------------------------\n# Parquet Reader and tf.data Pipeline (no augmentation — evaluation mode)\n# ---------------------------------------------------------------------------\ndef load_parquet_video(file_path):\n    try:\n        df = pd.read_parquet(file_path, columns=[\"x\", \"y\", \"z\"], engine=\"pyarrow\")\n        coords = df.values.astype(np.float32)\n        frames = len(coords) // ROWS_PER_FRAME\n        return coords.reshape(frames, ROWS_PER_FRAME, 3)\n    except Exception:\n        return np.zeros((0, ROWS_PER_FRAME, 3), dtype=np.float32)\n\ndef filter_nans_tf(x):\n    mask = tf.logical_not(tf.reduce_all(tf.math.is_nan(\n        tf.gather(x, POINT_LANDMARKS, axis=1)), axis=[-2, -1]))\n    return tf.boolean_mask(x, mask, axis=0)\n\ndef process_data_eval(coord, label):\n    coord     = filter_nans_tf(coord)\n    coord     = tf.ensure_shape(coord, (None, ROWS_PER_FRAME, 3))\n    processed = preprocess_layer(coord)\n    processed = tf.squeeze(processed, axis=0)\n    processed = tf.where(tf.math.is_nan(processed), tf.zeros_like(processed), processed)\n    processed = tf.where(tf.math.is_inf(processed), tf.zeros_like(processed), processed)\n    processed = tf.cast(processed, tf.float32)\n    label_oh  = tf.one_hot(label, NUM_CLASSES)\n    return processed, label_oh\n\ndef build_eval_dataset(df, batch_size=BATCH_SIZE):\n    paths  = df[\"path\"].astype(str).values\n    labels = df[\"label\"].values.astype(np.int32)\n    ds     = tf.data.Dataset.from_tensor_slices((paths, labels))\n\n    def py_load(path_val):\n        p = os.path.join(str(DATA_DIR), path_val.decode(\"utf-8\").replace(\"\\\\\", \"/\"))\n        return load_parquet_video(os.path.normpath(p))\n\n    def load_tf(path_t, label_t):\n        coords = tf.numpy_function(py_load, [path_t], tf.float32)\n        coords.set_shape((None, ROWS_PER_FRAME, 3))\n        return coords, label_t\n\n    ds = ds.map(load_tf, num_parallel_calls=tf.data.AUTOTUNE)\n    ds = ds.filter(lambda x, y: tf.shape(x)[0] > 0)\n    ds = ds.map(process_data_eval, num_parallel_calls=tf.data.AUTOTUNE)\n    ds = ds.padded_batch(\n        batch_size,\n        padding_values=(tf.cast(PAD, tf.float32), tf.cast(0.0, tf.float32)),\n        padded_shapes=([MAX_LEN, CHANNELS], [NUM_CLASSES]),\n        drop_remainder=False\n    )\n    ds = ds.prefetch(tf.data.AUTOTUNE)\n    return ds\n\ntest_dataset = build_eval_dataset(test_df)\nprint(\"Test dataset pipeline ready.\")\n","metadata":{"execution":{"iopub.status.busy":"2026-04-29T17:14:09.120777Z","iopub.execute_input":"2026-04-29T17:14:09.121524Z","iopub.status.idle":"2026-04-29T17:14:09.843886Z","shell.execute_reply.started":"2026-04-29T17:14:09.121498Z","shell.execute_reply":"2026-04-29T17:14:09.843270Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import layers, models\n\n\ndef get_model(max_len, channels, num_classes, dim=64, pad_value=-100.0):\n\n    class MultiHeadSelfAttention(tf.keras.layers.Layer):\n        def __init__(self, dim=256, num_heads=4, dropout=0.0):\n            super().__init__()\n            self.dim = dim\n            self.num_heads = num_heads\n            self.head_dim = dim // num_heads\n            self.scale = self.head_dim ** -0.5\n            self.qkv = layers.Dense(3 * dim, use_bias=False)\n            self.drop1 = layers.Dropout(dropout)\n            self.proj = layers.Dense(dim, use_bias=False)\n            self.supports_masking = True\n\n        def call(self, inputs, mask=None, training=None):\n            B = tf.shape(inputs)[0]\n            T = tf.shape(inputs)[1]\n\n            qkv = self.qkv(inputs)\n            qkv = tf.reshape(qkv, (B, T, self.num_heads, 3 * self.head_dim))\n            qkv = tf.transpose(qkv, perm=(0, 2, 1, 3))\n            q, k, v = tf.split(qkv, 3, axis=-1)\n\n            attn = tf.matmul(q, k, transpose_b=True) * self.scale\n\n            if mask is not None:\n                mask_expanded = tf.cast(mask, tf.float32)[:, None, None, :]\n                attn = attn + (1.0 - mask_expanded) * (-1e9)\n\n            attn = tf.nn.softmax(attn, axis=-1)\n            attn = self.drop1(attn, training=training)\n\n            x = tf.matmul(attn, v)\n            x = tf.transpose(x, perm=(0, 2, 1, 3))\n            x = tf.reshape(x, (B, T, self.dim))\n            return self.proj(x)\n\n    def TransformerBlock(dim=64, num_heads=4, expand=2, attn_dropout=0.1, drop_rate=0.1):\n        def apply(inputs):\n            x = layers.BatchNormalization(momentum=0.95)(inputs)\n            x = MultiHeadSelfAttention(dim=dim, num_heads=num_heads, dropout=attn_dropout)(x)\n            x = layers.Dropout(drop_rate, noise_shape=(None, 1, 1))(x)\n            x = layers.Add()([inputs, x])\n            attn_out = x\n\n            x = layers.BatchNormalization(momentum=0.95)(x)\n            x = layers.Dense(dim * expand, activation=\"swish\", use_bias=False)(x)\n            x = layers.Dense(dim, use_bias=False)(x)\n            x = layers.Dropout(drop_rate, noise_shape=(None, 1, 1))(x)\n            x = layers.Add()([attn_out, x])\n            return x\n        return apply\n\n    def Conv1DBlock(dim, ksize, drop_rate=0.1):\n        def apply(x):\n            shortcut = x\n            x = layers.BatchNormalization(momentum=0.95)(x)\n            x = layers.Conv1D(dim, ksize, padding=\"same\", activation=\"swish\", use_bias=False)(x)\n            x = layers.Dropout(drop_rate, noise_shape=(None, 1, 1))(x)\n            x = layers.Add()([shortcut, x])\n            return x\n        return apply\n\n    inp = layers.Input(shape=(max_len, channels), name=\"input_features\")\n    x = layers.Masking(mask_value=pad_value)(inp)\n\n    x = layers.Dense(dim, use_bias=False, name=\"stem_dense\")(x)\n    x = layers.BatchNormalization(momentum=0.95, name=\"stem_bn\")(x)\n\n    ksize = 17\n\n    x = Conv1DBlock(dim, ksize)(x)\n    x = Conv1DBlock(dim, ksize)(x)\n    x = Conv1DBlock(dim, ksize)(x)\n\n    x = TransformerBlock(dim, num_heads=4, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)  # head_dim = 64//4 = 16 ✓\n    x = TransformerBlock(dim, num_heads=4, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)\n\n    x = Conv1DBlock(dim, ksize)(x)\n    x = Conv1DBlock(dim, ksize)(x)\n    x = Conv1DBlock(dim, ksize)(x)\n\n    x = TransformerBlock(dim, num_heads=4, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)  # CHANGED: 8→4, head_dim=16 ✓\n    x = TransformerBlock(dim, num_heads=4, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)\n\n    x = layers.Dense(dim * 2, activation=\"swish\", name=\"top_dense\")(x)\n    x = layers.GlobalAveragePooling1D()(x)\n    x = layers.Dropout(0.4, name=\"top_dropout\")(x)\n    x = layers.Dense(dim, activation=\"swish\", name=\"pre_classifier\")(x)\n    outputs = layers.Dense(num_classes, name=\"classifier\")(x)\n\n    model = models.Model(inputs=inp, outputs=outputs, name=\"transformer_model\")\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:14:09.844934Z","iopub.execute_input":"2026-04-29T17:14:09.845310Z","iopub.status.idle":"2026-04-29T17:14:09.860907Z","shell.execute_reply.started":"2026-04-29T17:14:09.845287Z","shell.execute_reply":"2026-04-29T17:14:09.860367Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport sys\nimport importlib.util\nimport tensorflow as tf\nfrom tensorflow.keras import layers, Model\n\nBASE_DIR = \"/kaggle/input/datasets/swiri212/ensemble/ensemble_projec\"\n\ndef import_script(script_name, module_name):\n    script_path = os.path.join(BASE_DIR, script_name)\n    spec = importlib.util.spec_from_file_location(module_name, script_path)\n    module = importlib.util.module_from_spec(spec)\n\n    # inject missing globals into the script namespace\n    module.tf = tf\n    module.layers = layers\n    module.Model = Model\n\n    sys.modules[module_name] = module\n    spec.loader.exec_module(module)\n    return module\n\nbigru_mod = import_script(\"predict_bigru.py\", \"bigru_mod\")\nstgcn_mod = import_script(\"predict_stgcn.py\", \"stgcn_mod\")\ntrans_mod = import_script(\"predict_transformer.py\", \"trans_mod\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:14:09.861760Z","iopub.execute_input":"2026-04-29T17:14:09.862228Z","iopub.status.idle":"2026-04-29T17:14:09.900300Z","shell.execute_reply.started":"2026-04-29T17:14:09.862176Z","shell.execute_reply":"2026-04-29T17:14:09.899525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def setup_logger(model_name, log_dir, level=\"INFO\"):\n    os.makedirs(log_dir, exist_ok=True)\n    timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n    log_file  = os.path.join(log_dir, f\"{model_name}_{timestamp}.log\")\n\n    logger = logging.getLogger(model_name)\n    logger.setLevel(getattr(logging, level.upper(), logging.INFO))\n    logger.handlers.clear()\n\n    fmt = logging.Formatter(\n        \"[%(asctime)s] [%(name)s] [%(levelname)s]  %(message)s\",\n        datefmt=\"%Y-%m-%d %H:%M:%S\",\n    )\n    fh = logging.FileHandler(log_file, encoding=\"utf-8\")\n    fh.setFormatter(fmt)\n    logger.addHandler(fh)\n\n    sh = logging.StreamHandler(sys.stdout)\n    sh.setFormatter(fmt)\n    logger.addHandler(sh)\n\n    logger.propagate = False\n    logger.info(\"Logger initialised — writing to: %s\", log_file)\n    return logger\n\nlogger = logging.getLogger(\"pipeline\")\nlogging.basicConfig(level=logging.INFO)\nlogger.info(\"Temporary root logger active until output directories are ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:14:09.901250Z","iopub.execute_input":"2026-04-29T17:14:09.901527Z","iopub.status.idle":"2026-04-29T17:14:09.908568Z","shell.execute_reply.started":"2026-04-29T17:14:09.901497Z","shell.execute_reply":"2026-04-29T17:14:09.907929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n\nMODELS_CFG = [\n    {\n        \"name\": \"BiGRU\",\n        \"script\": os.path.join(BASE_DIR, \"predict_bigru.py\"),\n        \"path\": os.path.join(BASE_DIR, \"final_model_BiGRU.keras\"),\n        \"weight\": 0.30,\n    },\n    {\n        \"name\": \"ST_GCN\",\n        \"script\": os.path.join(BASE_DIR, \"predict_stgcn.py\"),\n        \"path\": os.path.join(BASE_DIR, \"st_gcn.weights.h5\"),\n        \"weight\": 0.15,\n    },\n    {\n        \"name\": \"Transformer\",\n        \"script\": os.path.join(BASE_DIR, \"predict_transformer.py\"),\n        \"path\": os.path.join(BASE_DIR, \"Transformer_Model_ASL_best (1).h5\"),\n        \"weight\": 0.55,\n    },\n]\n\nassert os.path.exists(BASE_DIR), f\"BASE_DIR not found: {BASE_DIR}\"\n\nfor m in MODELS_CFG:\n    assert os.path.exists(m[\"script\"]), f\"Script not found: {m['script']}\"\n    assert os.path.exists(m[\"path\"]), f\"Weights not found: {m['path']}\"\n\nassert abs(sum(m[\"weight\"] for m in MODELS_CFG) - 1.0) < 1e-6, \\\n    \"Weights must sum to 1.0\"\n\nENSEMBLE_BASE    = \"/kaggle/working/Ensemble\"\nENSEMBLE_PLOTS   = os.path.join(ENSEMBLE_BASE, \"plots\")\nENSEMBLE_METRICS = os.path.join(ENSEMBLE_BASE, \"metrics\")\nENSEMBLE_PREDS   = os.path.join(ENSEMBLE_BASE, \"predictions\")\nENSEMBLE_REPORTS = os.path.join(ENSEMBLE_BASE, \"reports\")\n\n\nfor d in [ENSEMBLE_BASE, ENSEMBLE_PLOTS, ENSEMBLE_METRICS, ENSEMBLE_PREDS, ENSEMBLE_REPORTS]:\n    os.makedirs(d, exist_ok=True)\n\nlogger.info(\"=\" * 60)\nlogger.info(\"  ENSEMBLE EVALUATION — %d models\", len(MODELS_CFG))\nfor m in MODELS_CFG:\n    logger.info(\"  %-20s  weight=%.2f\", m[\"name\"], m[\"weight\"])\nlogger.info(\"  Num classes : %d\", NUM_CLASSES)\nlogger.info(\"=\" * 60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:14:09.909354Z","iopub.execute_input":"2026-04-29T17:14:09.909629Z","iopub.status.idle":"2026-04-29T17:14:09.938763Z","shell.execute_reply.started":"2026-04-29T17:14:09.909597Z","shell.execute_reply":"2026-04-29T17:14:09.938047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Model Loader — Final\n# BiGRU: full .keras model\n# ST-GCN: weights-only .h5\n# Transformer: special case, rebuild in float32 + load weights\n# =============================================================================\n\nimport os\nimport sys\nimport gc\nimport importlib.util\nimport tensorflow as tf\nimport keras\nfrom tensorflow.keras import layers, Model\n\ntf.keras.mixed_precision.set_global_policy(\"float32\")\n\nBASE_DIR = \"/kaggle/input/datasets/swiri212/ensemble/ensemble_projec\"\n\n\ndef import_script(script_name, module_name, fresh=False):\n    script_path = os.path.join(BASE_DIR, script_name)\n\n    if not os.path.exists(script_path):\n        raise FileNotFoundError(f\"Script not found: {script_path}\")\n\n    if fresh and module_name in sys.modules:\n        del sys.modules[module_name]\n\n    spec = importlib.util.spec_from_file_location(module_name, script_path)\n    module = importlib.util.module_from_spec(spec)\n\n    module.tf = tf\n    module.layers = layers\n    module.Model = Model\n    module.keras = keras\n\n    sys.modules[module_name] = module\n    spec.loader.exec_module(module)\n\n    return module\n\n\nbigru_mod = import_script(\"predict_bigru.py\", \"bigru_mod\")\nstgcn_mod = import_script(\"predict_stgcn.py\", \"stgcn_mod\")\ntrans_mod = import_script(\"predict_transformer.py\", \"trans_mod\", fresh=True)\n\n\nCUSTOM_OBJECTS = {\n    \"MaskableConv1D\": bigru_mod.MaskableConv1D,\n    \"Squeeze\": bigru_mod.Squeeze,\n    \"ExpandDims\": bigru_mod.ExpandDims,\n    \"ReduceSum\": bigru_mod.ReduceSum,\n    \"MaskableGlobalMaxPool1D\": bigru_mod.MaskableGlobalMaxPool1D,\n\n    \"MaskGenerator\": stgcn_mod.MaskGenerator,\n    \"MaskedGAP\": stgcn_mod.MaskedGAP,\n    \"MaskedGMP\": stgcn_mod.MaskedGMP,\n    \"NodeWiseDense\": stgcn_mod.NodeWiseDense,\n    \"STGCNBlock\": stgcn_mod.STGCNBlock,\n\n    \"SmartMasking\": trans_mod.SmartMasking,\n    \"ECA\": trans_mod.ECA,\n    \"LateDropout\": trans_mod.LateDropout,\n    \"CausalDWConv1D\": trans_mod.CausalDWConv1D,\n    \"MultiHeadSelfAttention\": trans_mod.MultiHeadSelfAttention,\n\n    \"Bidirectional\": tf.keras.layers.Bidirectional,\n    \"Masking\": tf.keras.layers.Masking,\n    \"Dense\": tf.keras.layers.Dense,\n    \"Dropout\": tf.keras.layers.Dropout,\n    \"BatchNormalization\": tf.keras.layers.BatchNormalization,\n}\n\n\ndef build_stgcn_model():\n    if hasattr(stgcn_mod, \"build_model\"):\n        return stgcn_mod.build_model(\n            max_len=384,\n            channels=708,\n            num_classes=NUM_CLASSES\n        )\n\n    return stgcn_mod.get_model(\n        max_len=384,\n        channels=708,\n        num_classes=NUM_CLASSES\n    )\n\n\ndef build_transformer_model():\n    gc.collect()\n    tf.keras.mixed_precision.set_global_policy(\"float32\")\n\n    if hasattr(trans_mod, \"build_model\"):\n        return trans_mod.build_model(\n            max_len=384,\n            channels=708,\n            num_classes=NUM_CLASSES\n        )\n\n    raise AttributeError(\"predict_transformer.py must contain build_model().\")\n\n\ndef load_model_flexible(path, name=\"model\"):\n    path = str(path)\n\n    if not os.path.exists(path):\n        raise FileNotFoundError(f\"Model file not found: {path}\")\n\n    logger.info(\"[%s] Loading: %s\", name, path)\n\n    if name == \"ST_GCN\":\n        logger.info(\"[%s] Weights-only detected — building architecture first\", name)\n        model = build_stgcn_model()\n        model.load_weights(path)\n\n    elif name == \"Transformer\":\n        logger.info(\"[%s] Special case — rebuild float32 architecture + load weights\", name)\n        model = build_transformer_model()\n        model.load_weights(path)\n\n    elif path.endswith(\".keras\"):\n        model = keras.models.load_model(\n            path,\n            custom_objects=CUSTOM_OBJECTS,\n            compile=False,\n            safe_mode=False\n        )\n\n    elif path.endswith((\".h5\", \".hdf5\")):\n        with tf.keras.utils.custom_object_scope(CUSTOM_OBJECTS):\n            model = tf.keras.models.load_model(\n                path,\n                custom_objects=CUSTOM_OBJECTS,\n                compile=False\n            )\n\n    else:\n        model = keras.models.load_model(\n            path,\n            custom_objects=CUSTOM_OBJECTS,\n            compile=False,\n            safe_mode=False\n        )\n\n    logger.info(\n        \"[%s] OK — params: %s | input: %s | output: %s\",\n        name,\n        f\"{model.count_params():,}\",\n        model.input_shape,\n        model.output_shape\n    )\n\n    return model\n\n\nloaded_models = {}\n\nfor cfg in MODELS_CFG:\n    loaded_models[cfg[\"name\"]] = load_model_flexible(\n        cfg[\"path\"],\n        cfg[\"name\"]\n    )\n\nlogger.info(\"All %d models loaded.\", len(loaded_models))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:14:09.941848Z","iopub.execute_input":"2026-04-29T17:14:09.942135Z","iopub.status.idle":"2026-04-29T17:14:14.384041Z","shell.execute_reply.started":"2026-04-29T17:14:09.942114Z","shell.execute_reply":"2026-04-29T17:14:14.383400Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Load BiLSTM + Run Inference on All Models\n# =============================================================================\n\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\n\n# ---------------------------------------------------------------------------\n# Safety checks\n# ---------------------------------------------------------------------------\n\nrequired_vars = [\n    \"MAX_LEN\",\n    \"CHANNELS\",\n    \"NUM_CLASSES\",\n    \"test_dataset\",\n    \"test_df\",\n    \"loaded_models\",\n]\n\nmissing = [v for v in required_vars if v not in globals()]\nif missing:\n    raise RuntimeError(f\"Missing required variables: {missing}\")\n\n# ---------------------------------------------------------------------------\n# BiLSTM model loading\n# ---------------------------------------------------------------------------\n\nbilstm_weights_path = (\n    \"/kaggle/input/models/swiri212/bilstm-baseline/\"\n    \"tensorflow2/default/1/BiLSTM_Baseline_best.h5\"\n)\n\ndef get_bilstm_model(max_len, channels, num_classes, pad_value=-100.0):\n    inputs = layers.Input(\n        shape=(max_len, channels),\n        name=\"input_features\"\n    )\n\n    x = layers.Masking(\n        mask_value=pad_value,\n        name=\"masking_layer\"\n    )(inputs)\n\n    x = layers.Dense(\n        256,\n        activation=\"swish\",\n        name=\"feature_dense\"\n    )(x)\n\n    x = layers.BatchNormalization(\n        name=\"bn_1\"\n    )(x)\n\n    x = layers.Bidirectional(\n        layers.LSTM(\n            256,\n            return_sequences=True\n        ),\n        name=\"bilstm_1\"\n    )(x)\n\n    x = layers.Dropout(\n        0.3,\n        name=\"dropout\"\n    )(x)\n\n    x = layers.Bidirectional(\n        layers.LSTM(\n            128,\n            return_sequences=False\n        ),\n        name=\"bilstm_2\"\n    )(x)\n\n    x = layers.Dropout(\n        0.3,\n        name=\"dropout_1\"\n    )(x)\n\n    x = layers.Dense(\n        128,\n        activation=\"swish\",\n        name=\"dense_head\"\n    )(x)\n\n    x = layers.BatchNormalization(\n        name=\"bn_2\"\n    )(x)\n\n    x = layers.Dropout(\n        0.4,\n        name=\"dropout_2\"\n    )(x)\n\n    outputs = layers.Dense(\n        num_classes,\n        name=\"classifier\"\n    )(x)\n\n    return models.Model(\n        inputs=inputs,\n        outputs=outputs,\n        name=\"BiLSTM_Baseline\"\n    )\n\n\nprint(\"=\" * 80)\nprint(\"Loading BiLSTM model...\")\nprint(\"=\" * 80)\n\nbilstm_model = get_bilstm_model(\n    max_len=MAX_LEN,\n    channels=CHANNELS,\n    num_classes=NUM_CLASSES\n)\n\nbilstm_model.load_weights(bilstm_weights_path)\nloaded_models[\"BiLSTM\"] = bilstm_model\n\nprint(\"BiLSTM loaded successfully.\")\nprint(\"Loaded models:\", list(loaded_models.keys()))\n\n# ---------------------------------------------------------------------------\n# Inference configuration\n# ---------------------------------------------------------------------------\n\nINFERENCE_ORDER = [\n    \"BiLSTM\",\n    \"Transformer\",\n    \"BiGRU\",\n    \"ST_GCN\",\n]\n\nMODEL_INFER_BATCH = {\n    \"BiLSTM\": 64,\n    \"Transformer\": 64,\n    \"BiGRU\": 64,\n    \"ST_GCN\": 8,\n}\n\nNUM_TEST_SAMPLES = len(test_df)\n\n# Check all requested models exist\nmissing_models = [name for name in INFERENCE_ORDER if name not in loaded_models]\nif missing_models:\n    raise RuntimeError(f\"Models missing from loaded_models: {missing_models}\")\n\n# ---------------------------------------------------------------------------\n# Dataset / inference helpers\n# ---------------------------------------------------------------------------\n\ndef make_infer_dataset(dataset, batch_size):\n    return (\n        dataset\n        .unbatch()\n        .batch(batch_size)\n        .prefetch(tf.data.AUTOTUNE)\n    )\n\n\ndef run_inference(model, dataset, steps, num_samples, name=\"model\"):\n    all_probs_local = []\n    all_labels_local = []\n\n    logger.info(\"[%s] Inference — %d steps ...\", name, steps)\n\n    for step, (x_batch, y_batch) in enumerate(dataset.take(steps)):\n        y_np = y_batch.numpy()\n\n        if y_np.ndim == 2:\n            y_np = np.argmax(y_np, axis=1)\n\n        logits = model(x_batch, training=False).numpy()\n\n        logits = np.nan_to_num(\n            logits,\n            nan=0.0,\n            posinf=1e4,\n            neginf=-1e4\n        )\n\n        probs = tf.nn.softmax(logits, axis=1).numpy()\n\n        all_probs_local.append(probs)\n        all_labels_local.append(y_np)\n\n        if (step + 1) % 15 == 0:\n            logger.info(\"[%s] %d / %d steps done\", name, step + 1, steps)\n\n    probs_all = np.concatenate(all_probs_local, axis=0)[:num_samples]\n    labels_all = np.concatenate(all_labels_local, axis=0)[:num_samples]\n\n    row_sums = probs_all.sum(axis=1)\n\n    assert np.allclose(row_sums, 1.0, atol=1e-3), (\n        f\"[{name}] probs don't sum to 1 — \"\n        f\"min={row_sums.min():.4f} max={row_sums.max():.4f}\"\n    )\n\n    logger.info(\n        \"[%s] Done — %d samples, probs shape: %s\",\n        name,\n        len(labels_all),\n        probs_all.shape\n    )\n\n    print(\n        f\"{name:<16} | \"\n        f\"samples={len(labels_all):<6} | \"\n        f\"probs={probs_all.shape} | \"\n        f\"prob_sum_min={row_sums.min():.4f} | \"\n        f\"prob_sum_max={row_sums.max():.4f}\"\n    )\n\n    return probs_all, labels_all\n\n\n# ---------------------------------------------------------------------------\n# Run inference for all models\n# ---------------------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"Running inference on all models\")\nprint(\"=\" * 80)\n\nall_probs = {}\nall_labels = {}\n\nfor name in INFERENCE_ORDER:\n    model = loaded_models[name]\n    batch_size = MODEL_INFER_BATCH[name]\n\n    infer_dataset = make_infer_dataset(\n        test_dataset,\n        batch_size=batch_size\n    )\n\n    infer_steps = int(np.ceil(NUM_TEST_SAMPLES / batch_size))\n\n    logger.info(\n        \"[%s] Using inference batch size: %d | steps: %d\",\n        name,\n        batch_size,\n        infer_steps\n    )\n\n    print(\n        f\"\\n{name} inference | \"\n        f\"batch_size={batch_size} | \"\n        f\"steps={infer_steps}\"\n    )\n\n    probs, labels = run_inference(\n        model=model,\n        dataset=infer_dataset,\n        steps=infer_steps,\n        num_samples=NUM_TEST_SAMPLES,\n        name=name\n    )\n\n    all_probs[name] = probs\n    all_labels[name] = labels\n\n# ---------------------------------------------------------------------------\n# Verify labels are identical across models\n# ---------------------------------------------------------------------------\n\nnames = list(all_labels.keys())\n\nfor name in names[1:]:\n    assert np.array_equal(all_labels[names[0]], all_labels[name]), (\n        f\"Ground-truth mismatch between {names[0]} and {name}\"\n    )\n\ny_true = all_labels[names[0]]\n\nlogger.info(\"Ground-truth verified — %d samples.\", len(y_true))\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"Inference completed successfully.\")\nprint(\"=\" * 80)\nprint(\"Models evaluated:\", names)\nprint(\"Ground-truth samples:\", len(y_true))\nprint(\"y_true shape:\", y_true.shape)\n\nfor name in names:\n    print(\n        f\"{name:<16} probs shape: {all_probs[name].shape} | \"\n        f\"labels shape: {all_labels[name].shape}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:14:14.385082Z","iopub.execute_input":"2026-04-29T17:14:14.385553Z","iopub.status.idle":"2026-04-29T17:33:18.691528Z","shell.execute_reply.started":"2026-04-29T17:14:14.385529Z","shell.execute_reply":"2026-04-29T17:33:18.690704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Save All Models as TensorFlow SavedModel + Test + Archive\n# =============================================================================\n\nimport os\nimport shutil\nimport numpy as np\nimport tensorflow as tf\n\nSAVE_DIR    = \"/kaggle/working/saved_models\"\nARCHIVE_DIR = \"/kaggle/working/model_exports\"\nARCHIVE_NAME = \"all_models_savedmodel_export\"\nMAX_LEN     = 384\nCHANNELS    = 708\nNUM_CLASSES = 250\nBATCH_SIZE  = 2\n\nMODEL_NAMES = {\n    \"BiGRU\":       loaded_models.get(\"BiGRU\"),\n    \"ST_GCN\":      loaded_models.get(\"ST_GCN\"),\n    \"Transformer\": loaded_models.get(\"Transformer\"),\n    \"BiLSTM\":      bilstm_model,\n}\n\nos.makedirs(SAVE_DIR, exist_ok=True)\nos.makedirs(ARCHIVE_DIR, exist_ok=True)\n\nsaved_paths = {}\n\nfor name, model in MODEL_NAMES.items():\n    if model is None:\n        logger.warning(\"[%s] Model not loaded — skipping save.\", name)\n        continue\n\n    save_path = os.path.join(SAVE_DIR, name)\n\n    if os.path.exists(save_path):\n        shutil.rmtree(save_path)\n\n    try:\n        model.export(save_path)\n        saved_paths[name] = save_path\n        logger.info(\"[%s] Saved → %s\", name, save_path)\n\n    except Exception as exc:\n        logger.error(\"[%s] Save failed: %s\", name, exc)\n\nlogger.info(\"─\" * 60)\nlogger.info(\"Saved %d / %d models.\", len(saved_paths), len(MODEL_NAMES))\n\n\n# =============================================================================\n# Test SavedModel inference\n# =============================================================================\n\ndummy_input = np.random.randn(BATCH_SIZE, MAX_LEN, CHANNELS).astype(np.float32)\n\nall_passed = True\n\nfor name, save_path in saved_paths.items():\n    try:\n        reloaded = tf.saved_model.load(save_path)\n        infer = reloaded.signatures[\"serving_default\"]\n\n        input_key = list(infer.structured_input_signature[1].keys())[0]\n        output_key = list(infer.structured_outputs.keys())[0]\n\n        result = infer(**{input_key: tf.constant(dummy_input)})\n        logits = result[output_key].numpy()\n\n        expected_shape = (BATCH_SIZE, NUM_CLASSES)\n        assert logits.shape == expected_shape, (\n            f\"Shape mismatch: got {logits.shape}, expected {expected_shape}\"\n        )\n\n        preds = np.argmax(logits, axis=-1)\n        max_probs = np.max(tf.nn.softmax(logits, axis=-1).numpy(), axis=-1)\n\n        logger.info(\n            \"[%s] ✓ PASS — output shape: %s | preds: %s | max_prob: %.4f\",\n            name,\n            logits.shape,\n            preds,\n            max_probs.mean()\n        )\n\n    except Exception as exc:\n        logger.error(\"[%s] ✗ FAIL — %s\", name, exc)\n        all_passed = False\n\nlogger.info(\"─\" * 60)\n\nif all_passed:\n    logger.info(\"All models PASSED ✓\")\nelse:\n    logger.warning(\"Some models FAILED ✗ — check errors above.\")\n\n\n# =============================================================================\n# Create Single Archive File\n# =============================================================================\n\narchive_base_path = os.path.join(ARCHIVE_DIR, ARCHIVE_NAME)\narchive_path = archive_base_path + \".zip\"\n\nif os.path.exists(archive_path):\n    os.remove(archive_path)\n\nshutil.make_archive(\n    base_name=archive_base_path,\n    format=\"zip\",\n    root_dir=SAVE_DIR\n)\n\nlogger.info(\"Archive created → %s\", archive_path)\n\nprint(\"Saved models:\")\nfor name, path in saved_paths.items():\n    print(f\"{name}: {path}\")\n\nprint(f\"Archive: {archive_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:33:18.693238Z","iopub.execute_input":"2026-04-29T17:33:18.693829Z","iopub.status.idle":"2026-04-29T17:33:36.530188Z","shell.execute_reply.started":"2026-04-29T17:33:18.693802Z","shell.execute_reply":"2026-04-29T17:33:36.529381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------------------------------------------------------\n# Utility layers (mask-safe replacements for Lambda)\n# ---------------------------------------------------------------------------\n\nclass MaskableConv1D(layers.Conv1D):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.supports_masking = True\n\n\nclass Squeeze(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n    def call(self, x):\n        return tf.squeeze(x, axis=self.axis)\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass ExpandDims(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n    def call(self, x):\n        return tf.expand_dims(x, axis=self.axis)\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass ReduceSum(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n    def call(self, x):\n        return tf.reduce_sum(x, axis=self.axis)\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass MaskableGlobalMaxPool1D(layers.GlobalMaxPooling1D):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.supports_masking = True\n\n\nclass AttentionMaskFromInput(layers.Layer):\n    def __init__(self, pad_value=-100.0, **kwargs):\n        super().__init__(trainable=False, **kwargs)\n        self.supports_masking = True\n        self.pad_value = pad_value\n    def call(self, x):\n        bool_mask = tf.reduce_any(tf.not_equal(x, self.pad_value), axis=-1)\n        return tf.expand_dims(bool_mask, axis=1)\n    def get_config(self):\n        return {**super().get_config(), \"pad_value\": self.pad_value}\n\n\n# ---------------------------------------------------------------------------\n# Rotary Position Embedding (RoPE)\n# Su et al., \"RoFormer: Enhanced Transformer with Rotary Position Embedding\", 2021.\n# ---------------------------------------------------------------------------\n\nclass RotaryEmbedding(layers.Layer):\n    def __init__(self, head_dim, max_len=512, base=10000, **kwargs):\n        super().__init__(trainable=False, **kwargs)\n        self.head_dim = head_dim\n        self.max_len  = max_len\n        half          = head_dim // 2\n        theta         = 1.0 / (base ** (tf.cast(tf.range(half), tf.float32) / half))\n        pos           = tf.cast(tf.range(max_len), tf.float32)\n        freqs         = tf.einsum(\"i,j->ij\", pos, theta)\n        emb           = tf.concat([freqs, freqs], axis=-1)\n        # stored as float32 — cast to input dtype inside call()\n        self._cos = tf.cos(emb)[tf.newaxis, tf.newaxis]   # (1, 1, max_len, head_dim)\n        self._sin = tf.sin(emb)[tf.newaxis, tf.newaxis]\n\n    def _rotate_half(self, x):\n        half    = self.head_dim // 2\n        x1, x2 = x[..., :half], x[..., half:]\n        return tf.concat([-x2, x1], axis=-1)\n\n    def call(self, q, k):\n        t      = tf.shape(q)[2]\n        dtype  = q.dtype                                          # match AMP dtype\n        cos    = tf.cast(self._cos[:, :, :t, :], dtype)\n        sin    = tf.cast(self._sin[:, :, :t, :], dtype)\n        q      = q * cos + self._rotate_half(q) * sin\n        k      = k * cos + self._rotate_half(k) * sin\n        return q, k\n\n    def get_config(self):\n        return {**super().get_config(), \"head_dim\": self.head_dim, \"max_len\": self.max_len}\n\n\n# ---------------------------------------------------------------------------\n# RoPE Multi-Head Self-Attention\n# ---------------------------------------------------------------------------\n\nclass RoPEMultiHeadAttention(layers.Layer):\n    def __init__(self, dim, num_heads, dropout=0.0, max_len=512, **kwargs):\n        super().__init__(**kwargs)\n        self.supports_masking = True\n        self.num_heads = num_heads\n        self.head_dim  = dim // num_heads\n        self.scale     = tf.cast(self.head_dim ** -0.5, tf.float32)\n        self.q_proj    = layers.Dense(dim, use_bias=False)\n        self.k_proj    = layers.Dense(dim, use_bias=False)\n        self.v_proj    = layers.Dense(dim, use_bias=False)\n        self.out_proj  = layers.Dense(dim, use_bias=False)\n        self.attn_drop = layers.Dropout(dropout)\n        self.rope      = RotaryEmbedding(self.head_dim, max_len=max_len)\n\n    def _split_heads(self, x, b):\n        x = tf.reshape(x, (b, -1, self.num_heads, self.head_dim))\n        return tf.transpose(x, [0, 2, 1, 3])\n\n    def call(self, x, attention_mask=None, training=False):\n        b      = tf.shape(x)[0]\n        q      = self._split_heads(self.q_proj(x), b)\n        k      = self._split_heads(self.k_proj(x), b)\n        v      = self._split_heads(self.v_proj(x), b)\n        q, k   = self.rope(q, k)\n        # upcast scores to float32 for numerical stability under AMP\n        scale  = tf.cast(self.scale, q.dtype)\n        scores = tf.matmul(q, k, transpose_b=True) * scale       # (B, H, T, T)\n        if attention_mask is not None:\n            scores += tf.cast(~attention_mask[:, :, tf.newaxis, :], scores.dtype) * -1e4\n        weights = tf.cast(tf.nn.softmax(tf.cast(scores, tf.float32), axis=-1), q.dtype)\n        weights = self.attn_drop(weights, training=training)\n        out     = tf.matmul(weights, v)\n        out     = tf.transpose(out, [0, 2, 1, 3])\n        out     = tf.reshape(out, (b, -1, self.num_heads * self.head_dim))\n        return self.out_proj(out)\n\n    def get_config(self):\n        return {**super().get_config(), \"num_heads\": self.num_heads, \"head_dim\": self.head_dim}\n\n\n# ---------------------------------------------------------------------------\n# Conformer Block — FF(1/2) -> RoPE-MHSA -> DepthwiseConv+GLU -> FF(1/2) -> LN\n# Gulati et al., \"Conformer\", Interspeech 2020.\n# ---------------------------------------------------------------------------\n\nclass ConformerBlock(layers.Layer):\n    def __init__(self, dim, num_heads=4, ff_mult=4, kernel=17,\n                 dropout=0.1, max_len=512, **kwargs):\n        super().__init__(**kwargs)\n        self.supports_masking = True\n        self.dim = dim\n\n        self.ff1_ln   = layers.LayerNormalization(epsilon=1e-6)\n        self.ff1_fc1  = layers.Dense(dim * ff_mult, activation=\"swish\", use_bias=False)\n        self.ff1_drop = layers.Dropout(dropout)\n        self.ff1_fc2  = layers.Dense(dim, use_bias=False)\n\n        self.mhsa_ln   = layers.LayerNormalization(epsilon=1e-6)\n        self.mhsa      = RoPEMultiHeadAttention(dim, num_heads, dropout=dropout, max_len=max_len)\n        self.mhsa_drop = layers.Dropout(dropout)\n\n        self.conv_ln  = layers.LayerNormalization(epsilon=1e-6)\n        self.pw_exp   = layers.Dense(dim * 2, use_bias=False)\n        self.dw_conv  = MaskableConv1D(dim, kernel_size=kernel, padding=\"same\", groups=dim, use_bias=False)\n        self.conv_bn  = layers.BatchNormalization(momentum=0.95)\n        self.conv_act = layers.Activation(\"swish\")\n        self.pw_con   = layers.Dense(dim, use_bias=False)\n        self.conv_sp  = layers.SpatialDropout1D(dropout)\n\n        self.ff2_ln   = layers.LayerNormalization(epsilon=1e-6)\n        self.ff2_fc1  = layers.Dense(dim * ff_mult, activation=\"swish\", use_bias=False)\n        self.ff2_drop = layers.Dropout(dropout)\n        self.ff2_fc2  = layers.Dense(dim, use_bias=False)\n\n        self.out_ln = layers.LayerNormalization(epsilon=1e-6)\n\n    def _ff(self, x, ln, fc1, drop, fc2, training):\n        return x + 0.5 * fc2(drop(fc1(ln(x)), training=training))\n\n    def call(self, x, attention_mask=None, training=False):\n        x = self._ff(x, self.ff1_ln, self.ff1_fc1, self.ff1_drop, self.ff1_fc2, training)\n\n        h = self.mhsa(self.mhsa_ln(x), attention_mask=attention_mask, training=training)\n        x = x + self.mhsa_drop(h, training=training)\n\n        h    = self.pw_exp(self.conv_ln(x))\n        half = tf.shape(h)[-1] // 2\n        h    = h[..., :half] * tf.sigmoid(h[..., half:])\n        h    = self.dw_conv(h)\n        h    = self.conv_bn(h, training=training)\n        h    = self.conv_act(h)\n        h    = self.pw_con(h)\n        x    = x + self.conv_sp(h, training=training)\n\n        x = self._ff(x, self.ff2_ln, self.ff2_fc1, self.ff2_drop, self.ff2_fc2, training)\n        return self.out_ln(x)\n\n    def get_config(self):\n        return {**super().get_config(), \"dim\": self.dim}\n\n\n# ---------------------------------------------------------------------------\n# get_model\n# ---------------------------------------------------------------------------\n\ndef get_model(max_len, channels, num_classes, dim=256, num_blocks=5, dropout_rate=0.1):\n    inp = layers.Input(shape=(max_len, channels), name=\"input_features\")\n\n    x         = layers.Masking(mask_value=-100.0, name=\"masking\")(inp)\n    attn_mask = AttentionMaskFromInput(pad_value=-100.0, name=\"attn_mask\")(inp)  # (B, 1, T)\n\n    x = layers.Dense(dim, use_bias=False, name=\"stem_dense\")(x)\n    x = layers.BatchNormalization(momentum=0.95, name=\"stem_bn\")(x)\n    x = layers.Activation(\"swish\", name=\"stem_act\")(x)\n    x = layers.SpatialDropout1D(dropout_rate, name=\"stem_drop\")(x)\n\n    for i in range(num_blocks):\n        x = ConformerBlock(\n            dim=dim, num_heads=4, ff_mult=4, kernel=17,\n            dropout=dropout_rate, max_len=max_len + 16,\n            name=f\"conformer_{i}\",\n        )(x, attention_mask=attn_mask)\n\n    attn_w = layers.Dense(dim // 4, activation=\"tanh\", name=\"attn_proj\")(x)\n    attn_w = layers.Dense(1, name=\"attn_score\")(attn_w)\n    attn_w = Squeeze(axis=-1, name=\"attn_squeeze\")(attn_w)\n    attn_w = layers.Softmax(axis=1, name=\"attn_softmax\")(attn_w)\n    attn_w = ExpandDims(axis=-1, name=\"attn_expand\")(attn_w)\n    x_attn = ReduceSum(axis=1, name=\"attn_pool\")(\n                 layers.Multiply(name=\"attn_apply\")([x, attn_w]))\n\n    gap   = layers.GlobalAveragePooling1D(name=\"gap\")(x)\n    gmp   = MaskableGlobalMaxPool1D(name=\"gmp\")(x)\n\n    fused = layers.Concatenate(name=\"pool_concat\")([x_attn, gap, gmp])\n    fused = layers.Dense(dim, use_bias=False, name=\"pool_proj\")(fused)\n    fused = layers.LayerNormalization(epsilon=1e-6, name=\"pool_ln\")(fused)\n    fused = layers.Activation(\"swish\", name=\"pool_act\")(fused)\n\n    x       = layers.Dense(dim, use_bias=False, name=\"head_dense\")(fused)\n    x       = layers.BatchNormalization(momentum=0.95, name=\"head_bn\")(x)\n    x       = layers.Dropout(dropout_rate + 0.1, name=\"head_drop\")(x)\n    outputs = layers.Dense(num_classes, dtype=\"float32\", name=\"classifier\")(x)\n\n    return Model(inputs=inp, outputs=outputs, name=\"RoPE_Conformer\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:33:36.531508Z","iopub.execute_input":"2026-04-29T17:33:36.531822Z","iopub.status.idle":"2026-04-29T17:33:36.566149Z","shell.execute_reply.started":"2026-04-29T17:33:36.531799Z","shell.execute_reply":"2026-04-29T17:33:36.565206Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# FINAL SAFE PIPELINE — RoPE Conformer (STRICT INFERENCE MODE)\n# =============================================================================\n\nimport os\nimport numpy as np\nimport tensorflow as tf\nimport keras\n\nEXTRA_MODEL_NAME = \"RoPE_Conformer\"\nEXTRA_MODEL_PATH = \"/kaggle/input/models/hassanabdulrazeq/rope-conforme-100/keras/default/1/RoPE_Conformer.keras\"\nEXTRA_MODEL_BATCH_SIZE = 32\n\nassert os.path.exists(EXTRA_MODEL_PATH), f\"Model not found: {EXTRA_MODEL_PATH}\"\n\n# =============================================================================\n# Match training environment\n# =============================================================================\ntf.keras.mixed_precision.set_global_policy(\"mixed_float16\")\n\n\n# =============================================================================\n# Fix AttentionMask\n# =============================================================================\nclass AttentionMaskFromInputFixed(tf.keras.layers.Layer):\n    def __init__(self, pad_value=-100.0, **kwargs):\n        super().__init__(**kwargs)\n        self.trainable = False\n        self.supports_masking = True\n        self.pad_value = pad_value\n\n    def call(self, x):\n        bool_mask = tf.reduce_any(tf.not_equal(x, self.pad_value), axis=-1)\n        return tf.expand_dims(bool_mask, axis=1)\n\n    def get_config(self):\n        return {**super().get_config(), \"pad_value\": self.pad_value}\n\n\n# =============================================================================\n# Custom objects\n# =============================================================================\nEXTRA_CUSTOM_OBJECTS = {\n    \"AttentionMaskFromInput\": AttentionMaskFromInputFixed,\n    \"RotaryEmbedding\": RotaryEmbedding,\n    \"RoPEMultiHeadAttention\": RoPEMultiHeadAttention,\n    \"ConformerBlock\": ConformerBlock,\n    \"MaskableConv1D\": MaskableConv1D,\n    \"Squeeze\": Squeeze,\n    \"ExpandDims\": ExpandDims,\n    \"ReduceSum\": ReduceSum,\n    \"MaskableGlobalMaxPool1D\": MaskableGlobalMaxPool1D,\n}\n\n\n# =============================================================================\n# Load model\n# =============================================================================\nlogger.info(\"[%s] Loading model...\", EXTRA_MODEL_NAME)\n\nextra_model = keras.models.load_model(\n    EXTRA_MODEL_PATH,\n    custom_objects=EXTRA_CUSTOM_OBJECTS,\n    compile=False,\n    safe_mode=False\n)\n\nlogger.info(\"[%s] Loaded successfully\", EXTRA_MODEL_NAME)\n\n\n# =============================================================================\n# 🔥 CRITICAL — FORCE PURE INFERENCE MODE\n# =============================================================================\n\nextra_model.trainable = False\n\nfor layer in extra_model.layers:\n    layer.trainable = False\n\n# Disable any accidental training behavior\nextra_model.compile(run_eagerly=False)\n\n# Wrap model call to always enforce training=False\n@tf.function(reduce_retracing=True)\ndef model_infer(x):\n    return extra_model(x, training=False)\n\n\n# =============================================================================\n# Register model\n# =============================================================================\nloaded_models[EXTRA_MODEL_NAME] = extra_model\nMODEL_INFER_BATCH[EXTRA_MODEL_NAME] = EXTRA_MODEL_BATCH_SIZE\n\nif EXTRA_MODEL_NAME not in INFERENCE_ORDER:\n    INFERENCE_ORDER.append(EXTRA_MODEL_NAME)\n\n\n# =============================================================================\n# Inference (safe)\n# =============================================================================\nNUM_TEST_SAMPLES = len(test_df)\n\ninfer_dataset = make_infer_dataset(\n    test_dataset,\n    EXTRA_MODEL_BATCH_SIZE\n)\n\ninfer_steps = int(np.ceil(NUM_TEST_SAMPLES / EXTRA_MODEL_BATCH_SIZE))\n\nlogger.info(\n    \"[%s] Inference — batch=%d | steps=%d\",\n    EXTRA_MODEL_NAME,\n    EXTRA_MODEL_BATCH_SIZE,\n    infer_steps\n)\n\nall_probs_tmp = []\nall_labels_tmp = []\n\nfor step, (x_batch, y_batch) in enumerate(infer_dataset.take(infer_steps)):\n\n    y_np = y_batch.numpy()\n    if y_np.ndim == 2:\n        y_np = np.argmax(y_np, axis=1)\n\n    logits = model_infer(x_batch).numpy()\n\n    logits = np.nan_to_num(logits, nan=0.0, posinf=1e4, neginf=-1e4)\n    probs = tf.nn.softmax(logits, axis=1).numpy()\n\n    all_probs_tmp.append(probs)\n    all_labels_tmp.append(y_np)\n\nprobs_all = np.concatenate(all_probs_tmp, axis=0)[:NUM_TEST_SAMPLES]\nlabels_all = np.concatenate(all_labels_tmp, axis=0)[:NUM_TEST_SAMPLES]\n\n\n# =============================================================================\n# Add to ensemble\n# =============================================================================\nall_probs[EXTRA_MODEL_NAME] = probs_all\nall_labels[EXTRA_MODEL_NAME] = labels_all\n\nref_name = list(all_labels.keys())[0]\n\nassert np.array_equal(all_labels[ref_name], labels_all), \\\n    f\"Ground-truth mismatch between {ref_name} and {EXTRA_MODEL_NAME}\"\n\ny_true = all_labels[ref_name]\n\nextra_acc = (np.argmax(probs_all, axis=1) == y_true).mean()\n\nlogger.info(\"[%s] Done — acc=%.5f\", EXTRA_MODEL_NAME, extra_acc)\n\nprint(\"=\" * 70)\nprint(\"RoPE Conformer SAFE READY\")\nprint(\"Accuracy:\", extra_acc)\nprint(\"Models now:\", list(all_probs.keys()))\nprint(\"=\" * 70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:33:36.567244Z","iopub.execute_input":"2026-04-29T17:33:36.567771Z","iopub.status.idle":"2026-04-29T17:34:28.100110Z","shell.execute_reply.started":"2026-04-29T17:33:36.567734Z","shell.execute_reply":"2026-04-29T17:34:28.099348Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Full Evaluation Pipeline","metadata":{}},{"cell_type":"code","source":"# =============================================================================\n# Evaluation Setup: Directories, Naming Pattern, and Research Plot Style\n# =============================================================================\n\nimport os\nimport json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nEVAL_ROOT = Path(\"/kaggle/working/evaluation_results\")\nEVAL_ROOT.mkdir(parents=True, exist_ok=True)\n\nMODEL_NAMES = [\n    \"BiLSTM\",\n    \"Transformer\",\n    \"BiGRU\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\nMODEL_DISPLAY_NAMES = {\n    \"BiLSTM\": \"BiLSTM\",\n    \"Transformer\": \"Transformer\",\n    \"BiGRU\": \"BiGRU\",\n    \"ST_GCN\": \"ST-GCN\",\n    \"RoPE_Conformer\": \"RoPE-Conformer\",\n}\n\nMODEL_COLORS = {\n    \"BiLSTM\": \"#4C72B0\",\n    \"Transformer\": \"#55A868\",\n    \"BiGRU\": \"#C44E52\",\n    \"ST_GCN\": \"#8172B2\",\n    \"RoPE_Conformer\": \"#CCB974\",\n}\n\nMETRIC_DISPLAY_NAMES = {\n    \"accuracy\": \"Accuracy\",\n    \"macro_precision\": \"Macro Precision\",\n    \"macro_recall\": \"Macro Recall\",\n    \"macro_f1\": \"Macro F1-score\",\n    \"weighted_precision\": \"Weighted Precision\",\n    \"weighted_recall\": \"Weighted Recall\",\n    \"weighted_f1\": \"Weighted F1-score\",\n    \"top_3_accuracy\": \"Top-3 Accuracy\",\n    \"top_5_accuracy\": \"Top-5 Accuracy\",\n}\n\nDIR_NAMES = {\n    \"reports\": \"reports\",\n    \"metrics\": \"metrics\",\n    \"plots\": \"plots\",\n    \"predictions\": \"predictions\",\n    \"confusion_matrices\": \"confusion_matrices\",\n}\n\nMODEL_DIRS = {}\n\nfor model_name in MODEL_NAMES:\n    model_root = EVAL_ROOT / model_name\n\n    MODEL_DIRS[model_name] = {\n        \"root\": model_root,\n        \"reports\": model_root / DIR_NAMES[\"reports\"],\n        \"metrics\": model_root / DIR_NAMES[\"metrics\"],\n        \"plots\": model_root / DIR_NAMES[\"plots\"],\n        \"predictions\": model_root / DIR_NAMES[\"predictions\"],\n        \"confusion_matrices\": model_root / DIR_NAMES[\"confusion_matrices\"],\n    }\n\n    for path in MODEL_DIRS[model_name].values():\n        path.mkdir(parents=True, exist_ok=True)\n\nCOMPARISON_DIR = EVAL_ROOT / \"comparison\"\nCOMPARISON_DIR.mkdir(parents=True, exist_ok=True)\n\nCOMPARISON_DIRS = {\n    \"root\": COMPARISON_DIR,\n    \"reports\": COMPARISON_DIR / \"reports\",\n    \"metrics\": COMPARISON_DIR / \"metrics\",\n    \"plots\": COMPARISON_DIR / \"plots\",\n}\n\nfor path in COMPARISON_DIRS.values():\n    path.mkdir(parents=True, exist_ok=True)\n\ndef safe_name(name):\n    return name.replace(\" \", \"_\").replace(\"-\", \"_\").lower()\n\ndef model_file_name(model_name, artifact_name, extension):\n    return f\"{safe_name(model_name)}_{safe_name(artifact_name)}.{extension}\"\n\ndef metric_plot_name(model_name, metric_name):\n    return model_file_name(model_name, metric_name, \"png\")\n\ndef comparison_plot_name(metric_name):\n    return f\"comparison_{safe_name(metric_name)}.png\"\n\nplt.rcParams.update({\n    \"figure.figsize\": (8, 5),\n    \"figure.dpi\": 150,\n    \"savefig.dpi\": 300,\n    \"font.size\": 11,\n    \"axes.titlesize\": 13,\n    \"axes.labelsize\": 11,\n    \"xtick.labelsize\": 10,\n    \"ytick.labelsize\": 10,\n    \"legend.fontsize\": 10,\n    \"axes.grid\": True,\n    \"grid.alpha\": 0.25,\n    \"axes.spines.top\": False,\n    \"axes.spines.right\": False,\n})\n\nevaluation_manifest = {\n    \"eval_root\": str(EVAL_ROOT),\n    \"models\": MODEL_NAMES,\n    \"model_display_names\": MODEL_DISPLAY_NAMES,\n    \"model_colors\": MODEL_COLORS,\n    \"metric_display_names\": METRIC_DISPLAY_NAMES,\n    \"n_test_samples\": int(len(y_true)),\n    \"n_classes\": int(NUM_CLASSES),\n}\n\nwith open(EVAL_ROOT / \"evaluation_manifest.json\", \"w\") as f:\n    json.dump(evaluation_manifest, f, indent=4)\n\nprint(\"Evaluation setup completed\")\nprint(\"Evaluation root:\", EVAL_ROOT)\n\nfor model_name in MODEL_NAMES:\n    print(model_name, \"->\", MODEL_DIRS[model_name][\"root\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:28.101113Z","iopub.execute_input":"2026-04-29T17:34:28.101462Z","iopub.status.idle":"2026-04-29T17:34:28.117145Z","shell.execute_reply.started":"2026-04-29T17:34:28.101438Z","shell.execute_reply":"2026-04-29T17:34:28.116326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Metric 1: Accuracy\n# =============================================================================\n\nimport json\nimport numpy as np\nimport pandas as pd\nfrom sklearn.metrics import accuracy_score\n\nACCURACY_RESULTS = {}\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n    y_pred = np.argmax(y_prob, axis=1)\n\n    accuracy = accuracy_score(y_true, y_pred)\n    ACCURACY_RESULTS[model_name] = accuracy\n\n    accuracy_text = (\n        f\"Model: {MODEL_DISPLAY_NAMES[model_name]}\\n\"\n        f\"Metric: Accuracy\\n\"\n        f\"Value: {accuracy:.6f}\\n\"\n        f\"Correct Predictions: {int(np.sum(y_true == y_pred))}\\n\"\n        f\"Total Samples: {int(len(y_true))}\\n\"\n    )\n\n    txt_path = MODEL_DIRS[model_name][\"metrics\"] / model_file_name(\n        model_name,\n        \"accuracy\",\n        \"txt\"\n    )\n\n    json_path = MODEL_DIRS[model_name][\"metrics\"] / model_file_name(\n        model_name,\n        \"accuracy\",\n        \"json\"\n    )\n\n    with open(txt_path, \"w\") as f:\n        f.write(accuracy_text)\n\n    with open(json_path, \"w\") as f:\n        json.dump(\n            {\n                \"model\": model_name,\n                \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n                \"metric\": \"accuracy\",\n                \"value\": float(accuracy),\n                \"correct_predictions\": int(np.sum(y_true == y_pred)),\n                \"total_samples\": int(len(y_true)),\n            },\n            f,\n            indent=4\n        )\n\naccuracy_df = pd.DataFrame([\n    {\n        \"model\": model_name,\n        \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n        \"accuracy\": ACCURACY_RESULTS[model_name],\n    }\n    for model_name in MODEL_NAMES\n])\n\ncomparison_txt_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_accuracy.txt\"\ncomparison_csv_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_accuracy.csv\"\n\nwith open(comparison_txt_path, \"w\") as f:\n    f.write(\"Accuracy Comparison\\n\")\n    f.write(\"=\" * 40 + \"\\n\")\n    for _, row in accuracy_df.iterrows():\n        f.write(f\"{row['display_name']}: {row['accuracy']:.6f}\\n\")\n\naccuracy_df.to_csv(comparison_csv_path, index=False)\n\nprint(accuracy_df)\nprint(\"Accuracy files saved successfully.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:28.118311Z","iopub.execute_input":"2026-04-29T17:34:28.118660Z","iopub.status.idle":"2026-04-29T17:34:28.156203Z","shell.execute_reply.started":"2026-04-29T17:34:28.118636Z","shell.execute_reply":"2026-04-29T17:34:28.155531Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Metric 2: Precision / Recall / F1 — Research Figures\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom sklearn.metrics import precision_score, recall_score, f1_score\n\nPRF_RESULTS = {}\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n    y_pred = np.argmax(y_prob, axis=1)\n\n    PRF_RESULTS[model_name] = {\n        \"macro_precision\": precision_score(y_true, y_pred, average=\"macro\", zero_division=0),\n        \"macro_recall\": recall_score(y_true, y_pred, average=\"macro\", zero_division=0),\n        \"macro_f1\": f1_score(y_true, y_pred, average=\"macro\", zero_division=0),\n        \"weighted_precision\": precision_score(y_true, y_pred, average=\"weighted\", zero_division=0),\n        \"weighted_recall\": recall_score(y_true, y_pred, average=\"weighted\", zero_division=0),\n        \"weighted_f1\": f1_score(y_true, y_pred, average=\"weighted\", zero_division=0),\n    }\n\nprf_df = pd.DataFrame([\n    {\n        \"model\": model_name,\n        \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n        **PRF_RESULTS[model_name],\n    }\n    for model_name in MODEL_NAMES\n])\n\n# Save one clean numeric table only\nprf_csv_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_precision_recall_f1.csv\"\nprf_df.to_csv(prf_csv_path, index=False)\n\ndisplay(prf_df)\n\n# =============================================================================\n# Per-model figures\n# =============================================================================\n\nper_model_metrics = [\n    \"macro_precision\",\n    \"macro_recall\",\n    \"macro_f1\",\n    \"weighted_precision\",\n    \"weighted_recall\",\n    \"weighted_f1\",\n]\n\nfor model_name in MODEL_NAMES:\n    values = [PRF_RESULTS[model_name][m] for m in per_model_metrics]\n    labels = [METRIC_DISPLAY_NAMES[m] for m in per_model_metrics]\n\n    fig, ax = plt.subplots(figsize=(9, 5))\n\n    bars = ax.bar(\n        labels,\n        values,\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\",\n        linewidth=0.8\n    )\n\n    ax.set_title(f\"{MODEL_DISPLAY_NAMES[model_name]} — Precision, Recall, and F1-score\")\n    ax.set_ylabel(\"Score\")\n    ax.set_ylim(0, 1.0)\n    ax.tick_params(axis=\"x\", rotation=30)\n\n    for bar in bars:\n        height = bar.get_height()\n        ax.text(\n            bar.get_x() + bar.get_width() / 2,\n            height + 0.01,\n            f\"{height:.3f}\",\n            ha=\"center\",\n            va=\"bottom\",\n            fontsize=9\n        )\n\n    fig.tight_layout()\n\n    save_path = MODEL_DIRS[model_name][\"plots\"] / model_file_name(\n        model_name,\n        \"precision_recall_f1\",\n        \"png\"\n    )\n\n    fig.savefig(save_path, bbox_inches=\"tight\")\n    plt.show()\n\n# =============================================================================\n# Comparison figure: all models together\n# =============================================================================\n\ncomparison_metrics = [\n    \"macro_precision\",\n    \"macro_recall\",\n    \"macro_f1\",\n    \"weighted_precision\",\n    \"weighted_recall\",\n    \"weighted_f1\",\n]\n\nx = np.arange(len(comparison_metrics))\nwidth = 0.14\n\nfig, ax = plt.subplots(figsize=(12, 6))\n\nfor i, model_name in enumerate(MODEL_NAMES):\n    values = [PRF_RESULTS[model_name][m] for m in comparison_metrics]\n\n    ax.bar(\n        x + (i - len(MODEL_NAMES) / 2) * width + width / 2,\n        values,\n        width,\n        label=MODEL_DISPLAY_NAMES[model_name],\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\",\n        linewidth=0.6\n    )\n\nax.set_title(\"Precision, Recall, and F1-score Comparison Across Models\")\nax.set_ylabel(\"Score\")\nax.set_ylim(0, 1.0)\nax.set_xticks(x)\nax.set_xticklabels(\n    [METRIC_DISPLAY_NAMES[m] for m in comparison_metrics],\n    rotation=25,\n    ha=\"right\"\n)\n\nax.legend(frameon=False, ncol=3)\nfig.tight_layout()\n\ncomparison_save_path = COMPARISON_DIRS[\"plots\"] / comparison_plot_name(\n    \"precision_recall_f1\"\n)\n\nfig.savefig(comparison_save_path, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"Precision / Recall / F1 research figures saved successfully.\")\nprint(\"Table saved to:\", prf_csv_path)\nprint(\"Comparison plot saved to:\", comparison_save_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:28.157115Z","iopub.execute_input":"2026-04-29T17:34:28.157447Z","iopub.status.idle":"2026-04-29T17:34:31.270595Z","shell.execute_reply.started":"2026-04-29T17:34:28.157425Z","shell.execute_reply":"2026-04-29T17:34:31.269799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Metric 3: Top-K Accuracy — Research Figures\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import accuracy_score, top_k_accuracy_score\n\nTOPK_RESULTS = {}\n\nTOPK_DISPLAY_NAMES = {\n    \"top_1_accuracy\": \"Top-1 Accuracy\",\n    \"top_3_accuracy\": \"Top-3 Accuracy\",\n    \"top_5_accuracy\": \"Top-5 Accuracy\",\n}\n\ntopk_metrics = [\n    \"top_1_accuracy\",\n    \"top_3_accuracy\",\n    \"top_5_accuracy\",\n]\n\nclass_labels = np.arange(NUM_CLASSES)\n\n# =============================================================================\n# Compute Top-K Accuracy\n# =============================================================================\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n    y_pred = np.argmax(y_prob, axis=1)\n\n    TOPK_RESULTS[model_name] = {\n        \"top_1_accuracy\": accuracy_score(y_true, y_pred),\n        \"top_3_accuracy\": top_k_accuracy_score(\n            y_true,\n            y_prob,\n            k=3,\n            labels=class_labels\n        ),\n        \"top_5_accuracy\": top_k_accuracy_score(\n            y_true,\n            y_prob,\n            k=5,\n            labels=class_labels\n        ),\n    }\n\ntopk_df = pd.DataFrame([\n    {\n        \"model\": model_name,\n        \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n        **TOPK_RESULTS[model_name],\n    }\n    for model_name in MODEL_NAMES\n])\n\ntopk_csv_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_top_k_accuracy.csv\"\ntopk_df.to_csv(topk_csv_path, index=False)\n\ndisplay(topk_df)\n\n# =============================================================================\n# Per-model Top-K Accuracy Figures\n# =============================================================================\n\nfor model_name in MODEL_NAMES:\n    values = [TOPK_RESULTS[model_name][m] for m in topk_metrics]\n    labels = [TOPK_DISPLAY_NAMES[m] for m in topk_metrics]\n\n    fig, ax = plt.subplots(figsize=(7, 5))\n\n    bars = ax.bar(\n        labels,\n        values,\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\",\n        linewidth=0.8\n    )\n\n    ax.set_title(f\"{MODEL_DISPLAY_NAMES[model_name]} — Top-K Accuracy\")\n    ax.set_ylabel(\"Accuracy\")\n    ax.set_ylim(0, 1.0)\n\n    for bar in bars:\n        height = bar.get_height()\n        ax.text(\n            bar.get_x() + bar.get_width() / 2,\n            height + 0.01,\n            f\"{height:.3f}\",\n            ha=\"center\",\n            va=\"bottom\",\n            fontsize=10\n        )\n\n    fig.tight_layout()\n\n    save_path = MODEL_DIRS[model_name][\"plots\"] / model_file_name(\n        model_name,\n        \"top_k_accuracy\",\n        \"png\"\n    )\n\n    fig.savefig(save_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\n# =============================================================================\n# Comparison Top-K Accuracy Figure\n# =============================================================================\n\nx = np.arange(len(topk_metrics))\nwidth = 0.14\n\nfig, ax = plt.subplots(figsize=(10, 6))\n\nfor i, model_name in enumerate(MODEL_NAMES):\n    values = [TOPK_RESULTS[model_name][m] for m in topk_metrics]\n\n    ax.bar(\n        x + (i - len(MODEL_NAMES) / 2) * width + width / 2,\n        values,\n        width,\n        label=MODEL_DISPLAY_NAMES[model_name],\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\",\n        linewidth=0.6\n    )\n\nax.set_title(\"Top-K Accuracy Comparison Across Models\")\nax.set_ylabel(\"Accuracy\")\nax.set_ylim(0, 1.0)\n\nax.set_xticks(x)\nax.set_xticklabels([TOPK_DISPLAY_NAMES[m] for m in topk_metrics])\n\nax.legend(frameon=False, ncol=3)\nfig.tight_layout()\n\ncomparison_save_path = COMPARISON_DIRS[\"plots\"] / comparison_plot_name(\n    \"top_k_accuracy\"\n)\n\nfig.savefig(comparison_save_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"Top-K Accuracy figures saved successfully.\")\nprint(\"Table saved to:\", topk_csv_path)\nprint(\"Comparison plot saved to:\", comparison_save_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:31.271662Z","iopub.execute_input":"2026-04-29T17:34:31.271965Z","iopub.status.idle":"2026-04-29T17:34:34.494120Z","shell.execute_reply.started":"2026-04-29T17:34:31.271943Z","shell.execute_reply":"2026-04-29T17:34:34.493258Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Metric 4: Normalized Confusion Matrix — Research Figures\n# =============================================================================\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix\n\nCONFUSION_MATRICES = {}\n\nclass_labels = np.arange(NUM_CLASSES)\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n    y_pred = np.argmax(y_prob, axis=1)\n\n    cm = confusion_matrix(\n        y_true,\n        y_pred,\n        labels=class_labels,\n        normalize=\"true\"\n    )\n\n    CONFUSION_MATRICES[model_name] = cm\n\n    fig, ax = plt.subplots(figsize=(10, 9))\n\n    im = ax.imshow(\n        cm,\n        interpolation=\"nearest\",\n        aspect=\"auto\",\n        vmin=0,\n        vmax=1\n    )\n\n    ax.set_title(f\"{MODEL_DISPLAY_NAMES[model_name]} — Normalized Confusion Matrix\")\n    ax.set_xlabel(\"Predicted Class\")\n    ax.set_ylabel(\"True Class\")\n\n    ax.set_xticks([])\n    ax.set_yticks([])\n\n    cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n    cbar.set_label(\"Normalized Count\")\n\n    fig.tight_layout()\n\n    save_path = MODEL_DIRS[model_name][\"confusion_matrices\"] / model_file_name(\n        model_name,\n        \"normalized_confusion_matrix\",\n        \"png\"\n    )\n\n    fig.savefig(save_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\nprint(\"Normalized confusion matrix figures saved successfully.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:34.495309Z","iopub.execute_input":"2026-04-29T17:34:34.495653Z","iopub.status.idle":"2026-04-29T17:34:41.132881Z","shell.execute_reply.started":"2026-04-29T17:34:34.495628Z","shell.execute_reply":"2026-04-29T17:34:41.132054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Metric 5: Per-Class F1 Analysis — Research Figures\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import classification_report\n\nPER_CLASS_RESULTS = []\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n    y_pred = np.argmax(y_prob, axis=1)\n\n    report = classification_report(\n        y_true,\n        y_pred,\n        labels=np.arange(NUM_CLASSES),\n        output_dict=True,\n        zero_division=0\n    )\n\n    for class_id in range(NUM_CLASSES):\n        PER_CLASS_RESULTS.append({\n            \"model\": model_name,\n            \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n            \"class_id\": class_id,\n            \"precision\": report[str(class_id)][\"precision\"],\n            \"recall\": report[str(class_id)][\"recall\"],\n            \"f1_score\": report[str(class_id)][\"f1-score\"],\n            \"support\": report[str(class_id)][\"support\"],\n        })\n\nper_class_df = pd.DataFrame(PER_CLASS_RESULTS)\n\nper_class_csv_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_per_class_metrics.csv\"\nper_class_df.to_csv(per_class_csv_path, index=False)\n\ndisplay(per_class_df.head())\n\n# =============================================================================\n# Per-model: Top 15 and Bottom 15 Classes by F1-score\n# =============================================================================\n\nfor model_name in MODEL_NAMES:\n    model_df = per_class_df[per_class_df[\"model\"] == model_name].copy()\n\n    top_classes = model_df.sort_values(\"f1_score\", ascending=False).head(15)\n    bottom_classes = model_df.sort_values(\"f1_score\", ascending=True).head(15)\n\n    selected_df = pd.concat([\n        bottom_classes.assign(group=\"Lowest F1\"),\n        top_classes.assign(group=\"Highest F1\")\n    ])\n\n    fig, ax = plt.subplots(figsize=(10, 7))\n\n    labels = [\n        f\"C{int(row.class_id)}\"\n        for _, row in selected_df.iterrows()\n    ]\n\n    bars = ax.barh(\n        labels,\n        selected_df[\"f1_score\"],\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\",\n        linewidth=0.6\n    )\n\n    ax.set_title(\n        f\"{MODEL_DISPLAY_NAMES[model_name]} — Lowest and Highest Per-Class F1 Scores\"\n    )\n    ax.set_xlabel(\"F1-score\")\n    ax.set_xlim(0, 1.0)\n\n    for bar in bars:\n        width = bar.get_width()\n        ax.text(\n            width + 0.01,\n            bar.get_y() + bar.get_height() / 2,\n            f\"{width:.2f}\",\n            va=\"center\",\n            fontsize=8\n        )\n\n    fig.tight_layout()\n\n    save_path = MODEL_DIRS[model_name][\"plots\"] / model_file_name(\n        model_name,\n        \"per_class_f1_extremes\",\n        \"png\"\n    )\n\n    fig.savefig(save_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\n# =============================================================================\n# Comparison: Mean Per-Class F1 Distribution\n# =============================================================================\n\nfig, ax = plt.subplots(figsize=(10, 6))\n\nfor model_name in MODEL_NAMES:\n    model_df = per_class_df[per_class_df[\"model\"] == model_name]\n\n    ax.hist(\n        model_df[\"f1_score\"],\n        bins=20,\n        alpha=0.45,\n        label=MODEL_DISPLAY_NAMES[model_name],\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\"\n    )\n\nax.set_title(\"Distribution of Per-Class F1 Scores Across Models\")\nax.set_xlabel(\"Per-Class F1-score\")\nax.set_ylabel(\"Number of Classes\")\nax.set_xlim(0, 1.0)\n\nax.legend(frameon=False)\nfig.tight_layout()\n\ncomparison_save_path = COMPARISON_DIRS[\"plots\"] / comparison_plot_name(\n    \"per_class_f1_distribution\"\n)\n\nfig.savefig(comparison_save_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"Per-class F1 analysis saved successfully.\")\nprint(\"Table saved to:\", per_class_csv_path)\nprint(\"Comparison plot saved to:\", comparison_save_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:41.133980Z","iopub.execute_input":"2026-04-29T17:34:41.134342Z","iopub.status.idle":"2026-04-29T17:34:46.785952Z","shell.execute_reply.started":"2026-04-29T17:34:41.134318Z","shell.execute_reply":"2026-04-29T17:34:46.785061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Final Comparison Summary — Research Figure\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nsummary_df = pd.DataFrame([\n    {\n        \"model\": model_name,\n        \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n        \"accuracy\": ACCURACY_RESULTS[model_name],\n        \"macro_f1\": PRF_RESULTS[model_name][\"macro_f1\"],\n        \"weighted_f1\": PRF_RESULTS[model_name][\"weighted_f1\"],\n        \"top_3_accuracy\": TOPK_RESULTS[model_name][\"top_3_accuracy\"],\n        \"top_5_accuracy\": TOPK_RESULTS[model_name][\"top_5_accuracy\"],\n    }\n    for model_name in MODEL_NAMES\n])\n\nsummary_csv_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_final_summary.csv\"\nsummary_df.to_csv(summary_csv_path, index=False)\n\ndisplay(summary_df)\n\n# =============================================================================\n# Final grouped bar chart\n# =============================================================================\n\nsummary_metrics = [\n    \"accuracy\",\n    \"macro_f1\",\n    \"weighted_f1\",\n    \"top_3_accuracy\",\n    \"top_5_accuracy\",\n]\n\nSUMMARY_DISPLAY_NAMES = {\n    \"accuracy\": \"Accuracy\",\n    \"macro_f1\": \"Macro F1\",\n    \"weighted_f1\": \"Weighted F1\",\n    \"top_3_accuracy\": \"Top-3 Acc.\",\n    \"top_5_accuracy\": \"Top-5 Acc.\",\n}\n\nx = np.arange(len(summary_metrics))\nwidth = 0.14\n\nfig, ax = plt.subplots(figsize=(12, 6))\n\nfor i, model_name in enumerate(MODEL_NAMES):\n    values = [\n        summary_df.loc[\n            summary_df[\"model\"] == model_name,\n            metric\n        ].values[0]\n        for metric in summary_metrics\n    ]\n\n    ax.bar(\n        x + (i - len(MODEL_NAMES) / 2) * width + width / 2,\n        values,\n        width,\n        label=MODEL_DISPLAY_NAMES[model_name],\n        color=MODEL_COLORS[model_name],\n        edgecolor=\"black\",\n        linewidth=0.6\n    )\n\nax.set_title(\"Overall Performance Comparison Across Models\")\nax.set_ylabel(\"Score\")\nax.set_ylim(0, 1.0)\n\nax.set_xticks(x)\nax.set_xticklabels(\n    [SUMMARY_DISPLAY_NAMES[m] for m in summary_metrics],\n    rotation=20,\n    ha=\"right\"\n)\n\nax.legend(frameon=False, ncol=3)\n\nfig.tight_layout()\n\nsummary_plot_path = COMPARISON_DIRS[\"plots\"] / comparison_plot_name(\n    \"final_performance_summary\"\n)\n\nfig.savefig(summary_plot_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"Final performance summary saved successfully.\")\nprint(\"Table saved to:\", summary_csv_path)\nprint(\"Figure saved to:\", summary_plot_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:46.787256Z","iopub.execute_input":"2026-04-29T17:34:46.787604Z","iopub.status.idle":"2026-04-29T17:34:47.441721Z","shell.execute_reply.started":"2026-04-29T17:34:46.787580Z","shell.execute_reply":"2026-04-29T17:34:47.441074Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Statistical Significance — McNemar Test\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nfrom statsmodels.stats.contingency_tables import mcnemar\n\nSIGNIFICANCE_RESULTS = []\n\nalpha = 0.05  # significance level\n\n# =============================================================================\n# Build correctness matrix\n# =============================================================================\n\ncorrectness = {}\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n    y_pred = np.argmax(y_prob, axis=1)\n\n    correctness[model_name] = (y_pred == y_true).astype(int)\n\n# =============================================================================\n# Pairwise McNemar Tests\n# =============================================================================\n\nfor i in range(len(MODEL_NAMES)):\n    for j in range(i + 1, len(MODEL_NAMES)):\n        \n        m1 = MODEL_NAMES[i]\n        m2 = MODEL_NAMES[j]\n\n        c1 = correctness[m1]\n        c2 = correctness[m2]\n\n        # Contingency table\n        both_correct = np.sum((c1 == 1) & (c2 == 1))\n        m1_correct_m2_wrong = np.sum((c1 == 1) & (c2 == 0))\n        m1_wrong_m2_correct = np.sum((c1 == 0) & (c2 == 1))\n        both_wrong = np.sum((c1 == 0) & (c2 == 0))\n\n        table = [\n            [both_correct, m1_correct_m2_wrong],\n            [m1_wrong_m2_correct, both_wrong],\n        ]\n\n        result = mcnemar(table, exact=False, correction=True)\n\n        SIGNIFICANCE_RESULTS.append({\n            \"model_1\": m1,\n            \"model_2\": m2,\n            \"m1_better_count\": int(m1_correct_m2_wrong),\n            \"m2_better_count\": int(m1_wrong_m2_correct),\n            \"p_value\": result.pvalue,\n            \"significant\": result.pvalue < alpha,\n        })\n\nsignificance_df = pd.DataFrame(SIGNIFICANCE_RESULTS)\n\n# Save CSV\nsignificance_csv_path = COMPARISON_DIRS[\"metrics\"] / \"statistical_significance_mcnemar.csv\"\nsignificance_df.to_csv(significance_csv_path, index=False)\n\ndisplay(significance_df)\n\nprint(\"Statistical significance (McNemar) computed.\")\nprint(\"Saved to:\", significance_csv_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:47.442828Z","iopub.execute_input":"2026-04-29T17:34:47.443525Z","iopub.status.idle":"2026-04-29T17:34:47.499885Z","shell.execute_reply.started":"2026-04-29T17:34:47.443499Z","shell.execute_reply":"2026-04-29T17:34:47.499235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Statistical Significance Heatmap — McNemar p-values\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# =============================================================================\n# Build p-value matrix\n# =============================================================================\n\npvalue_matrix = pd.DataFrame(\n    np.ones((len(MODEL_NAMES), len(MODEL_NAMES))),\n    index=MODEL_NAMES,\n    columns=MODEL_NAMES\n)\n\nfor _, row in significance_df.iterrows():\n    m1 = row[\"model_1\"]\n    m2 = row[\"model_2\"]\n    p = row[\"p_value\"]\n\n    pvalue_matrix.loc[m1, m2] = p\n    pvalue_matrix.loc[m2, m1] = p\n\ndisplay(pvalue_matrix)\n\n# =============================================================================\n# Heatmap\n# =============================================================================\n\nfig, ax = plt.subplots(figsize=(8, 7))\n\nim = ax.imshow(\n    pvalue_matrix.values,\n    vmin=0,\n    vmax=0.05,\n    interpolation=\"nearest\"\n)\n\nax.set_title(\"Pairwise Statistical Significance Heatmap (McNemar p-values)\")\nax.set_xticks(np.arange(len(MODEL_NAMES)))\nax.set_yticks(np.arange(len(MODEL_NAMES)))\n\nax.set_xticklabels(\n    [MODEL_DISPLAY_NAMES[m] for m in MODEL_NAMES],\n    rotation=35,\n    ha=\"right\"\n)\nax.set_yticklabels(\n    [MODEL_DISPLAY_NAMES[m] for m in MODEL_NAMES]\n)\n\nfor i in range(len(MODEL_NAMES)):\n    for j in range(len(MODEL_NAMES)):\n        p = pvalue_matrix.iloc[i, j]\n\n        if i == j:\n            text = \"-\"\n        elif p < 0.001:\n            text = \"<0.001\"\n        else:\n            text = f\"{p:.3f}\"\n\n        ax.text(\n            j,\n            i,\n            text,\n            ha=\"center\",\n            va=\"center\",\n            fontsize=9\n        )\n\ncbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\ncbar.set_label(\"p-value\")\n\nfig.tight_layout()\n\npvalue_heatmap_path = COMPARISON_DIRS[\"plots\"] / comparison_plot_name(\n    \"mcnemar_pvalue_heatmap\"\n)\n\nfig.savefig(pvalue_heatmap_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"McNemar p-value heatmap saved to:\", pvalue_heatmap_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:47.500926Z","iopub.execute_input":"2026-04-29T17:34:47.501252Z","iopub.status.idle":"2026-04-29T17:34:48.377849Z","shell.execute_reply.started":"2026-04-29T17:34:47.501228Z","shell.execute_reply":"2026-04-29T17:34:48.377223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Highlight Best Model Statistically\n# =============================================================================\n\nalpha = 0.05\n\nbest_model = max(\n    MODEL_NAMES,\n    key=lambda m: ACCURACY_RESULTS[m]\n)\n\nbest_display = MODEL_DISPLAY_NAMES[best_model]\n\nbest_comparisons = []\n\nfor model_name in MODEL_NAMES:\n    if model_name == best_model:\n        continue\n\n    row = significance_df[\n        (\n            (significance_df[\"model_1\"] == best_model) &\n            (significance_df[\"model_2\"] == model_name)\n        )\n        |\n        (\n            (significance_df[\"model_1\"] == model_name) &\n            (significance_df[\"model_2\"] == best_model)\n        )\n    ].iloc[0]\n\n    p_value = row[\"p_value\"]\n\n    best_correct = correctness[best_model]\n    other_correct = correctness[model_name]\n\n    best_only = np.sum((best_correct == 1) & (other_correct == 0))\n    other_only = np.sum((best_correct == 0) & (other_correct == 1))\n\n    best_comparisons.append({\n        \"best_model\": best_model,\n        \"compared_with\": model_name,\n        \"best_only_correct\": int(best_only),\n        \"other_only_correct\": int(other_only),\n        \"p_value\": p_value,\n        \"statistically_significant\": p_value < alpha,\n        \"best_model_statistically_better\": (p_value < alpha) and (best_only > other_only)\n    })\n\nbest_stat_df = pd.DataFrame(best_comparisons)\n\nbest_stat_csv_path = COMPARISON_DIRS[\"metrics\"] / \"best_model_statistical_summary.csv\"\nbest_stat_df.to_csv(best_stat_csv_path, index=False)\n\ndisplay(best_stat_df)\n\n# =============================================================================\n# Text conclusion\n# =============================================================================\n\nnum_significant_wins = best_stat_df[\"best_model_statistically_better\"].sum()\ntotal_comparisons = len(best_stat_df)\n\nif num_significant_wins == total_comparisons:\n    conclusion = (\n        f\"{best_display} is statistically superior to all other evaluated models \"\n        f\"based on McNemar's test at alpha={alpha}.\"\n    )\nelif num_significant_wins > 0:\n    conclusion = (\n        f\"{best_display} is statistically superior to {num_significant_wins} out of \"\n        f\"{total_comparisons} competing models based on McNemar's test at alpha={alpha}.\"\n    )\nelse:\n    conclusion = (\n        f\"{best_display} has the highest accuracy, but its improvement is not statistically \"\n        f\"significant over the competing models based on McNemar's test at alpha={alpha}.\"\n    )\n\nprint(conclusion)\nprint(\"Saved to:\", best_stat_csv_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:48.379126Z","iopub.execute_input":"2026-04-29T17:34:48.379343Z","iopub.status.idle":"2026-04-29T17:34:48.401183Z","shell.execute_reply.started":"2026-04-29T17:34:48.379322Z","shell.execute_reply":"2026-04-29T17:34:48.400536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Curve-Based Metrics — ROC & PR (Micro + Macro)\n# =============================================================================\n\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nfrom sklearn.preprocessing import label_binarize\nfrom sklearn.metrics import (\n    roc_curve, auc,\n    precision_recall_curve,\n    average_precision_score\n)\n\n# Binarize labels\ny_true_bin = label_binarize(y_true, classes=np.arange(NUM_CLASSES))\n\n# =============================================================================\n# Loop over models\n# =============================================================================\n\nfor model_name in MODEL_NAMES:\n\n    y_prob = all_probs[model_name]\n\n    # ---------------------------------------------------------\n    # ROC (Micro)\n    # ---------------------------------------------------------\n    fpr_micro, tpr_micro, _ = roc_curve(\n        y_true_bin.ravel(),\n        y_prob.ravel()\n    )\n    roc_auc_micro = auc(fpr_micro, tpr_micro)\n\n    # ---------------------------------------------------------\n    # ROC (Macro)\n    # ---------------------------------------------------------\n    fpr = dict()\n    tpr = dict()\n    roc_auc = dict()\n\n    for i in range(NUM_CLASSES):\n        fpr[i], tpr[i], _ = roc_curve(y_true_bin[:, i], y_prob[:, i])\n        roc_auc[i] = auc(fpr[i], tpr[i])\n\n    all_fpr = np.unique(np.concatenate([fpr[i] for i in range(NUM_CLASSES)]))\n\n    mean_tpr = np.zeros_like(all_fpr)\n    for i in range(NUM_CLASSES):\n        mean_tpr += np.interp(all_fpr, fpr[i], tpr[i])\n\n    mean_tpr /= NUM_CLASSES\n    roc_auc_macro = auc(all_fpr, mean_tpr)\n\n    # ---------------------------------------------------------\n    # PR Curve (Micro)\n    # ---------------------------------------------------------\n    precision_micro, recall_micro, _ = precision_recall_curve(\n        y_true_bin.ravel(),\n        y_prob.ravel()\n    )\n    pr_auc_micro = auc(recall_micro, precision_micro)\n\n    # ---------------------------------------------------------\n    # PR Curve (Macro)\n    # ---------------------------------------------------------\n    pr_auc_macro = average_precision_score(\n        y_true_bin,\n        y_prob,\n        average=\"macro\"\n    )\n\n    # =============================================================================\n    # Plot\n    # =============================================================================\n\n    fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n\n    # ROC\n    ax[0].plot(\n        fpr_micro, tpr_micro,\n        label=f\"Micro AUC = {roc_auc_micro:.3f}\",\n        linewidth=2\n    )\n    ax[0].plot(\n        all_fpr, mean_tpr,\n        linestyle=\"--\",\n        label=f\"Macro AUC = {roc_auc_macro:.3f}\"\n    )\n\n    ax[0].plot([0, 1], [0, 1], linestyle=\":\")\n    ax[0].set_title(f\"{MODEL_DISPLAY_NAMES[model_name]} — ROC Curve\")\n    ax[0].set_xlabel(\"False Positive Rate\")\n    ax[0].set_ylabel(\"True Positive Rate\")\n    ax[0].legend()\n\n    # PR\n    ax[1].plot(\n        recall_micro, precision_micro,\n        label=f\"Micro AUC = {pr_auc_micro:.3f}\",\n        linewidth=2\n    )\n\n    ax[1].set_title(f\"{MODEL_DISPLAY_NAMES[model_name]} — Precision-Recall Curve\")\n    ax[1].set_xlabel(\"Recall\")\n    ax[1].set_ylabel(\"Precision\")\n    ax[1].legend()\n\n    fig.tight_layout()\n\n    save_path = MODEL_DIRS[model_name][\"plots\"] / model_file_name(\n        model_name,\n        \"roc_pr_curves\",\n        \"png\"\n    )\n\n    fig.savefig(save_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:34:48.402318Z","iopub.execute_input":"2026-04-29T17:34:48.402610Z","iopub.status.idle":"2026-04-29T17:35:23.097590Z","shell.execute_reply.started":"2026-04-29T17:34:48.402588Z","shell.execute_reply":"2026-04-29T17:35:23.096912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Curve-Based Metrics — ROC/PR Comparison Across Models\n# =============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom sklearn.preprocessing import label_binarize\nfrom sklearn.metrics import (\n    roc_curve,\n    auc,\n    precision_recall_curve,\n    average_precision_score\n)\n\n# =============================================================================\n# Prepare labels\n# =============================================================================\n\ny_true_bin = label_binarize(y_true, classes=np.arange(NUM_CLASSES))\n\nCURVE_RESULTS = {}\n\n# =============================================================================\n# Compute Micro-ROC and Micro-PR for each model\n# =============================================================================\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 5))\n\nfor model_name in MODEL_NAMES:\n    y_prob = all_probs[model_name]\n\n    # -------------------------------------------------------------------------\n    # ROC Curve - Micro average\n    # -------------------------------------------------------------------------\n    fpr_micro, tpr_micro, _ = roc_curve(\n        y_true_bin.ravel(),\n        y_prob.ravel()\n    )\n    roc_auc_micro = auc(fpr_micro, tpr_micro)\n\n    # -------------------------------------------------------------------------\n    # Precision-Recall Curve - Micro average\n    # -------------------------------------------------------------------------\n    precision_micro, recall_micro, _ = precision_recall_curve(\n        y_true_bin.ravel(),\n        y_prob.ravel()\n    )\n    pr_auc_micro = auc(recall_micro, precision_micro)\n\n    avg_precision_micro = average_precision_score(\n        y_true_bin,\n        y_prob,\n        average=\"micro\"\n    )\n\n    CURVE_RESULTS[model_name] = {\n        \"roc_auc_micro\": roc_auc_micro,\n        \"pr_auc_micro\": pr_auc_micro,\n        \"average_precision_micro\": avg_precision_micro,\n    }\n\n    # -------------------------------------------------------------------------\n    # Plot ROC\n    # -------------------------------------------------------------------------\n    axes[0].plot(\n        fpr_micro,\n        tpr_micro,\n        label=f\"{MODEL_DISPLAY_NAMES[model_name]} (AUC={roc_auc_micro:.3f})\",\n        color=MODEL_COLORS[model_name],\n        linewidth=2\n    )\n\n    # -------------------------------------------------------------------------\n    # Plot PR\n    # -------------------------------------------------------------------------\n    axes[1].plot(\n        recall_micro,\n        precision_micro,\n        label=f\"{MODEL_DISPLAY_NAMES[model_name]} (AUC={pr_auc_micro:.3f})\",\n        color=MODEL_COLORS[model_name],\n        linewidth=2\n    )\n\n# =============================================================================\n# Format ROC subplot\n# =============================================================================\n\naxes[0].plot(\n    [0, 1],\n    [0, 1],\n    linestyle=\"--\",\n    linewidth=1,\n    color=\"gray\"\n)\n\naxes[0].set_title(\"Micro-Averaged ROC Curves Across Models\")\naxes[0].set_xlabel(\"False Positive Rate\")\naxes[0].set_ylabel(\"True Positive Rate\")\naxes[0].set_xlim(0, 1)\naxes[0].set_ylim(0, 1.01)\naxes[0].legend(frameon=False, fontsize=8)\n\n# =============================================================================\n# Format PR subplot\n# =============================================================================\n\nbaseline_precision = 1 / NUM_CLASSES\n\naxes[1].axhline(\n    y=baseline_precision,\n    linestyle=\"--\",\n    linewidth=1,\n    color=\"gray\",\n    label=f\"Random baseline = {baseline_precision:.4f}\"\n)\n\naxes[1].set_title(\"Micro-Averaged Precision-Recall Curves Across Models\")\naxes[1].set_xlabel(\"Recall\")\naxes[1].set_ylabel(\"Precision\")\naxes[1].set_xlim(0, 1)\naxes[1].set_ylim(0, 1.01)\naxes[1].legend(frameon=False, fontsize=8)\n\nfig.tight_layout()\n\ncomparison_curve_path = COMPARISON_DIRS[\"plots\"] / comparison_plot_name(\n    \"micro_roc_pr_curves\"\n)\n\nfig.savefig(comparison_curve_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\n\n# =============================================================================\n# Save curve summary table\n# =============================================================================\n\ncurve_df = pd.DataFrame([\n    {\n        \"model\": model_name,\n        \"display_name\": MODEL_DISPLAY_NAMES[model_name],\n        **CURVE_RESULTS[model_name],\n    }\n    for model_name in MODEL_NAMES\n])\n\ncurve_csv_path = COMPARISON_DIRS[\"metrics\"] / \"comparison_curve_based_metrics.csv\"\ncurve_df.to_csv(curve_csv_path, index=False)\n\ndisplay(curve_df)\n\nprint(\"ROC/PR comparison figure saved successfully.\")\nprint(\"Figure saved to:\", comparison_curve_path)\nprint(\"Table saved to:\", curve_csv_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:35:23.098654Z","iopub.execute_input":"2026-04-29T17:35:23.099074Z","iopub.status.idle":"2026-04-29T17:35:54.922507Z","shell.execute_reply.started":"2026-04-29T17:35:23.099039Z","shell.execute_reply":"2026-04-29T17:35:54.921663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# FINAL CELL — Export All Loaded Models to TensorFlow Lite\n# =============================================================================\n# This cell assumes:\n#   1. The notebook already ran successfully up to the evaluation section.\n#   2. loaded_models already contains:\n#      BiLSTM, Transformer, BiGRU, ST_GCN, RoPE_Conformer\n#   3. test_dataset, MAX_LEN, CHANNELS, NUM_CLASSES, MODEL_NAMES are already defined.\n#\n# Output:\n#   /kaggle/working/tflite_exports/*.tflite\n#   /kaggle/working/tflite_exports/tflite_export_report.csv\n#   /kaggle/working/tflite_exports/tflite_export_report.json\n#   /kaggle/working/tflite_exports_all.zip\n# =============================================================================\n\nimport os\nimport gc\nimport json\nimport shutil\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom pathlib import Path\n\n# =============================================================================\n# Export settings\n# =============================================================================\n\nTFLITE_EXPORT_DIR = Path(\"/kaggle/working/tflite_exports\")\nTFLITE_SAVEDMODEL_DIR = TFLITE_EXPORT_DIR / \"_savedmodel_temp\"\nTFLITE_EXPORT_DIR.mkdir(parents=True, exist_ok=True)\nTFLITE_SAVEDMODEL_DIR.mkdir(parents=True, exist_ok=True)\n\n# Keep model outputs as logits, same as your evaluation pipeline.\n# Your notebook applies softmax after model inference.\nEXPORT_WITH_SOFTMAX = False\n\n# Input is kept float32 for all models.\n# For float16 models, this uses float16 weight quantization while preserving float32 I/O.\nTFLITE_CONFIG = {\n    \"BiLSTM\": {\n        \"precision\": \"float32\",\n        \"select_tf_ops\": True,\n        \"disable_tensorlist_lowering\": True,\n        \"description\": \"BiLSTM baseline; uses LSTM TensorList/Flex ops.\",\n    },\n    \"BiGRU\": {\n        \"precision\": \"float32\",\n        \"select_tf_ops\": True,\n        \"disable_tensorlist_lowering\": True,\n        \"description\": \"BiGRU with custom masking-safe layers.\",\n    },\n    \"ST_GCN\": {\n        \"precision\": \"float32\",\n        \"select_tf_ops\": True,\n        \"disable_tensorlist_lowering\": False,\n        \"description\": \"ST-GCN with custom graph layers; keep float32 for numerical stability.\",\n    },\n    \"Transformer\": {\n        \"precision\": \"float16\",\n        \"select_tf_ops\": True,\n        \"disable_tensorlist_lowering\": False,\n        \"description\": \"Transformer exported with float16 weight quantization.\",\n    },\n    \"RoPE_Conformer\": {\n        \"precision\": \"float16\",\n        \"select_tf_ops\": True,\n        \"disable_tensorlist_lowering\": False,\n        \"description\": \"RoPE-Conformer / mixed precision model exported with float16 weight quantization.\",\n    },\n}\n\n# Use the current evaluation order if available.\nif \"MODEL_NAMES\" in globals() and isinstance(MODEL_NAMES, (list, tuple)):\n    EXPORT_MODEL_ORDER = list(MODEL_NAMES)\nelse:\n    EXPORT_MODEL_ORDER = [\"BiLSTM\", \"Transformer\", \"BiGRU\", \"ST_GCN\", \"RoPE_Conformer\"]\n\n# Keep only models actually loaded.\nEXPORT_MODEL_ORDER = [\n    name for name in EXPORT_MODEL_ORDER\n    if \"loaded_models\" in globals() and name in loaded_models and loaded_models[name] is not None\n]\n\nprint(\"Models selected for TFLite export:\")\nprint(EXPORT_MODEL_ORDER)\n\n\n# =============================================================================\n# Validation sample from the real test dataset\n# =============================================================================\n\ndef get_validation_batch(dataset, batch_size=1):\n    \"\"\"\n    Use a real sample from test_dataset instead of random input.\n    This is safer because the models rely on the -100 padding pattern.\n    \"\"\"\n    for x_batch, y_batch in dataset.unbatch().batch(batch_size).take(1):\n        return x_batch.numpy().astype(np.float32)\n\n    raise RuntimeError(\"Could not read a validation batch from test_dataset.\")\n\n\nvalidation_x = get_validation_batch(test_dataset, batch_size=1)\n\nprint(\"Validation input shape:\", validation_x.shape)\nprint(\"Validation input dtype :\", validation_x.dtype)\n\n\n# =============================================================================\n# Helper functions\n# =============================================================================\n\ndef safe_model_filename(name):\n    return (\n        name.replace(\"/\", \"_\")\n            .replace(\"\\\\\", \"_\")\n            .replace(\" \", \"_\")\n            .replace(\"-\", \"_\")\n    )\n\n\ndef freeze_for_inference(model):\n    model.trainable = False\n    for layer in model.layers:\n        layer.trainable = False\n    return model\n\n\ndef build_inference_wrapper(model, model_name, export_with_softmax=False):\n    \"\"\"\n    Wraps each existing Keras model so conversion traces training=False explicitly.\n    This avoids Dropout / BatchNorm / LateDropout training behavior during export.\n    \"\"\"\n    inp = tf.keras.Input(\n        shape=(MAX_LEN, CHANNELS),\n        dtype=tf.float32,\n        name=f\"{safe_model_filename(model_name)}_input\"\n    )\n\n    logits = model(inp, training=False)\n\n    if export_with_softmax:\n        out = tf.keras.layers.Activation(\n            \"softmax\",\n            dtype=\"float32\",\n            name=f\"{safe_model_filename(model_name)}_probabilities\"\n        )(logits)\n    else:\n        # Keep logits as float32 where possible.\n        out = tf.keras.layers.Activation(\n            \"linear\",\n            dtype=\"float32\",\n            name=f\"{safe_model_filename(model_name)}_logits\"\n        )(logits)\n\n    wrapped = tf.keras.Model(\n        inputs=inp,\n        outputs=out,\n        name=f\"{safe_model_filename(model_name)}_tflite_wrapper\"\n    )\n\n    return wrapped\n\n\ndef configure_converter(converter, config):\n    \"\"\"\n    Applies model-specific TFLite conversion settings.\n    \"\"\"\n    if config.get(\"select_tf_ops\", True):\n        converter.target_spec.supported_ops = [\n            tf.lite.OpsSet.TFLITE_BUILTINS,\n            tf.lite.OpsSet.SELECT_TF_OPS,\n        ]\n    else:\n        converter.target_spec.supported_ops = [\n            tf.lite.OpsSet.TFLITE_BUILTINS,\n        ]\n\n    precision = config.get(\"precision\", \"float32\")\n\n    if precision == \"float16\":\n        converter.optimizations = [tf.lite.Optimize.DEFAULT]\n        converter.target_spec.supported_types = [tf.float16]\n        # Keep public input/output float32.\n        converter.inference_input_type = tf.float32\n        converter.inference_output_type = tf.float32\n\n    elif precision == \"float32\":\n        pass\n\n    else:\n        raise ValueError(f\"Unsupported precision: {precision}\")\n\n    # Important for LSTM/GRU TensorList ops when SELECT_TF_OPS is enabled.\n    if config.get(\"disable_tensorlist_lowering\", False):\n        converter._experimental_lower_tensor_list_ops = False\n\n    # Helps with resource variables in some custom/Keras 3 models.\n    try:\n        converter.experimental_enable_resource_variables = True\n    except Exception:\n        pass\n\n    return converter\n\n\ndef keras_predict_for_validation(model, x, export_with_softmax=False):\n    \"\"\"\n    Returns the reference output from the original Keras model.\n    \"\"\"\n    y = model(x, training=False).numpy()\n    y = np.nan_to_num(y, nan=0.0, posinf=1e4, neginf=-1e4)\n\n    if export_with_softmax:\n        y = tf.nn.softmax(y, axis=-1).numpy()\n\n    return y.astype(np.float32)\n\n\ndef run_tflite_validation(tflite_path, x):\n    \"\"\"\n    Runs one TFLite inference and returns output.\n    \"\"\"\n    interpreter = tf.lite.Interpreter(model_path=str(tflite_path))\n    interpreter.allocate_tensors()\n\n    input_details = interpreter.get_input_details()\n    output_details = interpreter.get_output_details()\n\n    input_index = input_details[0][\"index\"]\n    input_dtype = input_details[0][\"dtype\"]\n\n    x_in = x.astype(input_dtype)\n\n    interpreter.set_tensor(input_index, x_in)\n    interpreter.invoke()\n\n    output_index = output_details[0][\"index\"]\n    y = interpreter.get_tensor(output_index)\n\n    y = np.nan_to_num(y, nan=0.0, posinf=1e4, neginf=-1e4)\n    return y.astype(np.float32), input_details, output_details\n\n\ndef export_savedmodel_temp(wrapped_model, model_name):\n    \"\"\"\n    Exports a temporary SavedModel. Used as fallback if direct from_keras_model fails.\n    \"\"\"\n    export_path = TFLITE_SAVEDMODEL_DIR / safe_model_filename(model_name)\n\n    if export_path.exists():\n        shutil.rmtree(export_path)\n\n    try:\n        wrapped_model.export(str(export_path))\n    except Exception:\n        # Fallback for environments where .export behaves differently.\n        tf.saved_model.save(wrapped_model, str(export_path))\n\n    return export_path\n\n\ndef convert_one_model_to_tflite(model_name, model, config, validation_x):\n    \"\"\"\n    Converts one model to TFLite, validates it, and returns a report dictionary.\n    \"\"\"\n    print(\"\\n\" + \"=\" * 90)\n    print(f\"Exporting: {model_name}\")\n    print(\"Config:\", config)\n    print(\"=\" * 90)\n\n    tf.keras.backend.set_floatx(\"float32\")\n\n    model = freeze_for_inference(model)\n\n    wrapped_model = build_inference_wrapper(\n        model=model,\n        model_name=model_name,\n        export_with_softmax=EXPORT_WITH_SOFTMAX\n    )\n\n    # Build/trace once before conversion.\n    ref_output = keras_predict_for_validation(\n        wrapped_model,\n        validation_x,\n        export_with_softmax=False\n    )\n\n    print(\"Wrapped input shape :\", wrapped_model.input_shape)\n    print(\"Wrapped output shape:\", wrapped_model.output_shape)\n    print(\"Reference output    :\", ref_output.shape, ref_output.dtype)\n\n    safe_name = safe_model_filename(model_name)\n    precision = config.get(\"precision\", \"float32\")\n    suffix = \"probs\" if EXPORT_WITH_SOFTMAX else \"logits\"\n\n    tflite_path = TFLITE_EXPORT_DIR / f\"{safe_name}_{precision}_{suffix}.tflite\"\n\n    conversion_method = None\n    conversion_error = None\n    tflite_bytes = None\n\n    # -------------------------------------------------------------------------\n    # Attempt 1: Direct Keras -> TFLite\n    # -------------------------------------------------------------------------\n    try:\n        converter = tf.lite.TFLiteConverter.from_keras_model(wrapped_model)\n        converter = configure_converter(converter, config)\n        tflite_bytes = converter.convert()\n        conversion_method = \"from_keras_model\"\n\n    except Exception as exc_1:\n        conversion_error = traceback.format_exc()\n        print(f\"[{model_name}] Direct from_keras_model failed.\")\n        print(str(exc_1))\n\n        # ---------------------------------------------------------------------\n        # Attempt 2: SavedModel -> TFLite\n        # ---------------------------------------------------------------------\n        try:\n            savedmodel_path = export_savedmodel_temp(wrapped_model, model_name)\n\n            converter = tf.lite.TFLiteConverter.from_saved_model(str(savedmodel_path))\n            converter = configure_converter(converter, config)\n            tflite_bytes = converter.convert()\n            conversion_method = \"from_saved_model\"\n\n        except Exception as exc_2:\n            conversion_error = traceback.format_exc()\n            print(f\"[{model_name}] SavedModel conversion failed.\")\n            print(str(exc_2))\n\n            return {\n                \"model\": model_name,\n                \"status\": \"failed\",\n                \"precision\": precision,\n                \"path\": None,\n                \"size_mb\": None,\n                \"conversion_method\": None,\n                \"validation_status\": \"not_run\",\n                \"max_abs_output_diff\": None,\n                \"mean_abs_output_diff\": None,\n                \"keras_top1\": None,\n                \"tflite_top1\": None,\n                \"top1_match\": None,\n                \"error\": conversion_error,\n                \"description\": config.get(\"description\", \"\"),\n            }\n\n    # Save TFLite bytes.\n    with open(tflite_path, \"wb\") as f:\n        f.write(tflite_bytes)\n\n    size_mb = tflite_path.stat().st_size / (1024 ** 2)\n\n    print(f\"[{model_name}] TFLite saved:\")\n    print(\" \", tflite_path)\n    print(f\" Size: {size_mb:.2f} MB\")\n    print(\" Method:\", conversion_method)\n\n    # -------------------------------------------------------------------------\n    # Validate TFLite output against wrapped Keras output\n    # -------------------------------------------------------------------------\n    try:\n        tflite_output, input_details, output_details = run_tflite_validation(\n            tflite_path,\n            validation_x\n        )\n\n        max_abs_diff = float(np.max(np.abs(ref_output - tflite_output)))\n        mean_abs_diff = float(np.mean(np.abs(ref_output - tflite_output)))\n\n        keras_top1 = int(np.argmax(ref_output, axis=-1)[0])\n        tflite_top1 = int(np.argmax(tflite_output, axis=-1)[0])\n        top1_match = bool(keras_top1 == tflite_top1)\n\n        validation_status = \"passed\"\n\n        print(f\"[{model_name}] Validation passed\")\n        print(\" Keras top-1 :\", keras_top1)\n        print(\" TFLite top-1:\", tflite_top1)\n        print(\" Top-1 match :\", top1_match)\n        print(\" Max abs diff:\", max_abs_diff)\n        print(\" Mean abs diff:\", mean_abs_diff)\n        print(\" TFLite input :\", input_details)\n        print(\" TFLite output:\", output_details)\n\n        validation_error = None\n\n    except Exception:\n        validation_status = \"failed\"\n        validation_error = traceback.format_exc()\n\n        max_abs_diff = None\n        mean_abs_diff = None\n        keras_top1 = None\n        tflite_top1 = None\n        top1_match = None\n\n        print(f\"[{model_name}] Validation failed\")\n        print(validation_error)\n\n    return {\n        \"model\": model_name,\n        \"status\": \"success\",\n        \"precision\": precision,\n        \"path\": str(tflite_path),\n        \"size_mb\": float(size_mb),\n        \"conversion_method\": conversion_method,\n        \"validation_status\": validation_status,\n        \"max_abs_output_diff\": max_abs_diff,\n        \"mean_abs_output_diff\": mean_abs_diff,\n        \"keras_top1\": keras_top1,\n        \"tflite_top1\": tflite_top1,\n        \"top1_match\": top1_match,\n        \"error\": validation_error,\n        \"description\": config.get(\"description\", \"\"),\n    }\n\n\n# =============================================================================\n# Run export for all models\n# =============================================================================\n\ntflite_export_results = []\n\nfor model_name in EXPORT_MODEL_ORDER:\n    if model_name not in loaded_models:\n        print(f\"[SKIP] {model_name}: not found in loaded_models\")\n        tflite_export_results.append({\n            \"model\": model_name,\n            \"status\": \"missing\",\n            \"precision\": None,\n            \"path\": None,\n            \"size_mb\": None,\n            \"conversion_method\": None,\n            \"validation_status\": \"not_run\",\n            \"max_abs_output_diff\": None,\n            \"mean_abs_output_diff\": None,\n            \"keras_top1\": None,\n            \"tflite_top1\": None,\n            \"top1_match\": None,\n            \"error\": \"Model not found in loaded_models\",\n            \"description\": \"\",\n        })\n        continue\n\n    model = loaded_models[model_name]\n    config = TFLITE_CONFIG.get(\n        model_name,\n        {\n            \"precision\": \"float32\",\n            \"select_tf_ops\": True,\n            \"disable_tensorlist_lowering\": False,\n            \"description\": \"Default fallback config.\",\n        }\n    )\n\n    try:\n        result = convert_one_model_to_tflite(\n            model_name=model_name,\n            model=model,\n            config=config,\n            validation_x=validation_x,\n        )\n\n    except Exception:\n        result = {\n            \"model\": model_name,\n            \"status\": \"failed\",\n            \"precision\": config.get(\"precision\"),\n            \"path\": None,\n            \"size_mb\": None,\n            \"conversion_method\": None,\n            \"validation_status\": \"not_run\",\n            \"max_abs_output_diff\": None,\n            \"mean_abs_output_diff\": None,\n            \"keras_top1\": None,\n            \"tflite_top1\": None,\n            \"top1_match\": None,\n            \"error\": traceback.format_exc(),\n            \"description\": config.get(\"description\", \"\"),\n        }\n\n        print(f\"[{model_name}] FAILED unexpectedly:\")\n        print(result[\"error\"])\n\n    tflite_export_results.append(result)\n\n    # Cleanup between large conversions.\n    gc.collect()\n\n    try:\n        tf.experimental.async_wait()\n    except Exception:\n        pass\n\n\n# =============================================================================\n# Save report\n# =============================================================================\n\nreport_df = pd.DataFrame(tflite_export_results)\n\nreport_csv_path = TFLITE_EXPORT_DIR / \"tflite_export_report.csv\"\nreport_json_path = TFLITE_EXPORT_DIR / \"tflite_export_report.json\"\n\nreport_df.to_csv(report_csv_path, index=False)\n\nwith open(report_json_path, \"w\") as f:\n    json.dump(tflite_export_results, f, indent=4)\n\nprint(\"\\n\" + \"=\" * 90)\nprint(\"TFLite export summary\")\nprint(\"=\" * 90)\n\ndisplay(report_df)\n\nprint(\"Report CSV :\", report_csv_path)\nprint(\"Report JSON:\", report_json_path)\n\n\n# =============================================================================\n# Archive all exported TFLite files\n# =============================================================================\n\nzip_base = \"/kaggle/working/tflite_exports_all\"\n\nif os.path.exists(zip_base + \".zip\"):\n    os.remove(zip_base + \".zip\")\n\nzip_path = shutil.make_archive(\n    base_name=zip_base,\n    format=\"zip\",\n    root_dir=str(TFLITE_EXPORT_DIR)\n)\n\nprint(\"\\nZIP archive created:\")\nprint(zip_path)\n\nprint(\"\\nExported TFLite files:\")\nfor p in sorted(TFLITE_EXPORT_DIR.glob(\"*.tflite\")):\n    print(f\" - {p.name} | {p.stat().st_size / (1024 ** 2):.2f} MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:35:54.923857Z","iopub.execute_input":"2026-04-29T17:35:54.924157Z","iopub.status.idle":"2026-04-29T17:36:39.783786Z","shell.execute_reply.started":"2026-04-29T17:35:54.924133Z","shell.execute_reply":"2026-04-29T17:36:39.783083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport json\n\nreport_csv = \"/kaggle/working/tflite_exports/tflite_export_report.csv\"\nreport_json = \"/kaggle/working/tflite_exports/tflite_export_report.json\"\n\ndf = pd.read_csv(report_csv)\ndisplay(df)\n\nprint(\"\\nFAILED / NON-SUCCESS MODELS:\")\ndisplay(df[df[\"status\"] != \"success\"])\n\nwith open(report_json, \"r\") as f:\n    report = json.load(f)\n\nfor row in report:\n    if row[\"status\"] != \"success\":\n        print(\"\\n\" + \"=\"*100)\n        print(\"MODEL:\", row[\"model\"])\n        print(\"STATUS:\", row[\"status\"])\n        print(\"ERROR:\")\n        print(row.get(\"error\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:36:39.784961Z","iopub.execute_input":"2026-04-29T17:36:39.785503Z","iopub.status.idle":"2026-04-29T17:36:39.813612Z","shell.execute_reply.started":"2026-04-29T17:36:39.785476Z","shell.execute_reply":"2026-04-29T17:36:39.813064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# FIX FIRST: Export Missing BiLSTM + BiGRU, then verify/import ALL TFLite models\n# =============================================================================\n\nimport os\nimport gc\nimport json\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom pathlib import Path\nfrom tensorflow.keras import layers, Model, models\n\n# -----------------------------------------------------------------------------\n# Paths\n# -----------------------------------------------------------------------------\n\nTFLITE_EXPORT_DIR = Path(\"/kaggle/working/tflite_exports\")\nTFLITE_EXPORT_DIR.mkdir(parents=True, exist_ok=True)\n\nEXPECTED_TFLITE_MODELS = [\n    \"BiLSTM\",\n    \"BiGRU\",\n    \"Transformer\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\nprint(\"TFLite export directory:\", TFLITE_EXPORT_DIR)\n\n# -----------------------------------------------------------------------------\n# Required notebook variables\n# -----------------------------------------------------------------------------\n\nrequired_vars = [\"loaded_models\", \"MAX_LEN\", \"CHANNELS\", \"NUM_CLASSES\", \"test_dataset\"]\n\nmissing_vars = [v for v in required_vars if v not in globals()]\nif missing_vars:\n    raise RuntimeError(f\"Missing required variables from notebook runtime: {missing_vars}\")\n\n# Force float32 while rebuilding RNN models to avoid cuDNN / mixed-float16 traces.\ntry:\n    tf.keras.mixed_precision.set_global_policy(\"float32\")\nexcept Exception:\n    pass\n\n\n# =============================================================================\n# Utility: real validation sample\n# =============================================================================\n\ndef get_one_real_sample(dataset):\n    for xb, yb in dataset.unbatch().batch(1).take(1):\n        return xb.numpy().astype(np.float32)\n    raise RuntimeError(\"Could not get sample from test_dataset.\")\n\nvalidation_x = get_one_real_sample(test_dataset)\n\nprint(\"Validation sample:\", validation_x.shape, validation_x.dtype)\n\n\n# =============================================================================\n# Custom helper layers for BiGRU rebuild\n# =============================================================================\n\nclass TFLiteMaskableConv1D(layers.Conv1D):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.supports_masking = True\n\n\nclass TFLiteSqueeze(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n\n    def call(self, x):\n        return tf.squeeze(x, axis=self.axis)\n\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass TFLiteExpandDims(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n\n    def call(self, x):\n        return tf.expand_dims(x, axis=self.axis)\n\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass TFLiteReduceSum(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n\n    def call(self, x):\n        return tf.reduce_sum(x, axis=self.axis)\n\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass TFLiteMaskableGlobalMaxPool1D(layers.GlobalMaxPooling1D):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.supports_masking = True\n\n\n# =============================================================================\n# CPU-compatible rebuilds for BiLSTM and BiGRU\n# =============================================================================\n\ndef build_bilstm_cpu_model(max_len, channels, num_classes, dim=256, dropout_rate=0.4):\n    inp = layers.Input(shape=(max_len, channels), name=\"input_features\")\n\n    x = layers.Masking(mask_value=-100.0, name=\"masking_layer\")(inp)\n\n    x = layers.Dense(dim, activation=\"swish\", name=\"feature_dense\", dtype=\"float32\")(x)\n    x = layers.BatchNormalization(name=\"bn_1\", dtype=\"float32\")(x)\n    x = layers.Dropout(dropout_rate, name=\"dropout\", dtype=\"float32\")(x)\n\n    x = layers.Bidirectional(\n        layers.LSTM(\n            dim,\n            return_sequences=True,\n            implementation=1,\n            unroll=False,\n            dropout=0.0,\n            recurrent_dropout=0.0,\n            dtype=\"float32\",\n            name=\"lstm\",\n        ),\n        name=\"bilstm_1\",\n        dtype=\"float32\",\n    )(x)\n\n    x = layers.Dropout(0.2, name=\"dropout_1\", dtype=\"float32\")(x)\n\n    x = layers.Bidirectional(\n        layers.LSTM(\n            dim // 2,\n            return_sequences=False,\n            implementation=1,\n            unroll=False,\n            dropout=0.0,\n            recurrent_dropout=0.0,\n            dtype=\"float32\",\n            name=\"lstm\",\n        ),\n        name=\"bilstm_2\",\n        dtype=\"float32\",\n    )(x)\n\n    x = layers.Dropout(0.2, name=\"dropout_2\", dtype=\"float32\")(x)\n\n    x = layers.Dense(128, activation=\"swish\", name=\"dense_head\", dtype=\"float32\")(x)\n    x = layers.BatchNormalization(name=\"bn_2\", dtype=\"float32\")(x)\n    out = layers.Dense(num_classes, name=\"classifier\", dtype=\"float32\")(x)\n\n    return models.Model(inp, out, name=\"BiLSTM_CPU_TFLite\")\n\n\ndef build_bigru_cpu_model(max_len, channels, num_classes, dim=256, dropout_rate=0.4):\n    inp = layers.Input(shape=(max_len, channels), name=\"input_features\")\n\n    x = layers.Masking(mask_value=-100.0, name=\"masking\")(inp)\n\n    x = TFLiteMaskableConv1D(\n        dim,\n        kernel_size=3,\n        padding=\"same\",\n        use_bias=False,\n        name=\"stem_conv\",\n        dtype=\"float32\",\n    )(x)\n\n    x = layers.BatchNormalization(momentum=0.95, name=\"stem_bn\", dtype=\"float32\")(x)\n    x = layers.Activation(\"swish\", name=\"stem_act\", dtype=\"float32\")(x)\n    x = layers.SpatialDropout1D(dropout_rate, name=\"stem_drop\", dtype=\"float32\")(x)\n\n    skip1 = layers.Dense(dim * 2, use_bias=False, name=\"skip1_proj\", dtype=\"float32\")(x)\n\n    x = layers.Bidirectional(\n        layers.GRU(\n            dim,\n            return_sequences=True,\n            reset_after=True,\n            implementation=1,\n            unroll=False,\n            dropout=0.0,\n            recurrent_dropout=0.0,\n            dtype=\"float32\",\n            name=\"gru\",\n        ),\n        name=\"bigru_1\",\n        dtype=\"float32\",\n    )(x)\n\n    x = layers.LayerNormalization(epsilon=1e-6, name=\"ln_1\", dtype=\"float32\")(x)\n    x = layers.Add(name=\"res_1\")([x, skip1])\n\n    skip2 = layers.Dense(dim, use_bias=False, name=\"skip2_proj\", dtype=\"float32\")(x)\n\n    x = layers.Bidirectional(\n        layers.GRU(\n            dim // 2,\n            return_sequences=True,\n            reset_after=True,\n            implementation=1,\n            unroll=False,\n            dropout=0.0,\n            recurrent_dropout=0.0,\n            dtype=\"float32\",\n            name=\"gru\",\n        ),\n        name=\"bigru_2\",\n        dtype=\"float32\",\n    )(x)\n\n    x = layers.LayerNormalization(epsilon=1e-6, name=\"ln_2\", dtype=\"float32\")(x)\n    x = layers.Add(name=\"res_2\")([x, skip2])\n\n    attn = layers.Dense(dim // 4, activation=\"tanh\", name=\"attn_proj\", dtype=\"float32\")(x)\n    attn = layers.Dense(1, name=\"attn_score\", dtype=\"float32\")(attn)\n    attn = TFLiteSqueeze(axis=-1, name=\"attn_squeeze\", dtype=\"float32\")(attn)\n    attn = layers.Softmax(axis=1, name=\"attn_weights\", dtype=\"float32\")(attn)\n    attn = TFLiteExpandDims(axis=-1, name=\"attn_expand\", dtype=\"float32\")(attn)\n\n    x_attn = layers.Multiply(name=\"attn_apply\")([x, attn])\n    x_attn = TFLiteReduceSum(axis=1, name=\"attn_sum\", dtype=\"float32\")(x_attn)\n\n    gap = layers.GlobalAveragePooling1D(name=\"gap\", dtype=\"float32\")(x)\n    gmp = TFLiteMaskableGlobalMaxPool1D(name=\"gmp\", dtype=\"float32\")(x)\n\n    fused = layers.Concatenate(name=\"concat\")([x_attn, gap, gmp])\n    fused = layers.Dense(dim, use_bias=False, name=\"pool_proj\", dtype=\"float32\")(fused)\n    fused = layers.LayerNormalization(epsilon=1e-6, name=\"pool_ln\", dtype=\"float32\")(fused)\n    fused = layers.Activation(\"swish\", name=\"pool_act\", dtype=\"float32\")(fused)\n\n    x = layers.Dense(dim, activation=\"swish\", use_bias=False, name=\"head_dense\", dtype=\"float32\")(fused)\n    x = layers.BatchNormalization(momentum=0.95, name=\"head_bn\", dtype=\"float32\")(x)\n    x = layers.Dropout(dropout_rate + 0.1, name=\"head_drop\", dtype=\"float32\")(x)\n\n    out = layers.Dense(num_classes, dtype=\"float32\", name=\"classifier\")(x)\n\n    return Model(inp, out, name=\"BiGRU_CPU_TFLite\")\n\n\n# =============================================================================\n# Weight copy\n# =============================================================================\n\ndef copy_weights_by_layer_name(src_model, dst_model):\n    src_layers = {layer.name: layer for layer in src_model.layers}\n\n    copied = []\n    skipped = []\n\n    for dst_layer in dst_model.layers:\n        if dst_layer.name not in src_layers:\n            skipped.append({\n                \"layer\": dst_layer.name,\n                \"reason\": \"missing_in_source\",\n            })\n            continue\n\n        src_layer = src_layers[dst_layer.name]\n        src_w = src_layer.get_weights()\n        dst_w = dst_layer.get_weights()\n\n        if len(src_w) == 0 and len(dst_w) == 0:\n            continue\n\n        if len(src_w) != len(dst_w):\n            skipped.append({\n                \"layer\": dst_layer.name,\n                \"reason\": f\"weight_count_mismatch {len(src_w)} != {len(dst_w)}\",\n            })\n            continue\n\n        if not all(a.shape == b.shape for a, b in zip(src_w, dst_w)):\n            skipped.append({\n                \"layer\": dst_layer.name,\n                \"reason\": \"shape_mismatch\",\n                \"shapes\": [(a.shape, b.shape) for a, b in zip(src_w, dst_w)],\n            })\n            continue\n\n        dst_layer.set_weights([w.astype(np.float32) for w in src_w])\n        copied.append(dst_layer.name)\n\n    return copied, skipped\n\n\n# =============================================================================\n# Conversion\n# =============================================================================\n\ndef convert_to_tflite_from_concrete(model, out_path):\n    @tf.function(\n        input_signature=[\n            tf.TensorSpec(\n                shape=[None, MAX_LEN, CHANNELS],\n                dtype=tf.float32,\n                name=\"input_features\",\n            )\n        ]\n    )\n    def serving_fn(x):\n        logits = model(x, training=False)\n        return {\"logits\": tf.cast(logits, tf.float32)}\n\n    concrete_func = serving_fn.get_concrete_function()\n\n    converter = tf.lite.TFLiteConverter.from_concrete_functions(\n        [concrete_func],\n        model,\n    )\n\n    converter.target_spec.supported_ops = [\n        tf.lite.OpsSet.TFLITE_BUILTINS,\n        tf.lite.OpsSet.SELECT_TF_OPS,\n    ]\n\n    converter.experimental_enable_resource_variables = True\n\n    try:\n        tflite_bytes = converter.convert()\n        method = \"concrete_tensorlist_lowering_enabled\"\n\n    except Exception as e1:\n        print(\"First conversion failed. Retrying with TensorList lowering disabled.\")\n        print(str(e1))\n\n        converter = tf.lite.TFLiteConverter.from_concrete_functions(\n            [concrete_func],\n            model,\n        )\n\n        converter.target_spec.supported_ops = [\n            tf.lite.OpsSet.TFLITE_BUILTINS,\n            tf.lite.OpsSet.SELECT_TF_OPS,\n        ]\n\n        converter._experimental_lower_tensor_list_ops = False\n        converter.experimental_enable_resource_variables = True\n\n        tflite_bytes = converter.convert()\n        method = \"concrete_tensorlist_lowering_disabled\"\n\n    with open(out_path, \"wb\") as f:\n        f.write(tflite_bytes)\n\n    return method\n\n\ndef validate_tflite_file(model, tflite_path, x):\n    keras_y = model(x, training=False).numpy().astype(np.float32)\n\n    interpreter = tf.lite.Interpreter(model_path=str(tflite_path))\n\n    input_details_before = interpreter.get_input_details()\n    input_index = input_details_before[0][\"index\"]\n\n    # Resize dynamic batch to 1 before allocation.\n    interpreter.resize_tensor_input(\n        input_index,\n        np.array([x.shape[0], MAX_LEN, CHANNELS], dtype=np.int32),\n        strict=False,\n    )\n\n    interpreter.allocate_tensors()\n\n    inp = interpreter.get_input_details()[0]\n    out = interpreter.get_output_details()[0]\n\n    interpreter.set_tensor(inp[\"index\"], x.astype(inp[\"dtype\"]))\n    interpreter.invoke()\n\n    tflite_y = interpreter.get_tensor(out[\"index\"]).astype(np.float32)\n\n    return {\n        \"keras_top1\": int(np.argmax(keras_y, axis=-1)[0]),\n        \"tflite_top1\": int(np.argmax(tflite_y, axis=-1)[0]),\n        \"top1_match\": bool(np.argmax(keras_y, axis=-1)[0] == np.argmax(tflite_y, axis=-1)[0]),\n        \"max_abs_diff\": float(np.max(np.abs(keras_y - tflite_y))),\n        \"mean_abs_diff\": float(np.mean(np.abs(keras_y - tflite_y))),\n        \"input_shape\": str(inp[\"shape\"]),\n        \"output_shape\": str(out[\"shape\"]),\n        \"input_dtype\": str(inp[\"dtype\"]),\n        \"output_dtype\": str(out[\"dtype\"]),\n    }\n\n\n# =============================================================================\n# Export missing RNN models\n# =============================================================================\n\ndef existing_tflite_for_model(model_name):\n    return sorted(TFLITE_EXPORT_DIR.glob(f\"{model_name}*.tflite\"))\n\n\ndef export_rnn_if_missing(model_name):\n    existing = existing_tflite_for_model(model_name)\n\n    if len(existing) > 0:\n        print(f\"[OK] {model_name} already exists:\")\n        for p in existing:\n            print(\"  -\", p.name)\n        return {\n            \"model\": model_name,\n            \"status\": \"already_exists\",\n            \"path\": str(existing[0]),\n        }\n\n    if model_name not in loaded_models:\n        print(f\"[MISSING] {model_name} not found in loaded_models\")\n        return {\n            \"model\": model_name,\n            \"status\": \"missing_in_loaded_models\",\n        }\n\n    print(\"\\n\" + \"=\" * 100)\n    print(f\"Exporting missing model: {model_name}\")\n    print(\"=\" * 100)\n\n    src_model = loaded_models[model_name]\n\n    if model_name == \"BiLSTM\":\n        rebuilt = build_bilstm_cpu_model(MAX_LEN, CHANNELS, NUM_CLASSES)\n\n    elif model_name == \"BiGRU\":\n        rebuilt = build_bigru_cpu_model(MAX_LEN, CHANNELS, NUM_CLASSES)\n\n    else:\n        raise ValueError(f\"Unsupported RNN model: {model_name}\")\n\n    # Build once.\n    _ = rebuilt(validation_x, training=False)\n\n    copied, skipped = copy_weights_by_layer_name(src_model, rebuilt)\n\n    print(\"Copied layers:\", copied)\n    print(\"Skipped layers:\", skipped)\n\n    # Compare original loaded model vs rebuilt CPU model.\n    src_y = src_model(validation_x, training=False).numpy().astype(np.float32)\n    new_y = rebuilt(validation_x, training=False).numpy().astype(np.float32)\n\n    original_top1 = int(np.argmax(src_y, axis=-1)[0])\n    rebuilt_top1 = int(np.argmax(new_y, axis=-1)[0])\n\n    rebuild_max_diff = float(np.max(np.abs(src_y - new_y)))\n    rebuild_mean_diff = float(np.mean(np.abs(src_y - new_y)))\n\n    print(\"Original top1:\", original_top1)\n    print(\"Rebuilt top1 :\", rebuilt_top1)\n    print(\"Top1 match   :\", original_top1 == rebuilt_top1)\n    print(\"Max diff     :\", rebuild_max_diff)\n    print(\"Mean diff    :\", rebuild_mean_diff)\n\n    out_path = TFLITE_EXPORT_DIR / f\"{model_name}_float32_logits_cpu_rnn.tflite\"\n\n    method = convert_to_tflite_from_concrete(rebuilt, out_path)\n\n    val = validate_tflite_file(rebuilt, out_path, validation_x)\n\n    print(\"Saved:\", out_path)\n    print(\"Size MB:\", out_path.stat().st_size / (1024 ** 2))\n    print(\"Conversion method:\", method)\n    print(\"TFLite validation:\", val)\n\n    return {\n        \"model\": model_name,\n        \"status\": \"success\",\n        \"path\": str(out_path),\n        \"size_mb\": float(out_path.stat().st_size / (1024 ** 2)),\n        \"conversion_method\": method,\n        \"copied_layers\": copied,\n        \"skipped_layers\": skipped,\n        \"original_top1\": original_top1,\n        \"rebuilt_top1\": rebuilt_top1,\n        \"original_rebuilt_top1_match\": bool(original_top1 == rebuilt_top1),\n        \"original_rebuilt_max_abs_diff\": rebuild_max_diff,\n        \"original_rebuilt_mean_abs_diff\": rebuild_mean_diff,\n        **val,\n    }\n\n\nrnn_export_results = []\n\nfor model_name in [\"BiLSTM\", \"BiGRU\"]:\n    try:\n        result = export_rnn_if_missing(model_name)\n        rnn_export_results.append(result)\n\n    except Exception:\n        err = traceback.format_exc()\n        print(f\"[FAILED] {model_name}\")\n        print(err)\n\n        rnn_export_results.append({\n            \"model\": model_name,\n            \"status\": \"failed\",\n            \"error\": err,\n        })\n\n    gc.collect()\n\n\nrnn_export_df = pd.DataFrame(rnn_export_results)\ndisplay(rnn_export_df)\n\nrnn_export_df.to_csv(TFLITE_EXPORT_DIR / \"rnn_missing_export_report.csv\", index=False)\n\nwith open(TFLITE_EXPORT_DIR / \"rnn_missing_export_report.json\", \"w\") as f:\n    json.dump(rnn_export_results, f, indent=4, default=str)\n\n\n# =============================================================================\n# Verify all expected TFLite files exist\n# =============================================================================\n\ndef find_best_tflite_file(model_name):\n    files = sorted(TFLITE_EXPORT_DIR.glob(f\"{model_name}*.tflite\"))\n\n    if len(files) == 0:\n        return None\n\n    # Prefer CPU RNN fixed files for BiLSTM/BiGRU.\n    cpu_files = [p for p in files if \"cpu_rnn\" in p.name]\n    if len(cpu_files) > 0:\n        return cpu_files[0]\n\n    return files[0]\n\n\ntflite_registry = {}\n\nfor model_name in EXPECTED_TFLITE_MODELS:\n    path = find_best_tflite_file(model_name)\n    tflite_registry[model_name] = path\n\nregistry_rows = []\n\nfor model_name, path in tflite_registry.items():\n    registry_rows.append({\n        \"model\": model_name,\n        \"found\": path is not None,\n        \"file\": None if path is None else path.name,\n        \"path\": None if path is None else str(path),\n        \"size_mb\": None if path is None else path.stat().st_size / (1024 ** 2),\n    })\n\nregistry_df = pd.DataFrame(registry_rows)\n\nprint(\"\\n\" + \"=\" * 100)\nprint(\"TFLite Registry\")\nprint(\"=\" * 100)\n\ndisplay(registry_df)\n\nmissing_after_fix = registry_df[registry_df[\"found\"] == False][\"model\"].tolist()\n\nif len(missing_after_fix) > 0:\n    print(\"Still missing:\", missing_after_fix)\nelse:\n    print(\"All expected TFLite models were found.\")\n\n\n# =============================================================================\n# Import/load all found TFLite models as interpreters\n# =============================================================================\n\ntflite_interpreters = {}\ntflite_io_details = {}\n\nfor model_name, path in tflite_registry.items():\n    if path is None:\n        continue\n\n    try:\n        interpreter = tf.lite.Interpreter(model_path=str(path))\n\n        # Allocate with batch=1 for import/smoke test.\n        inp_before = interpreter.get_input_details()[0]\n        interpreter.resize_tensor_input(\n            inp_before[\"index\"],\n            np.array([1, MAX_LEN, CHANNELS], dtype=np.int32),\n            strict=False,\n        )\n\n        interpreter.allocate_tensors()\n\n        inp = interpreter.get_input_details()[0]\n        out = interpreter.get_output_details()[0]\n\n        # Smoke test\n        interpreter.set_tensor(inp[\"index\"], validation_x.astype(inp[\"dtype\"]))\n        interpreter.invoke()\n        y = interpreter.get_tensor(out[\"index\"])\n\n        tflite_interpreters[model_name] = interpreter\n        tflite_io_details[model_name] = {\n            \"file\": path.name,\n            \"path\": str(path),\n            \"input_shape\": str(inp[\"shape\"]),\n            \"input_dtype\": str(inp[\"dtype\"]),\n            \"output_shape\": str(out[\"shape\"]),\n            \"output_dtype\": str(out[\"dtype\"]),\n            \"top1_sample\": int(np.argmax(y, axis=-1)[0]),\n        }\n\n        print(f\"[LOADED] {model_name}: {path.name}\")\n\n    except Exception:\n        print(f\"[LOAD FAILED] {model_name}: {path}\")\n        print(traceback.format_exc())\n\n\ntflite_io_df = pd.DataFrame([\n    {\"model\": name, **details}\n    for name, details in tflite_io_details.items()\n])\n\nprint(\"\\nLoaded TFLite interpreters:\")\ndisplay(tflite_io_df)\n\nprint(\"\\nAvailable objects now:\")\nprint(\"- tflite_registry      : model name -> TFLite file path\")\nprint(\"- tflite_interpreters : model name -> loaded tf.lite.Interpreter\")\nprint(\"- tflite_io_details   : input/output metadata\")\n\nprint(\"\\nLoaded model names:\")\nprint(list(tflite_interpreters.keys()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:36:39.814640Z","iopub.execute_input":"2026-04-29T17:36:39.814931Z","iopub.status.idle":"2026-04-29T17:36:48.587316Z","shell.execute_reply.started":"2026-04-29T17:36:39.814909Z","shell.execute_reply":"2026-04-29T17:36:48.586437Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# EXPORT MISSING BiLSTM + BiGRU USING FIXED-BATCH UNROLLED RNN\n# Then verify/import all 5 TFLite models\n# =============================================================================\n\nimport os\nimport gc\nimport json\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom pathlib import Path\nfrom tensorflow.keras import layers, Model, models\n\nTFLITE_EXPORT_DIR = Path(\"/kaggle/working/tflite_exports\")\nTFLITE_EXPORT_DIR.mkdir(parents=True, exist_ok=True)\n\nEXPECTED_TFLITE_MODELS = [\n    \"BiLSTM\",\n    \"BiGRU\",\n    \"Transformer\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\ntry:\n    tf.keras.mixed_precision.set_global_policy(\"float32\")\nexcept Exception:\n    pass\n\n\n# =============================================================================\n# Validation sample, fixed batch = 1\n# =============================================================================\n\ndef get_one_real_sample(dataset):\n    for xb, yb in dataset.unbatch().batch(1).take(1):\n        return xb.numpy().astype(np.float32)\n    raise RuntimeError(\"Could not get sample from test_dataset.\")\n\nvalidation_x = get_one_real_sample(test_dataset)\n\nprint(\"validation_x:\", validation_x.shape, validation_x.dtype)\n\n\n# =============================================================================\n# Custom helper layers for BiGRU\n# =============================================================================\n\nclass TFLiteMaskableConv1D(layers.Conv1D):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.supports_masking = True\n\n\nclass TFLiteSqueeze(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n\n    def call(self, x):\n        return tf.squeeze(x, axis=self.axis)\n\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass TFLiteExpandDims(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n\n    def call(self, x):\n        return tf.expand_dims(x, axis=self.axis)\n\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass TFLiteReduceSum(layers.Layer):\n    def __init__(self, axis, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n        self.supports_masking = True\n\n    def call(self, x):\n        return tf.reduce_sum(x, axis=self.axis)\n\n    def get_config(self):\n        return {**super().get_config(), \"axis\": self.axis}\n\n\nclass TFLiteMaskableGlobalMaxPool1D(layers.GlobalMaxPooling1D):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.supports_masking = True\n\n\n# =============================================================================\n# RNN constructors: fixed unrolled CPU/export version\n# =============================================================================\n\ndef make_export_lstm(units, return_sequences, name=None):\n    kwargs = dict(\n        units=units,\n        return_sequences=return_sequences,\n        dropout=0.0,\n        recurrent_dropout=0.0,\n        unroll=True,\n        implementation=1,\n        dtype=\"float32\",\n        name=name,\n    )\n\n    try:\n        return layers.LSTM(**kwargs, use_cudnn=False)\n    except TypeError:\n        return layers.LSTM(**kwargs)\n\n\ndef make_export_gru(units, return_sequences, name=None):\n    kwargs = dict(\n        units=units,\n        return_sequences=return_sequences,\n        dropout=0.0,\n        recurrent_dropout=0.0,\n        reset_after=True,\n        unroll=True,\n        implementation=1,\n        dtype=\"float32\",\n        name=name,\n    )\n\n    try:\n        return layers.GRU(**kwargs, use_cudnn=False)\n    except TypeError:\n        return layers.GRU(**kwargs)\n\n\n# =============================================================================\n# Fixed-batch builders\n# =============================================================================\n\ndef build_bilstm_unrolled_b1(max_len, channels, num_classes, dim=256, dropout_rate=0.4):\n    with tf.device(\"/CPU:0\"):\n        inp = layers.Input(\n            batch_shape=(1, max_len, channels),\n            name=\"input_features\",\n            dtype=\"float32\",\n        )\n\n        x = layers.Masking(mask_value=-100.0, name=\"masking_layer\")(inp)\n\n        x = layers.Dense(dim, activation=\"swish\", name=\"feature_dense\", dtype=\"float32\")(x)\n        x = layers.BatchNormalization(name=\"bn_1\", dtype=\"float32\")(x)\n        x = layers.Dropout(dropout_rate, name=\"dropout\", dtype=\"float32\")(x)\n\n        x = layers.Bidirectional(\n            make_export_lstm(dim, return_sequences=True, name=\"lstm\"),\n            name=\"bilstm_1\",\n            dtype=\"float32\",\n        )(x)\n\n        x = layers.Dropout(0.2, name=\"dropout_1\", dtype=\"float32\")(x)\n\n        x = layers.Bidirectional(\n            make_export_lstm(dim // 2, return_sequences=False, name=\"lstm\"),\n            name=\"bilstm_2\",\n            dtype=\"float32\",\n        )(x)\n\n        x = layers.Dropout(0.2, name=\"dropout_2\", dtype=\"float32\")(x)\n\n        x = layers.Dense(128, activation=\"swish\", name=\"dense_head\", dtype=\"float32\")(x)\n        x = layers.BatchNormalization(name=\"bn_2\", dtype=\"float32\")(x)\n        out = layers.Dense(num_classes, name=\"classifier\", dtype=\"float32\")(x)\n\n        return models.Model(inp, out, name=\"BiLSTM_Unrolled_B1_TFLite\")\n\n\ndef build_bigru_unrolled_b1(max_len, channels, num_classes, dim=256, dropout_rate=0.4):\n    with tf.device(\"/CPU:0\"):\n        inp = layers.Input(\n            batch_shape=(1, max_len, channels),\n            name=\"input_features\",\n            dtype=\"float32\",\n        )\n\n        x = layers.Masking(mask_value=-100.0, name=\"masking\")(inp)\n\n        x = TFLiteMaskableConv1D(\n            dim,\n            kernel_size=3,\n            padding=\"same\",\n            use_bias=False,\n            name=\"stem_conv\",\n            dtype=\"float32\",\n        )(x)\n\n        x = layers.BatchNormalization(momentum=0.95, name=\"stem_bn\", dtype=\"float32\")(x)\n        x = layers.Activation(\"swish\", name=\"stem_act\", dtype=\"float32\")(x)\n        x = layers.SpatialDropout1D(dropout_rate, name=\"stem_drop\", dtype=\"float32\")(x)\n\n        skip1 = layers.Dense(dim * 2, use_bias=False, name=\"skip1_proj\", dtype=\"float32\")(x)\n\n        x = layers.Bidirectional(\n            make_export_gru(dim, return_sequences=True, name=\"gru\"),\n            name=\"bigru_1\",\n            dtype=\"float32\",\n        )(x)\n\n        x = layers.LayerNormalization(epsilon=1e-6, name=\"ln_1\", dtype=\"float32\")(x)\n        x = layers.Add(name=\"res_1\")([x, skip1])\n\n        skip2 = layers.Dense(dim, use_bias=False, name=\"skip2_proj\", dtype=\"float32\")(x)\n\n        x = layers.Bidirectional(\n            make_export_gru(dim // 2, return_sequences=True, name=\"gru\"),\n            name=\"bigru_2\",\n            dtype=\"float32\",\n        )(x)\n\n        x = layers.LayerNormalization(epsilon=1e-6, name=\"ln_2\", dtype=\"float32\")(x)\n        x = layers.Add(name=\"res_2\")([x, skip2])\n\n        attn = layers.Dense(dim // 4, activation=\"tanh\", name=\"attn_proj\", dtype=\"float32\")(x)\n        attn = layers.Dense(1, name=\"attn_score\", dtype=\"float32\")(attn)\n        attn = TFLiteSqueeze(axis=-1, name=\"attn_squeeze\", dtype=\"float32\")(attn)\n        attn = layers.Softmax(axis=1, name=\"attn_weights\", dtype=\"float32\")(attn)\n        attn = TFLiteExpandDims(axis=-1, name=\"attn_expand\", dtype=\"float32\")(attn)\n\n        x_attn = layers.Multiply(name=\"attn_apply\")([x, attn])\n        x_attn = TFLiteReduceSum(axis=1, name=\"attn_sum\", dtype=\"float32\")(x_attn)\n\n        gap = layers.GlobalAveragePooling1D(name=\"gap\", dtype=\"float32\")(x)\n        gmp = TFLiteMaskableGlobalMaxPool1D(name=\"gmp\", dtype=\"float32\")(x)\n\n        fused = layers.Concatenate(name=\"concat\")([x_attn, gap, gmp])\n        fused = layers.Dense(dim, use_bias=False, name=\"pool_proj\", dtype=\"float32\")(fused)\n        fused = layers.LayerNormalization(epsilon=1e-6, name=\"pool_ln\", dtype=\"float32\")(fused)\n        fused = layers.Activation(\"swish\", name=\"pool_act\", dtype=\"float32\")(fused)\n\n        x = layers.Dense(dim, activation=\"swish\", use_bias=False, name=\"head_dense\", dtype=\"float32\")(fused)\n        x = layers.BatchNormalization(momentum=0.95, name=\"head_bn\", dtype=\"float32\")(x)\n        x = layers.Dropout(dropout_rate + 0.1, name=\"head_drop\", dtype=\"float32\")(x)\n\n        out = layers.Dense(num_classes, dtype=\"float32\", name=\"classifier\")(x)\n\n        return Model(inp, out, name=\"BiGRU_Unrolled_B1_TFLite\")\n\n\n# =============================================================================\n# Weight transfer\n# =============================================================================\n\ndef copy_weights_by_layer_name(src_model, dst_model):\n    src_layers = {layer.name: layer for layer in src_model.layers}\n\n    copied = []\n    skipped = []\n\n    for dst_layer in dst_model.layers:\n        if dst_layer.name not in src_layers:\n            skipped.append({\n                \"layer\": dst_layer.name,\n                \"reason\": \"missing_in_source\",\n            })\n            continue\n\n        src_layer = src_layers[dst_layer.name]\n        src_w = src_layer.get_weights()\n        dst_w = dst_layer.get_weights()\n\n        if len(src_w) == 0 and len(dst_w) == 0:\n            continue\n\n        if len(src_w) != len(dst_w):\n            skipped.append({\n                \"layer\": dst_layer.name,\n                \"reason\": f\"weight_count_mismatch {len(src_w)} != {len(dst_w)}\",\n            })\n            continue\n\n        if not all(a.shape == b.shape for a, b in zip(src_w, dst_w)):\n            skipped.append({\n                \"layer\": dst_layer.name,\n                \"reason\": \"shape_mismatch\",\n                \"shapes\": [(a.shape, b.shape) for a, b in zip(src_w, dst_w)],\n            })\n            continue\n\n        dst_layer.set_weights([w.astype(np.float32) for w in src_w])\n        copied.append(dst_layer.name)\n\n    return copied, skipped\n\n\n# =============================================================================\n# Conversion + validation\n# =============================================================================\n\ndef convert_fixed_b1_model(model, out_path):\n    with tf.device(\"/CPU:0\"):\n        @tf.function(\n            input_signature=[\n                tf.TensorSpec(\n                    shape=[1, MAX_LEN, CHANNELS],\n                    dtype=tf.float32,\n                    name=\"input_features\",\n                )\n            ]\n        )\n        def serving_fn(x):\n            y = model(x, training=False)\n            return {\"logits\": tf.cast(y, tf.float32)}\n\n        concrete_func = serving_fn.get_concrete_function()\n\n    converter = tf.lite.TFLiteConverter.from_concrete_functions(\n        [concrete_func],\n        model,\n    )\n\n    converter.target_spec.supported_ops = [\n        tf.lite.OpsSet.TFLITE_BUILTINS,\n        tf.lite.OpsSet.SELECT_TF_OPS,\n    ]\n\n    converter.experimental_enable_resource_variables = True\n    converter.inference_input_type = tf.float32\n    converter.inference_output_type = tf.float32\n\n    tflite_bytes = converter.convert()\n\n    with open(out_path, \"wb\") as f:\n        f.write(tflite_bytes)\n\n\ndef validate_fixed_b1_tflite(model, tflite_path, x):\n    keras_y = model(x, training=False).numpy().astype(np.float32)\n\n    interpreter = tf.lite.Interpreter(model_path=str(tflite_path))\n    interpreter.allocate_tensors()\n\n    inp = interpreter.get_input_details()[0]\n    out = interpreter.get_output_details()[0]\n\n    interpreter.set_tensor(inp[\"index\"], x.astype(inp[\"dtype\"]))\n    interpreter.invoke()\n\n    tflite_y = interpreter.get_tensor(out[\"index\"]).astype(np.float32)\n\n    return {\n        \"keras_top1\": int(np.argmax(keras_y, axis=-1)[0]),\n        \"tflite_top1\": int(np.argmax(tflite_y, axis=-1)[0]),\n        \"top1_match\": bool(np.argmax(keras_y, axis=-1)[0] == np.argmax(tflite_y, axis=-1)[0]),\n        \"max_abs_diff\": float(np.max(np.abs(keras_y - tflite_y))),\n        \"mean_abs_diff\": float(np.mean(np.abs(keras_y - tflite_y))),\n        \"input_shape\": str(inp[\"shape\"]),\n        \"output_shape\": str(out[\"shape\"]),\n        \"input_dtype\": str(inp[\"dtype\"]),\n        \"output_dtype\": str(out[\"dtype\"]),\n    }\n\n\n# =============================================================================\n# Export missing models\n# =============================================================================\n\nunrolled_export_results = []\n\nfor model_name in [\"BiLSTM\", \"BiGRU\"]:\n    print(\"\\n\" + \"=\" * 100)\n    print(\"EXPORT FIXED-BATCH UNROLLED:\", model_name)\n    print(\"=\" * 100)\n\n    try:\n        if model_name not in loaded_models:\n            raise RuntimeError(f\"{model_name} not in loaded_models. Keys: {list(loaded_models.keys())}\")\n\n        src_model = loaded_models[model_name]\n\n        if model_name == \"BiLSTM\":\n            rebuilt = build_bilstm_unrolled_b1(MAX_LEN, CHANNELS, NUM_CLASSES)\n            out_path = TFLITE_EXPORT_DIR / \"BiLSTM_float32_logits_unrolled_b1.tflite\"\n        else:\n            rebuilt = build_bigru_unrolled_b1(MAX_LEN, CHANNELS, NUM_CLASSES)\n            out_path = TFLITE_EXPORT_DIR / \"BiGRU_float32_logits_unrolled_b1.tflite\"\n\n        # Build\n        _ = rebuilt(validation_x, training=False)\n\n        copied, skipped = copy_weights_by_layer_name(src_model, rebuilt)\n\n        print(\"Copied layers:\", copied)\n        print(\"Skipped layers:\", skipped)\n\n        src_y = src_model(validation_x, training=False).numpy().astype(np.float32)\n        rebuilt_y = rebuilt(validation_x, training=False).numpy().astype(np.float32)\n\n        original_top1 = int(np.argmax(src_y, axis=-1)[0])\n        rebuilt_top1 = int(np.argmax(rebuilt_y, axis=-1)[0])\n\n        print(\"Original top1:\", original_top1)\n        print(\"Rebuilt top1 :\", rebuilt_top1)\n        print(\"Top1 match   :\", original_top1 == rebuilt_top1)\n        print(\"Original vs rebuilt max diff :\", float(np.max(np.abs(src_y - rebuilt_y))))\n        print(\"Original vs rebuilt mean diff:\", float(np.mean(np.abs(src_y - rebuilt_y))))\n\n        if out_path.exists():\n            out_path.unlink()\n\n        convert_fixed_b1_model(rebuilt, out_path)\n\n        val = validate_fixed_b1_tflite(rebuilt, out_path, validation_x)\n\n        result = {\n            \"model\": model_name,\n            \"status\": \"success\",\n            \"file\": out_path.name,\n            \"path\": str(out_path),\n            \"size_mb\": float(out_path.stat().st_size / (1024 ** 2)),\n            \"copied_layers\": copied,\n            \"skipped_layers\": skipped,\n            \"original_top1\": original_top1,\n            \"rebuilt_top1\": rebuilt_top1,\n            \"original_rebuilt_top1_match\": bool(original_top1 == rebuilt_top1),\n            \"original_rebuilt_max_abs_diff\": float(np.max(np.abs(src_y - rebuilt_y))),\n            \"original_rebuilt_mean_abs_diff\": float(np.mean(np.abs(src_y - rebuilt_y))),\n            **val,\n        }\n\n        print(\"SAVED:\", out_path)\n        print(\"SIZE MB:\", result[\"size_mb\"])\n        print(\"VALIDATION:\", val)\n\n    except Exception:\n        result = {\n            \"model\": model_name,\n            \"status\": \"failed\",\n            \"error\": traceback.format_exc(),\n        }\n\n        print(\"FAILED:\", model_name)\n        print(result[\"error\"])\n\n    unrolled_export_results.append(result)\n    gc.collect()\n\n\nunrolled_export_df = pd.DataFrame(unrolled_export_results)\ndisplay(unrolled_export_df)\n\nunrolled_export_df.to_csv(\n    TFLITE_EXPORT_DIR / \"unrolled_b1_rnn_export_report.csv\",\n    index=False,\n)\n\nwith open(TFLITE_EXPORT_DIR / \"unrolled_b1_rnn_export_report.json\", \"w\") as f:\n    json.dump(unrolled_export_results, f, indent=4, default=str)\n\n\n# =============================================================================\n# Registry: pick best file for each model\n# =============================================================================\n\ndef find_tflite_for_model(model_name):\n    files = sorted(TFLITE_EXPORT_DIR.glob(f\"{model_name}*.tflite\"))\n\n    if not files:\n        return None\n\n    preferences = [\n        \"unrolled_b1\",\n        \"no_cudnn\",\n        \"cpu_rnn\",\n        \"float16_logits\",\n        \"float32_logits\",\n    ]\n\n    for pref in preferences:\n        preferred = [p for p in files if pref in p.name]\n        if preferred:\n            return preferred[0]\n\n    return files[0]\n\n\ntflite_registry = {\n    model_name: find_tflite_for_model(model_name)\n    for model_name in EXPECTED_TFLITE_MODELS\n}\n\nregistry_df = pd.DataFrame([\n    {\n        \"model\": model_name,\n        \"found\": path is not None,\n        \"file\": None if path is None else path.name,\n        \"path\": None if path is None else str(path),\n        \"size_mb\": None if path is None else path.stat().st_size / (1024 ** 2),\n    }\n    for model_name, path in tflite_registry.items()\n])\n\nprint(\"\\nTFLite registry:\")\ndisplay(registry_df)\n\n\n# =============================================================================\n# Import all available TFLite interpreters\n# =============================================================================\n\ntflite_interpreters = {}\ntflite_io_details = {}\n\nfor model_name, path in tflite_registry.items():\n    if path is None:\n        continue\n\n    try:\n        interpreter = tf.lite.Interpreter(model_path=str(path))\n        interpreter.allocate_tensors()\n\n        inp = interpreter.get_input_details()[0]\n        out = interpreter.get_output_details()[0]\n\n        # These unrolled RNN exports are fixed batch=1.\n        # Existing models may also accept batch=1 directly.\n        x = validation_x.astype(inp[\"dtype\"])\n\n        interpreter.set_tensor(inp[\"index\"], x)\n        interpreter.invoke()\n\n        y = interpreter.get_tensor(out[\"index\"])\n\n        tflite_interpreters[model_name] = interpreter\n        tflite_io_details[model_name] = {\n            \"file\": path.name,\n            \"path\": str(path),\n            \"input_shape\": str(inp[\"shape\"]),\n            \"input_dtype\": str(inp[\"dtype\"]),\n            \"output_shape\": str(out[\"shape\"]),\n            \"output_dtype\": str(out[\"dtype\"]),\n            \"top1_sample\": int(np.argmax(y, axis=-1)[0]),\n        }\n\n        print(\"[LOADED]\", model_name, \"=>\", path.name)\n\n    except Exception:\n        print(\"[LOAD FAILED]\", model_name, path)\n        print(traceback.format_exc())\n\n\ntflite_io_df = pd.DataFrame([\n    {\"model\": model_name, **details}\n    for model_name, details in tflite_io_details.items()\n])\n\nprint(\"\\nLoaded TFLite interpreters:\")\ndisplay(tflite_io_df)\n\nprint(\"\\nLoaded model names:\")\nprint(list(tflite_interpreters.keys()))\n\nmissing = [m for m in EXPECTED_TFLITE_MODELS if m not in tflite_interpreters]\nprint(\"\\nMissing after unrolled export:\")\nprint(missing)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:36:48.588446Z","iopub.execute_input":"2026-04-29T17:36:48.588768Z","iopub.status.idle":"2026-04-29T17:42:31.198401Z","shell.execute_reply.started":"2026-04-29T17:36:48.588739Z","shell.execute_reply":"2026-04-29T17:42:31.197758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# FINAL RESEARCH-READY TFLITE BENCHMARK\n# Latency, Throughput, Model Size, Parameters, Tables, Plots, ZIP\n# =============================================================================\n\nimport os\nimport gc\nimport time\nimport json\nimport shutil\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom IPython.display import FileLink, display\n\n# =============================================================================\n# Paths\n# =============================================================================\n\nTFLITE_EXPORT_DIR = Path(\"/kaggle/working/tflite_exports\")\nRESULTS_DIR = Path(\"/kaggle/working/final_tflite_research_benchmark\")\nPLOTS_DIR = RESULTS_DIR / \"plots\"\nTABLES_DIR = RESULTS_DIR / \"tables\"\n\nRESULTS_DIR.mkdir(parents=True, exist_ok=True)\nPLOTS_DIR.mkdir(parents=True, exist_ok=True)\nTABLES_DIR.mkdir(parents=True, exist_ok=True)\n\nEXPECTED_MODELS = [\n    \"BiLSTM\",\n    \"BiGRU\",\n    \"Transformer\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\n# =============================================================================\n# Benchmark settings\n# =============================================================================\n\n# Fair comparison: batch=1 for all models.\n# This is required because BiLSTM and BiGRU were exported as fixed batch=1 unrolled TFLite models.\nBENCHMARK_BATCH_SIZE = 1\n\nWARMUP_RUNS = 20\nBENCHMARK_RUNS = 200\n\nprint(\"Benchmark settings\")\nprint(\"Batch size     :\", BENCHMARK_BATCH_SIZE)\nprint(\"Warmup runs    :\", WARMUP_RUNS)\nprint(\"Benchmark runs :\", BENCHMARK_RUNS)\n\n# =============================================================================\n# Safety checks\n# =============================================================================\n\nrequired_objects = [\"test_dataset\", \"MAX_LEN\", \"CHANNELS\", \"NUM_CLASSES\", \"tflite_registry\"]\n\nmissing_objects = [x for x in required_objects if x not in globals()]\nif missing_objects:\n    raise RuntimeError(f\"Missing required runtime objects: {missing_objects}\")\n\nif \"loaded_models\" not in globals():\n    print(\"Warning: loaded_models not found. Parameter counts will be unavailable.\")\n\n# =============================================================================\n# Utilities\n# =============================================================================\n\ndef get_real_batch(dataset, batch_size=1):\n    \"\"\"\n    Uses real samples from test_dataset.\n    This matters because the models rely on the -100 padding pattern.\n    \"\"\"\n    for xb, yb in dataset.unbatch().batch(batch_size).take(1):\n        return xb.numpy().astype(np.float32), yb.numpy()\n    raise RuntimeError(\"Could not obtain a batch from test_dataset.\")\n\n\ndef get_model_parameter_count(model_name):\n    \"\"\"\n    Gets trainable + non-trainable parameter count from original Keras model if available.\n    \"\"\"\n    if \"loaded_models\" not in globals():\n        return None\n\n    if model_name not in loaded_models:\n        return None\n\n    try:\n        return int(loaded_models[model_name].count_params())\n    except Exception:\n        return None\n\n\ndef infer_precision_from_filename(path):\n    name = path.name.lower()\n\n    if \"float16\" in name:\n        return \"float16 weights\"\n    if \"float32\" in name:\n        return \"float32\"\n    if \"int8\" in name:\n        return \"int8\"\n\n    return \"unknown\"\n\n\ndef run_tflite_once(interpreter, input_details, output_details, x):\n    inp = input_details[0]\n    out = output_details[0]\n\n    interpreter.set_tensor(inp[\"index\"], x.astype(inp[\"dtype\"]))\n    interpreter.invoke()\n\n    y = interpreter.get_tensor(out[\"index\"])\n    return y\n\n\ndef load_interpreter_for_batch(tflite_path, batch_size):\n    \"\"\"\n    Loads a fresh interpreter and tries to resize to batch_size.\n    If model is fixed batch=1, batch_size must be 1.\n    \"\"\"\n    interpreter = tf.lite.Interpreter(model_path=str(tflite_path))\n\n    input_details_before = interpreter.get_input_details()\n    inp0 = input_details_before[0]\n    input_index = inp0[\"index\"]\n\n    target_shape = np.array([batch_size, MAX_LEN, CHANNELS], dtype=np.int32)\n\n    try:\n        interpreter.resize_tensor_input(input_index, target_shape, strict=False)\n    except Exception as e:\n        print(f\"Resize warning for {tflite_path.name}: {e}\")\n\n    interpreter.allocate_tensors()\n\n    input_details = interpreter.get_input_details()\n    output_details = interpreter.get_output_details()\n\n    return interpreter, input_details, output_details\n\n\ndef benchmark_single_tflite_model(model_name, tflite_path, x_batch, y_batch):\n    \"\"\"\n    Measures latency with perf_counter.\n    Reports batch=1 metrics suitable for paper comparison.\n    \"\"\"\n    print(\"\\n\" + \"=\" * 100)\n    print(f\"Benchmarking: {model_name}\")\n    print(\"File:\", tflite_path.name)\n    print(\"=\" * 100)\n\n    interpreter, input_details, output_details = load_interpreter_for_batch(\n        tflite_path=tflite_path,\n        batch_size=x_batch.shape[0]\n    )\n\n    inp = input_details[0]\n    out = output_details[0]\n\n    print(\"Input shape :\", inp[\"shape\"])\n    print(\"Input dtype :\", inp[\"dtype\"])\n    print(\"Output shape:\", out[\"shape\"])\n    print(\"Output dtype:\", out[\"dtype\"])\n\n    # Smoke test\n    y = run_tflite_once(interpreter, input_details, output_details, x_batch)\n    y = np.nan_to_num(y, nan=0.0, posinf=1e4, neginf=-1e4)\n\n    top1 = int(np.argmax(y, axis=-1)[0])\n\n    # Warmup\n    for _ in range(WARMUP_RUNS):\n        _ = run_tflite_once(interpreter, input_details, output_details, x_batch)\n\n    # Timed runs\n    latencies_ms = []\n\n    for _ in range(BENCHMARK_RUNS):\n        start = time.perf_counter()\n        _ = run_tflite_once(interpreter, input_details, output_details, x_batch)\n        end = time.perf_counter()\n\n        latencies_ms.append((end - start) * 1000.0)\n\n    latencies_ms = np.array(latencies_ms, dtype=np.float64)\n\n    mean_ms = float(np.mean(latencies_ms))\n    std_ms = float(np.std(latencies_ms))\n    median_ms = float(np.median(latencies_ms))\n    min_ms = float(np.min(latencies_ms))\n    max_ms = float(np.max(latencies_ms))\n    p95_ms = float(np.percentile(latencies_ms, 95))\n    p99_ms = float(np.percentile(latencies_ms, 99))\n\n    batch_size = int(x_batch.shape[0])\n\n    latency_per_sample_ms = mean_ms / batch_size\n    throughput_samples_sec = float(1000.0 * batch_size / mean_ms)\n\n    size_mb = float(tflite_path.stat().st_size / (1024 ** 2))\n    params = get_model_parameter_count(model_name)\n\n    result = {\n        \"model\": model_name,\n        \"tflite_file\": tflite_path.name,\n        \"precision\": infer_precision_from_filename(tflite_path),\n        \"batch_size\": batch_size,\n        \"parameters\": params,\n        \"model_size_mb\": size_mb,\n        \"mean_latency_ms\": mean_ms,\n        \"std_latency_ms\": std_ms,\n        \"median_latency_ms\": median_ms,\n        \"min_latency_ms\": min_ms,\n        \"max_latency_ms\": max_ms,\n        \"p95_latency_ms\": p95_ms,\n        \"p99_latency_ms\": p99_ms,\n        \"latency_per_sample_ms\": latency_per_sample_ms,\n        \"throughput_samples_per_sec\": throughput_samples_sec,\n        \"top1_sample\": top1,\n        \"input_shape\": str(inp[\"shape\"]),\n        \"input_dtype\": str(inp[\"dtype\"]),\n        \"output_shape\": str(out[\"shape\"]),\n        \"output_dtype\": str(out[\"dtype\"]),\n        \"status\": \"success\",\n        \"error\": None,\n    }\n\n    print(\"Mean latency/sample ms:\", f\"{latency_per_sample_ms:.4f}\")\n    print(\"Median latency ms     :\", f\"{median_ms:.4f}\")\n    print(\"P95 latency ms        :\", f\"{p95_ms:.4f}\")\n    print(\"Throughput samples/s  :\", f\"{throughput_samples_sec:.2f}\")\n    print(\"Model size MB         :\", f\"{size_mb:.2f}\")\n    print(\"Top-1 sample          :\", top1)\n\n    del interpreter\n    gc.collect()\n\n    return result, latencies_ms\n\n\n# =============================================================================\n# Resolve paths from registry\n# =============================================================================\n\nbenchmark_registry = {}\n\nfor model_name in EXPECTED_MODELS:\n    path = tflite_registry.get(model_name, None)\n\n    if path is None:\n        print(f\"[MISSING] {model_name}: no TFLite path in tflite_registry\")\n        continue\n\n    path = Path(path)\n\n    if not path.exists():\n        print(f\"[MISSING FILE] {model_name}: {path}\")\n        continue\n\n    benchmark_registry[model_name] = path\n\nprint(\"\\nModels to benchmark:\")\nfor model_name, path in benchmark_registry.items():\n    print(f\"- {model_name}: {path.name}\")\n\nif len(benchmark_registry) == 0:\n    raise RuntimeError(\"No TFLite models found for benchmarking.\")\n\n# =============================================================================\n# Prepare real input\n# =============================================================================\n\nx_batch, y_batch = get_real_batch(test_dataset, BENCHMARK_BATCH_SIZE)\n\nprint(\"\\nBenchmark input:\", x_batch.shape, x_batch.dtype)\n\n# =============================================================================\n# Run benchmark\n# =============================================================================\n\nbenchmark_rows = []\nlatency_distributions = {}\n\nfor model_name in EXPECTED_MODELS:\n    if model_name not in benchmark_registry:\n        benchmark_rows.append({\n            \"model\": model_name,\n            \"status\": \"missing\",\n            \"error\": \"No TFLite file found\",\n        })\n        continue\n\n    try:\n        row, latencies = benchmark_single_tflite_model(\n            model_name=model_name,\n            tflite_path=benchmark_registry[model_name],\n            x_batch=x_batch,\n            y_batch=y_batch,\n        )\n\n        benchmark_rows.append(row)\n        latency_distributions[model_name] = latencies\n\n    except Exception as e:\n        import traceback\n\n        err = traceback.format_exc()\n        print(f\"[FAILED] {model_name}\")\n        print(err)\n\n        benchmark_rows.append({\n            \"model\": model_name,\n            \"status\": \"failed\",\n            \"error\": err,\n        })\n\nbenchmark_df = pd.DataFrame(benchmark_rows)\n\n# =============================================================================\n# Add derived research metrics\n# =============================================================================\n\nsuccess_df = benchmark_df[benchmark_df[\"status\"] == \"success\"].copy()\n\nif len(success_df) == 0:\n    display(benchmark_df)\n    raise RuntimeError(\"No successful TFLite benchmark results.\")\n\n# Efficiency score:\n# Higher throughput and smaller model are better.\n# This is optional, useful for ranking deployment efficiency.\nsuccess_df[\"efficiency_score\"] = (\n    success_df[\"throughput_samples_per_sec\"] / success_df[\"model_size_mb\"]\n)\n\n# Approx storage per parameter if parameter count exists.\nsuccess_df[\"bytes_per_parameter_approx\"] = np.where(\n    success_df[\"parameters\"].notna(),\n    (success_df[\"model_size_mb\"] * 1024 * 1024) / success_df[\"parameters\"],\n    np.nan,\n)\n\n# Ranking columns\nsuccess_df[\"latency_rank\"] = success_df[\"latency_per_sample_ms\"].rank(method=\"min\", ascending=True).astype(int)\nsuccess_df[\"throughput_rank\"] = success_df[\"throughput_samples_per_sec\"].rank(method=\"min\", ascending=False).astype(int)\nsuccess_df[\"size_rank\"] = success_df[\"model_size_mb\"].rank(method=\"min\", ascending=True).astype(int)\nsuccess_df[\"efficiency_rank\"] = success_df[\"efficiency_score\"].rank(method=\"min\", ascending=False).astype(int)\n\n# Clean presentation order\npresentation_cols = [\n    \"model\",\n    \"precision\",\n    \"parameters\",\n    \"model_size_mb\",\n    \"mean_latency_ms\",\n    \"std_latency_ms\",\n    \"median_latency_ms\",\n    \"p95_latency_ms\",\n    \"p99_latency_ms\",\n    \"latency_per_sample_ms\",\n    \"throughput_samples_per_sec\",\n    \"efficiency_score\",\n    \"latency_rank\",\n    \"throughput_rank\",\n    \"size_rank\",\n    \"efficiency_rank\",\n    \"top1_sample\",\n    \"tflite_file\",\n]\n\npaper_table = success_df[presentation_cols].copy()\n\n# Rounded table for paper display\npaper_table_rounded = paper_table.copy()\n\nround_cols = [\n    \"model_size_mb\",\n    \"mean_latency_ms\",\n    \"std_latency_ms\",\n    \"median_latency_ms\",\n    \"p95_latency_ms\",\n    \"p99_latency_ms\",\n    \"latency_per_sample_ms\",\n    \"throughput_samples_per_sec\",\n    \"efficiency_score\",\n    \"bytes_per_parameter_approx\",\n]\n\nfor col in round_cols:\n    if col in paper_table_rounded.columns:\n        paper_table_rounded[col] = paper_table_rounded[col].astype(float).round(4)\n\n# =============================================================================\n# Save tables\n# =============================================================================\n\nraw_csv = TABLES_DIR / \"tflite_benchmark_raw.csv\"\npaper_csv = TABLES_DIR / \"tflite_benchmark_paper_table.csv\"\npaper_md = TABLES_DIR / \"tflite_benchmark_paper_table.md\"\njson_path = TABLES_DIR / \"tflite_benchmark_results.json\"\n\nbenchmark_df.to_csv(raw_csv, index=False)\npaper_table_rounded.to_csv(paper_csv, index=False)\n\nwith open(paper_md, \"w\") as f:\n    f.write(paper_table_rounded.to_markdown(index=False))\n\nwith open(json_path, \"w\") as f:\n    json.dump(benchmark_rows, f, indent=4, default=str)\n\n# Save latency distributions\nlatency_dist_dir = TABLES_DIR / \"latency_distributions\"\nlatency_dist_dir.mkdir(exist_ok=True)\n\nfor model_name, latencies in latency_distributions.items():\n    pd.DataFrame({\n        \"run\": np.arange(1, len(latencies) + 1),\n        \"latency_ms\": latencies,\n    }).to_csv(latency_dist_dir / f\"{model_name}_latency_runs.csv\", index=False)\n\nprint(\"\\nRaw benchmark table:\")\ndisplay(benchmark_df)\n\nprint(\"\\nPaper-ready table:\")\ndisplay(paper_table_rounded)\n\nprint(\"Saved:\")\nprint(raw_csv)\nprint(paper_csv)\nprint(paper_md)\nprint(json_path)\n\n# =============================================================================\n# Plot helper\n# =============================================================================\n\ndef save_current_plot(path):\n    plt.tight_layout()\n    plt.savefig(path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n    print(\"Saved plot:\", path)\n\n\n# =============================================================================\n# Plot 1: Latency per sample\n# =============================================================================\n\nlatency_plot_df = success_df.sort_values(\"latency_per_sample_ms\", ascending=True)\n\nplt.figure(figsize=(10, 6))\nplt.bar(latency_plot_df[\"model\"], latency_plot_df[\"latency_per_sample_ms\"])\nplt.title(\"TFLite Inference Latency per Sample\")\nplt.xlabel(\"Model\")\nplt.ylabel(\"Latency per Sample (ms)\")\nplt.xticks(rotation=25, ha=\"right\")\nplt.grid(axis=\"y\", alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"01_tflite_latency_per_sample.png\")\n\n# =============================================================================\n# Plot 2: Throughput\n# =============================================================================\n\nthroughput_plot_df = success_df.sort_values(\"throughput_samples_per_sec\", ascending=False)\n\nplt.figure(figsize=(10, 6))\nplt.bar(throughput_plot_df[\"model\"], throughput_plot_df[\"throughput_samples_per_sec\"])\nplt.title(\"TFLite Inference Throughput\")\nplt.xlabel(\"Model\")\nplt.ylabel(\"Throughput (samples/second)\")\nplt.xticks(rotation=25, ha=\"right\")\nplt.grid(axis=\"y\", alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"02_tflite_throughput.png\")\n\n# =============================================================================\n# Plot 3: Model size\n# =============================================================================\n\nsize_plot_df = success_df.sort_values(\"model_size_mb\", ascending=True)\n\nplt.figure(figsize=(10, 6))\nplt.bar(size_plot_df[\"model\"], size_plot_df[\"model_size_mb\"])\nplt.title(\"TFLite Model Size\")\nplt.xlabel(\"Model\")\nplt.ylabel(\"Model Size (MB)\")\nplt.xticks(rotation=25, ha=\"right\")\nplt.grid(axis=\"y\", alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"03_tflite_model_size.png\")\n\n# =============================================================================\n# Plot 4: Latency with error bars\n# =============================================================================\n\nerr_plot_df = success_df.sort_values(\"mean_latency_ms\", ascending=True)\n\nplt.figure(figsize=(10, 6))\nplt.bar(\n    err_plot_df[\"model\"],\n    err_plot_df[\"mean_latency_ms\"],\n    yerr=err_plot_df[\"std_latency_ms\"],\n    capsize=5,\n)\nplt.title(\"TFLite Mean Latency with Standard Deviation\")\nplt.xlabel(\"Model\")\nplt.ylabel(\"Latency (ms), batch=1\")\nplt.xticks(rotation=25, ha=\"right\")\nplt.grid(axis=\"y\", alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"04_tflite_latency_error_bars.png\")\n\n# =============================================================================\n# Plot 5: Size vs latency scatter\n# =============================================================================\n\nplt.figure(figsize=(9, 6))\nplt.scatter(\n    success_df[\"model_size_mb\"],\n    success_df[\"latency_per_sample_ms\"],\n    s=140,\n)\n\nfor _, row in success_df.iterrows():\n    plt.annotate(\n        row[\"model\"],\n        (row[\"model_size_mb\"], row[\"latency_per_sample_ms\"]),\n        textcoords=\"offset points\",\n        xytext=(7, 7),\n        ha=\"left\",\n    )\n\nplt.title(\"TFLite Deployment Trade-off: Size vs Latency\")\nplt.xlabel(\"Model Size (MB)\")\nplt.ylabel(\"Latency per Sample (ms)\")\nplt.grid(alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"05_tflite_size_vs_latency.png\")\n\n# =============================================================================\n# Plot 6: Efficiency score\n# =============================================================================\n\neff_plot_df = success_df.sort_values(\"efficiency_score\", ascending=False)\n\nplt.figure(figsize=(10, 6))\nplt.bar(eff_plot_df[\"model\"], eff_plot_df[\"efficiency_score\"])\nplt.title(\"TFLite Deployment Efficiency Score\")\nplt.xlabel(\"Model\")\nplt.ylabel(\"Throughput / Model Size\")\nplt.xticks(rotation=25, ha=\"right\")\nplt.grid(axis=\"y\", alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"06_tflite_efficiency_score.png\")\n\n# =============================================================================\n# Plot 7: Latency distribution boxplot\n# =============================================================================\n\nbox_models = list(latency_distributions.keys())\nbox_data = [latency_distributions[m] for m in box_models]\n\nplt.figure(figsize=(10, 6))\nplt.boxplot(box_data, labels=box_models, showmeans=True)\nplt.title(\"TFLite Latency Distribution Across Runs\")\nplt.xlabel(\"Model\")\nplt.ylabel(\"Latency (ms), batch=1\")\nplt.xticks(rotation=25, ha=\"right\")\nplt.grid(axis=\"y\", alpha=0.3)\nsave_current_plot(PLOTS_DIR / \"07_tflite_latency_distribution_boxplot.png\")\n\n# =============================================================================\n# Plot 8: Normalized radar chart\n# =============================================================================\n\n# Lower is better for latency and size; higher is better for throughput.\nradar_df = success_df[[\n    \"model\",\n    \"latency_per_sample_ms\",\n    \"throughput_samples_per_sec\",\n    \"model_size_mb\",\n    \"efficiency_score\",\n]].copy()\n\ndef normalize_higher_better(series):\n    min_v = series.min()\n    max_v = series.max()\n    if max_v == min_v:\n        return pd.Series(np.ones(len(series)), index=series.index)\n    return (series - min_v) / (max_v - min_v)\n\ndef normalize_lower_better(series):\n    min_v = series.min()\n    max_v = series.max()\n    if max_v == min_v:\n        return pd.Series(np.ones(len(series)), index=series.index)\n    return 1.0 - ((series - min_v) / (max_v - min_v))\n\nradar_df[\"Latency\"] = normalize_lower_better(radar_df[\"latency_per_sample_ms\"])\nradar_df[\"Throughput\"] = normalize_higher_better(radar_df[\"throughput_samples_per_sec\"])\nradar_df[\"Compactness\"] = normalize_lower_better(radar_df[\"model_size_mb\"])\nradar_df[\"Efficiency\"] = normalize_higher_better(radar_df[\"efficiency_score\"])\n\nradar_metrics = [\"Latency\", \"Throughput\", \"Compactness\", \"Efficiency\"]\nangles = np.linspace(0, 2 * np.pi, len(radar_metrics), endpoint=False).tolist()\nangles += angles[:1]\n\nplt.figure(figsize=(8, 8))\nax = plt.subplot(111, polar=True)\n\nfor _, row in radar_df.iterrows():\n    values = [row[m] for m in radar_metrics]\n    values += values[:1]\n    ax.plot(angles, values, linewidth=2, label=row[\"model\"])\n    ax.fill(angles, values, alpha=0.08)\n\nax.set_xticks(angles[:-1])\nax.set_xticklabels(radar_metrics)\nax.set_title(\"Normalized TFLite Deployment Profile\", pad=20)\nax.set_ylim(0, 1)\nax.legend(loc=\"upper right\", bbox_to_anchor=(1.35, 1.1))\nplt.tight_layout()\n\nradar_plot_path = PLOTS_DIR / \"08_tflite_normalized_deployment_radar.png\"\nplt.savefig(radar_plot_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\nprint(\"Saved plot:\", radar_plot_path)\n\n# =============================================================================\n# Best model summaries\n# =============================================================================\n\nbest_latency = success_df.loc[success_df[\"latency_per_sample_ms\"].idxmin()]\nbest_throughput = success_df.loc[success_df[\"throughput_samples_per_sec\"].idxmax()]\nsmallest_model = success_df.loc[success_df[\"model_size_mb\"].idxmin()]\nbest_efficiency = success_df.loc[success_df[\"efficiency_score\"].idxmax()]\n\nsummary_lines = [\n    \"# TFLite Deployment Benchmark Summary\",\n    \"\",\n    f\"- Fastest model by latency: **{best_latency['model']}** \"\n    f\"({best_latency['latency_per_sample_ms']:.4f} ms/sample).\",\n    f\"- Highest throughput model: **{best_throughput['model']}** \"\n    f\"({best_throughput['throughput_samples_per_sec']:.2f} samples/sec).\",\n    f\"- Smallest TFLite model: **{smallest_model['model']}** \"\n    f\"({smallest_model['model_size_mb']:.2f} MB).\",\n    f\"- Best deployment efficiency score: **{best_efficiency['model']}** \"\n    f\"({best_efficiency['efficiency_score']:.4f}).\",\n    \"\",\n    \"## Notes\",\n    \"- All models were benchmarked with batch size = 1 for a fair comparison.\",\n    \"- BiLSTM and BiGRU were exported as fixed-batch unrolled TFLite models to avoid cuDNN RNN conversion issues.\",\n    \"- Latency was measured using Python `time.perf_counter()` after warmup runs.\",\n    \"- The reported throughput is computed as `1000 / mean_latency_ms` for batch size 1.\",\n]\n\nsummary_path = RESULTS_DIR / \"benchmark_summary.md\"\n\nwith open(summary_path, \"w\") as f:\n    f.write(\"\\n\".join(summary_lines))\n\nprint(\"\\nBenchmark summary:\")\nprint(\"\\n\".join(summary_lines))\n\n# =============================================================================\n# Create ZIP with all tables and plots\n# =============================================================================\n\nzip_base = \"/kaggle/working/final_tflite_research_benchmark\"\nzip_path = zip_base + \".zip\"\n\nif os.path.exists(zip_path):\n    os.remove(zip_path)\n\nfinal_zip = shutil.make_archive(\n    base_name=zip_base,\n    format=\"zip\",\n    root_dir=str(RESULTS_DIR),\n)\n\nprint(\"\\nFinal benchmark package:\")\nprint(final_zip)\ndisplay(FileLink(final_zip))\n\nprint(\"\\nKey outputs:\")\nprint(\"Paper table CSV :\", paper_csv)\nprint(\"Paper table MD  :\", paper_md)\nprint(\"Plots directory :\", PLOTS_DIR)\nprint(\"Summary         :\", summary_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:42:31.199650Z","iopub.execute_input":"2026-04-29T17:42:31.199972Z","iopub.status.idle":"2026-04-29T17:46:00.538798Z","shell.execute_reply.started":"2026-04-29T17:42:31.199947Z","shell.execute_reply":"2026-04-29T17:46:00.538193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Download link for all exported TFLite models\n# =============================================================================\n\nimport os\nimport shutil\nfrom pathlib import Path\nfrom IPython.display import FileLink, display\n\nTFLITE_EXPORT_DIR = Path(\"/kaggle/working/tflite_exports\")\nZIP_BASE = \"/kaggle/working/tflite_exports_all\"\nZIP_PATH = ZIP_BASE + \".zip\"\n\nif os.path.exists(ZIP_PATH):\n    os.remove(ZIP_PATH)\n\nzip_path = shutil.make_archive(\n    base_name=ZIP_BASE,\n    format=\"zip\",\n    root_dir=str(TFLITE_EXPORT_DIR)\n)\n\nprint(\"ZIP created:\", zip_path)\ndisplay(FileLink(zip_path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:46:00.539845Z","iopub.execute_input":"2026-04-29T17:46:00.540185Z","iopub.status.idle":"2026-04-29T17:46:06.310410Z","shell.execute_reply.started":"2026-04-29T17:46:00.540162Z","shell.execute_reply":"2026-04-29T17:46:06.309800Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# END-TO-END TFLITE EXPORT\n# Raw landmarks -> Preprocess inside TFLite -> Model logits\n# Input shape: [1, any_frames, 543, 3]\n# Output shape: [1, 250]\n# =============================================================================\n\nimport os\nimport gc\nimport json\nimport time\nimport shutil\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom tensorflow.keras import layers, Model\nfrom IPython.display import FileLink, display\n\n# =============================================================================\n# Paths\n# =============================================================================\n\nE2E_EXPORT_DIR = Path(\"/kaggle/working/tflite_end_to_end_exports\")\nE2E_RESULTS_DIR = Path(\"/kaggle/working/tflite_end_to_end_benchmark\")\nE2E_PLOTS_DIR = E2E_RESULTS_DIR / \"plots\"\nE2E_TABLES_DIR = E2E_RESULTS_DIR / \"tables\"\n\nE2E_EXPORT_DIR.mkdir(parents=True, exist_ok=True)\nE2E_RESULTS_DIR.mkdir(parents=True, exist_ok=True)\nE2E_PLOTS_DIR.mkdir(parents=True, exist_ok=True)\nE2E_TABLES_DIR.mkdir(parents=True, exist_ok=True)\n\n# =============================================================================\n# Constants\n# =============================================================================\n\nROWS_PER_FRAME = 543\nMAX_LEN = 384\nNUM_CLASSES = 250\nPAD = -100.0\n\nEXPECTED_MODELS = [\n    \"BiLSTM\",\n    \"BiGRU\",\n    \"Transformer\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\nprint(\"End-to-end TFLite export\")\nprint(\"Raw input  : [1, T, 543, 3]\")\nprint(\"Model input: [1, 384, 708]\")\nprint(\"Output     : [1, 250]\")\n\n# =============================================================================\n# Safety checks\n# =============================================================================\n\nrequired_objects = [\"loaded_models\", \"MAX_LEN\", \"CHANNELS\", \"NUM_CLASSES\"]\n\nmissing = [x for x in required_objects if x not in globals()]\nif missing:\n    raise RuntimeError(f\"Missing required notebook objects: {missing}\")\n\n# These should already exist from the successful unrolled RNN export cell.\nrequired_rnn_builders = [\n    \"build_bilstm_unrolled_b1\",\n    \"build_bigru_unrolled_b1\",\n    \"copy_weights_by_layer_name\",\n]\n\nmissing_builders = [x for x in required_rnn_builders if x not in globals()]\nif missing_builders:\n    raise RuntimeError(\n        \"Missing RNN export builders from the previous successful cell: \"\n        f\"{missing_builders}. Run the unrolled BiLSTM/BiGRU export cell first.\"\n    )\n\ntry:\n    tf.keras.mixed_precision.set_global_policy(\"float32\")\nexcept Exception:\n    pass\n\n# =============================================================================\n# Landmark groups\n# =============================================================================\n\nNOSE  = [1, 2, 98, 327]\nLNOSE = [98]\nRNOSE = [327]\n\nLIP = [\n    0, 61, 185, 40, 39, 37, 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]\nLLIP  = [84, 181, 91, 146, 61, 185, 40, 39, 37, 87, 178, 88, 95, 78, 191, 80, 81, 82]\nRLIP  = [314, 405, 321, 375, 291, 409, 270, 269, 267, 317, 402, 318, 324, 308, 415, 310, 311, 312]\n\nPOSE  = [500, 502, 504, 501, 503, 505, 512, 513]\nLPOSE = [513, 505, 503, 501]\nRPOSE = [512, 504, 502, 500]\n\nREYE = [33, 7, 163, 144, 145, 153, 154, 155, 133, 246, 161, 160, 159, 158, 157, 173]\nLEYE = [263, 249, 390, 373, 374, 380, 381, 382, 362, 466, 388, 387, 386, 385, 384, 398]\n\nLHAND = np.arange(468, 489).tolist()\nRHAND = np.arange(522, 543).tolist()\n\nPOINT_LANDMARKS = LIP + LHAND + RHAND + NOSE + REYE + LEYE\nNUM_NODES = len(POINT_LANDMARKS)\nCHANNELS_FROM_PREPROCESS = 6 * NUM_NODES\n\nprint(f\"Selected Landmark Nodes : {NUM_NODES}\")\nprint(f\"Feature Channels        : {CHANNELS_FROM_PREPROCESS}\")\n\nassert CHANNELS_FROM_PREPROCESS == CHANNELS, (\n    f\"Preprocess CHANNELS={CHANNELS_FROM_PREPROCESS}, but notebook CHANNELS={CHANNELS}\"\n)\n\n# =============================================================================\n# NaN-safe preprocessing layers\n# =============================================================================\n\ndef tf_nan_mean(x, axis=0, keepdims=False):\n    valid = tf.where(tf.math.is_nan(x), tf.zeros_like(x), tf.ones_like(x))\n    values = tf.where(tf.math.is_nan(x), tf.zeros_like(x), x)\n    s = tf.reduce_sum(values, axis=axis, keepdims=keepdims)\n    n = tf.reduce_sum(valid, axis=axis, keepdims=keepdims)\n    return s / tf.maximum(n, tf.constant(1.0, dtype=x.dtype))\n\n\ndef tf_nan_std(x, center=None, axis=0, keepdims=False):\n    if center is None:\n        center = tf_nan_mean(x, axis=axis, keepdims=True)\n    d = x - center\n    return tf.sqrt(tf_nan_mean(d * d, axis=axis, keepdims=keepdims))\n\n\nclass EndToEndPreprocess(tf.keras.layers.Layer):\n    \"\"\"\n    Raw landmarks [B, T, 543, 3]\n    -> normalized selected landmarks\n    -> pos/vel/acc features [B, <=384, 708]\n    \"\"\"\n    def __init__(self, max_len=384, point_landmarks=None, **kwargs):\n        super().__init__(**kwargs)\n        self.max_len = max_len\n        self.point_landmarks = list(point_landmarks)\n\n    def call(self, inputs):\n        x = tf.cast(inputs, tf.float32)\n\n        # Accept [T, 543, 3] or [B, T, 543, 3]\n        if len(x.shape) == 3:\n            x = x[None, ...]\n\n        mean = tf_nan_mean(\n            tf.gather(x, [17], axis=2),\n            axis=[1, 2],\n            keepdims=True,\n        )\n        mean = tf.where(tf.math.is_nan(mean), tf.constant(0.5, x.dtype), mean)\n\n        x = tf.gather(x, self.point_landmarks, axis=2)\n\n        std = tf_nan_std(x, center=mean, axis=[1, 2], keepdims=True)\n        std = tf.where(\n            tf.logical_or(tf.math.is_nan(std), tf.less(std, tf.constant(1e-6, x.dtype))),\n            tf.ones_like(std),\n            std,\n        )\n\n        x = (x - mean) / std\n\n        if self.max_len is not None:\n            x = x[:, :self.max_len]\n\n        length = tf.shape(x)[1]\n\n        # Use x,y only\n        x = x[..., :2]\n\n        dx = tf.cond(\n            tf.shape(x)[1] > 1,\n            lambda: tf.pad(\n                x[:, 1:] - x[:, :-1],\n                [[0, 0], [0, 1], [0, 0], [0, 0]],\n            ),\n            lambda: tf.zeros_like(x),\n        )\n\n        dx2 = tf.cond(\n            tf.shape(x)[1] > 2,\n            lambda: tf.pad(\n                x[:, 2:] - x[:, :-2],\n                [[0, 0], [0, 2], [0, 0], [0, 0]],\n            ),\n            lambda: tf.zeros_like(x),\n        )\n\n        features = tf.concat(\n            [\n                tf.reshape(x,   (-1, length, 2 * len(self.point_landmarks))),\n                tf.reshape(dx,  (-1, length, 2 * len(self.point_landmarks))),\n                tf.reshape(dx2, (-1, length, 2 * len(self.point_landmarks))),\n            ],\n            axis=-1,\n        )\n\n        features = tf.where(\n            tf.math.is_nan(features),\n            tf.zeros_like(features),\n            features,\n        )\n\n        return features\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\n            \"max_len\": self.max_len,\n            \"point_landmarks\": self.point_landmarks,\n        })\n        return config\n\n\nclass PadOrCropToMaxLen(tf.keras.layers.Layer):\n    \"\"\"\n    Converts [B, T, C] to [B, MAX_LEN, C].\n    If T < MAX_LEN: pads with PAD=-100.\n    If T > MAX_LEN: crops.\n    \"\"\"\n    def __init__(self, max_len=384, channels=708, pad_value=-100.0, **kwargs):\n        super().__init__(**kwargs)\n        self.max_len = max_len\n        self.channels = channels\n        self.pad_value = pad_value\n\n    def call(self, x):\n        x = tf.cast(x, tf.float32)\n        x = x[:, :self.max_len, :]\n\n        batch = tf.shape(x)[0]\n        length = tf.shape(x)[1]\n        pad_len = tf.maximum(self.max_len - length, 0)\n\n        pad_tensor = tf.fill(\n            [batch, pad_len, self.channels],\n            tf.cast(self.pad_value, x.dtype),\n        )\n\n        x = tf.concat([x, pad_tensor], axis=1)\n        x = x[:, :self.max_len, :]\n\n        # Static shape helps TFLite/RNN unrolled models\n        x = tf.ensure_shape(x, [1, self.max_len, self.channels])\n        return x\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\n            \"max_len\": self.max_len,\n            \"channels\": self.channels,\n            \"pad_value\": self.pad_value,\n        })\n        return config\n\n\n# =============================================================================\n# Build deployable model object for each architecture\n# =============================================================================\n\ndef get_deploy_base_model(model_name):\n    \"\"\"\n    For Transformer/ST_GCN/RoPE_Conformer, use already-loaded model.\n    For BiLSTM/BiGRU, rebuild unrolled batch=1 models and copy weights,\n    because direct loaded RNN models contain cuDNN graphs.\n    \"\"\"\n    if model_name not in loaded_models:\n        raise RuntimeError(f\"{model_name} not found in loaded_models\")\n\n    if model_name == \"BiLSTM\":\n        rebuilt = build_bilstm_unrolled_b1(MAX_LEN, CHANNELS, NUM_CLASSES)\n        _ = rebuilt(tf.zeros([1, MAX_LEN, CHANNELS], dtype=tf.float32), training=False)\n        copied, skipped = copy_weights_by_layer_name(loaded_models[model_name], rebuilt)\n        print(f\"{model_name} rebuilt for E2E export.\")\n        print(\"Copied:\", copied)\n        print(\"Skipped:\", skipped)\n        return rebuilt\n\n    if model_name == \"BiGRU\":\n        rebuilt = build_bigru_unrolled_b1(MAX_LEN, CHANNELS, NUM_CLASSES)\n        _ = rebuilt(tf.zeros([1, MAX_LEN, CHANNELS], dtype=tf.float32), training=False)\n        copied, skipped = copy_weights_by_layer_name(loaded_models[model_name], rebuilt)\n        print(f\"{model_name} rebuilt for E2E export.\")\n        print(\"Copied:\", copied)\n        print(\"Skipped:\", skipped)\n        return rebuilt\n\n    base = loaded_models[model_name]\n    base.trainable = False\n    for layer in base.layers:\n        layer.trainable = False\n\n    return base\n\n\ndef build_end_to_end_wrapper(model_name, base_model):\n    \"\"\"\n    Raw input:\n        [1, T, 543, 3]\n    Output:\n        logits [1, 250]\n    \"\"\"\n    raw_inp = layers.Input(\n        batch_shape=(1, None, ROWS_PER_FRAME, 3),\n        dtype=tf.float32,\n        name=\"raw_landmarks\",\n    )\n\n    x = EndToEndPreprocess(\n        max_len=MAX_LEN,\n        point_landmarks=POINT_LANDMARKS,\n        name=\"preprocess\",\n    )(raw_inp)\n\n    x = PadOrCropToMaxLen(\n        max_len=MAX_LEN,\n        channels=CHANNELS,\n        pad_value=PAD,\n        name=\"pad_or_crop_to_384\",\n    )(x)\n\n    logits = base_model(x, training=False)\n\n    logits = layers.Activation(\n        \"linear\",\n        dtype=\"float32\",\n        name=\"logits\",\n    )(logits)\n\n    return Model(\n        inputs=raw_inp,\n        outputs=logits,\n        name=f\"{model_name}_raw_to_logits_tflite\",\n    )\n\n\n# =============================================================================\n# Convert to TFLite\n# =============================================================================\n\ndef convert_e2e_model_to_tflite(e2e_model, model_name, out_path):\n    @tf.function(\n        input_signature=[\n            tf.TensorSpec(\n                shape=[1, None, ROWS_PER_FRAME, 3],\n                dtype=tf.float32,\n                name=\"raw_landmarks\",\n            )\n        ]\n    )\n    def serving_fn(x):\n        y = e2e_model(x, training=False)\n        return {\"logits\": tf.cast(y, tf.float32)}\n\n    concrete_func = serving_fn.get_concrete_function()\n\n    converter = tf.lite.TFLiteConverter.from_concrete_functions(\n        [concrete_func],\n        e2e_model,\n    )\n\n    converter.target_spec.supported_ops = [\n        tf.lite.OpsSet.TFLITE_BUILTINS,\n        tf.lite.OpsSet.SELECT_TF_OPS,\n    ]\n\n    converter.experimental_enable_resource_variables = True\n    converter.inference_input_type = tf.float32\n    converter.inference_output_type = tf.float32\n\n    # Preserve float16 weight compression for already-light models\n    if model_name in [\"Transformer\", \"RoPE_Conformer\"]:\n        converter.optimizations = [tf.lite.Optimize.DEFAULT]\n        converter.target_spec.supported_types = [tf.float16]\n\n    try:\n        tflite_bytes = converter.convert()\n        method = \"from_concrete_function\"\n\n    except Exception as e1:\n        print(f\"[{model_name}] first conversion failed. Retrying with TensorList lowering disabled.\")\n        print(str(e1)[:3000])\n\n        converter = tf.lite.TFLiteConverter.from_concrete_functions(\n            [concrete_func],\n            e2e_model,\n        )\n\n        converter.target_spec.supported_ops = [\n            tf.lite.OpsSet.TFLITE_BUILTINS,\n            tf.lite.OpsSet.SELECT_TF_OPS,\n        ]\n\n        converter._experimental_lower_tensor_list_ops = False\n        converter.experimental_enable_resource_variables = True\n        converter.inference_input_type = tf.float32\n        converter.inference_output_type = tf.float32\n\n        if model_name in [\"Transformer\", \"RoPE_Conformer\"]:\n            converter.optimizations = [tf.lite.Optimize.DEFAULT]\n            converter.target_spec.supported_types = [tf.float16]\n\n        tflite_bytes = converter.convert()\n        method = \"from_concrete_function_tensorlist_lowering_disabled\"\n\n    with open(out_path, \"wb\") as f:\n        f.write(tflite_bytes)\n\n    return method\n\n\n# =============================================================================\n# Raw sample generator for validation/benchmark\n# =============================================================================\n\ndef make_dummy_raw_landmarks(num_frames):\n    \"\"\"\n    Synthetic raw landmark input for conversion and speed testing.\n    Shape: [1, T, 543, 3]\n    \"\"\"\n    rng = np.random.default_rng(42 + int(num_frames))\n    x = rng.normal(\n        loc=0.5,\n        scale=0.15,\n        size=(1, num_frames, ROWS_PER_FRAME, 3),\n    ).astype(np.float32)\n\n    # Inject a few NaNs to test NaN-safe preprocessing\n    if num_frames > 5:\n        x[:, ::17, :, :] = np.nan\n\n    return x\n\n\ndef run_e2e_tflite_once(tflite_path, raw_x):\n    interpreter = tf.lite.Interpreter(model_path=str(tflite_path))\n\n    inp0 = interpreter.get_input_details()[0]\n    interpreter.resize_tensor_input(\n        inp0[\"index\"],\n        np.array(raw_x.shape, dtype=np.int32),\n        strict=False,\n    )\n    interpreter.allocate_tensors()\n\n    inp = interpreter.get_input_details()[0]\n    out = interpreter.get_output_details()[0]\n\n    interpreter.set_tensor(inp[\"index\"], raw_x.astype(inp[\"dtype\"]))\n    interpreter.invoke()\n\n    y = interpreter.get_tensor(out[\"index\"])\n\n    return y, inp, out\n\n\n# =============================================================================\n# Export all end-to-end TFLite models\n# =============================================================================\n\ne2e_export_results = []\ne2e_registry = {}\n\nfor model_name in EXPECTED_MODELS:\n    print(\"\\n\" + \"=\" * 100)\n    print(\"EXPORT END-TO-END:\", model_name)\n    print(\"=\" * 100)\n\n    try:\n        base_model = get_deploy_base_model(model_name)\n        e2e_model = build_end_to_end_wrapper(model_name, base_model)\n\n        # Build once with variable frame input\n        dummy_raw = make_dummy_raw_landmarks(num_frames=96)\n        keras_y = e2e_model(dummy_raw, training=False).numpy().astype(np.float32)\n\n        out_path = E2E_EXPORT_DIR / f\"{model_name}_raw_preprocess_logits.tflite\"\n\n        if out_path.exists():\n            out_path.unlink()\n\n        method = convert_e2e_model_to_tflite(\n            e2e_model=e2e_model,\n            model_name=model_name,\n            out_path=out_path,\n        )\n\n        tflite_y, inp, out = run_e2e_tflite_once(out_path, dummy_raw)\n        tflite_y = tflite_y.astype(np.float32)\n\n        max_diff = float(np.max(np.abs(keras_y - tflite_y)))\n        mean_diff = float(np.mean(np.abs(keras_y - tflite_y)))\n\n        keras_top1 = int(np.argmax(keras_y, axis=-1)[0])\n        tflite_top1 = int(np.argmax(tflite_y, axis=-1)[0])\n\n        result = {\n            \"model\": model_name,\n            \"status\": \"success\",\n            \"file\": out_path.name,\n            \"path\": str(out_path),\n            \"size_mb\": float(out_path.stat().st_size / (1024 ** 2)),\n            \"conversion_method\": method,\n            \"raw_input_shape_signature\": \"[1, any_frames, 543, 3]\",\n            \"model_output_shape\": str(out[\"shape\"]),\n            \"input_dtype\": str(inp[\"dtype\"]),\n            \"output_dtype\": str(out[\"dtype\"]),\n            \"keras_top1\": keras_top1,\n            \"tflite_top1\": tflite_top1,\n            \"top1_match\": bool(keras_top1 == tflite_top1),\n            \"max_abs_diff\": max_diff,\n            \"mean_abs_diff\": mean_diff,\n        }\n\n        e2e_registry[model_name] = out_path\n\n        print(\"Saved:\", out_path)\n        print(\"Size MB:\", result[\"size_mb\"])\n        print(\"Input:\", inp)\n        print(\"Output:\", out)\n        print(\"Top1 match:\", result[\"top1_match\"])\n        print(\"Max diff:\", max_diff)\n\n    except Exception:\n        result = {\n            \"model\": model_name,\n            \"status\": \"failed\",\n            \"error\": traceback.format_exc(),\n        }\n\n        print(\"FAILED:\", model_name)\n        print(result[\"error\"])\n\n    e2e_export_results.append(result)\n    gc.collect()\n\n\ne2e_export_df = pd.DataFrame(e2e_export_results)\n\nprint(\"\\nEnd-to-end export table:\")\ndisplay(e2e_export_df)\n\ne2e_export_csv = E2E_TABLES_DIR / \"end_to_end_tflite_export_report.csv\"\ne2e_export_json = E2E_TABLES_DIR / \"end_to_end_tflite_export_report.json\"\n\ne2e_export_df.to_csv(e2e_export_csv, index=False)\n\nwith open(e2e_export_json, \"w\") as f:\n    json.dump(e2e_export_results, f, indent=4, default=str)\n\nprint(\"Export report:\", e2e_export_csv)\n\n# =============================================================================\n# Import/load all successful E2E TFLite models\n# =============================================================================\n\ne2e_interpreters = {}\ne2e_io_details = {}\n\nfor model_name, path in e2e_registry.items():\n    try:\n        raw_x = make_dummy_raw_landmarks(num_frames=128)\n        interpreter = tf.lite.Interpreter(model_path=str(path))\n\n        inp0 = interpreter.get_input_details()[0]\n        interpreter.resize_tensor_input(\n            inp0[\"index\"],\n            np.array(raw_x.shape, dtype=np.int32),\n            strict=False,\n        )\n        interpreter.allocate_tensors()\n\n        inp = interpreter.get_input_details()[0]\n        out = interpreter.get_output_details()[0]\n\n        interpreter.set_tensor(inp[\"index\"], raw_x.astype(inp[\"dtype\"]))\n        interpreter.invoke()\n        y = interpreter.get_tensor(out[\"index\"])\n\n        e2e_interpreters[model_name] = interpreter\n        e2e_io_details[model_name] = {\n            \"file\": path.name,\n            \"path\": str(path),\n            \"input_shape\": str(inp[\"shape\"]),\n            \"input_dtype\": str(inp[\"dtype\"]),\n            \"output_shape\": str(out[\"shape\"]),\n            \"output_dtype\": str(out[\"dtype\"]),\n            \"top1_sample\": int(np.argmax(y, axis=-1)[0]),\n        }\n\n        print(\"[LOADED E2E]\", model_name, \"=>\", path.name)\n\n    except Exception:\n        print(\"[LOAD FAILED E2E]\", model_name, path)\n        print(traceback.format_exc())\n\n\ne2e_io_df = pd.DataFrame([\n    {\"model\": model_name, **details}\n    for model_name, details in e2e_io_details.items()\n])\n\nprint(\"\\nLoaded end-to-end TFLite interpreters:\")\ndisplay(e2e_io_df)\n\nprint(\"\\nLoaded E2E model names:\")\nprint(list(e2e_interpreters.keys()))\n\n# =============================================================================\n# Benchmark across variable frame lengths\n# =============================================================================\n\nFRAME_LENGTHS = [32, 64, 128, 256, 384]\nWARMUP_RUNS = 10\nBENCHMARK_RUNS = 100\n\nbenchmark_rows = []\n\ndef benchmark_e2e_model(model_name, tflite_path, num_frames):\n    raw_x = make_dummy_raw_landmarks(num_frames=num_frames)\n\n    interpreter = tf.lite.Interpreter(model_path=str(tflite_path))\n\n    inp0 = interpreter.get_input_details()[0]\n    interpreter.resize_tensor_input(\n        inp0[\"index\"],\n        np.array(raw_x.shape, dtype=np.int32),\n        strict=False,\n    )\n    interpreter.allocate_tensors()\n\n    inp = interpreter.get_input_details()[0]\n    out = interpreter.get_output_details()[0]\n\n    for _ in range(WARMUP_RUNS):\n        interpreter.set_tensor(inp[\"index\"], raw_x.astype(inp[\"dtype\"]))\n        interpreter.invoke()\n        _ = interpreter.get_tensor(out[\"index\"])\n\n    times = []\n\n    for _ in range(BENCHMARK_RUNS):\n        start = time.perf_counter()\n        interpreter.set_tensor(inp[\"index\"], raw_x.astype(inp[\"dtype\"]))\n        interpreter.invoke()\n        y = interpreter.get_tensor(out[\"index\"])\n        end = time.perf_counter()\n        times.append((end - start) * 1000.0)\n\n    times = np.array(times, dtype=np.float64)\n    y = np.nan_to_num(y, nan=0.0, posinf=1e4, neginf=-1e4)\n\n    return {\n        \"model\": model_name,\n        \"frames\": num_frames,\n        \"file\": Path(tflite_path).name,\n        \"size_mb\": float(Path(tflite_path).stat().st_size / (1024 ** 2)),\n        \"mean_latency_ms\": float(times.mean()),\n        \"std_latency_ms\": float(times.std()),\n        \"median_latency_ms\": float(np.median(times)),\n        \"p95_latency_ms\": float(np.percentile(times, 95)),\n        \"p99_latency_ms\": float(np.percentile(times, 99)),\n        \"throughput_samples_per_sec\": float(1000.0 / times.mean()),\n        \"top1_sample\": int(np.argmax(y, axis=-1)[0]),\n        \"input_shape\": str(inp[\"shape\"]),\n        \"output_shape\": str(out[\"shape\"]),\n        \"status\": \"success\",\n    }\n\n\nfor model_name, path in e2e_registry.items():\n    for frames in FRAME_LENGTHS:\n        print(\"\\n\" + \"=\" * 100)\n        print(f\"Benchmark E2E: {model_name} | frames={frames}\")\n        print(\"=\" * 100)\n\n        try:\n            row = benchmark_e2e_model(model_name, path, frames)\n            benchmark_rows.append(row)\n\n            print(\"Mean latency ms:\", f\"{row['mean_latency_ms']:.4f}\")\n            print(\"P95 latency ms :\", f\"{row['p95_latency_ms']:.4f}\")\n            print(\"Throughput     :\", f\"{row['throughput_samples_per_sec']:.2f}\")\n\n        except Exception:\n            err = traceback.format_exc()\n            print(\"FAILED\")\n            print(err)\n            benchmark_rows.append({\n                \"model\": model_name,\n                \"frames\": frames,\n                \"status\": \"failed\",\n                \"error\": err,\n            })\n\n        gc.collect()\n\n\ne2e_benchmark_df = pd.DataFrame(benchmark_rows)\n\ne2e_benchmark_csv = E2E_TABLES_DIR / \"end_to_end_tflite_variable_frames_benchmark.csv\"\ne2e_benchmark_df.to_csv(e2e_benchmark_csv, index=False)\n\nprint(\"\\nEnd-to-end variable-frame benchmark:\")\ndisplay(e2e_benchmark_df)\n\nprint(\"Benchmark CSV:\", e2e_benchmark_csv)\n\n# =============================================================================\n# Paper-ready summary at 384 frames\n# =============================================================================\n\npaper_384_df = e2e_benchmark_df[\n    (e2e_benchmark_df[\"status\"] == \"success\") &\n    (e2e_benchmark_df[\"frames\"] == 384)\n].copy()\n\nif len(paper_384_df) > 0:\n    paper_384_df[\"latency_rank\"] = paper_384_df[\"mean_latency_ms\"].rank(\n        method=\"min\",\n        ascending=True,\n    ).astype(int)\n\n    paper_384_df[\"size_rank\"] = paper_384_df[\"size_mb\"].rank(\n        method=\"min\",\n        ascending=True,\n    ).astype(int)\n\n    paper_384_df[\"efficiency_score\"] = (\n        paper_384_df[\"throughput_samples_per_sec\"] / paper_384_df[\"size_mb\"]\n    )\n\n    paper_384_df[\"efficiency_rank\"] = paper_384_df[\"efficiency_score\"].rank(\n        method=\"min\",\n        ascending=False,\n    ).astype(int)\n\n    paper_cols = [\n        \"model\",\n        \"frames\",\n        \"size_mb\",\n        \"mean_latency_ms\",\n        \"std_latency_ms\",\n        \"median_latency_ms\",\n        \"p95_latency_ms\",\n        \"p99_latency_ms\",\n        \"throughput_samples_per_sec\",\n        \"efficiency_score\",\n        \"latency_rank\",\n        \"size_rank\",\n        \"efficiency_rank\",\n        \"file\",\n    ]\n\n    paper_384_table = paper_384_df[paper_cols].copy()\n\n    for col in [\n        \"size_mb\",\n        \"mean_latency_ms\",\n        \"std_latency_ms\",\n        \"median_latency_ms\",\n        \"p95_latency_ms\",\n        \"p99_latency_ms\",\n        \"throughput_samples_per_sec\",\n        \"efficiency_score\",\n    ]:\n        paper_384_table[col] = paper_384_table[col].astype(float).round(4)\n\n    paper_384_csv = E2E_TABLES_DIR / \"paper_table_e2e_384_frames.csv\"\n    paper_384_md = E2E_TABLES_DIR / \"paper_table_e2e_384_frames.md\"\n\n    paper_384_table.to_csv(paper_384_csv, index=False)\n\n    with open(paper_384_md, \"w\") as f:\n        f.write(paper_384_table.to_markdown(index=False))\n\n    print(\"\\nPaper-ready table at 384 frames:\")\n    display(paper_384_table)\n\n# =============================================================================\n# Plots\n# =============================================================================\n\nsuccess_bench = e2e_benchmark_df[e2e_benchmark_df[\"status\"] == \"success\"].copy()\n\nif len(success_bench) == 0:\n    raise RuntimeError(\"No successful E2E benchmark rows to plot.\")\n\n# Plot 1: latency vs frames\nplt.figure(figsize=(11, 7))\n\nfor model_name in EXPECTED_MODELS:\n    sub = success_bench[success_bench[\"model\"] == model_name].sort_values(\"frames\")\n    if len(sub) == 0:\n        continue\n    plt.plot(\n        sub[\"frames\"],\n        sub[\"mean_latency_ms\"],\n        marker=\"o\",\n        linewidth=2,\n        label=model_name,\n    )\n\nplt.title(\"End-to-End TFLite Latency vs Number of Frames\")\nplt.xlabel(\"Number of Input Frames\")\nplt.ylabel(\"Mean Latency (ms)\")\nplt.grid(alpha=0.3)\nplt.legend()\nplt.tight_layout()\n\nlatency_vs_frames_path = E2E_PLOTS_DIR / \"01_e2e_latency_vs_frames.png\"\nplt.savefig(latency_vs_frames_path, dpi=300, bbox_inches=\"tight\")\nplt.show()\n\n# Plot 2: 384-frame latency bar\nif len(paper_384_df) > 0:\n    plot_384 = paper_384_df.sort_values(\"mean_latency_ms\")\n\n    plt.figure(figsize=(10, 6))\n    plt.bar(plot_384[\"model\"], plot_384[\"mean_latency_ms\"])\n    plt.title(\"End-to-End TFLite Latency at 384 Frames\")\n    plt.xlabel(\"Model\")\n    plt.ylabel(\"Mean Latency (ms)\")\n    plt.xticks(rotation=25, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    plt.tight_layout()\n\n    e2e_latency_384_path = E2E_PLOTS_DIR / \"02_e2e_latency_384_frames.png\"\n    plt.savefig(e2e_latency_384_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\n    # Plot 3: throughput at 384 frames\n    throughput_384 = paper_384_df.sort_values(\"throughput_samples_per_sec\", ascending=False)\n\n    plt.figure(figsize=(10, 6))\n    plt.bar(throughput_384[\"model\"], throughput_384[\"throughput_samples_per_sec\"])\n    plt.title(\"End-to-End TFLite Throughput at 384 Frames\")\n    plt.xlabel(\"Model\")\n    plt.ylabel(\"Samples / Second\")\n    plt.xticks(rotation=25, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    plt.tight_layout()\n\n    e2e_throughput_384_path = E2E_PLOTS_DIR / \"03_e2e_throughput_384_frames.png\"\n    plt.savefig(e2e_throughput_384_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\n    # Plot 4: model size\n    size_plot = paper_384_df.sort_values(\"size_mb\")\n\n    plt.figure(figsize=(10, 6))\n    plt.bar(size_plot[\"model\"], size_plot[\"size_mb\"])\n    plt.title(\"End-to-End TFLite Model Size\")\n    plt.xlabel(\"Model\")\n    plt.ylabel(\"Model Size (MB)\")\n    plt.xticks(rotation=25, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    plt.tight_layout()\n\n    e2e_size_path = E2E_PLOTS_DIR / \"04_e2e_model_size.png\"\n    plt.savefig(e2e_size_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\n    # Plot 5: size vs latency\n    plt.figure(figsize=(9, 6))\n    plt.scatter(\n        paper_384_df[\"size_mb\"],\n        paper_384_df[\"mean_latency_ms\"],\n        s=140,\n    )\n\n    for _, row in paper_384_df.iterrows():\n        plt.annotate(\n            row[\"model\"],\n            (row[\"size_mb\"], row[\"mean_latency_ms\"]),\n            textcoords=\"offset points\",\n            xytext=(7, 7),\n            ha=\"left\",\n        )\n\n    plt.title(\"End-to-End Deployment Trade-off: Size vs Latency\")\n    plt.xlabel(\"Model Size (MB)\")\n    plt.ylabel(\"Mean Latency at 384 Frames (ms)\")\n    plt.grid(alpha=0.3)\n    plt.tight_layout()\n\n    e2e_tradeoff_path = E2E_PLOTS_DIR / \"05_e2e_size_vs_latency.png\"\n    plt.savefig(e2e_tradeoff_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n\n# =============================================================================\n# Summary markdown\n# =============================================================================\n\nsummary_lines = [\n    \"# End-to-End TFLite Benchmark Summary\",\n    \"\",\n    \"This benchmark evaluates TFLite models that include the preprocessing pipeline inside the model graph.\",\n    \"\",\n    \"## Input/Output\",\n    \"\",\n    \"- Raw input shape: `[1, T, 543, 3]`\",\n    \"- `T` is dynamic and can be any number of frames.\",\n    \"- Internal model input after preprocessing: `[1, 384, 708]`\",\n    \"- Output shape: `[1, 250]` logits\",\n    \"\",\n    \"## Notes\",\n    \"\",\n    \"- The preprocessing layer performs landmark selection, normalization, velocity, and acceleration feature extraction inside TFLite.\",\n    \"- Sequences shorter than 384 frames are padded with `-100.0`.\",\n    \"- Sequences longer than 384 frames are cropped.\",\n    \"- BiLSTM and BiGRU use fixed-batch unrolled RNN exports to avoid cuDNN RNN conversion issues.\",\n]\n\nif len(paper_384_df) > 0:\n    best_latency = paper_384_df.loc[paper_384_df[\"mean_latency_ms\"].idxmin()]\n    smallest = paper_384_df.loc[paper_384_df[\"size_mb\"].idxmin()]\n    best_throughput = paper_384_df.loc[paper_384_df[\"throughput_samples_per_sec\"].idxmax()]\n\n    summary_lines += [\n        \"\",\n        \"## 384-frame Results\",\n        \"\",\n        f\"- Fastest end-to-end model: **{best_latency['model']}** \"\n        f\"({best_latency['mean_latency_ms']:.4f} ms).\",\n        f\"- Smallest end-to-end model: **{smallest['model']}** \"\n        f\"({smallest['size_mb']:.2f} MB).\",\n        f\"- Highest throughput model: **{best_throughput['model']}** \"\n        f\"({best_throughput['throughput_samples_per_sec']:.2f} samples/sec).\",\n    ]\n\nsummary_path = E2E_RESULTS_DIR / \"end_to_end_benchmark_summary.md\"\n\nwith open(summary_path, \"w\") as f:\n    f.write(\"\\n\".join(summary_lines))\n\nprint(\"\\n\".join(summary_lines))\n\n# =============================================================================\n# ZIP outputs\n# =============================================================================\n\nexports_zip_base = \"/kaggle/working/tflite_end_to_end_exports\"\nexports_zip_path = exports_zip_base + \".zip\"\n\nif os.path.exists(exports_zip_path):\n    os.remove(exports_zip_path)\n\nexports_zip = shutil.make_archive(\n    base_name=exports_zip_base,\n    format=\"zip\",\n    root_dir=str(E2E_EXPORT_DIR),\n)\n\nresults_zip_base = \"/kaggle/working/tflite_end_to_end_benchmark\"\nresults_zip_path = results_zip_base + \".zip\"\n\nif os.path.exists(results_zip_path):\n    os.remove(results_zip_path)\n\nresults_zip = shutil.make_archive(\n    base_name=results_zip_base,\n    format=\"zip\",\n    root_dir=str(E2E_RESULTS_DIR),\n)\n\nprint(\"\\nDone.\")\nprint(\"End-to-end TFLite exports:\", exports_zip)\nprint(\"End-to-end benchmark results:\", results_zip)\n\ndisplay(FileLink(exports_zip))\ndisplay(FileLink(results_zip))\n\nprint(\"\\nImportant objects now available:\")\nprint(\"- e2e_registry\")\nprint(\"- e2e_interpreters\")\nprint(\"- e2e_io_details\")\nprint(\"- e2e_benchmark_df\")\nprint(\"- paper_384_table\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T17:46:06.311702Z","iopub.execute_input":"2026-04-29T17:46:06.312123Z","iopub.status.idle":"2026-04-29T17:59:45.408968Z","shell.execute_reply.started":"2026-04-29T17:46:06.312098Z","shell.execute_reply":"2026-04-29T17:59:45.408223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# PATCH + EXPORT RoPE_Conformer End-to-End TFLite\n# Fix dtype mismatch inside ConformerBlock residual additions\n# =============================================================================\n\nimport os\nimport gc\nimport json\nimport time\nimport shutil\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom pathlib import Path\nfrom tensorflow.keras import layers, Model\nfrom IPython.display import display, FileLink\n\nE2E_EXPORT_DIR = Path(\"/kaggle/working/tflite_end_to_end_exports\")\nE2E_RESULTS_DIR = Path(\"/kaggle/working/tflite_end_to_end_benchmark\")\nE2E_TABLES_DIR = E2E_RESULTS_DIR / \"tables\"\n\nE2E_EXPORT_DIR.mkdir(parents=True, exist_ok=True)\nE2E_TABLES_DIR.mkdir(parents=True, exist_ok=True)\n\nROWS_PER_FRAME = 543\nMAX_LEN = 384\nNUM_CLASSES = 250\nPAD = -100.0\n\n# -----------------------------------------------------------------------------\n# Required checks\n# -----------------------------------------------------------------------------\n\nif \"loaded_models\" not in globals():\n    raise RuntimeError(\"loaded_models is missing. Reload RoPE_Conformer first.\")\n\nif \"RoPE_Conformer\" not in loaded_models:\n    raise RuntimeError(\"RoPE_Conformer is missing from loaded_models.\")\n\nif \"ConformerBlock\" not in globals():\n    raise RuntimeError(\"ConformerBlock class is missing. Run/load the RoPE_Conformer definition first.\")\n\n# -----------------------------------------------------------------------------\n# Rebuild landmark constants if needed\n# -----------------------------------------------------------------------------\n\nLIP = [\n    0, 61, 185, 40, 39, 37, 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]\nNOSE = [1, 2, 98, 327]\nREYE = [33, 7, 163, 144, 145, 153, 154, 155, 133, 246, 161, 160, 159, 158, 157, 173]\nLEYE = [263, 249, 390, 373, 374, 380, 381, 382, 362, 466, 388, 387, 386, 385, 384, 398]\nLHAND = np.arange(468, 489).tolist()\nRHAND = np.arange(522, 543).tolist()\n\nPOINT_LANDMARKS = LIP + LHAND + RHAND + NOSE + REYE + LEYE\nNUM_NODES = len(POINT_LANDMARKS)\nCHANNELS = 6 * NUM_NODES\n\nprint(\"NUM_NODES:\", NUM_NODES)\nprint(\"CHANNELS :\", CHANNELS)\n\n# =============================================================================\n# 1) Patch ConformerBlock residual dtype handling\n# =============================================================================\n\ndef patched_conformer_ff(self, x, ln, fc1, drop, fc2, training):\n    y = fc2(drop(fc1(ln(x)), training=training))\n    y = tf.cast(y, x.dtype)\n    return x + tf.cast(0.5, x.dtype) * y\n\n\ndef patched_conformer_call(self, x, attention_mask=None, training=False):\n    # FFN 1\n    x = self._ff(\n        x,\n        self.ff1_ln,\n        self.ff1_fc1,\n        self.ff1_drop,\n        self.ff1_fc2,\n        training,\n    )\n\n    # MHSA residual\n    h = self.mhsa(\n        self.mhsa_ln(x),\n        attention_mask=attention_mask,\n        training=training,\n    )\n    h = self.mhsa_drop(h, training=training)\n    h = tf.cast(h, x.dtype)\n    x = x + h\n\n    # Conv residual\n    h = self.pw_exp(self.conv_ln(x))\n    half = tf.shape(h)[-1] // 2\n    h = h[..., :half] * tf.sigmoid(h[..., half:])\n    h = self.dw_conv(h)\n    h = self.conv_bn(h, training=training)\n    h = self.conv_act(h)\n    h = self.pw_con(h)\n    h = self.conv_sp(h, training=training)\n    h = tf.cast(h, x.dtype)\n    x = x + h\n\n    # FFN 2\n    x = self._ff(\n        x,\n        self.ff2_ln,\n        self.ff2_fc1,\n        self.ff2_drop,\n        self.ff2_fc2,\n        training,\n    )\n\n    return self.out_ln(x)\n\n\n# Monkey-patch the class used by existing layer instances\nConformerBlock._ff = patched_conformer_ff\nConformerBlock.call = patched_conformer_call\n\nprint(\"ConformerBlock patched successfully.\")\n\n# =============================================================================\n# 2) Preprocessing layers\n# =============================================================================\n\ndef tf_nan_mean(x, axis=0, keepdims=False):\n    valid = tf.where(tf.math.is_nan(x), tf.zeros_like(x), tf.ones_like(x))\n    values = tf.where(tf.math.is_nan(x), tf.zeros_like(x), x)\n    s = tf.reduce_sum(values, axis=axis, keepdims=keepdims)\n    n = tf.reduce_sum(valid, axis=axis, keepdims=keepdims)\n    return s / tf.maximum(n, tf.constant(1.0, dtype=x.dtype))\n\n\ndef tf_nan_std(x, center=None, axis=0, keepdims=False):\n    if center is None:\n        center = tf_nan_mean(x, axis=axis, keepdims=True)\n    d = x - center\n    return tf.sqrt(tf_nan_mean(d * d, axis=axis, keepdims=keepdims))\n\n\nclass EndToEndPreprocess(tf.keras.layers.Layer):\n    def __init__(self, max_len=384, point_landmarks=None, **kwargs):\n        super().__init__(**kwargs)\n        self.max_len = max_len\n        self.point_landmarks = list(point_landmarks)\n\n    def call(self, inputs):\n        x = tf.cast(inputs, tf.float32)\n\n        if len(x.shape) == 3:\n            x = x[None, ...]\n\n        mean = tf_nan_mean(\n            tf.gather(x, [17], axis=2),\n            axis=[1, 2],\n            keepdims=True,\n        )\n        mean = tf.where(tf.math.is_nan(mean), tf.constant(0.5, x.dtype), mean)\n\n        x = tf.gather(x, self.point_landmarks, axis=2)\n\n        std = tf_nan_std(x, center=mean, axis=[1, 2], keepdims=True)\n        std = tf.where(\n            tf.logical_or(\n                tf.math.is_nan(std),\n                tf.less(std, tf.constant(1e-6, x.dtype)),\n            ),\n            tf.ones_like(std),\n            std,\n        )\n\n        x = (x - mean) / std\n\n        if self.max_len is not None:\n            x = x[:, :self.max_len]\n\n        length = tf.shape(x)[1]\n        x = x[..., :2]\n\n        dx = tf.cond(\n            tf.shape(x)[1] > 1,\n            lambda: tf.pad(\n                x[:, 1:] - x[:, :-1],\n                [[0, 0], [0, 1], [0, 0], [0, 0]],\n            ),\n            lambda: tf.zeros_like(x),\n        )\n\n        dx2 = tf.cond(\n            tf.shape(x)[1] > 2,\n            lambda: tf.pad(\n                x[:, 2:] - x[:, :-2],\n                [[0, 0], [0, 2], [0, 0], [0, 0]],\n            ),\n            lambda: tf.zeros_like(x),\n        )\n\n        features = tf.concat(\n            [\n                tf.reshape(x,   (-1, length, 2 * len(self.point_landmarks))),\n                tf.reshape(dx,  (-1, length, 2 * len(self.point_landmarks))),\n                tf.reshape(dx2, (-1, length, 2 * len(self.point_landmarks))),\n            ],\n            axis=-1,\n        )\n\n        features = tf.where(\n            tf.math.is_nan(features),\n            tf.zeros_like(features),\n            features,\n        )\n\n        return features\n\n\nclass PadOrCropToMaxLen(tf.keras.layers.Layer):\n    def __init__(self, max_len=384, channels=708, pad_value=-100.0, **kwargs):\n        super().__init__(**kwargs)\n        self.max_len = max_len\n        self.channels = channels\n        self.pad_value = pad_value\n\n    def call(self, x):\n        x = tf.cast(x, tf.float32)\n        x = x[:, :self.max_len, :]\n\n        batch = tf.shape(x)[0]\n        length = tf.shape(x)[1]\n        pad_len = tf.maximum(self.max_len - length, 0)\n\n        pad_tensor = tf.fill(\n            [batch, pad_len, self.channels],\n            tf.cast(self.pad_value, x.dtype),\n        )\n\n        x = tf.concat([x, pad_tensor], axis=1)\n        x = x[:, :self.max_len, :]\n        x = tf.ensure_shape(x, [1, self.max_len, self.channels])\n\n        return x\n\n\nclass CastToFloat32(tf.keras.layers.Layer):\n    def call(self, x):\n        return tf.cast(x, tf.float32)\n\n\n# =============================================================================\n# 3) Build E2E RoPE wrapper\n# =============================================================================\n\nrope_model = loaded_models[\"RoPE_Conformer\"]\nrope_model.trainable = False\n\nfor layer in rope_model.layers:\n    layer.trainable = False\n\nraw_inp = layers.Input(\n    batch_shape=(1, None, ROWS_PER_FRAME, 3),\n    dtype=tf.float32,\n    name=\"raw_landmarks\",\n)\n\nx = EndToEndPreprocess(\n    max_len=MAX_LEN,\n    point_landmarks=POINT_LANDMARKS,\n    name=\"preprocess\",\n)(raw_inp)\n\nx = PadOrCropToMaxLen(\n    max_len=MAX_LEN,\n    channels=CHANNELS,\n    pad_value=PAD,\n    name=\"pad_or_crop_to_384\",\n)(x)\n\n# Do NOT force x to float16 here.\n# The patched ConformerBlock handles residual dtype casting internally.\nlogits = rope_model(x, training=False)\nlogits = CastToFloat32(name=\"cast_logits_to_float32\")(logits)\n\nrope_e2e_model = Model(\n    inputs=raw_inp,\n    outputs=logits,\n    name=\"RoPE_Conformer_raw_to_logits_tflite\",\n)\n\nprint(\"RoPE E2E wrapper built successfully.\")\n\n# =============================================================================\n# 4) Convert and validate\n# =============================================================================\n\ndef make_dummy_raw_landmarks(num_frames):\n    rng = np.random.default_rng(42 + int(num_frames))\n    x = rng.normal(\n        loc=0.5,\n        scale=0.15,\n        size=(1, num_frames, ROWS_PER_FRAME, 3),\n    ).astype(np.float32)\n\n    if num_frames > 5:\n        x[:, ::17, :, :] = np.nan\n\n    return x\n\n\ndummy_raw = make_dummy_raw_landmarks(96)\nkeras_y = rope_e2e_model(dummy_raw, training=False).numpy().astype(np.float32)\n\nout_path = E2E_EXPORT_DIR / \"RoPE_Conformer_raw_preprocess_logits.tflite\"\n\nif out_path.exists():\n    out_path.unlink()\n\n@tf.function(\n    input_signature=[\n        tf.TensorSpec(\n            shape=[1, None, ROWS_PER_FRAME, 3],\n            dtype=tf.float32,\n            name=\"raw_landmarks\",\n        )\n    ]\n)\ndef serving_fn(x):\n    y = rope_e2e_model(x, training=False)\n    return {\"logits\": tf.cast(y, tf.float32)}\n\nconcrete_func = serving_fn.get_concrete_function()\n\nconverter = tf.lite.TFLiteConverter.from_concrete_functions(\n    [concrete_func],\n    rope_e2e_model,\n)\n\nconverter.target_spec.supported_ops = [\n    tf.lite.OpsSet.TFLITE_BUILTINS,\n    tf.lite.OpsSet.SELECT_TF_OPS,\n]\n\nconverter.optimizations = [tf.lite.Optimize.DEFAULT]\nconverter.target_spec.supported_types = [tf.float16]\nconverter.experimental_enable_resource_variables = True\nconverter.inference_input_type = tf.float32\nconverter.inference_output_type = tf.float32\n\ntry:\n    tflite_bytes = converter.convert()\n    method = \"patched_conformer_float16_optimized\"\n\nexcept Exception as e1:\n    print(\"First conversion failed. Retrying without float16 optimization.\")\n    print(str(e1)[:3000])\n\n    converter = tf.lite.TFLiteConverter.from_concrete_functions(\n        [concrete_func],\n        rope_e2e_model,\n    )\n\n    converter.target_spec.supported_ops = [\n        tf.lite.OpsSet.TFLITE_BUILTINS,\n        tf.lite.OpsSet.SELECT_TF_OPS,\n    ]\n\n    converter.experimental_enable_resource_variables = True\n    converter.inference_input_type = tf.float32\n    converter.inference_output_type = tf.float32\n\n    tflite_bytes = converter.convert()\n    method = \"patched_conformer_float32_fallback\"\n\nwith open(out_path, \"wb\") as f:\n    f.write(tflite_bytes)\n\nprint(\"Saved:\", out_path)\nprint(\"Size MB:\", out_path.stat().st_size / (1024 ** 2))\nprint(\"Method:\", method)\n\n# Validate with dynamic frame input\ninterpreter = tf.lite.Interpreter(model_path=str(out_path))\n\ninp0 = interpreter.get_input_details()[0]\ninterpreter.resize_tensor_input(\n    inp0[\"index\"],\n    np.array(dummy_raw.shape, dtype=np.int32),\n    strict=False,\n)\ninterpreter.allocate_tensors()\n\ninp = interpreter.get_input_details()[0]\nout = interpreter.get_output_details()[0]\n\ninterpreter.set_tensor(inp[\"index\"], dummy_raw.astype(inp[\"dtype\"]))\ninterpreter.invoke()\n\ntflite_y = interpreter.get_tensor(out[\"index\"]).astype(np.float32)\n\nresult = {\n    \"model\": \"RoPE_Conformer\",\n    \"status\": \"success\",\n    \"file\": out_path.name,\n    \"path\": str(out_path),\n    \"size_mb\": float(out_path.stat().st_size / (1024 ** 2)),\n    \"conversion_method\": method,\n    \"raw_input_shape_signature\": \"[1, any_frames, 543, 3]\",\n    \"input_shape\": str(inp[\"shape\"]),\n    \"input_shape_signature\": str(inp[\"shape_signature\"]),\n    \"output_shape\": str(out[\"shape\"]),\n    \"input_dtype\": str(inp[\"dtype\"]),\n    \"output_dtype\": str(out[\"dtype\"]),\n    \"keras_top1\": int(np.argmax(keras_y, axis=-1)[0]),\n    \"tflite_top1\": int(np.argmax(tflite_y, axis=-1)[0]),\n    \"top1_match\": bool(np.argmax(keras_y, axis=-1)[0] == np.argmax(tflite_y, axis=-1)[0]),\n    \"max_abs_diff\": float(np.max(np.abs(keras_y - tflite_y))),\n    \"mean_abs_diff\": float(np.mean(np.abs(keras_y - tflite_y))),\n}\n\ndisplay(pd.DataFrame([result]))\n\npd.DataFrame([result]).to_csv(\n    E2E_TABLES_DIR / \"rope_conformer_e2e_patch_report.csv\",\n    index=False,\n)\n\nwith open(E2E_TABLES_DIR / \"rope_conformer_e2e_patch_report.json\", \"w\") as f:\n    json.dump(result, f, indent=4, default=str)\n\n# Update registry\nif \"e2e_registry\" not in globals():\n    e2e_registry = {}\n\ne2e_registry[\"RoPE_Conformer\"] = out_path\n\nprint(\"Updated e2e_registry:\")\nfor k, v in e2e_registry.items():\n    print(k, \"=>\", v)\n\n# Import test with 128 frames\ntest_raw = make_dummy_raw_landmarks(128)\n\nrope_e2e_interpreter = tf.lite.Interpreter(model_path=str(out_path))\ninp0 = rope_e2e_interpreter.get_input_details()[0]\n\nrope_e2e_interpreter.resize_tensor_input(\n    inp0[\"index\"],\n    np.array(test_raw.shape, dtype=np.int32),\n    strict=False,\n)\nrope_e2e_interpreter.allocate_tensors()\n\ninp = rope_e2e_interpreter.get_input_details()[0]\nout = rope_e2e_interpreter.get_output_details()[0]\n\nrope_e2e_interpreter.set_tensor(inp[\"index\"], test_raw.astype(inp[\"dtype\"]))\nrope_e2e_interpreter.invoke()\n\ny = rope_e2e_interpreter.get_tensor(out[\"index\"])\n\nprint(\"\\nRoPE E2E loaded successfully.\")\nprint(\"Input:\", inp)\nprint(\"Output:\", out)\nprint(\"Top1 sample:\", int(np.argmax(y, axis=-1)[0]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T18:02:49.974537Z","iopub.execute_input":"2026-04-29T18:02:49.975237Z","iopub.status.idle":"2026-04-29T18:03:05.308947Z","shell.execute_reply.started":"2026-04-29T18:02:49.975206Z","shell.execute_reply":"2026-04-29T18:03:05.308146Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# FULL DEPLOYMENT COMPARISON\n# Keras vs TFLite model-only vs End-to-End TFLite with preprocessing\n# =============================================================================\n\nimport os\nimport gc\nimport time\nimport json\nimport shutil\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom IPython.display import display, FileLink\n\n# =============================================================================\n# Output folders\n# =============================================================================\n\nCOMPARE_DIR = Path(\"/kaggle/working/full_deployment_comparison\")\nTABLES_DIR = COMPARE_DIR / \"tables\"\nPLOTS_DIR = COMPARE_DIR / \"plots\"\nRAW_LATENCY_DIR = TABLES_DIR / \"latency_runs\"\n\nfor d in [COMPARE_DIR, TABLES_DIR, PLOTS_DIR, RAW_LATENCY_DIR]:\n    d.mkdir(parents=True, exist_ok=True)\n\nTFLITE_MODEL_ONLY_DIR = Path(\"/kaggle/working/tflite_exports\")\nTFLITE_E2E_DIR = Path(\"/kaggle/working/tflite_end_to_end_exports\")\n\nEXPECTED_MODELS = [\n    \"BiLSTM\",\n    \"BiGRU\",\n    \"Transformer\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\n# =============================================================================\n# Benchmark settings\n# =============================================================================\n\n# Fair comparison across all models and stages.\n# BiLSTM/BiGRU TFLite exports are fixed batch=1, so we keep all comparisons batch=1.\nBATCH_SIZE = 1\n\n# Use fewer runs if notebook is slow.\nWARMUP_RUNS = 10\nBENCHMARK_RUNS = 80\n\n# For end-to-end comparison\nRAW_FRAME_LENGTHS = [32, 64, 128, 256, 384]\nPRIMARY_E2E_FRAMES = 384\n\nprint(\"Comparison setup\")\nprint(\"Batch size:\", BATCH_SIZE)\nprint(\"Warmup runs:\", WARMUP_RUNS)\nprint(\"Benchmark runs:\", BENCHMARK_RUNS)\nprint(\"E2E frame lengths:\", RAW_FRAME_LENGTHS)\n\n# =============================================================================\n# Required checks\n# =============================================================================\n\nrequired_base = [\"MAX_LEN\", \"NUM_CLASSES\", \"test_dataset\"]\n\nmissing_base = [x for x in required_base if x not in globals()]\nif missing_base:\n    raise RuntimeError(f\"Missing required notebook variables: {missing_base}\")\n\n# Restore CHANNELS if missing\nif \"CHANNELS\" not in globals():\n    CHANNELS = 708\n    print(\"CHANNELS was missing; restored CHANNELS=708\")\n\n# loaded_models is needed for Keras stage\nif \"loaded_models\" not in globals():\n    print(\"WARNING: loaded_models is missing. Keras-stage benchmark will be skipped.\")\n\n# =============================================================================\n# Landmark constants and preprocessing for raw E2E test inputs\n# =============================================================================\n\nROWS_PER_FRAME = 543\nPAD = -100.0\n\nLIP = [\n    0, 61, 185, 40, 39, 37, 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]\nNOSE = [1, 2, 98, 327]\nREYE = [33, 7, 163, 144, 145, 153, 154, 155, 133, 246, 161, 160, 159, 158, 157, 173]\nLEYE = [263, 249, 390, 373, 374, 380, 381, 382, 362, 466, 388, 387, 386, 385, 384, 398]\nLHAND = np.arange(468, 489).tolist()\nRHAND = np.arange(522, 543).tolist()\n\nPOINT_LANDMARKS = LIP + LHAND + RHAND + NOSE + REYE + LEYE\nNUM_NODES = len(POINT_LANDMARKS)\nCHANNELS_FROM_PREPROCESS = 6 * NUM_NODES\n\nprint(\"NUM_NODES:\", NUM_NODES)\nprint(\"CHANNELS_FROM_PREPROCESS:\", CHANNELS_FROM_PREPROCESS)\n\n# =============================================================================\n# Input batches\n# =============================================================================\n\ndef get_feature_batch(dataset, batch_size=1):\n    \"\"\"\n    Returns already-preprocessed model input:\n    [B, 384, 708]\n    \"\"\"\n    for xb, yb in dataset.unbatch().batch(batch_size).take(1):\n        y = yb.numpy()\n        if y.ndim == 2:\n            y = np.argmax(y, axis=1)\n        return xb.numpy().astype(np.float32), y\n    raise RuntimeError(\"Could not get feature batch from test_dataset.\")\n\n\ndef make_dummy_raw_landmarks(num_frames):\n    \"\"\"\n    Synthetic raw landmark input:\n    [1, T, 543, 3]\n    \n    Used only for deployment speed testing of end-to-end TFLite,\n    where the point is to measure the full graph including preprocessing.\n    \"\"\"\n    rng = np.random.default_rng(123 + int(num_frames))\n    x = rng.normal(\n        loc=0.5,\n        scale=0.15,\n        size=(1, num_frames, ROWS_PER_FRAME, 3),\n    ).astype(np.float32)\n\n    # Add a few NaNs to exercise NaN-safe preprocessing.\n    if num_frames > 5:\n        x[:, ::17, :, :] = np.nan\n\n    return x\n\n\nfeature_x, feature_y = get_feature_batch(test_dataset, batch_size=BATCH_SIZE)\n\nprint(\"Feature input:\", feature_x.shape, feature_x.dtype)\nprint(\"Feature label:\", feature_y)\n\n# =============================================================================\n# Path discovery\n# =============================================================================\n\ndef find_tflite_file_for_model(model_name, folder, e2e=False):\n    \"\"\"\n    Find best TFLite file for a model.\n    \"\"\"\n    folder = Path(folder)\n\n    if not folder.exists():\n        return None\n\n    files = sorted(folder.glob(f\"{model_name}*.tflite\"))\n\n    if len(files) == 0:\n        return None\n\n    if e2e:\n        preferred_keywords = [\n            \"raw_preprocess_logits\",\n            \"raw_to_logits\",\n            \"preprocess\",\n        ]\n    else:\n        preferred_keywords = [\n            \"unrolled_b1\",\n            \"no_cudnn\",\n            \"cpu_rnn\",\n            \"float16_logits\",\n            \"float32_logits\",\n        ]\n\n    for key in preferred_keywords:\n        matches = [p for p in files if key in p.name]\n        if matches:\n            return matches[0]\n\n    return files[0]\n\n\ndef infer_precision_from_file(path):\n    if path is None:\n        return None\n\n    name = path.name.lower()\n\n    if \"float16\" in name:\n        return \"float16 weights\"\n    if \"float32\" in name:\n        return \"float32\"\n    if \"int8\" in name:\n        return \"int8\"\n    return \"mixed/unknown\"\n\n\ndef get_model_size_mb_from_keras(model_name):\n    \"\"\"\n    Approximate Keras model weight size from count_params.\n    Uses float32 estimate.\n    \"\"\"\n    if \"loaded_models\" not in globals():\n        return None\n\n    if model_name not in loaded_models:\n        return None\n\n    try:\n        params = loaded_models[model_name].count_params()\n        return float(params * 4 / (1024 ** 2))\n    except Exception:\n        return None\n\n\ndef get_model_params(model_name):\n    if \"loaded_models\" not in globals():\n        return None\n\n    if model_name not in loaded_models:\n        return None\n\n    try:\n        return int(loaded_models[model_name].count_params())\n    except Exception:\n        return None\n\n\nmodel_only_paths = {}\ne2e_paths = {}\n\n# Prefer existing registries if available, otherwise discover from folders\nfor model_name in EXPECTED_MODELS:\n    path = None\n\n    if \"tflite_registry\" in globals():\n        try:\n            path = tflite_registry.get(model_name, None)\n        except Exception:\n            path = None\n\n    if path is None:\n        path = find_tflite_file_for_model(model_name, TFLITE_MODEL_ONLY_DIR, e2e=False)\n\n    model_only_paths[model_name] = None if path is None else Path(path)\n\n    path = None\n\n    if \"e2e_registry\" in globals():\n        try:\n            path = e2e_registry.get(model_name, None)\n        except Exception:\n            path = None\n\n    if path is None:\n        path = find_tflite_file_for_model(model_name, TFLITE_E2E_DIR, e2e=True)\n\n    e2e_paths[model_name] = None if path is None else Path(path)\n\ninventory_rows = []\n\nfor model_name in EXPECTED_MODELS:\n    inventory_rows.append({\n        \"model\": model_name,\n        \"keras_available\": (\"loaded_models\" in globals()) and (model_name in loaded_models),\n        \"model_only_tflite_found\": model_only_paths[model_name] is not None and model_only_paths[model_name].exists(),\n        \"model_only_tflite_file\": None if model_only_paths[model_name] is None else model_only_paths[model_name].name,\n        \"e2e_tflite_found\": e2e_paths[model_name] is not None and e2e_paths[model_name].exists(),\n        \"e2e_tflite_file\": None if e2e_paths[model_name] is None else e2e_paths[model_name].name,\n    })\n\ninventory_df = pd.DataFrame(inventory_rows)\n\nprint(\"\\nInventory:\")\ndisplay(inventory_df)\n\ninventory_df.to_csv(TABLES_DIR / \"deployment_inventory.csv\", index=False)\n\n# =============================================================================\n# Benchmark helpers\n# =============================================================================\n\ndef summarize_latencies(latencies_ms):\n    latencies_ms = np.array(latencies_ms, dtype=np.float64)\n\n    return {\n        \"mean_latency_ms\": float(np.mean(latencies_ms)),\n        \"std_latency_ms\": float(np.std(latencies_ms)),\n        \"median_latency_ms\": float(np.median(latencies_ms)),\n        \"min_latency_ms\": float(np.min(latencies_ms)),\n        \"max_latency_ms\": float(np.max(latencies_ms)),\n        \"p95_latency_ms\": float(np.percentile(latencies_ms, 95)),\n        \"p99_latency_ms\": float(np.percentile(latencies_ms, 99)),\n        \"throughput_samples_per_sec\": float(1000.0 / np.mean(latencies_ms)),\n    }\n\n\ndef benchmark_keras_model(model_name, model, x):\n    \"\"\"\n    Keras original model benchmark.\n    Input: preprocessed features [1,384,708].\n    \"\"\"\n    for _ in range(WARMUP_RUNS):\n        _ = model(x, training=False).numpy()\n\n    times = []\n\n    last_y = None\n\n    for _ in range(BENCHMARK_RUNS):\n        start = time.perf_counter()\n        y = model(x, training=False).numpy()\n        end = time.perf_counter()\n\n        times.append((end - start) * 1000.0)\n        last_y = y\n\n    last_y = np.nan_to_num(last_y, nan=0.0, posinf=1e4, neginf=-1e4)\n\n    result = summarize_latencies(times)\n    result.update({\n        \"top1_sample\": int(np.argmax(last_y, axis=-1)[0]),\n    })\n\n    return result, np.array(times)\n\n\ndef load_tflite_interpreter(path, input_shape):\n    interpreter = tf.lite.Interpreter(model_path=str(path))\n\n    inp0 = interpreter.get_input_details()[0]\n\n    try:\n        interpreter.resize_tensor_input(\n            inp0[\"index\"],\n            np.array(input_shape, dtype=np.int32),\n            strict=False,\n        )\n    except Exception:\n        pass\n\n    interpreter.allocate_tensors()\n\n    inp = interpreter.get_input_details()[0]\n    out = interpreter.get_output_details()[0]\n\n    return interpreter, inp, out\n\n\ndef benchmark_tflite_model(path, x):\n    \"\"\"\n    TFLite benchmark.\n    Works for model-only and end-to-end TFLite.\n    \"\"\"\n    interpreter, inp, out = load_tflite_interpreter(path, x.shape)\n\n    x_in = x.astype(inp[\"dtype\"])\n\n    for _ in range(WARMUP_RUNS):\n        interpreter.set_tensor(inp[\"index\"], x_in)\n        interpreter.invoke()\n        _ = interpreter.get_tensor(out[\"index\"])\n\n    times = []\n\n    last_y = None\n\n    for _ in range(BENCHMARK_RUNS):\n        start = time.perf_counter()\n        interpreter.set_tensor(inp[\"index\"], x_in)\n        interpreter.invoke()\n        y = interpreter.get_tensor(out[\"index\"])\n        end = time.perf_counter()\n\n        times.append((end - start) * 1000.0)\n        last_y = y\n\n    last_y = np.nan_to_num(last_y, nan=0.0, posinf=1e4, neginf=-1e4)\n\n    result = summarize_latencies(times)\n    result.update({\n        \"top1_sample\": int(np.argmax(last_y, axis=-1)[0]),\n        \"input_shape\": str(inp[\"shape\"]),\n        \"input_shape_signature\": str(inp.get(\"shape_signature\", inp[\"shape\"])),\n        \"input_dtype\": str(inp[\"dtype\"]),\n        \"output_shape\": str(out[\"shape\"]),\n        \"output_dtype\": str(out[\"dtype\"]),\n    })\n\n    del interpreter\n    gc.collect()\n\n    return result, np.array(times)\n\n\n# =============================================================================\n# Run comparison benchmark\n# =============================================================================\n\ncomparison_rows = []\nlatency_runs = {}\n\nfor model_name in EXPECTED_MODELS:\n    print(\"\\n\" + \"=\" * 100)\n    print(\"MODEL:\", model_name)\n    print(\"=\" * 100)\n\n    # -------------------------------------------------------------------------\n    # Stage 1: Keras original\n    # -------------------------------------------------------------------------\n    if \"loaded_models\" in globals() and model_name in loaded_models:\n        try:\n            print(\"Benchmarking Keras original...\")\n\n            row_metrics, latencies = benchmark_keras_model(\n                model_name=model_name,\n                model=loaded_models[model_name],\n                x=feature_x,\n            )\n\n            row = {\n                \"model\": model_name,\n                \"stage\": \"Keras original\",\n                \"input_type\": \"preprocessed features\",\n                \"input_description\": \"[1, 384, 708]\",\n                \"file\": \"in-memory Keras model\",\n                \"precision\": \"Keras runtime\",\n                \"parameters\": get_model_params(model_name),\n                \"size_mb\": get_model_size_mb_from_keras(model_name),\n                \"status\": \"success\",\n                \"error\": None,\n                **row_metrics,\n            }\n\n            comparison_rows.append(row)\n            latency_runs[(model_name, \"Keras original\")] = latencies\n\n            print(\"Keras mean latency:\", row[\"mean_latency_ms\"])\n\n        except Exception:\n            err = traceback.format_exc()\n            print(\"Keras benchmark failed:\")\n            print(err)\n\n            comparison_rows.append({\n                \"model\": model_name,\n                \"stage\": \"Keras original\",\n                \"status\": \"failed\",\n                \"error\": err,\n            })\n    else:\n        comparison_rows.append({\n            \"model\": model_name,\n            \"stage\": \"Keras original\",\n            \"status\": \"missing\",\n            \"error\": \"loaded_models missing or model not loaded\",\n        })\n\n    # -------------------------------------------------------------------------\n    # Stage 2: TFLite model-only\n    # -------------------------------------------------------------------------\n    model_only_path = model_only_paths.get(model_name)\n\n    if model_only_path is not None and model_only_path.exists():\n        try:\n            print(\"Benchmarking TFLite model-only:\", model_only_path.name)\n\n            row_metrics, latencies = benchmark_tflite_model(\n                path=model_only_path,\n                x=feature_x,\n            )\n\n            row = {\n                \"model\": model_name,\n                \"stage\": \"TFLite model-only\",\n                \"input_type\": \"preprocessed features\",\n                \"input_description\": \"[1, 384, 708]\",\n                \"file\": model_only_path.name,\n                \"precision\": infer_precision_from_file(model_only_path),\n                \"parameters\": get_model_params(model_name),\n                \"size_mb\": float(model_only_path.stat().st_size / (1024 ** 2)),\n                \"status\": \"success\",\n                \"error\": None,\n                **row_metrics,\n            }\n\n            comparison_rows.append(row)\n            latency_runs[(model_name, \"TFLite model-only\")] = latencies\n\n            print(\"TFLite model-only mean latency:\", row[\"mean_latency_ms\"])\n\n        except Exception:\n            err = traceback.format_exc()\n            print(\"TFLite model-only failed:\")\n            print(err)\n\n            comparison_rows.append({\n                \"model\": model_name,\n                \"stage\": \"TFLite model-only\",\n                \"status\": \"failed\",\n                \"file\": None if model_only_path is None else model_only_path.name,\n                \"error\": err,\n            })\n    else:\n        comparison_rows.append({\n            \"model\": model_name,\n            \"stage\": \"TFLite model-only\",\n            \"status\": \"missing\",\n            \"error\": \"model-only TFLite file missing\",\n        })\n\n    # -------------------------------------------------------------------------\n    # Stage 3: End-to-End TFLite at 384 frames\n    # -------------------------------------------------------------------------\n    e2e_path = e2e_paths.get(model_name)\n\n    if e2e_path is not None and e2e_path.exists():\n        try:\n            print(\"Benchmarking End-to-End TFLite:\", e2e_path.name)\n\n            raw_x = make_dummy_raw_landmarks(PRIMARY_E2E_FRAMES)\n\n            row_metrics, latencies = benchmark_tflite_model(\n                path=e2e_path,\n                x=raw_x,\n            )\n\n            row = {\n                \"model\": model_name,\n                \"stage\": \"TFLite end-to-end\",\n                \"input_type\": \"raw landmarks\",\n                \"input_description\": f\"[1, {PRIMARY_E2E_FRAMES}, 543, 3]\",\n                \"file\": e2e_path.name,\n                \"precision\": infer_precision_from_file(e2e_path),\n                \"parameters\": get_model_params(model_name),\n                \"size_mb\": float(e2e_path.stat().st_size / (1024 ** 2)),\n                \"status\": \"success\",\n                \"error\": None,\n                \"frames\": PRIMARY_E2E_FRAMES,\n                **row_metrics,\n            }\n\n            comparison_rows.append(row)\n            latency_runs[(model_name, \"TFLite end-to-end\")] = latencies\n\n            print(\"E2E mean latency:\", row[\"mean_latency_ms\"])\n\n        except Exception:\n            err = traceback.format_exc()\n            print(\"End-to-End TFLite failed:\")\n            print(err)\n\n            comparison_rows.append({\n                \"model\": model_name,\n                \"stage\": \"TFLite end-to-end\",\n                \"status\": \"failed\",\n                \"file\": None if e2e_path is None else e2e_path.name,\n                \"error\": err,\n            })\n    else:\n        comparison_rows.append({\n            \"model\": model_name,\n            \"stage\": \"TFLite end-to-end\",\n            \"status\": \"missing\",\n            \"error\": \"end-to-end TFLite file missing\",\n        })\n\ncomparison_df = pd.DataFrame(comparison_rows)\n\nprint(\"\\nFull comparison table:\")\ndisplay(comparison_df)\n\ncomparison_df.to_csv(TABLES_DIR / \"full_keras_tflite_e2e_comparison_raw.csv\", index=False)\n\n# Save latency distributions\nfor (model_name, stage), latencies in latency_runs.items():\n    safe_stage = stage.replace(\" \", \"_\").replace(\"-\", \"_\")\n    out_csv = RAW_LATENCY_DIR / f\"{model_name}_{safe_stage}_latency_runs.csv\"\n\n    pd.DataFrame({\n        \"run\": np.arange(1, len(latencies) + 1),\n        \"latency_ms\": latencies,\n    }).to_csv(out_csv, index=False)\n\n# =============================================================================\n# Derive paper tables\n# =============================================================================\n\nsuccess_df = comparison_df[comparison_df[\"status\"] == \"success\"].copy()\n\nif len(success_df) == 0:\n    raise RuntimeError(\"No successful benchmark rows.\")\n\n# stage order for plotting\nstage_order = {\n    \"Keras original\": 0,\n    \"TFLite model-only\": 1,\n    \"TFLite end-to-end\": 2,\n}\n\nsuccess_df[\"stage_order\"] = success_df[\"stage\"].map(stage_order)\nsuccess_df = success_df.sort_values([\"model\", \"stage_order\"])\n\n# Efficiency metric\nsuccess_df[\"efficiency_score\"] = success_df[\"throughput_samples_per_sec\"] / success_df[\"size_mb\"]\n\n# For each model, compute relative changes versus Keras and versus model-only\ndef add_relative_metrics(df):\n    df = df.copy()\n    df[\"latency_vs_keras_ratio\"] = np.nan\n    df[\"latency_vs_tflite_model_only_ratio\"] = np.nan\n    df[\"size_vs_keras_ratio\"] = np.nan\n    df[\"size_vs_tflite_model_only_ratio\"] = np.nan\n\n    for model_name in df[\"model\"].unique():\n        sub = df[df[\"model\"] == model_name]\n\n        keras_rows = sub[sub[\"stage\"] == \"Keras original\"]\n        model_only_rows = sub[sub[\"stage\"] == \"TFLite model-only\"]\n\n        keras_latency = None\n        keras_size = None\n        model_only_latency = None\n        model_only_size = None\n\n        if len(keras_rows) > 0:\n            keras_latency = float(keras_rows.iloc[0][\"mean_latency_ms\"])\n            keras_size = float(keras_rows.iloc[0][\"size_mb\"])\n\n        if len(model_only_rows) > 0:\n            model_only_latency = float(model_only_rows.iloc[0][\"mean_latency_ms\"])\n            model_only_size = float(model_only_rows.iloc[0][\"size_mb\"])\n\n        mask = df[\"model\"] == model_name\n\n        if keras_latency and keras_latency > 0:\n            df.loc[mask, \"latency_vs_keras_ratio\"] = df.loc[mask, \"mean_latency_ms\"] / keras_latency\n\n        if model_only_latency and model_only_latency > 0:\n            df.loc[mask, \"latency_vs_tflite_model_only_ratio\"] = df.loc[mask, \"mean_latency_ms\"] / model_only_latency\n\n        if keras_size and keras_size > 0:\n            df.loc[mask, \"size_vs_keras_ratio\"] = df.loc[mask, \"size_mb\"] / keras_size\n\n        if model_only_size and model_only_size > 0:\n            df.loc[mask, \"size_vs_tflite_model_only_ratio\"] = df.loc[mask, \"size_mb\"] / model_only_size\n\n    return df\n\n\nsuccess_df = add_relative_metrics(success_df)\n\npaper_cols = [\n    \"model\",\n    \"stage\",\n    \"input_type\",\n    \"input_description\",\n    \"precision\",\n    \"parameters\",\n    \"size_mb\",\n    \"mean_latency_ms\",\n    \"std_latency_ms\",\n    \"median_latency_ms\",\n    \"p95_latency_ms\",\n    \"p99_latency_ms\",\n    \"throughput_samples_per_sec\",\n    \"efficiency_score\",\n    \"latency_vs_keras_ratio\",\n    \"latency_vs_tflite_model_only_ratio\",\n    \"size_vs_keras_ratio\",\n    \"size_vs_tflite_model_only_ratio\",\n    \"top1_sample\",\n    \"file\",\n]\n\npaper_table = success_df[paper_cols].copy()\n\nround_cols = [\n    \"size_mb\",\n    \"mean_latency_ms\",\n    \"std_latency_ms\",\n    \"median_latency_ms\",\n    \"p95_latency_ms\",\n    \"p99_latency_ms\",\n    \"throughput_samples_per_sec\",\n    \"efficiency_score\",\n    \"latency_vs_keras_ratio\",\n    \"latency_vs_tflite_model_only_ratio\",\n    \"size_vs_keras_ratio\",\n    \"size_vs_tflite_model_only_ratio\",\n]\n\nfor col in round_cols:\n    if col in paper_table.columns:\n        paper_table[col] = paper_table[col].astype(float).round(4)\n\npaper_csv = TABLES_DIR / \"paper_table_full_deployment_comparison.csv\"\npaper_md = TABLES_DIR / \"paper_table_full_deployment_comparison.md\"\n\npaper_table.to_csv(paper_csv, index=False)\n\nwith open(paper_md, \"w\") as f:\n    f.write(paper_table.to_markdown(index=False))\n\nprint(\"\\nPaper-ready table:\")\ndisplay(paper_table)\n\nprint(\"Saved paper table:\")\nprint(paper_csv)\nprint(paper_md)\n\n# =============================================================================\n# Per-model summary table\n# =============================================================================\n\nper_model_rows = []\n\nfor model_name in EXPECTED_MODELS:\n    sub = success_df[success_df[\"model\"] == model_name].copy()\n\n    row = {\"model\": model_name}\n\n    for stage in [\"Keras original\", \"TFLite model-only\", \"TFLite end-to-end\"]:\n        s = sub[sub[\"stage\"] == stage]\n\n        if len(s) == 0:\n            row[f\"{stage} latency ms\"] = np.nan\n            row[f\"{stage} size MB\"] = np.nan\n            row[f\"{stage} throughput\"] = np.nan\n        else:\n            s = s.iloc[0]\n            row[f\"{stage} latency ms\"] = s[\"mean_latency_ms\"]\n            row[f\"{stage} size MB\"] = s[\"size_mb\"]\n            row[f\"{stage} throughput\"] = s[\"throughput_samples_per_sec\"]\n\n    # deltas\n    if not np.isnan(row.get(\"Keras original latency ms\", np.nan)) and not np.isnan(row.get(\"TFLite model-only latency ms\", np.nan)):\n        row[\"TFLite/Keras latency ratio\"] = row[\"TFLite model-only latency ms\"] / row[\"Keras original latency ms\"]\n    else:\n        row[\"TFLite/Keras latency ratio\"] = np.nan\n\n    if not np.isnan(row.get(\"TFLite model-only latency ms\", np.nan)) and not np.isnan(row.get(\"TFLite end-to-end latency ms\", np.nan)):\n        row[\"E2E/model-only latency ratio\"] = row[\"TFLite end-to-end latency ms\"] / row[\"TFLite model-only latency ms\"]\n    else:\n        row[\"E2E/model-only latency ratio\"] = np.nan\n\n    if not np.isnan(row.get(\"Keras original size MB\", np.nan)) and not np.isnan(row.get(\"TFLite model-only size MB\", np.nan)):\n        row[\"TFLite/Keras size ratio\"] = row[\"TFLite model-only size MB\"] / row[\"Keras original size MB\"]\n    else:\n        row[\"TFLite/Keras size ratio\"] = np.nan\n\n    per_model_rows.append(row)\n\nper_model_summary = pd.DataFrame(per_model_rows)\n\nfor col in per_model_summary.columns:\n    if col != \"model\":\n        per_model_summary[col] = per_model_summary[col].astype(float).round(4)\n\nper_model_csv = TABLES_DIR / \"per_model_stage_summary.csv\"\nper_model_md = TABLES_DIR / \"per_model_stage_summary.md\"\n\nper_model_summary.to_csv(per_model_csv, index=False)\n\nwith open(per_model_md, \"w\") as f:\n    f.write(per_model_summary.to_markdown(index=False))\n\nprint(\"\\nPer-model summary:\")\ndisplay(per_model_summary)\n\n# =============================================================================\n# Plot helpers\n# =============================================================================\n\ndef save_plot(path):\n    plt.tight_layout()\n    plt.savefig(path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n    print(\"Saved:\", path)\n\n\ndef grouped_bar(df, value_col, title, ylabel, filename):\n    pivot = df.pivot_table(\n        index=\"model\",\n        columns=\"stage\",\n        values=value_col,\n        aggfunc=\"first\",\n    )\n\n    available_stages = [s for s in [\"Keras original\", \"TFLite model-only\", \"TFLite end-to-end\"] if s in pivot.columns]\n    pivot = pivot[available_stages]\n\n    ax = pivot.plot(kind=\"bar\", figsize=(12, 6))\n    ax.set_title(title)\n    ax.set_xlabel(\"Model\")\n    ax.set_ylabel(ylabel)\n    ax.grid(axis=\"y\", alpha=0.3)\n    plt.xticks(rotation=25, ha=\"right\")\n    plt.legend(title=\"Stage\")\n    save_plot(PLOTS_DIR / filename)\n\n\n# =============================================================================\n# Overall plots\n# =============================================================================\n\n# 1. Latency grouped by stage\ngrouped_bar(\n    success_df,\n    value_col=\"mean_latency_ms\",\n    title=\"Latency Comparison Across Deployment Stages\",\n    ylabel=\"Mean Latency (ms)\",\n    filename=\"01_latency_keras_vs_tflite_vs_e2e.png\",\n)\n\n# 2. Throughput grouped by stage\ngrouped_bar(\n    success_df,\n    value_col=\"throughput_samples_per_sec\",\n    title=\"Throughput Comparison Across Deployment Stages\",\n    ylabel=\"Samples / Second\",\n    filename=\"02_throughput_keras_vs_tflite_vs_e2e.png\",\n)\n\n# 3. Model size grouped by stage\ngrouped_bar(\n    success_df,\n    value_col=\"size_mb\",\n    title=\"Model Size Comparison Across Deployment Stages\",\n    ylabel=\"Size (MB)\",\n    filename=\"03_size_keras_vs_tflite_vs_e2e.png\",\n)\n\n# 4. Efficiency grouped by stage\ngrouped_bar(\n    success_df,\n    value_col=\"efficiency_score\",\n    title=\"Deployment Efficiency Across Stages\",\n    ylabel=\"Throughput / MB\",\n    filename=\"04_efficiency_keras_vs_tflite_vs_e2e.png\",\n)\n\n# 5. Size vs latency scatter\nplt.figure(figsize=(10, 7))\n\nfor stage in [\"Keras original\", \"TFLite model-only\", \"TFLite end-to-end\"]:\n    sub = success_df[success_df[\"stage\"] == stage]\n\n    if len(sub) == 0:\n        continue\n\n    plt.scatter(\n        sub[\"size_mb\"],\n        sub[\"mean_latency_ms\"],\n        s=130,\n        label=stage,\n    )\n\n    for _, row in sub.iterrows():\n        plt.annotate(\n            row[\"model\"],\n            (row[\"size_mb\"], row[\"mean_latency_ms\"]),\n            textcoords=\"offset points\",\n            xytext=(6, 6),\n            fontsize=8,\n        )\n\nplt.title(\"Deployment Trade-off: Model Size vs Latency\")\nplt.xlabel(\"Size (MB)\")\nplt.ylabel(\"Mean Latency (ms)\")\nplt.grid(alpha=0.3)\nplt.legend()\nsave_plot(PLOTS_DIR / \"05_size_vs_latency_all_stages.png\")\n\n# 6. TFLite overhead ratio\nratio_df = success_df[\n    success_df[\"stage\"].isin([\"TFLite model-only\", \"TFLite end-to-end\"])\n].copy()\n\nif len(ratio_df) > 0:\n    grouped_bar(\n        ratio_df,\n        value_col=\"latency_vs_keras_ratio\",\n        title=\"Latency Ratio Relative to Keras Original\",\n        ylabel=\"Latency Ratio vs Keras\",\n        filename=\"06_latency_ratio_vs_keras.png\",\n    )\n\n# 7. E2E overhead over model-only\ne2e_overhead_rows = []\n\nfor model_name in EXPECTED_MODELS:\n    sub = success_df[success_df[\"model\"] == model_name]\n\n    model_only = sub[sub[\"stage\"] == \"TFLite model-only\"]\n    e2e = sub[sub[\"stage\"] == \"TFLite end-to-end\"]\n\n    if len(model_only) > 0 and len(e2e) > 0:\n        model_only_latency = float(model_only.iloc[0][\"mean_latency_ms\"])\n        e2e_latency = float(e2e.iloc[0][\"mean_latency_ms\"])\n\n        e2e_overhead_rows.append({\n            \"model\": model_name,\n            \"model_only_latency_ms\": model_only_latency,\n            \"e2e_latency_ms\": e2e_latency,\n            \"absolute_overhead_ms\": e2e_latency - model_only_latency,\n            \"relative_overhead_ratio\": e2e_latency / model_only_latency,\n        })\n\ne2e_overhead_df = pd.DataFrame(e2e_overhead_rows)\n\nif len(e2e_overhead_df) > 0:\n    e2e_overhead_df.to_csv(TABLES_DIR / \"e2e_preprocessing_overhead.csv\", index=False)\n\n    plt.figure(figsize=(10, 6))\n    plt.bar(e2e_overhead_df[\"model\"], e2e_overhead_df[\"absolute_overhead_ms\"])\n    plt.title(\"End-to-End Preprocessing Overhead\")\n    plt.xlabel(\"Model\")\n    plt.ylabel(\"E2E latency - model-only latency (ms)\")\n    plt.xticks(rotation=25, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    save_plot(PLOTS_DIR / \"07_e2e_preprocessing_overhead_ms.png\")\n\n    plt.figure(figsize=(10, 6))\n    plt.bar(e2e_overhead_df[\"model\"], e2e_overhead_df[\"relative_overhead_ratio\"])\n    plt.title(\"End-to-End / Model-only Latency Ratio\")\n    plt.xlabel(\"Model\")\n    plt.ylabel(\"Latency Ratio\")\n    plt.xticks(rotation=25, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    save_plot(PLOTS_DIR / \"08_e2e_over_model_only_latency_ratio.png\")\n\n    print(\"\\nE2E preprocessing overhead:\")\n    display(e2e_overhead_df.round(4))\n\n# =============================================================================\n# Per-model plots\n# =============================================================================\n\nPER_MODEL_PLOT_DIR = PLOTS_DIR / \"per_model\"\nPER_MODEL_PLOT_DIR.mkdir(exist_ok=True)\n\nfor model_name in EXPECTED_MODELS:\n    sub = success_df[success_df[\"model\"] == model_name].sort_values(\"stage_order\")\n\n    if len(sub) == 0:\n        continue\n\n    # Latency per model\n    plt.figure(figsize=(8, 5))\n    plt.bar(sub[\"stage\"], sub[\"mean_latency_ms\"])\n    plt.title(f\"{model_name}: Latency Across Stages\")\n    plt.xlabel(\"Deployment Stage\")\n    plt.ylabel(\"Mean Latency (ms)\")\n    plt.xticks(rotation=20, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    save_plot(PER_MODEL_PLOT_DIR / f\"{model_name}_latency_stages.png\")\n\n    # Size per model\n    plt.figure(figsize=(8, 5))\n    plt.bar(sub[\"stage\"], sub[\"size_mb\"])\n    plt.title(f\"{model_name}: Size Across Stages\")\n    plt.xlabel(\"Deployment Stage\")\n    plt.ylabel(\"Size (MB)\")\n    plt.xticks(rotation=20, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    save_plot(PER_MODEL_PLOT_DIR / f\"{model_name}_size_stages.png\")\n\n    # Throughput per model\n    plt.figure(figsize=(8, 5))\n    plt.bar(sub[\"stage\"], sub[\"throughput_samples_per_sec\"])\n    plt.title(f\"{model_name}: Throughput Across Stages\")\n    plt.xlabel(\"Deployment Stage\")\n    plt.ylabel(\"Samples / Second\")\n    plt.xticks(rotation=20, ha=\"right\")\n    plt.grid(axis=\"y\", alpha=0.3)\n    save_plot(PER_MODEL_PLOT_DIR / f\"{model_name}_throughput_stages.png\")\n\n# =============================================================================\n# Optional: E2E variable-frame plots if old benchmark exists\n# =============================================================================\n\ne2e_variable_csv = Path(\n    \"/kaggle/working/tflite_end_to_end_benchmark/tables/end_to_end_tflite_variable_frames_benchmark.csv\"\n)\n\nif e2e_variable_csv.exists():\n    e2e_variable_df = pd.read_csv(e2e_variable_csv)\n\n    e2e_variable_success = e2e_variable_df[e2e_variable_df[\"status\"] == \"success\"].copy()\n\n    if len(e2e_variable_success) > 0:\n        e2e_variable_success.to_csv(\n            TABLES_DIR / \"e2e_variable_frames_benchmark_copy.csv\",\n            index=False,\n        )\n\n        plt.figure(figsize=(11, 7))\n\n        for model_name in e2e_variable_success[\"model\"].unique():\n            sub = e2e_variable_success[\n                e2e_variable_success[\"model\"] == model_name\n            ].sort_values(\"frames\")\n\n            plt.plot(\n                sub[\"frames\"],\n                sub[\"mean_latency_ms\"],\n                marker=\"o\",\n                linewidth=2,\n                label=model_name,\n            )\n\n        plt.title(\"End-to-End TFLite Latency vs Number of Raw Frames\")\n        plt.xlabel(\"Number of Raw Input Frames\")\n        plt.ylabel(\"Mean Latency (ms)\")\n        plt.grid(alpha=0.3)\n        plt.legend()\n        save_plot(PLOTS_DIR / \"09_e2e_latency_vs_frame_count.png\")\n\n# =============================================================================\n# Best-stage summary\n# =============================================================================\n\nsummary_lines = [\n    \"# Full Deployment Comparison Summary\",\n    \"\",\n    \"This comparison evaluates three deployment stages:\",\n    \"\",\n    \"1. **Keras original**: original TensorFlow/Keras model using preprocessed features `[1, 384, 708]`.\",\n    \"2. **TFLite model-only**: converted TFLite model using the same preprocessed features `[1, 384, 708]`.\",\n    \"3. **TFLite end-to-end**: converted TFLite graph that receives raw landmarks `[1, T, 543, 3]` and performs preprocessing inside the model.\",\n    \"\",\n    f\"Primary end-to-end frame length used for stage comparison: **{PRIMARY_E2E_FRAMES} frames**.\",\n    \"\",\n]\n\nif len(success_df) > 0:\n    for stage in [\"Keras original\", \"TFLite model-only\", \"TFLite end-to-end\"]:\n        sub = success_df[success_df[\"stage\"] == stage]\n\n        if len(sub) == 0:\n            continue\n\n        fastest = sub.loc[sub[\"mean_latency_ms\"].idxmin()]\n        smallest = sub.loc[sub[\"size_mb\"].idxmin()]\n        efficient = sub.loc[sub[\"efficiency_score\"].idxmax()]\n\n        summary_lines += [\n            f\"## {stage}\",\n            \"\",\n            f\"- Fastest model: **{fastest['model']}** ({fastest['mean_latency_ms']:.4f} ms).\",\n            f\"- Smallest model: **{smallest['model']}** ({smallest['size_mb']:.4f} MB).\",\n            f\"- Best efficiency score: **{efficient['model']}** ({efficient['efficiency_score']:.4f}).\",\n            \"\",\n        ]\n\nif len(e2e_overhead_df) > 0:\n    min_overhead = e2e_overhead_df.loc[e2e_overhead_df[\"absolute_overhead_ms\"].idxmin()]\n    max_overhead = e2e_overhead_df.loc[e2e_overhead_df[\"absolute_overhead_ms\"].idxmax()]\n\n    summary_lines += [\n        \"## Preprocessing Overhead\",\n        \"\",\n        f\"- Lowest absolute E2E overhead: **{min_overhead['model']}** \"\n        f\"({min_overhead['absolute_overhead_ms']:.4f} ms).\",\n        f\"- Highest absolute E2E overhead: **{max_overhead['model']}** \"\n        f\"({max_overhead['absolute_overhead_ms']:.4f} ms).\",\n        \"\",\n    ]\n\nsummary_path = COMPARE_DIR / \"full_deployment_comparison_summary.md\"\n\nwith open(summary_path, \"w\") as f:\n    f.write(\"\\n\".join(summary_lines))\n\nprint(\"\\n\".join(summary_lines))\n\n# =============================================================================\n# ZIP all outputs\n# =============================================================================\n\nzip_base = \"/kaggle/working/full_deployment_comparison\"\nzip_path = zip_base + \".zip\"\n\nif os.path.exists(zip_path):\n    os.remove(zip_path)\n\nfinal_zip = shutil.make_archive(\n    base_name=zip_base,\n    format=\"zip\",\n    root_dir=str(COMPARE_DIR),\n)\n\nprint(\"\\nDone.\")\nprint(\"Comparison package:\", final_zip)\n\ndisplay(FileLink(final_zip))\n\nprint(\"\\nImportant outputs:\")\nprint(\"Inventory CSV:\", TABLES_DIR / \"deployment_inventory.csv\")\nprint(\"Raw comparison CSV:\", TABLES_DIR / \"full_keras_tflite_e2e_comparison_raw.csv\")\nprint(\"Paper table CSV:\", paper_csv)\nprint(\"Per-model summary CSV:\", per_model_csv)\nprint(\"Plots folder:\", PLOTS_DIR)\nprint(\"Summary markdown:\", summary_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T18:06:14.715273Z","iopub.execute_input":"2026-04-29T18:06:14.715831Z","iopub.status.idle":"2026-04-29T18:15:11.850966Z","shell.execute_reply.started":"2026-04-29T18:06:14.715789Z","shell.execute_reply":"2026-04-29T18:15:11.850338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# SAVE FULL MODEL PACKAGE\n# Keras models + weights + TFLite model-only + End-to-End TFLite + configs + README\n# =============================================================================\n\nimport os\nimport json\nimport shutil\nimport traceback\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom pathlib import Path\nfrom IPython.display import FileLink, display\n\n# =============================================================================\n# Output package paths\n# =============================================================================\n\nPACKAGE_DIR = Path(\"/kaggle/working/asl_model_export_package\")\n\nKERAS_DIR = PACKAGE_DIR / \"keras_models\"\nWEIGHTS_DIR = PACKAGE_DIR / \"weights\"\nTFLITE_MODEL_ONLY_DIR = PACKAGE_DIR / \"tflite_model_only\"\nTFLITE_E2E_DIR = PACKAGE_DIR / \"tflite_end_to_end\"\nCONFIG_DIR = PACKAGE_DIR / \"configs\"\nREPORT_DIR = PACKAGE_DIR / \"reports\"\n\nfor d in [\n    PACKAGE_DIR,\n    KERAS_DIR,\n    WEIGHTS_DIR,\n    TFLITE_MODEL_ONLY_DIR,\n    TFLITE_E2E_DIR,\n    CONFIG_DIR,\n    REPORT_DIR,\n]:\n    d.mkdir(parents=True, exist_ok=True)\n\nEXPECTED_MODELS = [\n    \"BiLSTM\",\n    \"BiGRU\",\n    \"Transformer\",\n    \"ST_GCN\",\n    \"RoPE_Conformer\",\n]\n\n# =============================================================================\n# Required checks\n# =============================================================================\n\nif \"loaded_models\" not in globals():\n    raise RuntimeError(\n        \"loaded_models is missing. Reload the Keras models first before saving .keras / weights.\"\n    )\n\nrequired_vars = [\"MAX_LEN\", \"CHANNELS\", \"NUM_CLASSES\"]\nmissing = [v for v in required_vars if v not in globals()]\n\nif missing:\n    raise RuntimeError(f\"Missing required variables: {missing}\")\n\n# =============================================================================\n# Landmark / preprocessing config\n# =============================================================================\n\nROWS_PER_FRAME = 543\nPAD = -100.0\n\nLIP = [\n    0, 61, 185, 40, 39, 37, 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]\nNOSE = [1, 2, 98, 327]\nREYE = [33, 7, 163, 144, 145, 153, 154, 155, 133, 246, 161, 160, 159, 158, 157, 173]\nLEYE = [263, 249, 390, 373, 374, 380, 381, 382, 362, 466, 388, 387, 386, 385, 384, 398]\nLHAND = np.arange(468, 489).tolist()\nRHAND = np.arange(522, 543).tolist()\n\nPOINT_LANDMARKS = LIP + LHAND + RHAND + NOSE + REYE + LEYE\nNUM_NODES = len(POINT_LANDMARKS)\n\n# =============================================================================\n# Helpers\n# =============================================================================\n\ndef safe_name(name):\n    return (\n        name.replace(\"/\", \"_\")\n            .replace(\"\\\\\", \"_\")\n            .replace(\" \", \"_\")\n            .replace(\"-\", \"_\")\n    )\n\n\ndef find_tflite_file(model_name, folder, e2e=False):\n    folder = Path(folder)\n\n    if not folder.exists():\n        return None\n\n    files = sorted(folder.glob(f\"{model_name}*.tflite\"))\n\n    if len(files) == 0:\n        return None\n\n    if e2e:\n        preferences = [\n            \"raw_preprocess_logits\",\n            \"raw_to_logits\",\n            \"preprocess\",\n        ]\n    else:\n        preferences = [\n            \"unrolled_b1\",\n            \"no_cudnn\",\n            \"cpu_rnn\",\n            \"float16_logits\",\n            \"float32_logits\",\n        ]\n\n    for pref in preferences:\n        matched = [p for p in files if pref in p.name]\n        if matched:\n            return matched[0]\n\n    return files[0]\n\n\ndef copy_if_exists(src, dst_dir, new_name=None):\n    if src is None:\n        return None\n\n    src = Path(src)\n\n    if not src.exists():\n        return None\n\n    dst_name = new_name if new_name is not None else src.name\n    dst = dst_dir / dst_name\n\n    shutil.copy2(src, dst)\n\n    return str(dst)\n\n\n# =============================================================================\n# Save Keras models and weights\n# =============================================================================\n\nsave_report = []\n\nfor model_name in EXPECTED_MODELS:\n    print(\"\\n\" + \"=\" * 100)\n    print(\"Saving:\", model_name)\n    print(\"=\" * 100)\n\n    row = {\n        \"model\": model_name,\n        \"keras_saved\": False,\n        \"weights_saved\": False,\n        \"tflite_model_only_saved\": False,\n        \"tflite_e2e_saved\": False,\n        \"keras_path\": None,\n        \"weights_path\": None,\n        \"tflite_model_only_path\": None,\n        \"tflite_e2e_path\": None,\n        \"parameters\": None,\n        \"error\": None,\n    }\n\n    try:\n        if model_name not in loaded_models:\n            row[\"error\"] = \"missing from loaded_models\"\n            print(f\"[SKIP] {model_name} missing from loaded_models.\")\n            save_report.append(row)\n            continue\n\n        model = loaded_models[model_name]\n\n        try:\n            row[\"parameters\"] = int(model.count_params())\n        except Exception:\n            row[\"parameters\"] = None\n\n        # ---------------------------------------------------------------------\n        # Save full Keras model\n        # ---------------------------------------------------------------------\n        keras_path = KERAS_DIR / f\"{safe_name(model_name)}.keras\"\n\n        try:\n            model.save(keras_path)\n            row[\"keras_saved\"] = True\n            row[\"keras_path\"] = str(keras_path)\n            print(\"Saved Keras model:\", keras_path)\n\n        except Exception as e:\n            print(f\"[WARNING] Failed to save full .keras for {model_name}\")\n            print(str(e))\n\n            # fallback: SavedModel export\n            try:\n                savedmodel_path = KERAS_DIR / f\"{safe_name(model_name)}_savedmodel\"\n                if savedmodel_path.exists():\n                    shutil.rmtree(savedmodel_path)\n\n                model.export(str(savedmodel_path))\n                row[\"keras_saved\"] = True\n                row[\"keras_path\"] = str(savedmodel_path)\n                print(\"Saved fallback SavedModel:\", savedmodel_path)\n\n            except Exception as e2:\n                print(f\"[FAILED] Keras/SavedModel export failed for {model_name}\")\n                print(str(e2))\n\n        # ---------------------------------------------------------------------\n        # Save weights\n        # ---------------------------------------------------------------------\n        weights_path = WEIGHTS_DIR / f\"{safe_name(model_name)}.weights.h5\"\n\n        try:\n            model.save_weights(weights_path)\n            row[\"weights_saved\"] = True\n            row[\"weights_path\"] = str(weights_path)\n            print(\"Saved weights:\", weights_path)\n\n        except Exception as e:\n            print(f\"[FAILED] Weights save failed for {model_name}\")\n            print(str(e))\n\n        # ---------------------------------------------------------------------\n        # Copy TFLite model-only\n        # ---------------------------------------------------------------------\n        src_model_only = None\n\n        if \"tflite_registry\" in globals():\n            try:\n                src_model_only = tflite_registry.get(model_name, None)\n            except Exception:\n                src_model_only = None\n\n        if src_model_only is None:\n            src_model_only = find_tflite_file(\n                model_name,\n                \"/kaggle/working/tflite_exports\",\n                e2e=False,\n            )\n\n        copied_model_only = copy_if_exists(\n            src_model_only,\n            TFLITE_MODEL_ONLY_DIR,\n            new_name=f\"{safe_name(model_name)}_model_only.tflite\",\n        )\n\n        if copied_model_only is not None:\n            row[\"tflite_model_only_saved\"] = True\n            row[\"tflite_model_only_path\"] = copied_model_only\n            print(\"Copied model-only TFLite:\", copied_model_only)\n        else:\n            print(\"[WARNING] model-only TFLite not found:\", model_name)\n\n        # ---------------------------------------------------------------------\n        # Copy End-to-End TFLite\n        # ---------------------------------------------------------------------\n        src_e2e = None\n\n        if \"e2e_registry\" in globals():\n            try:\n                src_e2e = e2e_registry.get(model_name, None)\n            except Exception:\n                src_e2e = None\n\n        if src_e2e is None:\n            src_e2e = find_tflite_file(\n                model_name,\n                \"/kaggle/working/tflite_end_to_end_exports\",\n                e2e=True,\n            )\n\n        copied_e2e = copy_if_exists(\n            src_e2e,\n            TFLITE_E2E_DIR,\n            new_name=f\"{safe_name(model_name)}_end_to_end_raw_preprocess.tflite\",\n        )\n\n        if copied_e2e is not None:\n            row[\"tflite_e2e_saved\"] = True\n            row[\"tflite_e2e_path\"] = copied_e2e\n            print(\"Copied end-to-end TFLite:\", copied_e2e)\n        else:\n            print(\"[WARNING] end-to-end TFLite not found:\", model_name)\n\n    except Exception:\n        row[\"error\"] = traceback.format_exc()\n        print(\"[FAILED]\", model_name)\n        print(row[\"error\"])\n\n    save_report.append(row)\n\n# =============================================================================\n# Save configs\n# =============================================================================\n\npreprocessing_config = {\n    \"rows_per_frame\": ROWS_PER_FRAME,\n    \"max_len\": int(MAX_LEN),\n    \"num_classes\": int(NUM_CLASSES),\n    \"pad_value\": float(PAD),\n    \"num_nodes\": int(NUM_NODES),\n    \"channels\": int(CHANNELS),\n    \"feature_definition\": \"(x,y) position + velocity + acceleration over selected landmarks\",\n    \"point_landmarks\": POINT_LANDMARKS,\n    \"landmark_groups\": {\n        \"LIP\": LIP,\n        \"NOSE\": NOSE,\n        \"REYE\": REYE,\n        \"LEYE\": LEYE,\n        \"LHAND\": LHAND,\n        \"RHAND\": RHAND,\n    },\n    \"raw_input_shape_for_end_to_end_tflite\": \"[1, T, 543, 3]\",\n    \"model_input_shape_for_model_only\": f\"[1, {MAX_LEN}, {CHANNELS}]\",\n    \"output_shape\": f\"[1, {NUM_CLASSES}]\",\n}\n\nwith open(CONFIG_DIR / \"preprocessing_config.json\", \"w\") as f:\n    json.dump(preprocessing_config, f, indent=4)\n\nmodel_config = {\n    \"models\": EXPECTED_MODELS,\n    \"formats\": {\n        \"keras\": \"Full Keras model for retraining/fine-tuning when custom layers are available.\",\n        \"weights_h5\": \"Weights-only checkpoint for rebuilding architecture from source code.\",\n        \"tflite_model_only\": \"Inference-only TFLite model that expects preprocessed features [1,384,708].\",\n        \"tflite_end_to_end\": \"Inference-only TFLite model that expects raw landmarks [1,T,543,3] and includes preprocessing.\",\n    },\n    \"notes\": [\n        \"TFLite files are for inference/deployment, not retraining.\",\n        \"Use .keras or .weights.h5 for retraining/fine-tuning.\",\n        \"BiLSTM and BiGRU TFLite exports may use fixed batch=1 unrolled RNN graphs.\",\n        \"End-to-end TFLite models include preprocessing inside the model graph.\",\n    ],\n}\n\nwith open(CONFIG_DIR / \"model_package_config.json\", \"w\") as f:\n    json.dump(model_config, f, indent=4)\n\n# =============================================================================\n# Copy benchmark reports if available\n# =============================================================================\n\npossible_reports = [\n    \"/kaggle/working/full_deployment_comparison/tables/paper_table_full_deployment_comparison.csv\",\n    \"/kaggle/working/full_deployment_comparison/tables/per_model_stage_summary.csv\",\n    \"/kaggle/working/full_deployment_comparison/tables/deployment_inventory.csv\",\n    \"/kaggle/working/full_deployment_comparison/full_deployment_comparison_summary.md\",\n    \"/kaggle/working/tflite_end_to_end_benchmark/tables/end_to_end_tflite_export_report.csv\",\n    \"/kaggle/working/tflite_end_to_end_benchmark/tables/end_to_end_tflite_variable_frames_benchmark.csv\",\n]\n\nfor src in possible_reports:\n    src = Path(src)\n    if src.exists():\n        shutil.copy2(src, REPORT_DIR / src.name)\n\n# Copy plots folder if exists\nplots_src = Path(\"/kaggle/working/full_deployment_comparison/plots\")\nplots_dst = REPORT_DIR / \"plots\"\n\nif plots_src.exists():\n    if plots_dst.exists():\n        shutil.rmtree(plots_dst)\n    shutil.copytree(plots_src, plots_dst)\n\n# =============================================================================\n# Save report CSV / JSON\n# =============================================================================\n\nsave_report_df = pd.DataFrame(save_report)\n\nsave_report_csv = PACKAGE_DIR / \"model_package_save_report.csv\"\nsave_report_json = PACKAGE_DIR / \"model_package_save_report.json\"\n\nsave_report_df.to_csv(save_report_csv, index=False)\n\nwith open(save_report_json, \"w\") as f:\n    json.dump(save_report, f, indent=4, default=str)\n\ndisplay(save_report_df)\n\n# =============================================================================\n# README\n# =============================================================================\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T18:47:06.084821Z","iopub.execute_input":"2026-04-29T18:47:06.085187Z","iopub.status.idle":"2026-04-29T18:47:07.928112Z","shell.execute_reply.started":"2026-04-29T18:47:06.085159Z","shell.execute_reply":"2026-04-29T18:47:07.927284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# List all files and folders in Kaggle /working\n# =============================================================================\n\nimport os\nfrom pathlib import Path\nimport pandas as pd\nfrom IPython.display import display, FileLink\n\nWORKING_DIR = Path(\"/kaggle/working\")\n\ndef format_size(num_bytes):\n    if num_bytes is None:\n        return \"\"\n    for unit in [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"]:\n        if num_bytes < 1024:\n            return f\"{num_bytes:.2f} {unit}\"\n        num_bytes /= 1024\n    return f\"{num_bytes:.2f} PB\"\n\nrows = []\n\nfor path in sorted(WORKING_DIR.rglob(\"*\")):\n    try:\n        stat = path.stat()\n        is_file = path.is_file()\n        is_dir = path.is_dir()\n\n        rows.append({\n            \"type\": \"file\" if is_file else \"folder\" if is_dir else \"other\",\n            \"name\": path.name,\n            \"extension\": path.suffix.lower() if is_file else \"\",\n            \"relative_path\": str(path.relative_to(WORKING_DIR)),\n            \"absolute_path\": str(path),\n            \"size_bytes\": stat.st_size if is_file else None,\n            \"size_readable\": format_size(stat.st_size) if is_file else \"\",\n            \"modified_time\": pd.to_datetime(stat.st_mtime, unit=\"s\"),\n        })\n\n    except Exception as e:\n        rows.append({\n            \"type\": \"error\",\n            \"name\": path.name,\n            \"extension\": \"\",\n            \"relative_path\": str(path),\n            \"absolute_path\": str(path),\n            \"size_bytes\": None,\n            \"size_readable\": \"\",\n            \"modified_time\": None,\n            \"error\": str(e),\n        })\n\ninventory_df = pd.DataFrame(rows)\n\n# Sort folders first, then largest files\ninventory_df = inventory_df.sort_values(\n    by=[\"type\", \"size_bytes\"],\n    ascending=[True, False],\n    na_position=\"last\"\n)\n\nprint(\"Total items:\", len(inventory_df))\nprint(\"Total files:\", (inventory_df[\"type\"] == \"file\").sum())\nprint(\"Total folders:\", (inventory_df[\"type\"] == \"folder\").sum())\n\ntotal_size = inventory_df.loc[inventory_df[\"type\"] == \"file\", \"size_bytes\"].sum()\nprint(\"Total file size:\", format_size(total_size))\n\ndisplay(inventory_df)\n\n# Save report\nreport_path = WORKING_DIR / \"working_directory_inventory.csv\"\ninventory_df.to_csv(report_path, index=False)\n\nprint(\"\\nInventory CSV saved to:\")\nprint(report_path)\n\ndisplay(FileLink(str(report_path)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T18:47:31.812554Z","iopub.execute_input":"2026-04-29T18:47:31.813303Z","iopub.status.idle":"2026-04-29T18:47:31.926773Z","shell.execute_reply.started":"2026-04-29T18:47:31.813271Z","shell.execute_reply":"2026-04-29T18:47:31.925828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# Zip EVERYTHING inside /kaggle/working for download\n# =============================================================================\n\nimport os\nimport zipfile\nfrom pathlib import Path\nfrom IPython.display import FileLink, display\n\nWORKING_DIR = Path(\"/kaggle/working\")\nZIP_PATH = WORKING_DIR / \"ALL_KAGGLE_WORKING_FILES.zip\"\n\n# Remove old zip if exists\nif ZIP_PATH.exists():\n    ZIP_PATH.unlink()\n\ndef format_size(num_bytes):\n    for unit in [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"]:\n        if num_bytes < 1024:\n            return f\"{num_bytes:.2f} {unit}\"\n        num_bytes /= 1024\n    return f\"{num_bytes:.2f} PB\"\n\nfile_count = 0\ntotal_bytes = 0\n\nwith zipfile.ZipFile(ZIP_PATH, \"w\", compression=zipfile.ZIP_DEFLATED) as zipf:\n    for path in WORKING_DIR.rglob(\"*\"):\n        if not path.is_file():\n            continue\n\n        # Do not include the output zip inside itself\n        if path.resolve() == ZIP_PATH.resolve():\n            continue\n\n        try:\n            arcname = path.relative_to(WORKING_DIR)\n            zipf.write(path, arcname=arcname)\n\n            file_count += 1\n            total_bytes += path.stat().st_size\n\n        except Exception as e:\n            print(\"Skipped:\", path, \"|\", e)\n\nzip_size = ZIP_PATH.stat().st_size\n\nprint(\"=\" * 80)\nprint(\"ZIP created successfully\")\nprint(\"=\" * 80)\nprint(\"Files compressed :\", file_count)\nprint(\"Original size    :\", format_size(total_bytes))\nprint(\"ZIP size         :\", format_size(zip_size))\nprint(\"ZIP path         :\", ZIP_PATH)\n\ndisplay(FileLink(str(ZIP_PATH)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T18:48:32.797452Z","iopub.execute_input":"2026-04-29T18:48:32.798420Z","iopub.status.idle":"2026-04-29T18:49:17.743125Z","shell.execute_reply.started":"2026-04-29T18:48:32.798376Z","shell.execute_reply":"2026-04-29T18:49:17.742459Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}