{"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":12101487,"sourceType":"datasetVersion","datasetId":7618540},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Thank you to [@salmanahmedtamu](https://www.kaggle.com/salmanahmedtamu)、[@samithsachidanandan](https://www.kaggle.com/samithsachidanandan) and [@kitsuha](https://www.kaggle.com/kitsuha) for providing the baseline. I have added the HF1 validation method to their work, which makes it easier to retain suitable models.","metadata":{}},{"cell_type":"code","source":"# PyTorch Implementation of https://www.kaggle.com/code/samithsachidanandan/lb-0-72-cmi-2025-tensor-flow\n\nimport os, json, joblib, numpy as np, pandas as pd\nfrom pathlib import Path\nimport warnings \nwarnings.filterwarnings(\"ignore\")\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim import Adam\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nimport polars as pl\nfrom tqdm import tqdm\nimport torchmetrics\nfrom cmi_2025_metric_copy_for_import import CompetitionMetric\nimport random\n\n# Configuration\nTRAIN = True                     # ← set to True when you want to train\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi-exp1\")  # used when TRAIN=False\nEXPORT_DIR = Path(\"./\")                                    # artefacts will be saved here\nBATCH_SIZE = 64\nPAD_PERCENTILE = 95\nLR_INIT = 1e-3\nWD = 3e-3\nMIXUP_ALPHA = 0.4\nEPOCHS = 250\nPATIENCE = 40\nSEED = 42\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"▶ imports ready · pytorch {torch.__version__} · device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:40:20.921599Z","iopub.execute_input":"2025-06-09T07:40:20.921950Z","iopub.status.idle":"2025-06-09T07:40:33.970079Z","shell.execute_reply.started":"2025-06-09T07:40:20.921923Z","shell.execute_reply":"2025-06-09T07:40:33.969440Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed=1234):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    \nseed_everything(SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:40:33.971091Z","iopub.execute_input":"2025-06-09T07:40:33.971453Z","iopub.status.idle":"2025-06-09T07:40:33.980692Z","shell.execute_reply.started":"2025-06-09T07:40:33.971433Z","shell.execute_reply":"2025-06-09T07:40:33.979935Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SEBlock(nn.Module):\n    def __init__(self, channels, reduction = 8):\n        super().__init__()\n        self.fc1 = nn.Linear(channels, channels // reduction, bias=True)\n        self.fc2 = nn.Linear(channels // reduction, channels, bias=True)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        # x: (B, C, L)\n        se = F.adaptive_avg_pool1d(x, 1).squeeze(-1)      # -> (B, C)\n        se = F.relu(self.fc1(se), inplace=True)          # -> (B, C//r)\n        se = self.sigmoid(self.fc2(se)).unsqueeze(-1)    # -> (B, C, 1)\n        return x * se                                    # (B, C, L)\n\n\nclass MultiScaleCNNBranchConcat(nn.Module):\n    def __init__(self, in_channels, out_channels = 64):\n        super().__init__()\n        kernel_sizes = [3, 5, 7, 11]\n        self.branches = nn.ModuleList([\n            nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, kernel_size=ks,\n                          padding=ks // 2, bias=False),\n                nn.BatchNorm1d(out_channels),\n                nn.ReLU(inplace=True)\n            )\n            for ks in kernel_sizes\n        ])\n\n    def forward(self, x) :\n        # x: (B, C_in, L)\n        feats = [b(x) for b in self.branches]  #  -> (B, C_out, L)\n        return torch.cat(feats, dim=1)        # (B, C_out * 4, L)\n\n\nclass ResNetSEBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, wd = 1e-4):\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels,\n                               kernel_size=3, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        self.conv2 = nn.Conv1d(out_channels, out_channels,\n                               kernel_size=3, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n        # SE\n        self.se = SEBlock(out_channels)\n        \n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, kernel_size=1,\n                          padding=0, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n        else:\n            self.shortcut = nn.Identity()\n\n        self.relu = nn.ReLU(inplace=True)\n\n    def forward(self, x) :\n        identity = self.shortcut(x)              # (B, out, L)\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out = self.se(out)                       # (B, out, L)\n        out = out + identity\n        return self.relu(out)\n\n\nclass AttentionLayer(nn.Module):\n    def __init__(self, feature_dim):\n        super().__init__()\n        self.score_fn = nn.Linear(feature_dim, 1, bias=True)\n        self.softmax = nn.Softmax(dim=1)\n\n    def forward(self, x):\n        # x: (B, L, F)\n        score = torch.tanh(self.score_fn(x))     # (B, L, 1)\n        weights = self.softmax(score.squeeze(-1))# (B, L)\n        weights = weights.unsqueeze(-1)          # (B, L, 1)\n        context = x * weights                    # (B, L, F)\n        return context.sum(dim=1)                # (B, F)\n\n\nclass TwoBranchModel(nn.Module):\n    def __init__(self,\n                 pad_len,\n                 imu_dim,\n                 tof_dim,\n                 n_classes,\n                 weight_decay = 1e-4):\n        super().__init__()\n        self.tof_dim = tof_dim\n        self.imu_branch = nn.Sequential(\n            MultiScaleCNNBranchConcat(imu_dim, out_channels=64),\n            ResNetSEBlock(256, 128, wd=weight_decay),\n            nn.MaxPool1d(kernel_size=2),\n            nn.Dropout(0.3),\n            nn.MaxPool1d(kernel_size=2)\n        )\n        self.tof_branch = nn.Sequential(\n            nn.Conv1d(tof_dim, 64, kernel_size=3, padding=1, bias=False),\n            nn.BatchNorm1d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool1d(2),\n            nn.Dropout(0.3),\n\n            nn.Conv1d(64, 128, kernel_size=3, padding=1, bias=False),\n            nn.BatchNorm1d(128),\n            nn.ReLU(inplace=True),\n            nn.MaxPool1d(2),\n            nn.Dropout(0.3),\n        )\n\n        self.lstm = nn.LSTM(\n            input_size=128 + 128,\n            hidden_size=128,\n            num_layers=1,\n            batch_first=True,\n            bidirectional=True\n        )\n        self.attn = AttentionLayer(feature_dim=256)  # 2*hidden_size\n\n        self.classifier = nn.Sequential(\n            nn.Linear(256, 256, bias=False),\n            nn.BatchNorm1d(256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n\n            nn.Linear(256, 128, bias=False),\n            nn.BatchNorm1d(128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.3),\n\n            nn.Linear(128, n_classes)  \n        )\n\n    def forward(self, inp):\n        # inp: (B, pad_len, imu_dim+tof_dim)\n        B, L, D = inp.shape\n        x = inp.permute(0, 2, 1)   # -> (B, D, L)\n        imu = x[:, :D - self.tof_dim, :]  # (B, imu_dim, L)\n        tof = x[:, D - self.tof_dim:, :]  # (B, tof_dim, L)\n\n        f1 = self.imu_branch(imu)    # (B, 128, L//4) \n        f2 = self.tof_branch(tof)    # (B, 128, L//4)\n\n        merged = torch.cat([f1, f2], dim=1)  # (B, 256, L//4)\n\n        merged = merged.permute(0, 2, 1)     # (B, L//4, 256)\n        lstm_out, _ = self.lstm(merged)      # (B, L//4, 2*128)\n        attn_out = self.attn(lstm_out)       # (B, 256)\n\n        logits = self.classifier(attn_out)   # (B, n_classes)\n        return logits\n\n\n# ================================\n# Data Handling\n# ================================\n\ndef pad_sequences_torch(sequences, maxlen, padding='post', truncating='post', value=0.0):\n    \"\"\"PyTorch equivalent of Keras pad_sequences\"\"\"\n    result = []\n    for seq in sequences:\n        if len(seq) >= maxlen:\n            if truncating == 'post':\n                seq = seq[:maxlen]\n            else:  # 'pre'\n                seq = seq[-maxlen:]\n        else:\n            pad_len = maxlen - len(seq)\n            if padding == 'post':\n                seq = np.concatenate([seq, np.full((pad_len, seq.shape[1]), value)])\n            else:  # 'pre'\n                seq = np.concatenate([np.full((pad_len, seq.shape[1]), value), seq])\n        result.append(seq)\n    return np.array(result, dtype=np.float32)\n\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list, scaler: StandardScaler):\n    \"\"\"Normalizes and cleans the time series sequence\"\"\"\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(mat).astype('float32')\n\nclass MixupDataset(Dataset):\n    \"\"\"Dataset with MixUp augmentation\"\"\"\n    def __init__(self, X, y, alpha=0.2):\n        self.X = torch.FloatTensor(X)\n        self.y = torch.FloatTensor(y)\n        self.alpha = alpha\n        \n    def __len__(self):\n        return len(self.X)\n    \n    def __getitem__(self, idx):\n        x, y = self.X[idx], self.y[idx]\n        \n        if self.alpha > 0:\n            lam = np.random.beta(self.alpha, self.alpha)\n            rand_idx = np.random.randint(0, len(self.X))\n            x_rand, y_rand = self.X[rand_idx], self.y[rand_idx]\n            \n            x = lam * x + (1 - lam) * x_rand\n            y = lam * y + (1 - lam) * y_rand\n            \n        return x, y\n\nclass EarlyStopping:\n    \"\"\"Early stopping utility\"\"\"\n    def __init__(self, patience=7, min_delta=0, restore_best_weights=True):\n        self.patience = patience\n        self.min_delta = min_delta\n        self.restore_best_weights = restore_best_weights\n        self.best_loss = None\n        self.counter = 0\n        self.best_weights = None\n        \n    def __call__(self, val_loss, model):\n        if self.best_loss is None:\n            self.best_loss = val_loss\n            self.save_checkpoint(model)\n        elif val_loss > self.best_loss - self.min_delta:\n            self.best_loss = val_loss\n            self.counter = 0\n            self.save_checkpoint(model)\n        else:\n            self.counter += 1\n            \n        if self.counter >= self.patience:\n            if self.restore_best_weights:\n                model.load_state_dict(self.best_weights)\n            return True\n        return False\n    \n    def save_checkpoint(self, model):\n        self.best_weights = model.state_dict().copy()\n\ndef label_smoothing_loss(pred, target, smoothing=0.1):\n    \"\"\"Label smoothing loss\"\"\"\n    confidence = 1.0 - smoothing\n    log_probs = F.log_softmax(pred, dim=-1)\n    nll_loss = -log_probs.gather(dim=-1, index=target.unsqueeze(1))\n    nll_loss = nll_loss.squeeze(1)\n    smooth_loss = -log_probs.mean(dim=-1)\n    loss = confidence * nll_loss + smoothing * smooth_loss\n    return loss.mean()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:40:33.981438Z","iopub.execute_input":"2025-06-09T07:40:33.981689Z","iopub.status.idle":"2025-06-09T07:40:34.012091Z","shell.execute_reply.started":"2025-06-09T07:40:33.981663Z","shell.execute_reply":"2025-06-09T07:40:34.011411Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import math\nfrom torch.optim.lr_scheduler import _LRScheduler\n\nclass ConstantCosineLR(_LRScheduler):\n    \"\"\"\n    Constant learning rate followed by CosineAnnealing.\n    \"\"\"\n    def __init__(\n        self, \n        optimizer,\n        total_steps, \n        pct_cosine, \n        last_epoch=-1,\n        ):\n        self.total_steps = total_steps\n        self.milestone = int(total_steps * (1 - pct_cosine))\n        self.cosine_steps = max(total_steps - self.milestone, 1)\n        self.min_lr = 0\n        super().__init__(optimizer, last_epoch)\n\n    def get_lr(self):\n        step = self.last_epoch + 1\n        if step <= self.milestone:\n            factor = 1.0\n        else:\n            s = step - self.milestone\n            factor = 0.5 * (1 + math.cos(math.pi * s / self.cosine_steps))\n        return [lr * factor for lr in self.base_lrs]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:40:34.013331Z","iopub.execute_input":"2025-06-09T07:40:34.013521Z","iopub.status.idle":"2025-06-09T07:40:34.032656Z","shell.execute_reply.started":"2025-06-09T07:40:34.013506Z","shell.execute_reply":"2025-06-09T07:40:34.032005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class 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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:40:34.033285Z","iopub.execute_input":"2025-06-09T07:40:34.033470Z","iopub.status.idle":"2025-06-09T07:40:34.045583Z","shell.execute_reply.started":"2025-06-09T07:40:34.033448Z","shell.execute_reply":"2025-06-09T07:40:34.044903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================\n# Training Pipeline\n# ================================\n\ndef train():\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n\n    # Label encoding\n    le = LabelEncoder()\n    df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n\n    # Feature list\n    meta_cols = {'gesture', 'gesture_int', 'sequence_type', 'behavior', 'orientation',\n                 'row_id', 'subject', 'phase', 'sequence_id', 'sequence_counter'}\n    feature_cols = [c for c in df.columns if c not in meta_cols]\n\n    imu_cols = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n    tof_cols = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n    print(f\"  IMU {len(imu_cols)} | TOF/THM {len(tof_cols)} | total {len(feature_cols)} features\")\n\n    # Global scaler\n    scaler = StandardScaler().fit(df[feature_cols].ffill().bfill().fillna(0).values)\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n\n    # Build sequences\n    seq_gp = df.groupby('sequence_id')\n    X_list, y_list, lens = [], [], []\n    for seq_id, seq in seq_gp:\n        mat = preprocess_sequence(seq, feature_cols, scaler)\n        X_list.append(mat)\n        y_list.append(seq['gesture_int'].iloc[0])\n        lens.append(len(mat))\n    \n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(feature_cols))\n\n    X = pad_sequences_torch(X_list, maxlen=pad_len, padding='post', truncating='post')\n    y = np.eye(len(le.classes_))[y_list].astype(np.float32)  # One-hot encoding\n    print(\"X shape is :\",X.shape)\n    print(\"Y shape is :\",y.shape)\n    # Split\n    X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=SEED, \n                                                stratify=y_list)\n\n    # Class weights\n    cw_vals = compute_class_weight('balanced', classes=np.arange(len(le.classes_)), y=y_list)\n    class_weight = torch.FloatTensor(cw_vals).to(device)\n\n    # Data loaders\n    train_dataset = MixupDataset(X_tr, y_tr, alpha=MIXUP_ALPHA)\n    val_dataset = MixupDataset(X_val, y_val, alpha=0.0)  # No mixup for validation\n    \n    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\n    # Model\n    model = TwoBranchModel(pad_len, len(imu_cols), len(tof_cols), \n                          len(le.classes_), weight_decay=WD).to(device)\n    \n    # Optimizer and scheduler\n    optimizer = Adam(model.parameters(), lr=LR_INIT, weight_decay=WD)\n    steps_per_epoch = len(train_loader)\n    scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=5*steps_per_epoch)\n    # scheduler = ConstantCosineLR(optimizer, total_steps=steps_per_epoch*EPOCHS, pct_cosine=0.3)\n    # Early stopping\n    early_stopping = EarlyStopping(patience=PATIENCE, restore_best_weights=True)\n\n    # Training loop\n    print(\"▶ Starting training...\")\n    for epoch in range(EPOCHS):\n        # Training\n        model.train()\n        train_loss = 0.0\n        train_acc = 0.0\n        \n        for batch_x, batch_y in train_loader:\n            batch_x, batch_y = batch_x.to(device), batch_y.to(device)\n            \n            optimizer.zero_grad()\n            logits = model(batch_x)\n            \n            # Handle mixup targets\n            if len(batch_y.shape) == 2 and batch_y.shape[1] > 1:  # One-hot or mixed\n                loss = -torch.sum(F.log_softmax(logits, dim=1) * batch_y, dim=1).mean()\n                # For accuracy, use argmax of target\n                targets = batch_y.argmax(dim=1)\n            else:\n                targets = batch_y.long()\n                loss = label_smoothing_loss(logits, targets, smoothing=0.1)\n            \n            # Apply class weights\n            if len(batch_y.shape) == 2:\n                sample_weights = torch.sum(batch_y * class_weight.unsqueeze(0), dim=1)\n                loss = (loss * sample_weights).mean()\n            \n            loss.backward()\n            optimizer.step()\n            scheduler.step()\n            \n            train_loss += loss.item()\n            train_acc += (logits.argmax(dim=1) == targets).float().mean().item()\n        \n        # Validation\n        model.eval()\n        val_loss = 0.0\n        val_acc = 0.0\n        val_hf1 = HierarchicalF1()\n        with torch.no_grad():\n            for batch_x, batch_y in val_loader:\n                batch_x, batch_y = batch_x.to(device), batch_y.to(device)\n                logits = model(batch_x)\n                targets = batch_y.argmax(dim=1)\n                loss = F.cross_entropy(logits, targets)\n                val_loss += loss.item()\n                val_acc += (logits.argmax(dim=1) == targets).float().mean().item()\n                val_hf1.update(logits.argmax(1), targets)\n\n        train_loss /= len(train_loader)\n        train_acc /= len(train_loader)\n        val_loss /= len(val_loader)\n        val_acc /= len(val_loader)\n        val_hf1_score = val_hf1.compute()\n        print(f\"Epoch {epoch+1}/{EPOCHS} - \"\n              f\"train_loss: {train_loss:.4f} - train_acc: {train_acc:.4f} - \"\n              f\"val_loss: {val_loss:.4f} - val_acc: {val_acc:.4f} - \"\n              f\"val_hf1: {val_hf1_score:.4f}\"\n        )\n        \n        # Early stopping\n        if early_stopping(val_hf1_score, model):\n            print(f\"Early stopping at epoch {epoch+1}\")\n            break\n    \n    # Save model\n    torch.save({\n        'model_state_dict': model.state_dict(),\n        'imu_dim': len(imu_cols),\n        'tof_dim': len(tof_cols),\n        'n_classes': len(le.classes_),\n        'pad_len': pad_len\n    }, EXPORT_DIR / \"gesture_two_branch_mixup.pth\")\n    \n    print(\"✔ Training done – artefacts saved in\", EXPORT_DIR)\n\n    # Quick metric (if competition metric is available)\n    try:\n        checkpoint = torch.load(EXPORT_DIR / \"gesture_two_branch_mixup.pth\", map_location=device)\n        model.load_state_dict(checkpoint['model_state_dict'])\n        model.eval()\n        with torch.no_grad():\n            val_preds = []\n            val_targets = []\n            for batch_x, batch_y in val_loader:\n                batch_x = batch_x.to(device)\n                logits = model(batch_x)\n                val_preds.extend(logits.argmax(dim=1).cpu().numpy())\n                val_targets.extend(batch_y.argmax(dim=1).numpy())\n        \n        h_f1 = CompetitionMetric().calculate_hierarchical_f1(\n            pd.DataFrame({'gesture': le.classes_[val_targets]}),\n            pd.DataFrame({'gesture': le.classes_[val_preds]}))\n        print(\"Hold‑out H‑F1 =\", round(h_f1, 4))\n    except ImportError:\n        print(\"Competition metric not available\")\n    \n\nif not TRAIN:\n    print(\"▶ INFERENCE MODE – loading artefacts from\", PRETRAINED_DIR)\n    feature_cols = np.load(PRETRAINED_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n    pad_len = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\n    scaler = joblib.load(PRETRAINED_DIR / \"scaler.pkl\")\n    gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n\n    imu_cols = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n    tof_cols = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n\n    # Load model\n    checkpoint = torch.load(PRETRAINED_DIR / \"gesture_two_branch_mixup.pth\", map_location=device)\n    model = TwoBranchModel(\n        checkpoint['pad_len'], \n        checkpoint['imu_dim'], \n        checkpoint['tof_dim'], \n        checkpoint['n_classes']\n    ).to(device)\n    model.load_state_dict(checkpoint['model_state_dict'])\n    model.eval()\n    print(\"  model, scaler, pads loaded – ready for evaluation\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:40:34.046382Z","iopub.execute_input":"2025-06-09T07:40:34.046539Z","iopub.status.idle":"2025-06-09T07:40:34.067388Z","shell.execute_reply.started":"2025-06-09T07:40:34.046527Z","shell.execute_reply":"2025-06-09T07:40:34.066650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Make sure gesture_classes exists in both modes\ncount = 0\ngesture_classes = None\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"Prediction function for Kaggle competition\"\"\"\n    global gesture_classes, count, feature_cols, scaler, pad_len, model\n    \n    if count == 0 and TRAIN:\n        train()\n        count += 1\n        checkpoint = torch.load(EXPORT_DIR / \"gesture_two_branch_mixup.pth\", map_location=device)\n        model = TwoBranchModel(\n            checkpoint['pad_len'], \n            checkpoint['imu_dim'], \n            checkpoint['tof_dim'], \n            checkpoint['n_classes']\n        ).to(device)\n        model.load_state_dict(checkpoint['model_state_dict'])\n        model.eval()\n        feature_cols = np.load(EXPORT_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n        pad_len = int(np.load(EXPORT_DIR / \"sequence_maxlen.npy\"))\n        scaler = joblib.load(EXPORT_DIR / \"scaler.pkl\")\n        gesture_classes = np.load(EXPORT_DIR / \"gesture_classes.npy\", allow_pickle=True)\n        imu_cols = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n        tof_cols = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n    \n    if gesture_classes is None:\n        try:\n            gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n        except:\n            gesture_classes = np.load(EXPORT_DIR / \"gesture_classes.npy\", allow_pickle=True)\n    \n    df_seq = sequence.to_pandas()\n    mat = preprocess_sequence(df_seq, feature_cols, scaler)\n    pad = pad_sequences_torch([mat], maxlen=pad_len, padding='post', truncating='post')\n    \n    model.eval()\n    with torch.no_grad():\n        x = torch.FloatTensor(pad).to(device)\n        logits = model(x)\n        idx = int(logits.argmax(dim=1)[0].cpu().numpy())\n    \n    return str(gesture_classes[idx])\n\n# Kaggle competition interface\nimport kaggle_evaluation.cmi_inference_server\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    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T07:42:22.049840Z","iopub.execute_input":"2025-06-09T07:42:22.050128Z","iopub.status.idle":"2025-06-09T07:43:29.201879Z","shell.execute_reply.started":"2025-06-09T07:42:22.050105Z","shell.execute_reply":"2025-06-09T07:43:29.201221Z"}},"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}]}