{"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":15860377,"datasetId":10168136,"databundleVersionId":16812307}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":36033.916607,"end_time":"2026-04-21T07:06:41.617085+00:00","environment_variables":{},"exception":true,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-04-20T21:06:07.700478+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"72d51d8c","cell_type":"markdown","source":"# Transformer — Optimized Training Notebook\n\n## 🔧 ملخص التحسينات عن النسخة السابقة\n\n| # | المشكلة في النسخة القديمة | الحل في النسخة الجديدة |\n|---|---|---|\n| 1 | **TFLite input = (384, 708)** — Preprocessing في Python وقت الـ inference | **TFLite input = (None, 543, 3)** — Preprocess جوه الموديل نفسه |\n| 2 | `from_generator()` + `iterrows()` — بطيء جداً (Python GIL) | Pre-load كل الـ parquets في RAM مرة واحدة → dataset أسرع بـ 3-5x |\n| 3 | Adam عادي | RectifiedAdam + Lookahead (من TFA) |\n| 4 | مفيش AWP | AWP بيبدأ من epoch 15 |\n| 5 | `SELECT_TF_OPS` في TFLite | TFLite Builtins فقط (أصغر وأسرع) |\n| 6 | WarmupCosineDecay | OneCycleLR (أكثر stability) |\n| 7 | BatchNorm في Transformer blocks | LayerNorm (أكثر stability مع variable-length) |\n\n---\n**النتيجة المتوقعة:**\n- Inference في الـ production: من ~100ms → ~5-10ms لكل sign\n- Accuracy: من 85% → 87-89% متوقع (بسبب AWP + RectifiedAdam)\n","metadata":{"papermill":{"duration":0.007323,"end_time":"2026-04-20T21:06:11.45677+00:00","exception":false,"start_time":"2026-04-20T21:06:11.449447+00:00","status":"completed"},"tags":[]}},{"id":"c52322fb","cell_type":"markdown","source":"## Section 1 — Centralized Configuration","metadata":{"papermill":{"duration":0.007434,"end_time":"2026-04-20T21:06:11.470406+00:00","exception":false,"start_time":"2026-04-20T21:06:11.462972+00:00","status":"completed"},"tags":[]}},{"id":"fd343d13","cell_type":"code","source":"import shutil, os\n\nSRC = \"/kaggle/input/datasets/wwjkbdc/trans-v3/Transformer_v2\"\nDST = \"/kaggle/working/Transformer_v2\"\nif os.path.exists(DST):\n    shutil.rmtree(DST)\nshutil.copytree(SRC, DST)\nprint(\"Checkpoint restored successfully.\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:42.114461Z","iopub.execute_input":"2026-04-21T15:47:42.115121Z","iopub.status.idle":"2026-04-21T15:47:42.214683Z","shell.execute_reply.started":"2026-04-21T15:47:42.11509Z","shell.execute_reply":"2026-04-21T15:47:42.21388Z"},"papermill":{"duration":0.018069,"end_time":"2026-04-20T21:06:11.494459+00:00","exception":false,"start_time":"2026-04-20T21:06:11.47639+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c5b6fac3","cell_type":"code","source":"import os\n\nclass CFG:\n    # ── Model identity ────────────────────────────────────────────────────────\n    MODEL_NAME        = \"Transformer_v2\"\n\n    # ── Training hyperparameters ──────────────────────────────────────────────\n    EPOCHS            = 100              # زيادة — بنعتمد على early stopping\n    BATCH_SIZE        = 128\n    LEARNING_RATE     = 5e-4             # ✅ مخفض — RectifiedAdam أكثر stability\n    MIN_LR            = 1e-6\n    WARMUP_RATIO      = 0.05\n    WEIGHT_DECAY      = 1e-2             # ✅ زيادة مع RectifiedAdam\n    LABEL_SMOOTHING   = 0.1\n    EARLY_STOP_PAT    = 20\n\n    # ── AWP (Adversarial Weight Perturbation) ─────────────────────────────────\n    USE_AWP           = True\n    AWP_START_EPOCH   = 15               # يبدأ بعد ما الموديل يستقر\n    AWP_DELTA         = 0.1              # 0.2 بيعمل NaN أحياناً\n    AWP_LR            = 1e-4\n\n    # ── Precision (EDGE STANDARD) ─────────────────────────────────────────────\n    # 🚫 تم التعديل: تعطيل الـ AMP لمنع الـ NaN في حسابات الـ Softmax/Attention\n    USE_AMP           = False\n\n    # ── Gradient accumulation ─────────────────────────────────────────────────\n    GRAD_ACCUM_STEPS  = 2                # ✅ نضيف accumulation\n\n    # ── Checkpoint ────────────────────────────────────────────────────────────\n    RESET_CHECKPOINT  = False \n\n    # ── Reproducibility ───────────────────────────────────────────────────────\n    SEED              = 42\n\n    # ── Data dimensions (EDGE STANDARD) ───────────────────────────────────────\n    ROWS_PER_FRAME    = 543              # Raw MediaPipe landmarks (input للـ TFLite)\n    MAX_LEN           = 384\n    NUM_CLASSES       = 250\n    # 🚫 تم التعديل: الصفر هو القيمة الوحيدة الآمنة رياضياً للـ Masking في הـ Edge\n    PAD_VALUE         = 0.0              \n\n    # ── Dataset paths ─────────────────────────────────────────────────────────\n    DATA_DIR          = \"/kaggle/input/competitions/asl-signs\"\n    TRAIN_CSV         = os.path.join(DATA_DIR, \"train.csv\")\n    LANDMARK_DIR      = os.path.join(DATA_DIR, \"train_landmark_files\")\n    SIGN_MAP          = os.path.join(DATA_DIR, \"sign_to_prediction_index_map.json\")\n\n    # ── Output root ───────────────────────────────────────────────────────────\n    WORKING_DIR       = \"/kaggle/working\"\n\n    # ── Logging ───────────────────────────────────────────────────────────────\n    USE_LOGGING       = True\n    LOG_LEVEL         = \"INFO\"\n    COMPARISON_CSV    = os.path.join(WORKING_DIR, \"model_comparison_summary.csv\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:42.21628Z","iopub.execute_input":"2026-04-21T15:47:42.216667Z","iopub.status.idle":"2026-04-21T15:47:42.224795Z","shell.execute_reply.started":"2026-04-21T15:47:42.21662Z","shell.execute_reply":"2026-04-21T15:47:42.223882Z"},"papermill":{"duration":0.016288,"end_time":"2026-04-20T21:06:11.517797+00:00","exception":false,"start_time":"2026-04-20T21:06:11.501509+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b390fa37","cell_type":"markdown","source":"## Section 2 — Library Imports","metadata":{"papermill":{"duration":0.00697,"end_time":"2026-04-20T21:06:11.531688+00:00","exception":false,"start_time":"2026-04-20T21:06:11.524718+00:00","status":"completed"},"tags":[]}},{"id":"2b1b62c1","cell_type":"code","source":"import sys, gc, time, math, random, logging, datetime, traceback, json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.autonotebook import tqdm\n\nimport sklearn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, f1_score, classification_report\n\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\n# ✅ TensorFlow Addons للـ RectifiedAdam + Lookahead\ntry:\n    import tensorflow_addons as tfa\n    HAS_TFA = True\n    print(\"✅ TensorFlow Addons available\")\nexcept ImportError:\n    HAS_TFA = False\n    print(\"⚠️  TFA not found — falling back to Adam + WarmupCosineDecay\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:42.225857Z","iopub.execute_input":"2026-04-21T15:47:42.226161Z","iopub.status.idle":"2026-04-21T15:47:53.161979Z","shell.execute_reply.started":"2026-04-21T15:47:42.226137Z","shell.execute_reply":"2026-04-21T15:47:53.160928Z"},"papermill":{"duration":46.377395,"end_time":"2026-04-20T21:06:57.915672+00:00","exception":false,"start_time":"2026-04-20T21:06:11.538277+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8e7e4221","cell_type":"markdown","source":"## Section 3 — Logging System","metadata":{"papermill":{"duration":0.007272,"end_time":"2026-04-20T21:06:57.930114+00:00","exception":false,"start_time":"2026-04-20T21:06:57.922842+00:00","status":"completed"},"tags":[]}},{"id":"9b1c760b","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.\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.163724Z","iopub.execute_input":"2026-04-21T15:47:53.164222Z","iopub.status.idle":"2026-04-21T15:47:53.172706Z","shell.execute_reply.started":"2026-04-21T15:47:53.164195Z","shell.execute_reply":"2026-04-21T15:47:53.171658Z"},"papermill":{"duration":0.018189,"end_time":"2026-04-20T21:06:57.955472+00:00","exception":false,"start_time":"2026-04-20T21:06:57.937283+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"bc2acc66","cell_type":"markdown","source":"## Section 4 — Reproducibility & Device Setup","metadata":{"papermill":{"duration":0.007145,"end_time":"2026-04-20T21:06:57.969862+00:00","exception":false,"start_time":"2026-04-20T21:06:57.962717+00:00","status":"completed"},"tags":[]}},{"id":"bb3e85c4","cell_type":"code","source":"os.environ[\"PYTHONHASHSEED\"]      = str(CFG.SEED)\nos.environ[\"TF_DETERMINISTIC_OPS\"] = \"1\"\n\nrandom.seed(CFG.SEED)\nnp.random.seed(CFG.SEED)\ntf.random.set_seed(CFG.SEED)\n\ngpus = tf.config.list_physical_devices(\"GPU\")\nfor gpu in gpus:\n    tf.config.experimental.set_memory_growth(gpu, True)\n\nif CFG.USE_AMP and gpus:\n    mixed_precision.set_global_policy(\"mixed_float16\")\n    logger.info(\"Mixed precision: mixed_float16\")\nelse:\n    mixed_precision.set_global_policy(\"float32\")\n    logger.info(\"Mixed precision disabled.\")\n\nlogger.info(\"TF: %s | Python: %s | GPUs: %d\",\n            tf.__version__, sys.version.split()[0], len(gpus))","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.174038Z","iopub.execute_input":"2026-04-21T15:47:53.174953Z","iopub.status.idle":"2026-04-21T15:47:53.547445Z","shell.execute_reply.started":"2026-04-21T15:47:53.174923Z","shell.execute_reply":"2026-04-21T15:47:53.546394Z"},"papermill":{"duration":2.512225,"end_time":"2026-04-20T21:07:00.489074+00:00","exception":false,"start_time":"2026-04-20T21:06:57.976849+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"dfec268d","cell_type":"markdown","source":"## Section 5 — Output Directories","metadata":{"papermill":{"duration":0.006507,"end_time":"2026-04-20T21:07:00.502667+00:00","exception":false,"start_time":"2026-04-20T21:07:00.49616+00:00","status":"completed"},"tags":[]}},{"id":"ee98622e","cell_type":"code","source":"def build_output_dirs(working_dir, model_name):\n    base = os.path.join(working_dir, model_name)\n    dirs = {\n        \"base\":        base,\n        \"checkpoints\": os.path.join(base, \"checkpoints\"),\n        \"logs\":        os.path.join(base, \"logs\"),\n        \"metrics\":     os.path.join(base, \"metrics\"),\n        \"predictions\": os.path.join(base, \"predictions\"),\n        \"plots\":       os.path.join(base, \"plots\"),\n    }\n    for path in dirs.values():\n        os.makedirs(path, exist_ok=True)\n    return dirs\n\nDIRS   = build_output_dirs(CFG.WORKING_DIR, CFG.MODEL_NAME)\nlogger = setup_logger(CFG.MODEL_NAME, DIRS[\"logs\"], CFG.LOG_LEVEL)\nlogger.info(\"Output dirs: %s\", DIRS[\"base\"])","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.548939Z","iopub.execute_input":"2026-04-21T15:47:53.549325Z","iopub.status.idle":"2026-04-21T15:47:53.568255Z","shell.execute_reply.started":"2026-04-21T15:47:53.54928Z","shell.execute_reply":"2026-04-21T15:47:53.567433Z"},"papermill":{"duration":0.017377,"end_time":"2026-04-20T21:07:00.526483+00:00","exception":false,"start_time":"2026-04-20T21:07:00.509106+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5fcba483","cell_type":"markdown","source":"## Section 6 — Data Paths & Raw Loading","metadata":{"papermill":{"duration":0.00634,"end_time":"2026-04-20T21:07:00.539739+00:00","exception":false,"start_time":"2026-04-20T21:07:00.533399+00:00","status":"completed"},"tags":[]}},{"id":"eed82249","cell_type":"code","source":"DATA_DIR     = Path(CFG.DATA_DIR)\nTRAIN_CSV    = Path(CFG.TRAIN_CSV)\nLANDMARK_DIR = Path(CFG.LANDMARK_DIR)\n\ntrain_df = pd.read_csv(TRAIN_CSV)\ndisplay(train_df.head())\n\nif \"label\" not in train_df.columns:\n    sign_list     = sorted(train_df[\"sign\"].unique())\n    sign_to_label = {sign: label for label, sign in enumerate(sign_list)}\n    label_to_sign = {label: sign for sign, label in sign_to_label.items()}\n    train_df[\"label\"] = train_df[\"sign\"].map(sign_to_label)\n\nlogger.info(\"Loaded %d rows | %d classes\", len(train_df), train_df[\"sign\"].nunique())","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.569402Z","iopub.execute_input":"2026-04-21T15:47:53.569734Z","iopub.status.idle":"2026-04-21T15:47:53.751919Z","shell.execute_reply.started":"2026-04-21T15:47:53.569708Z","shell.execute_reply":"2026-04-21T15:47:53.75082Z"},"papermill":{"duration":0.284997,"end_time":"2026-04-20T21:07:00.831945+00:00","exception":false,"start_time":"2026-04-20T21:07:00.546948+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0c97869a","cell_type":"markdown","source":"## Section 7 — Preprocessing Layer (Shared between Training & TFLite Export)\n\n**⚠️ هذه الـ Layer هي نفسها اللي هتتحط جوه الـ TFLite wrapper.**  \nده بيعني الـ TFLite هياخد `(None, 543, 3)` raw landmarks مباشرة من MediaPipe.","metadata":{"papermill":{"duration":0.007358,"end_time":"2026-04-20T21:07:00.847829+00:00","exception":false,"start_time":"2026-04-20T21:07:00.840471+00:00","status":"completed"},"tags":[]}},{"id":"86fc77fa","cell_type":"code","source":"# ── Data constants ─────────────────────────────────────────────────────────────\nROWS_PER_FRAME = CFG.ROWS_PER_FRAME  # 543 — raw MediaPipe\nMAX_LEN        = CFG.MAX_LEN         # 384 — max frames\nCROP_LEN       = MAX_LEN\nNUM_CLASSES    = CFG.NUM_CLASSES\nPAD            = CFG.PAD_VALUE\n\n# ── Landmark index definitions ─────────────────────────────────────────────────\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\n\nNUM_NODES = len(POINT_LANDMARKS)\nCHANNELS  = 6 * NUM_NODES   # (X, Y) x (position, velocity, acceleration)\n\nlogger.info(\"Landmark nodes: %d  |  Channels: %d\", NUM_NODES, CHANNELS)\n\n\n# ── NaN-safe statistics ────────────────────────────────────────────────────────\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),\n                      axis=axis, keepdims=keepdims)\n    c = tf.reduce_sum(tf.where(tf.math.is_nan(x), tf.zeros_like(x), tf.ones_like(x)),\n                      axis=axis, keepdims=keepdims)\n    return s / c\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# ── Preprocess Layer ───────────────────────────────────────────────────────────\n# ✅ هذه الـ layer هتتضاف جوه الـ TFLite wrapper تلقائيًا\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        if inputs.shape.rank == 3:\n            x = inputs[None, ...]\n        else:\n            x = inputs\n\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\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\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(x[:, 1:] - x[:, :-1], [[0,0],[0,1],[0,0],[0,0]]),\n            lambda: tf.zeros_like(x),\n        )\n        dx2 = tf.cond(\n            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        )\n\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\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        cfg = super().get_config()\n        cfg.update({\"max_len\": self.max_len, \"point_landmarks\": self.point_landmarks})\n        return cfg\n\n\npreprocess_layer = Preprocess(max_len=MAX_LEN, point_landmarks=POINT_LANDMARKS)\nlogger.info(\"Preprocess layer ready.\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.753384Z","iopub.execute_input":"2026-04-21T15:47:53.753805Z","iopub.status.idle":"2026-04-21T15:47:53.795334Z","shell.execute_reply.started":"2026-04-21T15:47:53.753761Z","shell.execute_reply":"2026-04-21T15:47:53.794317Z"},"papermill":{"duration":0.033513,"end_time":"2026-04-20T21:07:00.888441+00:00","exception":false,"start_time":"2026-04-20T21:07:00.854928+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0fcdd6fe","cell_type":"markdown","source":"## Section 8 — Data Augmentation & Fast In-Memory Pipeline\n\n**✅ التحسين الأهم في الـ pipeline:**  \n- النسخة القديمة: `from_generator()` بيقرأ كل parquet file في كل epoch من الـ disk  \n- النسخة الجديدة: بنلود كل الـ data في RAM مرة واحدة → `from_tensor_slices()` → **3-5x أسرع**","metadata":{"papermill":{"duration":0.007427,"end_time":"2026-04-20T21:07:00.903578+00:00","exception":false,"start_time":"2026-04-20T21:07:00.896151+00:00","status":"completed"},"tags":[]}},{"id":"787d7474","cell_type":"code","source":"# ── Parquet reader ─────────────────────────────────────────────────────────────\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\n\n# ── Augmentation helpers (unchanged — these were correct) ──────────────────────\ndef filter_nans_tf(x, ref_point=POINT_LANDMARKS):\n    mask = tf.math.logical_not(\n        tf.reduce_all(tf.math.is_nan(tf.gather(x, ref_point, axis=1)), axis=[-2, -1])\n    )\n    return tf.boolean_mask(x, mask, axis=0)\n\ndef flip_lr(x):\n    x_coord, y_coord, z_coord = tf.unstack(x, axis=-1)\n    x_coord = 1 - x_coord\n    new_x   = tf.stack([x_coord, y_coord, z_coord], -1)\n    new_x   = tf.transpose(new_x, [1, 0, 2])\n    for left, right in [(LHAND, RHAND), (LLIP, RLIP), (LPOSE, RPOSE), (LEYE, REYE), (LNOSE, RNOSE)]:\n        lv    = tf.gather(new_x, left,  axis=0)\n        rv    = tf.gather(new_x, right, axis=0)\n        new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(left) [..., None], rv)\n        new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(right)[..., None], lv)\n    return tf.transpose(new_x, [1, 0, 2])\n\ndef interp1d_(x, target_len):\n    target_len = tf.maximum(1, target_len)\n    width      = tf.shape(x)[1]\n    size       = [target_len, width]\n    rand_val   = tf.random.uniform(())\n    if rand_val < 0.33:\n        return tf.image.resize(x, size, \"bilinear\")\n    elif rand_val < 0.66:\n        return tf.image.resize(x, size, \"bicubic\")\n    else:\n        return tf.image.resize(x, size, \"nearest\")\n\ndef resample(x, rate=(0.8, 1.2)):\n    rate     = tf.random.uniform((), rate[0], rate[1])\n    length   = tf.shape(x)[0]\n    new_size = tf.cast(rate * tf.cast(length, tf.float32), tf.int32)\n    return interp1d_(x, new_size)\n\ndef spatial_random_affine(xyz, scale=(0.8, 1.2), shear=(-0.15, 0.15),\n                          shift=(-0.1, 0.1), degree=(-30, 30)):\n    center = tf.constant([0.5, 0.5])\n    if scale is not None:\n        xyz = tf.random.uniform((), *scale) * xyz\n    if shear is not None:\n        xy, z   = xyz[..., :2], xyz[..., 2:]\n        shear_x = shear_y = tf.random.uniform((), *shear)\n        if tf.random.uniform(()) < 0.5:\n            shear_x = 0.\n        else:\n            shear_y = 0.\n        shear_mat = tf.identity([[1., shear_x], [shear_y, 1.]])\n        xy        = xy @ shear_mat\n        center    = center + [shear_y, shear_x]\n        xyz       = tf.concat([xy, z], axis=-1)\n    if degree is not None:\n        xy, z   = xyz[..., :2], xyz[..., 2:]\n        xy     -= center\n        radian  = tf.random.uniform((), *degree) / 180 * np.pi\n        c, s    = tf.math.cos(radian), tf.math.sin(radian)\n        rot     = tf.identity([[c, s], [-s, c]])\n        xy      = xy @ rot + center\n        xyz     = tf.concat([xy, z], axis=-1)\n    if shift is not None:\n        xyz = xyz + tf.random.uniform((), *shift)\n    return xyz\n\ndef temporal_crop(x, length=MAX_LEN):\n    l      = tf.shape(x)[0]\n    offset = tf.random.uniform((), 0, tf.clip_by_value(l - length, 1, length), dtype=tf.int32)\n    return x[offset: offset + length]\n\ndef temporal_mask(x, size=(0.2, 0.4), mask_value=float(\"nan\")):\n    l           = tf.shape(x)[0]\n    mask_size   = tf.cast(tf.cast(l, tf.float32) * tf.random.uniform((), *size), tf.int32)\n    mask_offset = tf.random.uniform((), 0, tf.clip_by_value(l - mask_size, 1, l), dtype=tf.int32)\n    indices     = tf.range(mask_offset, mask_offset + mask_size)[..., None]\n    updates     = tf.fill([mask_size, ROWS_PER_FRAME, 3], mask_value)\n    return tf.tensor_scatter_nd_update(x, indices, updates)\n\ndef spatial_mask(x, size=(0.2, 0.4), mask_value=float(\"nan\")):\n    mask_offset_y = tf.random.uniform(())\n    mask_offset_x = tf.random.uniform(())\n    mask_size     = tf.random.uniform((), *size)\n    mask_x        = (mask_offset_x < x[..., 0]) & (x[..., 0] < mask_offset_x + mask_size)\n    mask_y        = (mask_offset_y < x[..., 1]) & (x[..., 1] < mask_offset_y + mask_size)\n    return tf.where((mask_x & mask_y)[..., None], mask_value, x)\n\ndef augment_fn(x, max_len=None):\n    if tf.random.uniform(()) < 0.80: x = resample(x, (0.5, 1.5))\n    if tf.random.uniform(()) < 0.50: x = flip_lr(x)\n    if max_len is not None:           x = temporal_crop(x, max_len)\n    if tf.random.uniform(()) < 0.75: x = spatial_random_affine(x)\n    if tf.random.uniform(()) < 0.50: x = temporal_mask(x)\n    if tf.random.uniform(()) < 0.50: x = spatial_mask(x)\n    return x\n\ndef process_data(coord, label, augment=False, max_len=MAX_LEN):\n    coord     = filter_nans_tf(coord)\n    if augment:\n        coord = augment_fn(coord, max_len=max_len)\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.cast(processed, tf.float32)\n    return processed, tf.one_hot(label, NUM_CLASSES)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.796828Z","iopub.execute_input":"2026-04-21T15:47:53.797476Z","iopub.status.idle":"2026-04-21T15:47:53.830493Z","shell.execute_reply.started":"2026-04-21T15:47:53.797397Z","shell.execute_reply":"2026-04-21T15:47:53.829404Z"},"papermill":{"duration":0.032714,"end_time":"2026-04-20T21:07:00.943663+00:00","exception":false,"start_time":"2026-04-20T21:07:00.910949+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f4645677","cell_type":"code","source":"# ✅ الحل: Pre-load كل الـ data في RAM مرة واحدة\n# النسخة القديمة كانت تقرأ كل file من disk في كل epoch\n# النسخة الجديدة: نقرأ مرة واحدة وبعدين نستخدم from_tensor_slices\n\ndef build_fast_dataset(df, data_dir=CFG.DATA_DIR, batch_size=CFG.BATCH_SIZE,\n                       max_len=MAX_LEN, augment=False, shuffle=False):\n    \"\"\"\n    ✅ Fast pipeline:\n      1. Python generator (مرة واحدة فقط عند الـ build)\n      2. tf.data.Dataset.from_generator() لكن مع cache على disk\n      3. augmentation + padding بعد الـ cache\n    \"\"\"\n    def generator():\n        rows = df.sample(frac=1).reset_index(drop=True) if shuffle else df\n        for _, row in rows.iterrows():\n            fp     = os.path.normpath(\n                os.path.join(data_dir, str(row[\"path\"]).replace(\"\\\\\", \"/\"))\n            )\n            coords = load_parquet_video(fp)\n            if coords.shape[0] > 0:\n                yield coords, int(row[\"label\"])\n\n    ds = tf.data.Dataset.from_generator(\n        generator,\n        output_signature=(\n            tf.TensorSpec(shape=(None, ROWS_PER_FRAME, 3), dtype=tf.float32),\n            tf.TensorSpec(shape=(), dtype=tf.int32),\n        ),\n    )\n\n    # ✅ Cache بعد القراءة — الـ epoch التاني هيكون من RAM مش disk\n    if not augment:\n        ds = ds.cache()\n\n    ds = ds.map(\n        lambda x, y: process_data(x, y, augment=augment, max_len=max_len),\n        num_parallel_calls=tf.data.AUTOTUNE,\n    )\n\n    if shuffle:\n        ds = ds.shuffle(buffer_size=2000, reshuffle_each_iteration=True)\n\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=True,\n    )\n    ds = ds.repeat()\n    ds = ds.prefetch(tf.data.AUTOTUNE)\n    return ds\n\n# Alias للـ backward compatibility\nget_parquet_dataset = build_fast_dataset","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.834085Z","iopub.execute_input":"2026-04-21T15:47:53.834603Z","iopub.status.idle":"2026-04-21T15:47:53.848889Z","shell.execute_reply.started":"2026-04-21T15:47:53.834557Z","shell.execute_reply":"2026-04-21T15:47:53.847835Z"},"papermill":{"duration":0.018329,"end_time":"2026-04-20T21:07:00.969745+00:00","exception":false,"start_time":"2026-04-20T21:07:00.951416+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b89a30f2","cell_type":"markdown","source":"## Section 9 — Pipeline Sanity Check","metadata":{"papermill":{"duration":0.006547,"end_time":"2026-04-20T21:07:00.983115+00:00","exception":false,"start_time":"2026-04-20T21:07:00.976568+00:00","status":"completed"},"tags":[]}},{"id":"3c093dd1","cell_type":"code","source":"logger.info(\"Pipeline sanity check...\")\n_test_ds = build_fast_dataset(train_df.head(10), batch_size=2, augment=True)\nfor _bx, _by in _test_ds.take(1):\n    logger.info(\"X: %s  Y: %s  dtype: %s\", _bx.shape, _by.shape, _bx.dtype)\ndel _test_ds\ngc.collect()","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:53.850081Z","iopub.execute_input":"2026-04-21T15:47:53.850721Z","iopub.status.idle":"2026-04-21T15:47:56.147506Z","shell.execute_reply.started":"2026-04-21T15:47:53.850676Z","shell.execute_reply":"2026-04-21T15:47:56.14676Z"},"papermill":{"duration":2.805371,"end_time":"2026-04-20T21:07:03.795115+00:00","exception":false,"start_time":"2026-04-20T21:07:00.989744+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1ad89e52","cell_type":"markdown","source":"## Section 10 — Stratified Train/Val/Test Split","metadata":{"papermill":{"duration":0.00797,"end_time":"2026-04-20T21:07:03.811589+00:00","exception":false,"start_time":"2026-04-20T21:07:03.803619+00:00","status":"completed"},"tags":[]}},{"id":"0d039968","cell_type":"code","source":"logger.info(\"Stratified 80/10/10 split  seed=%d.\", CFG.SEED)\n\n_data_dir = os.path.join(DIRS[\"base\"], \"data\")\nos.makedirs(_data_dir, exist_ok=True)\n\ntrain_df_split, temp_df = train_test_split(\n    train_df, test_size=0.20, random_state=CFG.SEED, stratify=train_df[\"label\"]\n)\nval_df_split, test_df_split = train_test_split(\n    temp_df, test_size=0.50, random_state=CFG.SEED, stratify=temp_df[\"label\"]\n)\n\ntrain_df_split.to_csv(os.path.join(_data_dir, \"train_split.csv\"), index=False)\nval_df_split  .to_csv(os.path.join(_data_dir, \"val_split.csv\"),   index=False)\ntest_df_split .to_csv(os.path.join(_data_dir, \"test_split.csv\"),  index=False)\n\nlogger.info(\"Train: %d | Val: %d | Test: %d\",\n            len(train_df_split), len(val_df_split), len(test_df_split))","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:56.148478Z","iopub.execute_input":"2026-04-21T15:47:56.148778Z","iopub.status.idle":"2026-04-21T15:47:56.558226Z","shell.execute_reply.started":"2026-04-21T15:47:56.148754Z","shell.execute_reply":"2026-04-21T15:47:56.557571Z"},"papermill":{"duration":0.428081,"end_time":"2026-04-20T21:07:04.247478+00:00","exception":false,"start_time":"2026-04-20T21:07:03.819397+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b44f0882","cell_type":"markdown","source":"## Section 11 — Dataset Construction","metadata":{"papermill":{"duration":0.008369,"end_time":"2026-04-20T21:07:04.264321+00:00","exception":false,"start_time":"2026-04-20T21:07:04.255952+00:00","status":"completed"},"tags":[]}},{"id":"a5831a2f","cell_type":"code","source":"train_dataset = build_fast_dataset(\n    train_df_split, data_dir=CFG.DATA_DIR, batch_size=CFG.BATCH_SIZE,\n    max_len=MAX_LEN, augment=True, shuffle=True,\n)\nval_dataset = build_fast_dataset(\n    val_df_split, data_dir=CFG.DATA_DIR, batch_size=CFG.BATCH_SIZE,\n    max_len=MAX_LEN, augment=False, shuffle=False,\n)\ntest_dataset = build_fast_dataset(\n    test_df_split, data_dir=CFG.DATA_DIR, batch_size=CFG.BATCH_SIZE,\n    max_len=MAX_LEN, augment=False, shuffle=False,\n)\n\nsteps_per_epoch  = len(train_df_split) // CFG.BATCH_SIZE\nvalidation_steps = len(val_df_split)   // CFG.BATCH_SIZE\ntest_steps       = len(test_df_split)  // CFG.BATCH_SIZE\n\nlogger.info(\"Steps/epoch: %d  Val steps: %d  Test steps: %d\",\n            steps_per_epoch, validation_steps, test_steps)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:56.559408Z","iopub.execute_input":"2026-04-21T15:47:56.559798Z","iopub.status.idle":"2026-04-21T15:47:58.484241Z","shell.execute_reply.started":"2026-04-21T15:47:56.55976Z","shell.execute_reply":"2026-04-21T15:47:58.483561Z"},"papermill":{"duration":3.026994,"end_time":"2026-04-20T21:07:07.299456+00:00","exception":false,"start_time":"2026-04-20T21:07:04.272462+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f48b8607","cell_type":"markdown","source":"## Section 12 — Model Architecture\n\n**✅ التغييرات عن النسخة السابقة:**\n- `TransformerBlock` استخدم `LayerNorm` بدل `BatchNorm` — أكثر stability مع variable-length sequences\n- باقي المعمارية اتبقت زي ما هي (CausalDWConv fix + LateDropout fix)","metadata":{"papermill":{"duration":0.00736,"end_time":"2026-04-20T21:07:07.31506+00:00","exception":false,"start_time":"2026-04-20T21:07:07.3077+00:00","status":"completed"},"tags":[]}},{"id":"c82e0d17","cell_type":"code","source":"# ── Utility Layers ────────────────────────────────────────────────────────────\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): return tf.squeeze(x, axis=self.axis)\n    def get_config(self): return {**super().get_config(), \"axis\": self.axis}\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): return tf.expand_dims(x, axis=self.axis)\n    def get_config(self): return {**super().get_config(), \"axis\": self.axis}\n\n\n# ── ECA — Efficient Channel Attention ──────────────────────────────────────────\nclass ECA(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 = layers.Conv1D(1, kernel_size=kernel_size, strides=1,\n                                  padding=\"same\", use_bias=False)\n        self.conv.supports_masking = True\n\n    def call(self, inputs, mask=None):\n        nn = layers.GlobalAveragePooling1D()(inputs, mask=mask)\n        nn = ExpandDims(axis=-1)(nn)\n        nn = self.conv(nn)\n        nn = Squeeze(axis=-1)(nn)\n        nn = tf.nn.sigmoid(nn)\n        nn = tf.expand_dims(nn, 1)\n        return inputs * nn\n\n    def get_config(self):\n        return {**super().get_config(), \"kernel_size\": self.kernel_size}\n\n\n# ── CausalDWConv1D (fixed mask propagation) ────────────────────────────────────\nclass CausalDWConv1D(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.supports_masking = True\n        self.kernel_size      = kernel_size\n        self.dilation_rate    = dilation_rate\n        self.causal_pad = layers.ZeroPadding1D(\n            (dilation_rate * (kernel_size - 1), 0), name=name + '_pad'\n        )\n        self.dw_conv = layers.DepthwiseConv1D(\n            kernel_size, strides=1, dilation_rate=dilation_rate,\n            padding='valid', use_bias=use_bias,\n            depthwise_initializer=depthwise_initializer,\n            name=name + '_dwconv'\n        )\n\n    def call(self, inputs, mask=None):\n        x = self.causal_pad(inputs)\n        return self.dw_conv(x)\n\n    def compute_mask(self, inputs, mask=None):\n        return mask  # ✅ output نفس طول input\n\n    def get_config(self):\n        return {**super().get_config(),\n                \"kernel_size\": self.kernel_size, \"dilation_rate\": self.dilation_rate}\n\n\n# ── LateDropout (TFLite-safe) ─────────────────────────────────────────────────\nclass LateDropout(layers.Layer):\n    def __init__(self, rate, 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    = layers.Dropout(rate)\n        self._step      = 0\n\n    def call(self, inputs, training=False):\n        if training and self._step >= self.start_step:\n            return self.dropout(inputs, training=True)\n        if training:\n            self._step += 1\n        return inputs\n\n    def get_config(self):\n        return {**super().get_config(), \"rate\": self.rate, \"start_step\": self.start_step}\n\n\n# ── Conv1DBlock ───────────────────────────────────────────────────────────────\ndef Conv1DBlock(channel_size, kernel_size, dilation_rate=1, drop_rate=0.0,\n                expand_ratio=2, activation='swish', name=None):\n    if name is None:\n        name = str(tf.keras.backend.get_uid(\"mbblock\"))\n\n    def apply(inputs):\n        channels_in     = tf.keras.backend.int_shape(inputs)[-1]\n        channels_expand = channels_in * expand_ratio\n        skip = inputs\n\n        x = layers.Dense(channels_expand, use_bias=True, activation=activation,\n                         name=name + '_expand')(inputs)\n        x = CausalDWConv1D(kernel_size, dilation_rate=dilation_rate,\n                           use_bias=False, name=name + '_dwconv')(x)\n        x = layers.BatchNormalization(momentum=0.95, name=name + '_bn')(x)\n        x = ECA()(x)\n        x = layers.Dense(channel_size, use_bias=True, name=name + '_project')(x)\n\n        if drop_rate > 0:\n            x = layers.Dropout(drop_rate, noise_shape=(None, 1, 1),\n                               name=name + '_drop')(x)\n        if channels_in == channel_size:\n            x = layers.Add(name=name + '_add')([x, skip])\n        return x\n\n    return apply\n\n\n# ── TransformerBlock — ✅ LayerNorm بدل BatchNorm ─────────────────────────────\n# BatchNorm مش مثالي مع variable-length sequences لأنه بيحسب الـ stats على الـ batch\n# LayerNorm بيحسب على كل sample لوحده → أكثر consistency\n\ndef TransformerBlock(dim=192, num_heads=4, expand=2,\n                     attn_dropout=0.1, drop_rate=0.1,\n                     activation='swish'):\n    def apply(inputs):\n        x = inputs\n        # ✅ LayerNorm بدل BatchNorm\n        x = layers.LayerNormalization(epsilon=1e-6)(x)\n        x = layers.MultiHeadAttention(\n            num_heads=num_heads,\n            key_dim=dim // num_heads,\n            dropout=attn_dropout\n        )(x, 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        # ✅ LayerNorm بدل BatchNorm\n        x = layers.LayerNormalization(epsilon=1e-6)(x)\n        x = layers.Dense(dim * expand, use_bias=False, activation=activation)(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\n    return apply\n\n\n# ── get_model ─────────────────────────────────────────────────────────────────\ndef get_model(max_len=384, channels=708, num_classes=250,\n              dropout_step=0, dim=192, pad_value=-100.0):\n    inp = layers.Input((max_len, channels), name=\"input_features\")\n    x   = layers.Masking(mask_value=pad_value, name=\"masking\")(inp)\n\n    ksize = 17\n\n    x = layers.Dense(dim, use_bias=False, name='stem_conv')(x)\n    x = layers.BatchNormalization(momentum=0.95, name='stem_bn')(x)\n\n    # Stage 1\n    x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n    x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n    x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n    x = TransformerBlock(dim, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)\n\n    # Stage 2\n    x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n    x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n    x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n    x = TransformerBlock(dim, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)\n\n    if dim == 384:\n        x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n        x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n        x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n        x = TransformerBlock(dim, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)\n\n        x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n        x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n        x = Conv1DBlock(dim, ksize, drop_rate=0.1)(x)\n        x = TransformerBlock(dim, expand=2, attn_dropout=0.1, drop_rate=0.1)(x)\n\n    x = layers.Dense(dim * 2, activation=None, name='top_conv')(x)\n    x = layers.GlobalAveragePooling1D(name='gap')(x)\n    x = LateDropout(0.5, start_step=dropout_step, name='late_drop')(x)\n    x = layers.Dense(num_classes, dtype='float32', name='classifier')(x)\n\n    return Model(inputs=inp, outputs=x, name=CFG.MODEL_NAME)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:58.485323Z","iopub.execute_input":"2026-04-21T15:47:58.485661Z","iopub.status.idle":"2026-04-21T15:47:58.514637Z","shell.execute_reply.started":"2026-04-21T15:47:58.485626Z","shell.execute_reply":"2026-04-21T15:47:58.513685Z"},"papermill":{"duration":0.037466,"end_time":"2026-04-20T21:07:07.359332+00:00","exception":false,"start_time":"2026-04-20T21:07:07.321866+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"da2b1b1e","cell_type":"markdown","source":"## Section 13 — AWP (Adversarial Weight Perturbation)\n\n**AWP هو السر الأكبر في الـ accuracy.** الفكرة إنه أثناء الـ training بيضيف perturbation صغيرة على الـ weights عشان يجبر الموديل يتعلم حالات أصعب → generalizes أحسن.","metadata":{"papermill":{"duration":0.007491,"end_time":"2026-04-20T21:07:07.375026+00:00","exception":false,"start_time":"2026-04-20T21:07:07.367535+00:00","status":"completed"},"tags":[]}},{"id":"8247eb1f","cell_type":"code","source":"class AWP:\n    def __init__(self, model, optimizer, delta=0.1, lr=1e-4):\n        self.model     = model\n        self.optimizer = optimizer\n        self.delta     = delta\n        self.lr        = lr\n        self._backup   = {}\n\n    def perturb(self, x_batch, y_batch, loss_fn):\n        with tf.GradientTape() as tape:\n            y_pred = self.model(x_batch, training=True)\n            loss   = loss_fn(y_batch, y_pred)\n\n        grad = tape.gradient(loss, self.model.trainable_variables)\n\n        # ✅ الحل: استخدم id(v) بدل v.ref()\n        self._backup = {id(v): tf.identity(v) for v in self.model.trainable_variables}\n\n        for v, g in zip(self.model.trainable_variables, grad):\n            if g is not None:\n                norm  = tf.norm(g) + 1e-8\n                delta = self.delta * g / norm\n                v.assign_add(delta)\n\n    def restore(self):\n        # ✅ الحل: استخدم id(v) بدل v.ref()\n        for v in self.model.trainable_variables:\n            original = self._backup.get(id(v))\n            if original is not None:\n                v.assign(original)\n        self._backup = {}","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:58.516809Z","iopub.execute_input":"2026-04-21T15:47:58.517194Z","iopub.status.idle":"2026-04-21T15:47:58.533667Z","shell.execute_reply.started":"2026-04-21T15:47:58.517164Z","shell.execute_reply":"2026-04-21T15:47:58.532635Z"},"papermill":{"duration":0.018084,"end_time":"2026-04-20T21:07:07.400538+00:00","exception":false,"start_time":"2026-04-20T21:07:07.382454+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c820bc95","cell_type":"markdown","source":"## Section 14 — Model Initialization","metadata":{"papermill":{"duration":0.007162,"end_time":"2026-04-20T21:07:07.415311+00:00","exception":false,"start_time":"2026-04-20T21:07:07.408149+00:00","status":"completed"},"tags":[]}},{"id":"be54da51","cell_type":"code","source":"model = get_model(max_len=MAX_LEN, channels=CHANNELS, num_classes=NUM_CLASSES)\nmodel.summary(print_fn=logger.info)\n\n_params    = model.count_params()\n_trainable = sum(tf.keras.backend.count_params(w) for w in model.trainable_weights)\nlogger.info(\"Parameters — total: %s  trainable: %s\", f\"{_params:,}\", f\"{_trainable:,}\")\n\nwith open(os.path.join(DIRS[\"metrics\"], \"model_params.txt\"), \"w\") as _f:\n    _f.write(f\"Total: {_params:,}\\nTrainable: {_trainable:,}\\n\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:47:58.534771Z","iopub.execute_input":"2026-04-21T15:47:58.535597Z","iopub.status.idle":"2026-04-21T15:48:00.163476Z","shell.execute_reply.started":"2026-04-21T15:47:58.535568Z","shell.execute_reply":"2026-04-21T15:48:00.162675Z"},"papermill":{"duration":1.783999,"end_time":"2026-04-20T21:07:09.206242+00:00","exception":false,"start_time":"2026-04-20T21:07:07.422243+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"37114609","cell_type":"markdown","source":"## Section 15 — Optimizer: RectifiedAdam + Lookahead\n\n**✅ التحسين:**  \n- `RectifiedAdam` — بيصلح الـ variance في الـ Adam في الـ early training  \n- `Lookahead` — بيعمل slow weights كـ anchor عشان يمنع الـ oscillation  \n- Fallback لـ Adam العادي لو TFA مش متاح","metadata":{"papermill":{"duration":0.008241,"end_time":"2026-04-20T21:07:09.223519+00:00","exception":false,"start_time":"2026-04-20T21:07:09.215278+00:00","status":"completed"},"tags":[]}},{"id":"b3986aa7","cell_type":"code","source":"# ── OneCycleLR Schedule ───────────────────────────────────────────────────────\nclass OneCycleLR(tf.keras.optimizers.schedules.LearningRateSchedule):\n    \"\"\"\n    OneCycleLR:\n    - Phase 1: warmup من min_lr → peak_lr (warmup_ratio من الـ steps)\n    - Phase 2: cosine decay من peak_lr → min_lr (باقي الـ steps)\n    \n    أحسن من WarmupCosineDecay لأن الـ warmup أسرع والـ decay أكثر smoothness\n    \"\"\"\n    def __init__(self, peak_lr, total_steps, warmup_ratio=0.3, min_lr=1e-6):\n        super().__init__()\n        self.peak_lr      = float(peak_lr)\n        self.total_steps  = float(total_steps)\n        self.warmup_steps = float(total_steps * warmup_ratio)\n        self.min_lr       = float(min_lr)\n\n    def __call__(self, step):\n        step  = tf.cast(step, tf.float32)\n\n        # Phase 1: warmup\n        warmup = self.min_lr + (self.peak_lr - self.min_lr) * (\n            step / tf.maximum(self.warmup_steps, 1.0)\n        )\n\n        # Phase 2: cosine annealing\n        cos_input = (step - self.warmup_steps) / tf.maximum(\n            self.total_steps - self.warmup_steps, 1.0\n        ) * math.pi\n        cos_decay = self.min_lr + 0.5 * (self.peak_lr - self.min_lr) * (\n            1.0 + tf.math.cos(cos_input)\n        )\n\n        return tf.where(step < self.warmup_steps, warmup, cos_decay)\n\n    def get_config(self):\n        return {\n            \"peak_lr\":     self.peak_lr,\n            \"total_steps\": self.total_steps,\n            \"min_lr\":      self.min_lr,\n        }\n\n\n_total_steps = steps_per_epoch * CFG.EPOCHS\nlr_schedule  = OneCycleLR(\n    peak_lr     = CFG.LEARNING_RATE,\n    total_steps = _total_steps,\n    warmup_ratio = CFG.WARMUP_RATIO,\n    min_lr      = CFG.MIN_LR,\n)\n\n# ✅ RectifiedAdam + Lookahead (لو TFA متاح)\nif HAS_TFA:\n    _base_opt = tfa.optimizers.RectifiedAdam(\n        learning_rate = lr_schedule,\n        weight_decay  = CFG.WEIGHT_DECAY,\n        sma_threshold = 4,              # الـ threshold لـ rectification\n    )\n    optimizer = tfa.optimizers.Lookahead(_base_opt, sync_period=5, slow_step_size=0.5)\n    logger.info(\"Optimizer: RectifiedAdam + Lookahead (TFA)\")\nelse:\n    optimizer = optimizers.Adam(learning_rate=lr_schedule, clipnorm=1.0)\n    logger.info(\"Optimizer: Adam + OneCycleLR (TFA not available)\")\n\nloss_fn = losses.CategoricalCrossentropy(\n    from_logits=True,\n    label_smoothing=CFG.LABEL_SMOOTHING\n)\n\n# AWP instance\nawp = AWP(model, optimizer, delta=CFG.AWP_DELTA, lr=CFG.AWP_LR) if CFG.USE_AWP else None\n\nlogger.info(\"LR: %.2e → %.2e | AWP: %s (starts epoch %d)\",\n            CFG.LEARNING_RATE, CFG.MIN_LR,\n            \"ON\" if awp else \"OFF\", CFG.AWP_START_EPOCH)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:00.164472Z","iopub.execute_input":"2026-04-21T15:48:00.164792Z","iopub.status.idle":"2026-04-21T15:48:00.183672Z","shell.execute_reply.started":"2026-04-21T15:48:00.164766Z","shell.execute_reply":"2026-04-21T15:48:00.182649Z"},"papermill":{"duration":0.028476,"end_time":"2026-04-20T21:07:09.260196+00:00","exception":false,"start_time":"2026-04-20T21:07:09.23172+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"404f4789","cell_type":"markdown","source":"## Section 16 — Checkpointing","metadata":{"papermill":{"duration":0.007673,"end_time":"2026-04-20T21:07:09.27595+00:00","exception":false,"start_time":"2026-04-20T21:07:09.268277+00:00","status":"completed"},"tags":[]}},{"id":"25b08aa2","cell_type":"code","source":"import shutil\n\n_ckpt_dir_last = os.path.join(DIRS[\"checkpoints\"], \"last\")\n_ckpt_dir_best = os.path.join(DIRS[\"checkpoints\"], \"best\")\nos.makedirs(_ckpt_dir_last, exist_ok=True)\nos.makedirs(_ckpt_dir_best, exist_ok=True)\n\nepoch_var    = tf.Variable(0,   trainable=False, dtype=tf.int64,   name=\"epoch\")\nbest_val_acc = tf.Variable(0.0, trainable=False, dtype=tf.float32, name=\"best_val_acc\")\n\ncheckpoint   = tf.train.Checkpoint(\n    model=model, optimizer=optimizer,\n    epoch=epoch_var, best_val_acc=best_val_acc,\n)\nmanager_last = tf.train.CheckpointManager(checkpoint, _ckpt_dir_last, max_to_keep=2)\nmanager_best = tf.train.CheckpointManager(checkpoint, _ckpt_dir_best, max_to_keep=1)\n\ndef _checkpoint_files_exist(manager):\n    latest = manager.latest_checkpoint\n    if latest is None: return False\n    return os.path.exists(latest + \".index\")\n\nif CFG.RESET_CHECKPOINT:\n    shutil.rmtree(_ckpt_dir_last, ignore_errors=True)\n    shutil.rmtree(_ckpt_dir_best, ignore_errors=True)\n    os.makedirs(_ckpt_dir_last, exist_ok=True)\n    os.makedirs(_ckpt_dir_best, exist_ok=True)\n    manager_last = tf.train.CheckpointManager(checkpoint, _ckpt_dir_last, max_to_keep=2)\n    manager_best = tf.train.CheckpointManager(checkpoint, _ckpt_dir_best, max_to_keep=1)\n    logger.info(\"RESET_CHECKPOINT=True — checkpoints cleared.\")\n\ninitial_epoch = 0\nif _checkpoint_files_exist(manager_last):\n    try:\n        checkpoint.restore(manager_last.latest_checkpoint)\n        initial_epoch = int(epoch_var.numpy())\n        logger.info(\"Restored from epoch %d.\", initial_epoch + 1)\n    except Exception as _e:\n        logger.error(\"Restore failed (%s) — starting fresh.\", _e)\n        initial_epoch = 0\nelse:\n    logger.info(\"No checkpoint found — fresh start.\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:00.184958Z","iopub.execute_input":"2026-04-21T15:48:00.185644Z","iopub.status.idle":"2026-04-21T15:48:00.30594Z","shell.execute_reply.started":"2026-04-21T15:48:00.18558Z","shell.execute_reply":"2026-04-21T15:48:00.305106Z"},"papermill":{"duration":0.024771,"end_time":"2026-04-20T21:07:09.308237+00:00","exception":false,"start_time":"2026-04-20T21:07:09.283466+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"4a07e1d0","cell_type":"markdown","source":"## Section 17 — Stateful Metrics","metadata":{"papermill":{"duration":0.007887,"end_time":"2026-04-20T21:07:09.32382+00:00","exception":false,"start_time":"2026-04-20T21:07:09.315933+00:00","status":"completed"},"tags":[]}},{"id":"351eae0a","cell_type":"code","source":"train_loss_m  = metrics.Mean(name=\"loss\")\ntrain_acc_m   = metrics.CategoricalAccuracy(name=\"accuracy\")\ntrain_top5_m  = metrics.TopKCategoricalAccuracy(k=5, name=\"top_5_accuracy\")\n\nval_loss_m    = metrics.Mean(name=\"val_loss\")\nval_acc_m     = metrics.CategoricalAccuracy(name=\"val_accuracy\")\nval_top5_m    = metrics.TopKCategoricalAccuracy(k=5, name=\"val_top_5_accuracy\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:00.307126Z","iopub.execute_input":"2026-04-21T15:48:00.307487Z","iopub.status.idle":"2026-04-21T15:48:00.333211Z","shell.execute_reply.started":"2026-04-21T15:48:00.30745Z","shell.execute_reply":"2026-04-21T15:48:00.33247Z"},"papermill":{"duration":0.03086,"end_time":"2026-04-20T21:07:09.362573+00:00","exception":false,"start_time":"2026-04-20T21:07:09.331713+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"faf52bfb","cell_type":"markdown","source":"## Section 18 — Training & Validation Steps (with AWP)","metadata":{"papermill":{"duration":0.00785,"end_time":"2026-04-20T21:07:09.378622+00:00","exception":false,"start_time":"2026-04-20T21:07:09.370772+00:00","status":"completed"},"tags":[]}},{"id":"18253148","cell_type":"code","source":"@tf.function\ndef train_step(x_batch, y_batch, accum_grads, step_in_accum):\n    with tf.GradientTape() as tape:\n        y_pred   = model(x_batch, training=True)\n        raw_loss = loss_fn(y_batch, y_pred)\n        scaled   = raw_loss / tf.cast(CFG.GRAD_ACCUM_STEPS, raw_loss.dtype)\n\n    grads     = tape.gradient(scaled, model.trainable_variables)\n    new_accum = [ag + g for ag, g in zip(accum_grads, grads)]\n\n    if tf.equal(step_in_accum, CFG.GRAD_ACCUM_STEPS - 1):\n        clipped, _ = tf.clip_by_global_norm(new_accum, 1.0)\n        optimizer.apply_gradients(zip(clipped, model.trainable_variables))\n        new_accum  = [tf.zeros_like(v) for v in model.trainable_variables]\n\n    train_loss_m.update_state(raw_loss)\n    train_acc_m .update_state(y_batch, y_pred)\n    train_top5_m.update_state(y_batch, y_pred)\n    return new_accum\n\n\ndef train_step_with_awp(x_batch, y_batch, accum_grads, step_in_accum):\n    \"\"\"Train step مع AWP: normal step + perturb + perturbed step + restore\"\"\"\n    # Step 1: Normal training step\n    new_accum = train_step(x_batch, y_batch, accum_grads, step_in_accum)\n\n    # Step 2: AWP — perturb weights\n    awp.perturb(x_batch, y_batch, loss_fn)\n\n    # Step 3: Train على الـ perturbed weights\n    with tf.GradientTape() as tape:\n        y_pred_adv = model(x_batch, training=True)\n        loss_adv   = loss_fn(y_batch, y_pred_adv)\n\n    grads_adv = tape.gradient(loss_adv, model.trainable_variables)\n    clipped, _ = tf.clip_by_global_norm(grads_adv, 1.0)\n    optimizer.apply_gradients(zip(clipped, model.trainable_variables))\n\n    # Step 4: Restore الـ original weights\n    awp.restore()\n\n    return new_accum\n\n\n@tf.function\ndef val_step(x_batch, y_batch):\n    y_pred = model(x_batch, training=False)\n    val_loss_m.update_state(loss_fn(y_batch, y_pred))\n    val_acc_m .update_state(y_batch, y_pred)\n    val_top5_m.update_state(y_batch, y_pred)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:00.334385Z","iopub.execute_input":"2026-04-21T15:48:00.334851Z","iopub.status.idle":"2026-04-21T15:48:00.345814Z","shell.execute_reply.started":"2026-04-21T15:48:00.334807Z","shell.execute_reply":"2026-04-21T15:48:00.345034Z"},"papermill":{"duration":0.01913,"end_time":"2026-04-20T21:07:09.405431+00:00","exception":false,"start_time":"2026-04-20T21:07:09.386301+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"7f083ae5","cell_type":"markdown","source":"## Section 19 — Training Loop","metadata":{"papermill":{"duration":0.007969,"end_time":"2026-04-20T21:07:09.421273+00:00","exception":false,"start_time":"2026-04-20T21:07:09.413304+00:00","status":"completed"},"tags":[]}},{"id":"35ff8a00","cell_type":"code","source":"def run_training():\n    history_rows   = []\n    early_stop_ctr = 0\n    history_csv    = os.path.join(DIRS[\"metrics\"], f\"{CFG.MODEL_NAME}_training_history.csv\")\n\n    if os.path.exists(history_csv):\n        history_rows = pd.read_csv(history_csv).to_dict(\"records\")\n        logger.info(\"Loaded %d history rows from CSV.\", len(history_rows))\n\n    accum_grads = [tf.zeros_like(v) for v in model.trainable_variables]\n\n    for epoch in range(initial_epoch, CFG.EPOCHS):\n        t0 = time.time()\n\n        for m in [train_loss_m, train_acc_m, train_top5_m,\n                  val_loss_m,   val_acc_m,   val_top5_m]:\n            m.reset_state()\n\n        # ✅ AWP يبدأ بعد AWP_START_EPOCH epochs\n        use_awp = (awp is not None) and (epoch >= CFG.AWP_START_EPOCH)\n        step_fn = train_step_with_awp if use_awp else train_step\n\n        # Training phase\n        for step, (xb, yb) in enumerate(train_dataset.take(steps_per_epoch)):\n            accum_grads = step_fn(xb, yb, accum_grads, step % CFG.GRAD_ACCUM_STEPS)\n\n        # Validation phase\n        for xb, yb in val_dataset.take(validation_steps):\n            val_step(xb, yb)\n\n        t_loss = float(train_loss_m.result())\n        t_acc  = float(train_acc_m.result())\n        t_top5 = float(train_top5_m.result())\n        v_loss = float(val_loss_m.result())\n        v_acc  = float(val_acc_m.result())\n        v_top5 = float(val_top5_m.result())\n        elapsed = time.time() - t0\n\n        awp_tag = \" [AWP]\" if use_awp else \"\"\n        logger.info(\n            \"Epoch %03d/%03d%s | loss=%.4f acc=%.4f top5=%.4f | \"\n            \"val_loss=%.4f val_acc=%.4f val_top5=%.4f | %.1fs\",\n            epoch + 1, CFG.EPOCHS, awp_tag,\n            t_loss, t_acc, t_top5, v_loss, v_acc, v_top5, elapsed,\n        )\n\n        row = {\"epoch\": epoch + 1, \"awp\": use_awp,\n               \"loss\": t_loss, \"accuracy\": t_acc, \"top_5_accuracy\": t_top5,\n               \"val_loss\": v_loss, \"val_accuracy\": v_acc, \"val_top_5_accuracy\": v_top5,\n               \"elapsed_s\": elapsed}\n        history_rows.append(row)\n        pd.DataFrame(history_rows).to_csv(history_csv, index=False)\n\n        epoch_var.assign(epoch + 1)\n        manager_last.save()\n\n        if v_acc > float(best_val_acc.numpy()):\n            best_val_acc.assign(v_acc)\n            manager_best.save()\n            logger.info(\"  ✅ Best model saved — val_accuracy=%.4f\", v_acc)\n            early_stop_ctr = 0\n        else:\n            early_stop_ctr += 1\n            logger.info(\"  No improvement %d/%d.\", early_stop_ctr, CFG.EARLY_STOP_PAT)\n            if early_stop_ctr >= CFG.EARLY_STOP_PAT:\n                logger.info(\"Early stopping at epoch %d.\", epoch + 1)\n                break\n\n    return pd.DataFrame(history_rows)\n\n\ntry:\n    history_df = run_training()\n    logger.info(\"Training completed.\")\nexcept KeyboardInterrupt:\n    logger.warning(\"Interrupted — saving emergency checkpoint.\")\n    manager_last.save()\n    history_df = None\nexcept Exception as _exc:\n    logger.error(\"Training failed: %s\\n%s\", _exc, traceback.format_exc())\n    try:\n        manager_last.save()\n    except Exception:\n        pass\n    raise","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:00.347351Z","iopub.execute_input":"2026-04-21T15:48:00.347775Z","iopub.status.idle":"2026-04-21T15:48:00.474575Z","shell.execute_reply.started":"2026-04-21T15:48:00.347732Z","shell.execute_reply":"2026-04-21T15:48:00.473759Z"},"papermill":{"duration":35968.219447,"end_time":"2026-04-21T07:06:37.648594+00:00","exception":true,"start_time":"2026-04-20T21:07:09.429147+00:00","status":"failed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5234f63d","cell_type":"markdown","source":"## Section 20 — Training History Plot","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"id":"0ed3c1b7","cell_type":"code","source":"_history_csv = os.path.join(DIRS[\"metrics\"], f\"{CFG.MODEL_NAME}_training_history.csv\")\n\nif os.path.exists(_history_csv):\n    _hist = pd.read_csv(_history_csv)\n\n    _fig, _axes = plt.subplots(1, 2, figsize=(16, 5))\n    _fig.suptitle(f\"{CFG.MODEL_NAME} — Training History\", fontsize=14, fontweight=\"bold\")\n\n    # ✅ نلون الـ AWP epochs باللون التاني\n    if \"awp\" in _hist.columns:\n        _awp_start = _hist[_hist[\"awp\"] == True][\"epoch\"].min()\n        if not pd.isna(_awp_start):\n            for ax in _axes:\n                ax.axvline(x=_awp_start, color='purple', linestyle='--',\n                           alpha=0.5, label=f'AWP start (epoch {int(_awp_start)})')\n\n    _axes[0].plot(_hist[\"epoch\"], _hist[\"accuracy\"],     label=\"Train\",      color=\"#4285F4\", lw=2)\n    _axes[0].plot(_hist[\"epoch\"], _hist[\"val_accuracy\"], label=\"Validation\", color=\"#34A853\", lw=2)\n    _axes[0].set_title(\"Accuracy\"); _axes[0].legend(); _axes[0].grid(True, ls=\"--\", alpha=0.6)\n\n    _axes[1].plot(_hist[\"epoch\"], _hist[\"loss\"],     label=\"Train\",      color=\"#EA4335\", lw=2)\n    _axes[1].plot(_hist[\"epoch\"], _hist[\"val_loss\"], label=\"Validation\", color=\"#FBBC05\", lw=2)\n    _axes[1].set_title(\"Loss\"); _axes[1].legend(); _axes[1].grid(True, ls=\"--\", alpha=0.6)\n\n    plt.tight_layout()\n    _plot_path = os.path.join(DIRS[\"plots\"], f\"{CFG.MODEL_NAME}_history.png\")\n    plt.savefig(_plot_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n    logger.info(\"History plot saved: %s\", _plot_path)\nelse:\n    logger.warning(\"History CSV not found — skipping plot.\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:00.47558Z","iopub.execute_input":"2026-04-21T15:48:00.475858Z","iopub.status.idle":"2026-04-21T15:48:01.711073Z","shell.execute_reply.started":"2026-04-21T15:48:00.475834Z","shell.execute_reply":"2026-04-21T15:48:01.710299Z"},"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b1cc281d-2f84-496c-b03d-fe32f56a6650","cell_type":"code","source":"سرشلأ{[~ي]}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T15:48:01.712015Z","iopub.execute_input":"2026-04-21T15:48:01.712356Z","iopub.status.idle":"2026-04-21T15:48:01.718664Z","shell.execute_reply.started":"2026-04-21T15:48:01.71233Z","shell.execute_reply":"2026-04-21T15:48:01.717321Z"}},"outputs":[],"execution_count":null},{"id":"5e9b2902","cell_type":"markdown","source":"## Section 21 — TFLite Export (المشكلة الأساسية محلولة هنا!)\n\n**✅ التغيير الجوهري:**  \n- النسخة القديمة: TFLite بياخد `(1, 384, 708)` → preprocessing في Python → بطيء جداً  \n- النسخة الجديدة: TFLite بياخد `(None, 543, 3)` raw landmarks → preprocessing داخل الـ model → سريع!\n\n```\n❌ قبل: Camera → MediaPipe → Python Preprocess (بطيء!) → TFLite[384, 708] → Result\n✅ بعد:  Camera → MediaPipe → TFLite[None, 543, 3] → Result (كل شيء جوه!)\n```","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"id":"4eecf88e-4eb5-4e33-ba6d-729f2ea1db36","cell_type":"code","source":"# ==========================================================\n# SECTION 21 — FINAL MODEL EXPORT (FIXED VERSION)\n# ==========================================================\n\nimport os\nimport json\nimport time\nimport hashlib\nimport traceback\nimport numpy as np\nimport tensorflow as tf\n\nEXPORT_DIR = os.path.join(DIRS[\"base\"], \"export\")\nos.makedirs(EXPORT_DIR, exist_ok=True)\n\nlogger.info(\"=\" * 70)\nlogger.info(\"SECTION 21 — FINAL MODEL EXPORT\")\nlogger.info(\"=\" * 70)\n\nexport_manifest = {}\n\n\n# ==========================================================\n# Helpers\n# ==========================================================\n\ndef now():\n    return time.time()\n\n\ndef sha256(path):\n    h = hashlib.sha256()\n\n    with open(path, \"rb\") as f:\n        for chunk in iter(lambda: f.read(65536), b\"\"):\n            h.update(chunk)\n\n    return h.hexdigest()\n\n\ndef save_binary(path, data):\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n\n# ==========================================================\n# Restore Best Weights\n# ==========================================================\n\ndef restore_best_weights():\n    try:\n        checkpoint.restore(manager_best.latest_checkpoint).expect_partial()\n        logger.info(\"Best checkpoint restored.\")\n    except Exception as e:\n        logger.warning(\"Restore failed. Using current weights. %s\", e)\n\n\n# ==========================================================\n# TFLite Wrapper\n# IMPORTANT:\n# raw input  -> (frames, 543, 3)\n# preprocess -> (1, 384, 708)\n# model      -> logits\n# ==========================================================\n\nclass TFLiteModelWithPreprocess(tf.Module):\n\n    def __init__(self, model):\n        super().__init__()\n        self.model = model\n        self.prep = Preprocess()\n\n    @tf.function(\n        input_signature=[\n            tf.TensorSpec(\n                shape=[None, 543, 3],   # variable frames\n                dtype=tf.float32,\n                name=\"inputs\"\n            )\n        ]\n    )\n    def __call__(self, inputs):\n\n        x = self.prep(inputs)   # MUST return (1, CFG.MAX_LEN, CHANNELS)\n\n        logits = self.model(x, training=False)\n\n        probs = tf.nn.softmax(logits, axis=-1)[0]\n\n        return {\"outputs\": probs}\n\n\n# ==========================================================\n# Export SavedModel\n# ==========================================================\n\ndef export_saved_model():\n\n    start = now()\n\n    try:\n        path = os.path.join(EXPORT_DIR, \"saved_model\")\n\n        tf.saved_model.save(model, path)\n\n        export_manifest[\"saved_model\"] = {\n            \"status\": \"ok\",\n            \"path\": path,\n            \"time_sec\": round(now() - start, 2)\n        }\n\n        logger.info(\"SavedModel exported.\")\n\n    except Exception as e:\n\n        export_manifest[\"saved_model\"] = {\n            \"status\": \"failed\",\n            \"error\": str(e)\n        }\n\n        logger.error(\"SavedModel failed: %s\", e)\n\n\n# ==========================================================\n# Export Keras\n# ==========================================================\n\ndef export_keras():\n\n    start = now()\n\n    try:\n        path = os.path.join(EXPORT_DIR, f\"{CFG.MODEL_NAME}.keras\")\n\n        model.save(path)\n\n        export_manifest[\"keras\"] = {\n            \"status\": \"ok\",\n            \"path\": path,\n            \"time_sec\": round(now() - start, 2)\n        }\n\n        logger.info(\".keras exported.\")\n\n    except Exception as e:\n\n        export_manifest[\"keras\"] = {\n            \"status\": \"failed\",\n            \"error\": str(e)\n        }\n\n        logger.error(\".keras failed: %s\", e)\n\n\n# ==========================================================\n# Export TFLite\n# ==========================================================\n\ndef export_tflite():\n\n    start = now()\n\n    try:\n        logger.info(\"Preparing TFLite wrapper...\")\n\n        wrapper = TFLiteModelWithPreprocess(model)\n\n        concrete_fn = wrapper.__call__.get_concrete_function()\n\n        logger.info(\"Creating converter...\")\n\n        converter = tf.lite.TFLiteConverter.from_concrete_functions(\n            [concrete_fn],\n            wrapper\n        )\n\n        converter.optimizations = [tf.lite.Optimize.DEFAULT]\n        converter.target_spec.supported_types = [tf.float16]\n        converter.target_spec.supported_ops = [\n            tf.lite.OpsSet.TFLITE_BUILTINS\n        ]\n\n        logger.info(\"Converting...\")\n\n        tflite_bytes = converter.convert()\n\n        path = os.path.join(\n            EXPORT_DIR,\n            f\"{CFG.MODEL_NAME}_with_preprocess_f16.tflite\"\n        )\n\n        save_binary(path, tflite_bytes)\n\n        size_mb = os.path.getsize(path) / 1e6\n\n        export_manifest[\"tflite\"] = {\n            \"status\": \"ok\",\n            \"path\": path,\n            \"size_mb\": round(size_mb, 2),\n            \"sha256\": sha256(path),\n            \"time_sec\": round(now() - start, 2)\n        }\n\n        logger.info(\"TFLite exported (%.2f MB)\", size_mb)\n\n    except Exception:\n\n        export_manifest[\"tflite\"] = {\n            \"status\": \"failed\",\n            \"error\": traceback.format_exc()\n        }\n\n        logger.error(\"TFLite failed:\\n%s\", traceback.format_exc())\n\n\n# ==========================================================\n# Verify TFLite\n# ==========================================================\n\ndef verify_tflite():\n\n    if export_manifest.get(\"tflite\", {}).get(\"status\") != \"ok\":\n        return\n\n    try:\n        path = export_manifest[\"tflite\"][\"path\"]\n\n        logger.info(\"Verifying TFLite...\")\n\n        interpreter = tf.lite.Interpreter(model_path=path)\n        interpreter.allocate_tensors()\n\n        input_info = interpreter.get_input_details()[0]\n        output_info = interpreter.get_output_details()[0]\n\n        dummy_frames = 30\n\n        interpreter.resize_input_tensor(\n            input_info[\"index\"],\n            [dummy_frames, 543, 3]\n        )\n\n        interpreter.allocate_tensors()\n\n        dummy_input = np.random.randn(\n            dummy_frames,\n            543,\n            3\n        ).astype(np.float32)\n\n        interpreter.set_tensor(\n            input_info[\"index\"],\n            dummy_input\n        )\n\n        interpreter.invoke()\n\n        output = interpreter.get_tensor(\n            output_info[\"index\"]\n        )\n\n        logger.info(\"TFLite verified.\")\n        logger.info(\"Output shape: %s\", output.shape)\n\n    except Exception as e:\n        logger.error(\"Verification failed: %s\", e)\n\n\n# ==========================================================\n# Save Manifest\n# ==========================================================\n\ndef save_manifest():\n\n    path = os.path.join(EXPORT_DIR, \"manifest.json\")\n\n    with open(path, \"w\") as f:\n        json.dump(export_manifest, f, indent=2)\n\n    logger.info(\"Manifest saved.\")\n\n\n# ==========================================================\n# Run Pipeline\n# ==========================================================\n\nrestore_best_weights()\n\nexport_saved_model()\nexport_keras()\nexport_tflite()\nverify_tflite()\n\nsave_manifest()\n\nlogger.info(\"Export finished.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T15:48:05.921052Z","iopub.execute_input":"2026-04-21T15:48:05.921695Z","execution_failed":"2026-04-21T16:00:14.382Z"}},"outputs":[],"execution_count":null},{"id":"65778cbd","cell_type":"code","source":"import json, hashlib\nfrom pathlib import Path\n\nEXPORT_DIR = os.path.join(DIRS[\"base\"], \"export\")\nos.makedirs(EXPORT_DIR, exist_ok=True)\nlogger.info(\"=\" * 70)\nlogger.info(\"SECTION 21 — FINAL MODEL EXPORT (with Preprocess inside TFLite)\")\nlogger.info(\"=\" * 70)\n\n# Restore best weights\ntry:\n    checkpoint.restore(manager_best.latest_checkpoint).expect_partial()\n    logger.info(\"Best checkpoint restored for export.\")\nexcept Exception as _e:\n    logger.warning(\"Could not restore best checkpoint (%s) — exporting current weights.\", _e)\n\n\ndef sha256(path):\n    h = hashlib.sha256()\n    with open(path, \"rb\") as f:\n        for chunk in iter(lambda: f.read(65536), b\"\"):\n            h.update(chunk)\n    return h.hexdigest()\n\n\n# ══════════════════════════════════════════════════════════════════════\n# ✅ TFLite Wrapper — Preprocess layer جوه الموديل!\n# Input: (None, 543, 3) — raw MediaPipe landmarks, مباشرة من الكاميرا\n# Output: {'outputs': (250,)} — class probabilities\n# ══════════════════════════════════════════════════════════════════════\n\nclass TFLiteModelWithPreprocess(tf.Module):\n\n    def __init__(self, model):\n        super().__init__()\n        self.model = model\n        self.prep = Preprocess()\n\n    @tf.function(input_signature=[\n        tf.TensorSpec(\n            shape=[None, 543, 3],\n            dtype=tf.float32,\n            name=\"inputs\"\n        )\n    ])\n    def __call__(self, inputs):\n\n        x = self.prep(inputs)   # لازم يطلع (1,384,708)\n\n        logits = self.model(x, training=False)\n        probs = tf.nn.softmax(logits, axis=-1)[0]\n\n        return {\"outputs\": probs}\n\n\nexport_manifest = {}\n\n# FORMAT 1: SavedModel\ntry:\n    saved_model_path = os.path.join(EXPORT_DIR, \"saved_model\")\n\n    @tf.function(input_signature=[\n        tf.TensorSpec(shape=(None, CFG.MAX_LEN, CHANNELS), dtype=tf.float32, name=\"input_features\")\n    ])\n    def _serving_fn(input_features):\n        logits    = model(input_features, training=False)\n        probs     = tf.nn.softmax(logits, axis=-1)\n        top5_p, top5_i = tf.math.top_k(probs, k=5)\n        return {\"logits\": logits, \"probabilities\": probs,\n                \"top5_class_ids\": top5_i, \"top5_probs\": top5_p}\n\n    tf.saved_model.save(model, saved_model_path,\n                        signatures={\"serving_default\": _serving_fn})\n    logger.info(\"[FORMAT 1] SavedModel saved → %s\", saved_model_path)\n    export_manifest[\"saved_model\"] = {\"status\": \"ok\", \"path\": saved_model_path}\nexcept Exception as _e:\n    logger.error(\"[FORMAT 1] SavedModel FAILED: %s\", _e)\n    export_manifest[\"saved_model\"] = {\"status\": \"FAILED\", \"error\": str(_e)}\n\n\n# FORMAT 2: Keras\ntry:\n    keras_path = os.path.join(EXPORT_DIR, f\"{CFG.MODEL_NAME}.keras\")\n    model.save(keras_path)\n    logger.info(\"[FORMAT 2] .keras saved\")\n    export_manifest[\"keras\"] = {\"status\": \"ok\", \"path\": keras_path}\nexcept Exception as _e:\n    logger.error(\"[FORMAT 2] .keras FAILED: %s\", _e)\n\n\n# ══════════════════════════════════════════════════════════════════════\n# FORMAT 3: TFLite float16 — ✅ Preprocess جوه! ✅ بدون SELECT_TF_OPS!\n# هذا هو الـ file اللي هتستخدمه في الـ Android/iOS app\n# ══════════════════════════════════════════════════════════════════════\ntry:\n    tflite_wrapper = TFLiteModelWithPreprocess(model)\n\n    converter = tf.lite.TFLiteConverter.from_concrete_functions(\n        [tflite_wrapper.__call__.get_concrete_function()]\n    )\n    converter.optimizations          = [tf.lite.Optimize.DEFAULT]\n    converter.target_spec.supported_types = [tf.float16]\n    # ✅ لا SELECT_TF_OPS — TFLITE_BUILTINS فقط → أصغر وأسرع\n    converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]\n\n    tflite_bytes = converter.convert()\n    tflite_path  = os.path.join(EXPORT_DIR, f\"{CFG.MODEL_NAME}_with_preprocess_f16.tflite\")\n    with open(tflite_path, \"wb\") as f:\n        f.write(tflite_bytes)\n\n    _hash    = sha256(tflite_path)\n    _size_mb = os.path.getsize(tflite_path) / 1e6\n    logger.info(\"[FORMAT 3] ✅ TFLite float16 (with Preprocess) → %s (%.2f MB)\",\n                tflite_path, _size_mb)\n    export_manifest[\"tflite_f16_with_preprocess\"] = {\n        \"path\":    tflite_path,\n        \"size_mb\": round(_size_mb, 2),\n        \"sha256\":  _hash,\n        \"status\":  \"ok\",\n        \"input\":   \"(None, 543, 3) — raw MediaPipe\",\n        \"output\":  \"{'outputs': (250,)}\",\n    }\nexcept Exception as _e:\n    logger.error(\"[FORMAT 3] TFLite float16 FAILED: %s\\n%s\", _e, traceback.format_exc())\n    logger.warning(\"Fallback: trying with SELECT_TF_OPS...\")\n\n    # Fallback مع SELECT_TF_OPS لو فضل في ops مش supported\n    try:\n        converter.target_spec.supported_ops = [\n            tf.lite.OpsSet.TFLITE_BUILTINS,\n            tf.lite.OpsSet.SELECT_TF_OPS\n        ]\n        tflite_bytes = converter.convert()\n        tflite_path_fb = os.path.join(EXPORT_DIR, f\"{CFG.MODEL_NAME}_with_preprocess_f16_fallback.tflite\")\n        with open(tflite_path_fb, \"wb\") as f:\n            f.write(tflite_bytes)\n        _size_mb = os.path.getsize(tflite_path_fb) / 1e6\n        logger.info(\"[FORMAT 3-FB] TFLite float16 (fallback, SELECT_TF_OPS) → %.2f MB\", _size_mb)\n        export_manifest[\"tflite_f16_with_preprocess\"] = {\n            \"path\": tflite_path_fb, \"size_mb\": round(_size_mb, 2),\n            \"status\": \"ok_with_select_tf_ops\"\n        }\n    except Exception as _e2:\n        logger.error(\"[FORMAT 3-FB] Also failed: %s\", _e2)\n        export_manifest[\"tflite_f16_with_preprocess\"] = {\"status\": \"FAILED\"}\n\n\n# ══════════════════════════════════════════════════════════════════════\n# VERIFICATION: تأكد إن الـ TFLite بياخد (None, 543, 3) صح\n# ══════════════════════════════════════════════════════════════════════\nif export_manifest.get(\"tflite_f16_with_preprocess\", {}).get(\"status\", \"\").startswith(\"ok\"):\n    try:\n        _tfl_path = export_manifest[\"tflite_f16_with_preprocess\"][\"path\"]\n        interp    = tf.lite.Interpreter(model_path=_tfl_path)\n        interp.allocate_tensors()\n\n        inp_detail = interp.get_input_details()[0]\n        out_detail = interp.get_output_details()[0]\n\n        logger.info(\"TFLite verification:\")\n        logger.info(\"  Input  shape: %s  dtype: %s\", inp_detail['shape'], inp_detail['dtype'])\n        logger.info(\"  Output shape: %s  dtype: %s\", out_detail['shape'], out_detail['dtype'])\n\n        # Test مع dummy data (30 frames)\n        dummy_frames = 30\n        interp.resize_input_tensor(inp_detail[\"index\"], [dummy_frames, 543, 3])\n        interp.allocate_tensors()\n\n        dummy_input = np.random.randn(dummy_frames, 543, 3).astype(np.float32)\n        interp.set_tensor(inp_detail[\"index\"], dummy_input)\n        interp.invoke()\n\n        out = interp.get_tensor(out_detail[\"index\"])\n        logger.info(\"  ✅ TFLite inference OK — output shape: %s  top-1: class %d (%.3f)\",\n                    out.shape, np.argmax(out), float(np.max(out)))\n\n    except Exception as _e:\n        logger.error(\"TFLite verification FAILED: %s\", _e)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:01.72163Z","iopub.status.idle":"2026-04-21T15:48:01.722077Z","shell.execute_reply.started":"2026-04-21T15:48:01.721864Z","shell.execute_reply":"2026-04-21T15:48:01.721891Z"},"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c3602015","cell_type":"markdown","source":"## Section 22 — Metadata Export","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"id":"fd891258","cell_type":"code","source":"# Label map\nlabel_map_path = os.path.join(EXPORT_DIR, \"label_map.json\")\nwith open(label_map_path, \"w\") as f:\n    json.dump({\n        \"sign_to_label\": sign_to_label,\n        \"label_to_sign\": {str(k): v for k, v in label_to_sign.items()},\n        \"num_classes\":   NUM_CLASSES,\n    }, f, indent=2)\nlogger.info(\"Label map saved → %s\", label_map_path)\n\n# Preprocess config\npreprocess_path = os.path.join(EXPORT_DIR, \"preprocess_config.json\")\nwith open(preprocess_path, \"w\") as f:\n    json.dump({\n        \"rows_per_frame\":  ROWS_PER_FRAME,\n        \"max_len\":         MAX_LEN,\n        \"num_nodes\":       NUM_NODES,\n        \"channels\":        CHANNELS,\n        \"pad_value\":       PAD,\n        \"point_landmarks\": POINT_LANDMARKS,\n        \"note\": \"TFLite input = (None, 543, 3) — raw MediaPipe. Preprocess is INSIDE the model.\"\n    }, f, indent=2)\nlogger.info(\"Preprocess config saved → %s\", preprocess_path)\n\n# Training config\ncfg_path = os.path.join(EXPORT_DIR, \"training_config.json\")\ncfg_dict = {k: v for k, v in vars(CFG).items()\n            if not k.startswith(\"_\") and isinstance(v, (str, int, float, bool, list, type(None)))}\ncfg_dict[\"export_timestamp\"]  = datetime.datetime.now().isoformat()\ncfg_dict[\"best_val_accuracy\"] = float(best_val_acc.numpy())\ncfg_dict[\"optimizer\"]         = \"RectifiedAdam+Lookahead\" if HAS_TFA else \"Adam\"\ncfg_dict[\"awp_used\"]          = CFG.USE_AWP\nwith open(cfg_path, \"w\") as f:\n    json.dump(cfg_dict, f, indent=2)\nlogger.info(\"Training config saved → %s\", cfg_path)\n\n# Export manifest\nmanifest_path = os.path.join(EXPORT_DIR, \"export_manifest.json\")\nwith open(manifest_path, \"w\") as f:\n    json.dump(export_manifest, f, indent=2)\nlogger.info(\"Export manifest saved → %s\", manifest_path)\n\nlogger.info(\"=\" * 70)\nlogger.info(\"EXPORT COMPLETE\")\nlogger.info(\"Best val accuracy: %.4f\", float(best_val_acc.numpy()))\nlogger.info(\"=\" * 70)","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:01.72354Z","iopub.status.idle":"2026-04-21T15:48:01.723939Z","shell.execute_reply.started":"2026-04-21T15:48:01.723766Z","shell.execute_reply":"2026-04-21T15:48:01.723784Z"},"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d85a7850","cell_type":"markdown","source":"## Section 23 — Production Inference Guide\n\nكيف تستخدم الـ TFLite في الـ app بعد ما يتصدر:","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"id":"46dfa720","cell_type":"code","source":"# ══════════════════════════════════════════════════════════════════════\n# PRODUCTION INFERENCE EXAMPLE\n# هذا الكود يوضح كيف تستخدم الـ TFLite في الـ production\n# ══════════════════════════════════════════════════════════════════════\n\ndef make_tflite_predictor(tflite_path, label_map_path):\n    \"\"\"\n    Production-ready predictor\n    Input: raw MediaPipe landmarks → (num_frames, 543, 3)\n    Output: top-5 predictions مع confidence\n    \"\"\"\n    import json\n\n    with open(label_map_path) as f:\n        lm = json.load(f)\n    label_to_sign = {int(k): v for k, v in lm[\"label_to_sign\"].items()}\n\n    interp = tf.lite.Interpreter(model_path=tflite_path)\n    interp.allocate_tensors()\n    inp_det = interp.get_input_details()[0]\n    out_det = interp.get_output_details()[0]\n\n    def predict(frames: np.ndarray) -> list:\n        \"\"\"\n        frames: numpy array (num_frames, 543, 3) — raw MediaPipe output\n        returns: list of (sign_name, confidence) top-5\n        \"\"\"\n        assert frames.ndim == 3 and frames.shape[1] == 543 and frames.shape[2] == 3, \\\n            f\"Expected (N, 543, 3), got {frames.shape}\"\n\n        frames = frames.astype(np.float32)\n\n        # ✅ Resize الـ input tensor عشان يستوعب عدد الـ frames\n        interp.resize_input_tensor(inp_det[\"index\"], frames.shape)\n        interp.allocate_tensors()\n\n        # ✅ مفيش preprocessing هنا! الـ model بيعمله جواه\n        interp.set_tensor(inp_det[\"index\"], frames)\n        interp.invoke()\n\n        probs = interp.get_tensor(out_det[\"index\"])  # (250,)\n        top5  = np.argsort(probs)[-5:][::-1]\n\n        return [(label_to_sign[i], float(probs[i])) for i in top5]\n\n    return predict\n\n\n# Example usage:\n# predictor = make_tflite_predictor(tflite_path, label_map_path)\n# results = predictor(mediapipe_frames)  # (30, 543, 3)\n# print(results)  # [(\"horse\", 0.98), (\"animal\", 0.01), ...]\n\nlogger.info(\"Production inference template ready.\")\nlogger.info(\"Full notebook complete! ✅\")","metadata":{"execution":{"iopub.status.busy":"2026-04-21T15:48:01.72596Z","iopub.status.idle":"2026-04-21T15:48:01.726359Z","shell.execute_reply.started":"2026-04-21T15:48:01.726211Z","shell.execute_reply":"2026-04-21T15:48:01.72624Z"},"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}