{"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,"sourceType":"competition"},{"sourceId":242561727,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, random, math, time, warnings\nimport numpy as np\nimport pandas as pd\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.model_selection import train_test_split\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport pytorch_lightning as pl\nfrom pytorch_lightning.callbacks import EarlyStopping, ModelCheckpoint, LearningRateMonitor","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:04:55.342934Z","iopub.execute_input":"2025-07-13T14:04:55.343189Z","iopub.status.idle":"2025-07-13T14:05:10.368054Z","shell.execute_reply.started":"2025-07-13T14:04:55.343164Z","shell.execute_reply":"2025-07-13T14:05:10.367292Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Preparation","metadata":{}},{"cell_type":"code","source":"SEED = 42\npl.seed_everything(SEED, workers=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:05:10.369653Z","iopub.execute_input":"2025-07-13T14:05:10.370025Z","iopub.status.idle":"2025-07-13T14:05:10.379845Z","shell.execute_reply.started":"2025-07-13T14:05:10.370008Z","shell.execute_reply":"2025-07-13T14:05:10.379025Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"KAGGLE_ROOT = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\nTRAIN_CSV   = os.path.join(KAGGLE_ROOT, \"train.csv\")\nTEST_CSV    = os.path.join(KAGGLE_ROOT, \"test.csv\")\nTEST_DEMO   = os.path.join(KAGGLE_ROOT, \"test_demographics.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:05:10.380720Z","iopub.execute_input":"2025-07-13T14:05:10.381058Z","iopub.status.idle":"2025-07-13T14:05:10.406015Z","shell.execute_reply.started":"2025-07-13T14:05:10.381036Z","shell.execute_reply":"2025-07-13T14:05:10.405335Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load & preprocess train val datasets","metadata":{}},{"cell_type":"code","source":"print(\"Loading train.csv …\")\ndf = pd.read_csv(TRAIN_CSV)\nprint(f\"Loaded {len(df):,} rows.\")\n\nle = LabelEncoder()\ndf[\"gesture\"] = le.fit_transform(df[\"gesture\"].astype(str))\nnp.save(\"gesture_classes.npy\", le.classes_)\n\nimu_cols = [\"acc_x\", \"acc_y\", \"acc_z\", \"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\n\n# preprocess one sequence\ndef preprocess_sequence(seq_df: pd.DataFrame) -> np.ndarray:\n    data = seq_df[imu_cols].copy()\n    data = data.ffill().bfill().fillna(0)\n    data = StandardScaler().fit_transform(data)\n    return data.astype(\"float32\")\n\n# build sequences\nprint(\"Building padded sequences …\")\nseq_groups = df.groupby(\"sequence_id\")\nX_list, y_list, lengths = [], [], []\nfor seq_id, seq_df in seq_groups:\n    X_seq = preprocess_sequence(seq_df)\n    X_list.append(X_seq)\n    lengths.append(X_seq.shape[0])\n    y_list.append(seq_df[\"gesture\"].iloc[0])  # one label per sequence\n\nPAD_LEN = int(np.percentile(lengths, 90))\nprint(f\"Pad/trunc length = {PAD_LEN}\")\nnp.save(\"sequence_maxlen.npy\", PAD_LEN)\n\n\ndef pad_to_len(arr: np.ndarray, maxlen: int, dtype=\"float32\") -> np.ndarray:\n    \"\"\"Pad or truncate a (T, C) array along time axis to exactly maxlen.\"\"\"\n    t, c = arr.shape\n    if t >= maxlen:\n        return arr[:maxlen].astype(dtype, copy=False)\n    out = np.zeros((maxlen, c), dtype=dtype)\n    out[:t] = arr\n    return out\n\nX = np.stack([pad_to_len(a, PAD_LEN) for a in X_list])  # shape (N, L, C)\ny = np.array(y_list, dtype=\"int64\")\nNUM_CLASSES = len(le.classes_)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:05:10.406735Z","iopub.execute_input":"2025-07-13T14:05:10.407062Z","iopub.status.idle":"2025-07-13T14:06:00.020463Z","shell.execute_reply.started":"2025-07-13T14:05:10.407042Z","shell.execute_reply":"2025-07-13T14:06:00.019663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# split\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=SEED, stratify=y)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:06:00.021263Z","iopub.execute_input":"2025-07-13T14:06:00.021535Z","iopub.status.idle":"2025-07-13T14:06:00.035854Z","shell.execute_reply.started":"2025-07-13T14:06:00.021511Z","shell.execute_reply":"2025-07-13T14:06:00.035140Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n# Dataset & DataModule","metadata":{}},{"cell_type":"code","source":"\nclass GestureDataset(Dataset):\n    def __init__(self, X, y):\n        self.X = X; self.y = y\n    def __len__(self):\n        return len(self.X)\n    def __getitem__(self, idx):\n        x = torch.from_numpy(self.X[idx]).transpose(0,1)  # (C, L)\n        y = torch.tensor(self.y[idx], dtype=torch.long)\n        return x, y\n\nclass GestureDataModule(pl.LightningDataModule):\n    def __init__(self, X_train, y_train, X_val, y_val, batch_size=128):\n        super().__init__()\n        self.X_train, self.y_train = X_train, y_train\n        self.X_val,   self.y_val   = X_val,   y_val\n        self.batch_size = batch_size\n    def setup(self, stage=None):\n        self.train_ds = GestureDataset(self.X_train, self.y_train)\n        self.val_ds   = GestureDataset(self.X_val,   self.y_val)\n    def train_dataloader(self):\n        return DataLoader(self.train_ds, batch_size=self.batch_size, shuffle=True, num_workers=4, pin_memory=True)\n    def val_dataloader(self):\n        return DataLoader(self.val_ds, batch_size=self.batch_size, shuffle=False, num_workers=4, pin_memory=True)\n\ndata_module = GestureDataModule(X_train, y_train, X_val, y_val)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:06:00.036635Z","iopub.execute_input":"2025-07-13T14:06:00.037245Z","iopub.status.idle":"2025-07-13T14:06:00.044127Z","shell.execute_reply.started":"2025-07-13T14:06:00.037222Z","shell.execute_reply":"2025-07-13T14:06:00.043315Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Temporal Convolutional Network(TCN) definition","metadata":{}},{"cell_type":"code","source":"\nclass ResidualBlock(nn.Module):\n    def __init__(self, in_ch, out_ch, kernel_size, dilation, dropout):\n        super().__init__()\n        pad = (kernel_size - 1) * dilation\n        self.conv1 = nn.Conv1d(in_ch, out_ch, kernel_size, padding=pad, dilation=dilation)\n        self.bn1   = nn.BatchNorm1d(out_ch)\n        self.conv2 = nn.Conv1d(out_ch, out_ch, kernel_size, padding=pad, dilation=dilation)\n        self.bn2   = nn.BatchNorm1d(out_ch)\n        self.dropout = nn.Dropout(dropout)\n        self.downsample = nn.Conv1d(in_ch, out_ch, 1) if in_ch != out_ch else nn.Identity()\n        self.crop = pad * 2\n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.dropout(out)\n        out = self.bn2(self.conv2(out))\n        out = self.dropout(out)\n        res = self.downsample(x)\n        out = out[:, :, :-self.crop]  # causal crop\n        res = res[:, :, -out.shape[2]:]\n        return F.relu(out + res)\n\nclass TCN(nn.Module):\n    def __init__(self, n_feats, n_classes, channels=[128, 128, 256, 256], k_size=5, dropout=0.2):\n        super().__init__()\n        layers = []\n        in_ch = n_feats\n        for i, out_ch in enumerate(channels):\n            layers.append(ResidualBlock(in_ch, out_ch, k_size, dilation=2**i, dropout=dropout))\n            in_ch = out_ch\n        self.tcn = nn.Sequential(*layers)\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        self.head = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(in_ch, 256),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(256, n_classes)\n        )\n    def forward(self, x):  # x: (B, C, L)\n        x = self.tcn(x)\n        x = self.pool(x)\n        return self.head(x)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:06:00.045941Z","iopub.execute_input":"2025-07-13T14:06:00.046141Z","iopub.status.idle":"2025-07-13T14:06:00.065373Z","shell.execute_reply.started":"2025-07-13T14:06:00.046126Z","shell.execute_reply":"2025-07-13T14:06:00.064762Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Metric","metadata":{}},{"cell_type":"code","source":"\nimport torchmetrics\nfrom metric import CompetitionMetric\n\nclass HierarchicalF1(torchmetrics.Metric):\n    def __init__(self):\n        super().__init__(dist_sync_on_step=False)\n        self.add_state(\"y_true\", default=[], dist_reduce_fx=None)\n        self.add_state(\"y_pred\", default=[], dist_reduce_fx=None)\n        self.metric = CompetitionMetric()\n\n    def update(self, preds, targets):\n        self.y_pred.extend(preds.detach().cpu().tolist())\n        self.y_true.extend(targets.detach().cpu().tolist())\n\n    def compute(self):\n        import pandas as pd\n        sol = pd.DataFrame({\"idx\": range(len(self.y_true)),\n                            \"gesture\": [self.metric.all_classes[i] for i in self.y_true]})\n        sub = pd.DataFrame({\"idx\": range(len(self.y_pred)),\n                            \"gesture\": [self.metric.all_classes[i] for i in self.y_pred]})\n        return self.metric.calculate_hierarchical_f1(sol, sub)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:06:00.065995Z","iopub.execute_input":"2025-07-13T14:06:00.066200Z","iopub.status.idle":"2025-07-13T14:06:00.092132Z","shell.execute_reply.started":"2025-07-13T14:06:00.066184Z","shell.execute_reply":"2025-07-13T14:06:00.091550Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LightningModule","metadata":{}},{"cell_type":"code","source":"class GestureLitModule(pl.LightningModule):\n    def __init__(\n        self,\n        n_feats: int,\n        n_classes: int,\n        lr: float = 1e-3,\n        label_smoothing: float = 0.02,\n    ):\n        super().__init__()\n        self.save_hyperparameters()\n\n        self.model = TCN(n_feats, n_classes, channels=[128, 256, 256], k_size=3, dropout=0.2)\n\n        self.criterion = nn.CrossEntropyLoss(label_smoothing=label_smoothing)\n\n        self.train_acc = torchmetrics.classification.Accuracy(\n            task=\"multiclass\", num_classes=n_classes\n        )\n        self.val_acc  = torchmetrics.classification.Accuracy(\n            task=\"multiclass\", num_classes=n_classes\n        )\n        self.val_hf1  = HierarchicalF1()\n\n    def forward(self, x):\n        return self.model(x)\n\n    def _step(self, batch, stage: str):\n        x, y = batch                           \n        logits = self(x)\n        loss = self.criterion(logits, y)\n\n        probs = F.softmax(logits, dim=-1)\n\n        if stage == \"train\":\n            self.train_acc.update(probs, y)\n        else:\n            self.val_acc.update(probs, y)\n            self.val_hf1.update(logits.argmax(1), y)\n\n        self.log(f\"{stage}_loss\", loss,\n                 on_step=False, on_epoch=True, batch_size=x.size(0))\n        return loss\n\n    def training_step(self, batch, _):\n        return self._step(batch, \"train\")\n\n    def validation_step(self, batch, _):\n        self._step(batch, \"val\")\n\n    def on_train_epoch_end(self):\n        self.log(\"train_acc\", self.train_acc.compute(), prog_bar=True)\n        self.train_acc.reset()\n\n    def on_validation_epoch_end(self):\n        self.log(\"val_acc\",  self.val_acc.compute(),  prog_bar=True)\n        self.log(\"val_hf1\",  self.val_hf1.compute(),  prog_bar=True)\n        self.val_acc.reset()\n        self.val_hf1.reset()\n\n    def configure_optimizers(self):\n        opt = torch.optim.Adam(self.parameters(), lr=self.hparams.lr)\n        sched = torch.optim.lr_scheduler.ReduceLROnPlateau(\n            opt, mode=\"max\", factor=0.5, patience=4, min_lr=1e-6\n        )\n        return {\n            \"optimizer\": opt,\n            \"lr_scheduler\": {\n                \"scheduler\": sched,\n                \"monitor\": \"val_hf1\",\n            },\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:06:00.092753Z","iopub.execute_input":"2025-07-13T14:06:00.093275Z","iopub.status.idle":"2025-07-13T14:06:00.102285Z","shell.execute_reply.started":"2025-07-13T14:06:00.093259Z","shell.execute_reply":"2025-07-13T14:06:00.101681Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training with Trainer & Check CV","metadata":{}},{"cell_type":"code","source":"checkpt = ModelCheckpoint(\n    monitor=\"val_hf1\", mode=\"max\", save_top_k=1, filename=\"best\")\ncallbacks = [checkpt,\n             EarlyStopping(monitor=\"val_hf1\", mode=\"max\", patience=15),\n             LearningRateMonitor(\"epoch\")]\n\ntrainer = pl.Trainer(\n    max_epochs=100,\n    accelerator=\"auto\",\n    precision=\"16-mixed\" if torch.cuda.is_available() else 32,\n    callbacks=callbacks,\n    log_every_n_steps=50,\n)\n\nlit_model = GestureLitModule(n_feats=X.shape[2], n_classes=NUM_CLASSES)\ntrainer.fit(lit_model, data_module)\nprint(\"Best ckpt:\", checkpt.best_model_path)\n\n# save state_dict for inference (lighter than Lightning ckpt)\nbest_ckpt = torch.load(checkpt.best_model_path, map_location=\"cpu\")\nstate_dict = {k.replace(\"model.\",\"\"):v for k,v in best_ckpt[\"state_dict\"].items()}  # strip prefix\ntorch.save(state_dict, \"gesture_tcn_pt.pth\")\nprint(\"Saved gesture_tcn_pt.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:06:00.102896Z","iopub.execute_input":"2025-07-13T14:06:00.103102Z","iopub.status.idle":"2025-07-13T14:08:14.022789Z","shell.execute_reply.started":"2025-07-13T14:06:00.103088Z","shell.execute_reply":"2025-07-13T14:08:14.021928Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef compute_val_hf1(\n    best_ckpt_path: str,\n    data_module: pl.LightningDataModule,\n    n_feats: int,\n    n_classes: int,\n    device: torch.device | str | None = None,\n) -> float:\n\n    if device is None:\n        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    elif isinstance(device, str):\n        device = torch.device(device)\n\n    best_model = GestureLitModule.load_from_checkpoint(\n        best_ckpt_path,\n        n_feats=n_feats,\n        n_classes=n_classes,\n    ).to(device)\n    best_model.eval()\n\n    hf1_metric = HierarchicalF1().to(device)\n\n    with torch.no_grad():\n        for x, y in data_module.val_dataloader():\n            x, y = x.to(device), y.to(device)\n            logits = best_model(x)\n            hf1_metric.update(logits.argmax(1), y)\n\n    final_hf1: float = hf1_metric.compute()\n    return final_hf1\n\nbest_ckpt_path = checkpt.best_model_path  \nhf1_val = compute_val_hf1(\n    best_ckpt_path=best_ckpt_path,\n    data_module=data_module,\n    n_feats=X.shape[2],\n    n_classes=NUM_CLASSES,\n)\n\nprint(f\"Hierarchical F1 (validation, best ckpt): {hf1_val:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:08:14.023938Z","iopub.execute_input":"2025-07-13T14:08:14.024237Z","iopub.status.idle":"2025-07-13T14:08:14.728035Z","shell.execute_reply.started":"2025-07-13T14:08:14.024205Z","shell.execute_reply":"2025-07-13T14:08:14.727119Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference function","metadata":{}},{"cell_type":"code","source":"\nimport polars as pl\n\ngesture_classes = np.load(\"gesture_classes.npy\", allow_pickle=True)\nMAXLEN = int(np.load(\"sequence_maxlen.npy\"))\n_infer_model = TCN(n_feats=X.shape[2], n_classes=NUM_CLASSES, channels=[128, 256, 256], k_size=3, dropout=0.2)\n_infer_model.load_state_dict(torch.load(\"gesture_tcn_pt.pth\", map_location=\"cpu\"))\n_infer_model.eval()\n\n@torch.no_grad()\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    arr = preprocess_sequence(sequence.to_pandas())\n    arr = pad_to_len(arr, MAXLEN)\n    x = torch.from_numpy(arr).transpose(0,1).unsqueeze(0)\n    logits = _infer_model(x)\n    idx = logits.argmax(1).item()\n    return str(gesture_classes[idx])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:08:14.729204Z","iopub.execute_input":"2025-07-13T14:08:14.729480Z","iopub.status.idle":"2025-07-13T14:08:15.171750Z","shell.execute_reply.started":"2025-07-13T14:08:14.729454Z","shell.execute_reply":"2025-07-13T14:08:15.171160Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# API Submission ","metadata":{}},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\n\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:08:15.172584Z","iopub.execute_input":"2025-07-13T14:08:15.173134Z","iopub.status.idle":"2025-07-13T14:08:16.720373Z","shell.execute_reply.started":"2025-07-13T14:08:15.173109Z","shell.execute_reply":"2025-07-13T14:08:16.719704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Manual test (only runs outside Kaggle gateway) (credit: https://www.kaggle.com/code/richolson/cmi-2025-1d-cnn-imu-only-baseline)\nif not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print(\"\\nRunning manual test...\")\n    test_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv')\n    sample_seq_id = test_df['sequence_id'].unique()[0]\n    test_seq = test_df[test_df['sequence_id'] == sample_seq_id]\n    prediction = predict(pl.DataFrame(test_seq), None)\n    print(f\"Manual prediction result for sequence_id {sample_seq_id}: {prediction}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-13T14:08:16.721122Z","iopub.execute_input":"2025-07-13T14:08:16.721466Z","iopub.status.idle":"2025-07-13T14:08:16.772915Z","shell.execute_reply.started":"2025-07-13T14:08:16.721447Z","shell.execute_reply":"2025-07-13T14:08:16.772258Z"}},"outputs":[],"execution_count":null}]}