{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12394092,"sourceType":"datasetVersion","datasetId":7815512},{"sourceId":12394094,"sourceType":"datasetVersion","datasetId":7815514},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Hybrid Two-Stage Inference for Kaggle CMI 2025  \n*A simple ensemble that automatically switches between an IMU-only model and a full-sensor (IMU + THM + TOF) model.*\n\n## 1. What this notebook does\n**Auto-routing**  \n* If at least one Thermopile (THM) or Time-of-Flight (TOF) value is present in a sequence, we invoke a **full-sensor TorchScript ensemble**.  \n* Otherwise we fall back to a lighter **IMU-only TorchScript ensemble**.\n\n## 2. Model provenance & credits\n| Sub-model | Training notebook (public kernel) |\n|-----------|-----------------------------------|\n| IMU-only  | “CMI2025 IMU Data Only – SGKFold 0.73/0.02 LB 0.73” · Thomas Dubail [[link]](https://www.kaggle.com/code/thomasdubail/cmi2025-imu-data-only-sgkfold-0-73-0-02-lb-0-73)\n| IMU + THM + TOF | “CMI3 PyTorch Baseline Model (add aug folds)” · myso1987 [[link]](https://www.kaggle.com/code/myso1987/cmi3-pyroch-baseline-model-add-aug-folds)\n\nHuge thanks to **Thomas Dubail** and **myso1987** for open-sourcing their training pipelines.  \nThis inference notebook merely repackages their checkpoints into a single, easy-to-deploy script.\n\n## 3. File layout expected by this notebook\n/kaggle/input/\n├── 1.Imu-only-model/\n│ ├── best_model_fold1.pt … best_model_fold5.pt\n│ ├── feature_cols.npy\n│ ├── gesture_classes.npy\n│ └── sequence_maxlen.npy\n└── 2.Hybrid-model/\n├── best_model_fold0.pt … best_model_fold4.pt\n├── feature_cols.npy\n├── gesture_classes.npy\n├── scaler.pkl\n└── sequence_maxlen.npy\n","metadata":{}},{"cell_type":"code","source":"!python --version","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T15:29:30.015679Z","iopub.execute_input":"2025-06-18T15:29:30.015935Z","iopub.status.idle":"2025-06-18T15:29:30.150943Z","shell.execute_reply.started":"2025-06-18T15:29:30.015874Z","shell.execute_reply":"2025-06-18T15:29:30.149983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===============================================================\n# Hybrid inference pipeline for Kaggle CMI 2025\n#   ① IMU-only \n#   ② IMU + THM + TOF \n# ===============================================================\nimport os, warnings, joblib\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport torch\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences as keras_pad_sequences\nimport tensorflow as tf\n\nimport kaggle_evaluation.cmi_inference_server as cmi\n\nwarnings.filterwarnings(\"ignore\")\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ===============================================================\n# ②  IMU + THM + TOF\n# ===============================================================\nprint(\"▶ Loading FULL-sensor TorchScript ensemble …\")\nMODEL_DIR_FULL = Path(\"/kaggle/input/02-hybrid/02_hybrid_model\") \n\n# ---- artefacts ----\nFEATURE_COLS_FULL    = np.load(MODEL_DIR_FULL / \"feature_cols.npy\", allow_pickle=True).tolist()\nGESTURE_CLASSES_FULL = np.load(MODEL_DIR_FULL / \"gesture_classes.npy\", allow_pickle=True)\nPAD_LEN_FULL         = int(np.load(MODEL_DIR_FULL / \"sequence_maxlen.npy\"))\nSCALER_FULL          = joblib.load(MODEL_DIR_FULL / \"scaler.pkl\")\n\n# ---- TorchScript models ----\nMODELS_FULL = [\n    torch.jit.load(MODEL_DIR_FULL / f\"best_model_fold{i}.pt\", map_location=DEVICE).eval()\n    for i in range(5)\n]\nprint(f\"▶ FULL ensemble loaded (5 models) | pad_len={PAD_LEN_FULL}\")\n\n# ---- columns used to decide whether THM/TOF data are present ----\nTHM_COLS = [c for c in FEATURE_COLS_FULL if c.startswith(\"thm_\")]\nTOF_COLS = [c for c in FEATURE_COLS_FULL if c.startswith(\"tof_\")]\n\ndef has_full_sensors(seq: pl.DataFrame) -> bool:\n    \"\"\"Return True if at least one THM or TOF value is not NaN.\"\"\"\n    pdf = seq.to_pandas()\n    return pdf[THM_COLS + TOF_COLS].notna().any().any()\n\n# ---- helpers ----\ndef _pad_np(arr: np.ndarray, maxlen: int, mode: str = \"pre\", fill: float = 0.0) -> np.ndarray:\n    if len(arr) >= maxlen:\n        return arr[-maxlen:] if mode == \"pre\" else arr[:maxlen]\n    pad = np.full((maxlen - len(arr), arr.shape[1]), fill, dtype=arr.dtype)\n    return np.vstack([pad, arr]) if mode == \"pre\" else np.vstack([arr, pad])\n\ndef full_preprocess(df_seq: pd.DataFrame) -> torch.Tensor:\n    \"\"\"Scale, pad and convert FULL-sensor sequence → tensor shape (1, T, C).\"\"\"\n    mat = (df_seq[FEATURE_COLS_FULL]\n           .ffill().bfill().fillna(0)\n           .values.astype(\"float32\"))\n    mat = SCALER_FULL.transform(mat)\n    mat = _pad_np(mat, PAD_LEN_FULL, mode=\"pre\")\n    return torch.from_numpy(mat).unsqueeze(0).to(DEVICE)\n\n# ===============================================================\n# ①  IMU-only \n# ===============================================================\nprint(\"▶ PyTorch:\", torch.__version__)\nIMU_COLS      = ['acc_x','acc_y','acc_z','rot_w','rot_x','rot_y','rot_z']\nMODEL_DIR_IMU = Path('/kaggle/input/01-imu-only/01_imu_only_model')\n\nGESTURE_CLASSES_IMU = np.load(MODEL_DIR_IMU / 'gesture_classes.npy', allow_pickle=True)\nPAD_LEN_IMU         = int(np.load(MODEL_DIR_IMU / 'sequence_maxlen.npy'))\nprint(f\"▶ IMU PAD_LEN: {PAD_LEN_IMU}\")\n\ndef apply_symmetry(df: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"Flip right-hand data so it matches the left-hand coordinate system.\"\"\"\n    df = df.copy()\n    df[['acc_z','acc_y','rot_y','rot_z']] *= -1\n    return df\n\ndef imu_preprocess(df_seq: pd.DataFrame) -> torch.Tensor:\n    \"\"\"Pad IMU-only sequence (plus dummy THM/TOF columns) → tensor shape (1, C, T).\"\"\"\n    ttf_cols  = [c for c in df_seq.columns if c.startswith(('thm_','tof_'))]\n    feat_cols = IMU_COLS + ttf_cols\n    data      = df_seq[feat_cols].ffill().bfill().fillna(0).values\n    padded    = keras_pad_sequences(\n        [data], maxlen=PAD_LEN_IMU,\n        padding='post', truncating='post',\n        dtype='float32'\n    )[0]\n    return torch.from_numpy(padded.T).unsqueeze(0).to(DEVICE)\n\nMODELS_IMU = [\n    torch.jit.load(MODEL_DIR_IMU / f'best_model_fold{i}.pt', map_location=DEVICE).eval()\n    for i in range(1, 6)\n]\nprint(\"▶ IMU ensemble loaded (5 TorchScript models)\")\n\n# ===============================================================\n# Prediction entry-point (called by Kaggle runtime)\n# ===============================================================\n@torch.no_grad()\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Choose ② if THM/TOF data exist, otherwise fall back to ①.\n    Returns a gesture label (string).\n    \"\"\"\n    if has_full_sensors(sequence):\n        # ---------- ② FULL sensors ----------\n        x = full_preprocess(sequence.to_pandas())            # (1, T, C)\n        logits = torch.mean(torch.stack([m(x).softmax(dim=-1) for m in MODELS_FULL]), dim=0)\n        idx = int(logits.argmax(dim=1))\n        return str(GESTURE_CLASSES_FULL[idx])\n\n    else:\n        # ---------- ① IMU-only ----------\n        df_seq  = sequence.to_pandas()\n        df_demo = demographics.to_pandas()\n\n        # Merge handedness info\n        df_seq = df_seq.merge(df_demo, on='subject', how='left', validate='many_to_one')\n\n        # Flip right-hand sequences\n        right = df_seq['handedness'] == 1\n        df_seq.loc[right, IMU_COLS] = apply_symmetry(df_seq.loc[right, IMU_COLS])\n\n        # Preprocess and ensemble average\n        x = imu_preprocess(df_seq)                           # (1, C, T)\n        logits = torch.mean(torch.stack([m(x).softmax(dim=-1) for m in MODELS_IMU]), dim=0)\n        idx = int(logits.argmax(dim=1))\n        return str(GESTURE_CLASSES_IMU[idx])\n\n# ===============================================================\n# Start inference server (Kaggle online)\n# ===============================================================\nserver = cmi.CMIInferenceServer(predict)\n\nRAW_DIR = Path('/kaggle/input/cmi-detect-behavior-with-sensor-data')\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    server.serve()\nelse:\n    server.run_local_gateway(\n        data_paths=(\n            RAW_DIR / 'test.csv',\n            RAW_DIR / 'test_demographics.csv'\n        )\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T07:29:09.051027Z","iopub.execute_input":"2025-07-02T07:29:09.0513Z","iopub.status.idle":"2025-07-02T07:29:50.085413Z","shell.execute_reply.started":"2025-07-02T07:29:09.05127Z","shell.execute_reply":"2025-07-02T07:29:50.084711Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}