{"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.11.11"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":514.400872,"end_time":"2025-06-14T09:53:36.250389","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-06-14T09:45:01.849517","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"1c5255e3","cell_type":"markdown","source":"# **Imports**","metadata":{"papermill":{"duration":0.003884,"end_time":"2025-06-14T09:45:05.931312","exception":false,"start_time":"2025-06-14T09:45:05.927428","status":"completed"},"tags":[]}},{"id":"7511cc3e","cell_type":"code","source":"import random\nimport numpy as np\nimport torch\nimport os\n\ndef seed_everything(seed=42):\n    \"\"\"Set all random seeds for reproducibility\"\"\"\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'\n    torch.use_deterministic_algorithms(True, warn_only=True)\n\nSEED = 42\nseed_everything(seed=SEED)\n\nimport pandas as pd\nimport polars as pl\nfrom sklearn.metrics import f1_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.utils.class_weight import compute_class_weight\nimport joblib\nfrom tqdm import tqdm\n\nfrom torch import nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences as keras_pad_sequences\n\nimport kaggle_evaluation.cmi_inference_server\nfrom matplotlib import pyplot as plt","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:45:05.938504Z","iopub.status.busy":"2025-06-14T09:45:05.938222Z","iopub.status.idle":"2025-06-14T09:45:26.344148Z","shell.execute_reply":"2025-06-14T09:45:26.343381Z"},"papermill":{"duration":20.411251,"end_time":"2025-06-14T09:45:26.345700","exception":false,"start_time":"2025-06-14T09:45:05.934449","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"4f0a0744","cell_type":"markdown","source":"# **Read data**","metadata":{"papermill":{"duration":0.002878,"end_time":"2025-06-14T09:45:26.352143","exception":false,"start_time":"2025-06-14T09:45:26.349265","status":"completed"},"tags":[]}},{"id":"4481aeff","cell_type":"code","source":"print(\"Loading datasets...\")\ntrain_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntrain_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\ntest_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")\ntest_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")\nprint(f\"Train rows: {len(train_df)}, Test rows: {len(test_df)}\")\n\n# Encode labels\nlabel_encoder = LabelEncoder()\ntrain_df['gesture'] = label_encoder.fit_transform(train_df['gesture'].astype(str))\ngesture_classes = label_encoder.classes_\n\nbfrb_gestures = [\n    'Above ear - pull hair',\n    'Forehead - pull hairline',\n    'Forehead - scratch',\n    'Eyebrow - pull hair',\n    'Eyelash - pull hair',\n    'Neck - pinch skin',\n    'Neck - scratch',\n    'Cheek - pinch skin'\n]\nbfrb_indices = label_encoder.transform(bfrb_gestures)\n\nimu_cols = ['acc_x', 'acc_y', 'acc_z', 'rot_w', 'rot_x', 'rot_y', 'rot_z']\ntof_thm_cols = [c for c in train_df.columns if c.startswith('thm_') or c.startswith('tof_')]\n\n# Reorder so that IMU features come first\nfeature_cols = imu_cols + tof_thm_cols\nimu_dim = len(imu_cols)\ntof_thm_dim = len(tof_thm_cols)\n\nprint(f\"IMU features: {imu_dim}, TOF/Thermal features: {tof_thm_dim}, Total features: {len(feature_cols)}\")\n\n# Check for missing values\nnan_counts = train_df[feature_cols].isna().sum().sum()\nprint(\"Total NaNs in train features:\", nan_counts)\n\n\n# to remove hand dependency in IMU data\n# im not sure if the rotation is on the x axis but this give me the best CV\ndef apply_symmetry(data):\n    transformed = data.copy()\n    transformed['acc_z'] = -transformed['acc_z']\n    transformed['acc_y'] = -transformed['acc_y']\n    \n    transformed['rot_w'] = transformed['rot_w']\n    transformed['rot_x'] = transformed['rot_x']\n    transformed['rot_y'] = -transformed['rot_y']\n    transformed['rot_z'] = -transformed['rot_z']\n    return transformed\n\n\ntrain_df = train_df.merge(\n    train_dem_df,\n    on='subject',\n    how='left',\n    validate='many_to_one'\n)\n\nright_handed_mask = train_df['handedness'] == 1\ntrain_df.loc[right_handed_mask, imu_cols] = apply_symmetry(train_df.loc[right_handed_mask, imu_cols])","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:45:26.359159Z","iopub.status.busy":"2025-06-14T09:45:26.358723Z","iopub.status.idle":"2025-06-14T09:45:58.432585Z","shell.execute_reply":"2025-06-14T09:45:58.431977Z"},"papermill":{"duration":32.078977,"end_time":"2025-06-14T09:45:58.434039","exception":false,"start_time":"2025-06-14T09:45:26.355062","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"14344d2c","cell_type":"markdown","source":"# Create dataset","metadata":{"papermill":{"duration":0.003008,"end_time":"2025-06-14T09:45:58.440588","exception":false,"start_time":"2025-06-14T09:45:58.437580","status":"completed"},"tags":[]}},{"id":"992262fd","cell_type":"code","source":"sequences = train_df.groupby('sequence_id')\nX_list = []\nlengths = []\ny_list = []\n\nsequence_info = []\nfor i, (seq_id, seq) in enumerate(sequences):\n    seq_data = seq[feature_cols].ffill().bfill().fillna(0).values\n    X_list.append(seq_data)\n    lengths.append(seq_data.shape[0])\n    sequence_info.append({\n        'sequence_id': seq_id,\n        'subject': seq['subject'].iloc[0],\n        'gesture': seq['gesture'].iloc[0]\n    })\n\npad_len = int(np.percentile(lengths, 90))\nprint(f\"Pad/truncate all sequences to length {pad_len} (90th percentile).\")\n\nseq_df = pd.DataFrame(sequence_info)\nX_array = keras_pad_sequences(\n    X_list,\n    maxlen=pad_len,\n    dtype='float32',\n    padding='post',\n    truncating='post'\n)  # shape: (n_samples, pad_len, total_features)\n\ny_array = seq_df['gesture'].values  # shape: (n_samples,)\n\nnum_classes = len(np.unique(y_array))\ny_array = np.eye(num_classes)[y_array]  # shape: (n_samples, num_classes)\n\n# Transpose to (n_samples, features, seq_len) for PyTorch\nX_array = np.transpose(X_array, (0, 2, 1))\n\n\nclass SequenceDataset(Dataset):\n    def __init__(self, X, y=None):\n        \"\"\"\n        X: np.ndarray of shape (n_samples, features, seq_len)\n        y: np.ndarray of shape (n_samples, num_classes) or None for test\n        \"\"\"\n        self.X = torch.from_numpy(X).float()\n        self.y = torch.from_numpy(y).float() if y is not None else None\n\n    def __len__(self):\n        return self.X.size(0)\n\n    def __getitem__(self, idx):\n        if self.y is not None:\n            return self.X[idx], self.y[idx]\n        else:\n            return self.X[idx]","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:45:58.447812Z","iopub.status.busy":"2025-06-14T09:45:58.447576Z","iopub.status.idle":"2025-06-14T09:46:09.108155Z","shell.execute_reply":"2025-06-14T09:46:09.107585Z"},"papermill":{"duration":10.6658,"end_time":"2025-06-14T09:46:09.109534","exception":false,"start_time":"2025-06-14T09:45:58.443734","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"63f288aa","cell_type":"markdown","source":"# **Model**","metadata":{"papermill":{"duration":0.003078,"end_time":"2025-06-14T09:46:09.116145","exception":false,"start_time":"2025-06-14T09:46:09.113067","status":"completed"},"tags":[]}},{"id":"389ddca3","cell_type":"code","source":"class SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super(SEBlock, self).__init__()\n        self.fc1 = nn.Linear(channels, channels // reduction, bias=True)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc2 = nn.Linear(channels // reduction, channels, bias=True)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        # x: (batch, channels, seq_len)\n        # Squeeze: global average pooling over time dimension\n        se = x.mean(dim=2)                      # (batch, channels)\n        se = self.relu(self.fc1(se))            # (batch, channels//reduction)\n        se = self.sigmoid(self.fc2(se))         # (batch, channels)\n        se = se.unsqueeze(2)                    # (batch, channels, 1)\n        return x * se                           # scale channels\n\nclass ResidualSEBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size=3, pool_size=2, dropout_rate=0.3):\n        super(ResidualSEBlock, self).__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        self.relu = nn.ReLU(inplace=True)\n\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n\n        self.se = SEBlock(out_channels, reduction=8)\n\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n        else:\n            self.shortcut = nn.Identity()\n\n        self.pool = nn.MaxPool1d(kernel_size=pool_size)\n        self.dropout = nn.Dropout(dropout_rate)\n\n    def forward(self, x):\n        # x: (batch, in_channels, seq_len)\n        shortcut = self.shortcut(x)                                 # (batch, out_channels, seq_len)\n        out = self.conv1(x)                                          # (batch, out_channels, seq_len)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)                                        # (batch, out_channels, seq_len)\n        out = self.bn2(out)\n\n        out = self.se(out)                                           # SE scaling\n\n        out = out + shortcut                                         # skip connection\n        out = self.relu(out)\n\n        out = self.pool(out)                                         # (batch, out_channels, seq_len//pool_size)\n        out = self.dropout(out)\n        return out\n\nclass Attention(nn.Module):\n    def __init__(self, input_dim):\n        super(Attention, self).__init__()\n        self.score_fc = nn.Linear(input_dim, 1)\n\n    def forward(self, x):\n        # x: (batch, seq_len, features)\n        scores = torch.tanh(self.score_fc(x))            # (batch, seq_len, 1)\n        scores = scores.squeeze(2)                       # (batch, seq_len)\n        weights = F.softmax(scores, dim=1)               # (batch, seq_len)\n        weights = weights.unsqueeze(2)                   # (batch, seq_len, 1)\n        weighted = x * weights                           # (batch, seq_len, features)\n        context = weighted.sum(dim=1)                    # (batch, features)\n        return context\n        \nclass IMU_HARModel(nn.Module):\n    def __init__(self, total_features, imu_dim, pad_len, num_classes):\n        super(IMU_HARModel, self).__init__()\n        # IMU branch\n        self.resblock1 = ResidualSEBlock(imu_dim, 64, kernel_size=3, pool_size=2, dropout_rate=0.1)\n        self.resblock2 = ResidualSEBlock(64, 128, kernel_size=3, pool_size=2, dropout_rate=0.1)\n\n        # After pooling twice, seq_len reduced by factor of 4\n        reduced_len = pad_len // 4\n        merged_channels = 128 #+ 128  # from IMU and TTF here we use IMU only\n\n        self.bigru = nn.GRU(\n            input_size=merged_channels,\n            hidden_size=merged_channels//2,\n            num_layers=2,\n            batch_first=True,\n            bidirectional=True,\n            dropout=0.1,\n        )\n\n        # Attention\n        self.attention = Attention(input_dim=merged_channels)\n\n        # Dense head\n        self.fc1 = nn.Linear(merged_channels, 128, bias=True)\n        self.bn_fc1 = nn.BatchNorm1d(128)\n        self.drop_fc1 = nn.Dropout(0.1)\n\n        self.fc2 = nn.Linear(128, 64, bias=True)\n        self.bn_fc2 = nn.BatchNorm1d(64)\n        self.drop_fc2 = nn.Dropout(0.1)\n\n        self.out = nn.Linear(64, num_classes)\n\n    def forward(self, x):\n        # x: (batch, total_features, seq_len)\n        x_imu = x[:, :imu_dim, :]           # (batch, imu_dim, seq_len)\n        x_ttf = x[:, imu_dim:, :]           # (batch, rest_dim, seq_len)\n\n        # IMU branch\n        b1 = self.resblock1(x_imu)          # (batch, 64, seq_len/2)\n        b1 = self.resblock2(b1)             # (batch, 128, seq_len/4)\n\n        # b2 is reserved for tof branch when will be using it\n\n        # Concatenate branches along channel dimension\n        merged = b1 # torch.cat([b1, b2], dim=1)  # (batch, 256, seq_len/4)\n\n        # Prepare for GRU: (batch, seq_len/4, 256)\n        merged = merged.permute(0, 2, 1)\n\n        # BiGRU\n        lstm_out, _ = self.bigru(merged)       # (batch, seq_len/4, 256)\n\n        # Attention\n        context = self.attention(lstm_out)    # (batch, 256)\n\n        # Dense head\n        x = self.fc1(context)                 # (batch, 256)\n        x = self.bn_fc1(x)\n        x = F.relu(x)\n        x = self.drop_fc1(x)\n\n        x = self.fc2(x)                       # (batch, 128)\n        x = self.bn_fc2(x)\n        x = F.relu(x)\n        x = self.drop_fc2(x)\n\n        out = self.out(x)                     # (batch, num_classes)\n        return out\n\n# Device setup\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:46:09.123975Z","iopub.status.busy":"2025-06-14T09:46:09.123721Z","iopub.status.idle":"2025-06-14T09:46:09.201938Z","shell.execute_reply":"2025-06-14T09:46:09.201140Z"},"papermill":{"duration":0.083553,"end_time":"2025-06-14T09:46:09.202903","exception":false,"start_time":"2025-06-14T09:46:09.119350","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f0b81519","cell_type":"markdown","source":"# **Utils**","metadata":{"papermill":{"duration":0.003115,"end_time":"2025-06-14T09:46:09.209456","exception":false,"start_time":"2025-06-14T09:46:09.206341","status":"completed"},"tags":[]}},{"id":"dfc390a7","cell_type":"code","source":"def soft_cross_entropy(pred, soft_targets):\n    \"\"\"\n    pred: (batch, num_classes) raw scores (no softmax)\n    soft_targets: (batch, num_classes) probabilities\n    \"\"\"\n    log_probs = F.log_softmax(pred, dim=1)\n    loss = -torch.sum(soft_targets * log_probs, dim=1).mean()\n    return loss\n\ndef mixup_data(x, y, alpha=0.2):\n    \"\"\"\n    Return mixed inputs and mixed targets (one-hot) for mixup.\n    x: Tensor of shape (batch_size, features, seq_len)\n    y: Tensor of shape (batch_size, num_classes)\n    \"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1.0\n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n\n    mixed_x = lam * x + (1 - lam) * x[index, :]\n    mixed_y = lam * y + (1 - lam) * y[index, :]\n    return mixed_x, mixed_y","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:46:09.216593Z","iopub.status.busy":"2025-06-14T09:46:09.216334Z","iopub.status.idle":"2025-06-14T09:46:09.221597Z","shell.execute_reply":"2025-06-14T09:46:09.220903Z"},"papermill":{"duration":0.010109,"end_time":"2025-06-14T09:46:09.222665","exception":false,"start_time":"2025-06-14T09:46:09.212556","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"055a1f13","cell_type":"markdown","source":"# **Training loop**","metadata":{"papermill":{"duration":0.003048,"end_time":"2025-06-14T09:46:09.228893","exception":false,"start_time":"2025-06-14T09:46:09.225845","status":"completed"},"tags":[]}},{"id":"a373c020","cell_type":"code","source":"seed_everything(seed=SEED)\n\ncriterion = soft_cross_entropy\n\nn_splits = 5\nbatch_size = 128\ngkf = GroupKFold(n_splits=n_splits)\n\nfold_metrics = []\nbest_fold_metrics = []\nbest_models = []\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(X_array, y_array, groups=seq_df['subject'])):\n    print(f\"\\n{'='*50}\")\n    print(f\"Fold {fold + 1}/{n_splits}\")\n    print(f\"Train subjects: {len(np.unique(seq_df.iloc[train_idx]['subject']))}\")\n    print(f\"Val subjects: {len(np.unique(seq_df.iloc[val_idx]['subject']))}\")\n    print(f\"{'='*50}\")\n    \n    train_dataset = SequenceDataset(X_array[train_idx], y_array[train_idx])\n    val_dataset = SequenceDataset(X_array[val_idx], y_array[val_idx])\n    \n    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)\n    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n    \n    seed_everything(seed=SEED + fold)\n    model = IMU_HARModel(\n        total_features=len(feature_cols),\n        imu_dim=imu_dim,\n        pad_len=pad_len,\n        num_classes=num_classes,\n    ).to(device)\n    \n    # Optimizer et scheduler\n    optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)\n    steps_per_epoch = len(train_loader)\n    scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(\n        optimizer,\n        T_0=5 * steps_per_epoch,\n        T_mult=2,\n        eta_min=1e-5,\n    )\n    \n    # Early stopping\n    best_metric = -np.inf\n    best_binary_f1 = -np.inf\n    best_macro_f1 = -np.inf\n    patience = 15\n    epochs_no_improve = 0\n    num_epochs = 100\n    \n    for epoch in range(1, num_epochs + 1):\n        # Training phase\n        model.train()\n        train_loss = 0.0\n        total = 0\n        for batch_x, batch_y in train_loader:\n            batch_x = batch_x.to(device)\n            batch_y = batch_y.to(device)\n    \n            # Apply mixup\n            mixed_x, mixed_y = mixup_data(batch_x, batch_y, alpha=0.2)\n    \n            optimizer.zero_grad()\n            outputs = model(mixed_x)\n            loss = criterion(outputs, mixed_y)\n            loss.backward()\n            optimizer.step()\n            scheduler.step()\n    \n            train_loss += loss.item() * batch_x.size(0)\n            total += batch_x.size(0)\n        train_loss /= total\n    \n        # Validation phase\n        model.eval()\n        val_loss = 0.0\n        total = 0\n        all_true = []\n        all_pred = []\n        \n        with torch.no_grad():\n            for batch_x, batch_y in val_loader:\n                batch_x = batch_x.to(device)\n                batch_y = batch_y.to(device)\n                \n                outputs = model(batch_x)\n                loss = criterion(outputs, batch_y)\n                val_loss += loss.item() * batch_x.size(0)\n                total += batch_x.size(0)\n                \n                # Get predicted class indices\n                preds = torch.argmax(outputs, dim=1).cpu().numpy()\n                # Get true class indices from one-hot\n                trues = torch.argmax(batch_y, dim=1).cpu().numpy()\n                \n                all_true.append(trues)\n                all_pred.append(preds)\n        \n        val_loss /= total\n        all_true = np.concatenate(all_true)\n        all_pred = np.concatenate(all_pred)\n        \n        # Compute competition metrics\n        # Binary classification: BFRB (1) vs non-BFRB (0)\n        binary_true = np.isin(all_true, bfrb_indices).astype(int)\n        binary_pred = np.isin(all_pred, bfrb_indices).astype(int)\n        binary_f1 = f1_score(binary_true, binary_pred)\n        \n        # Collapse non-BFRB gestures into a single class\n        collapsed_true = np.where(\n            np.isin(all_true, bfrb_indices),\n            all_true,\n            len(bfrb_gestures)  # Single non-BFRB class\n        )\n        collapsed_pred = np.where(\n            np.isin(all_pred, bfrb_indices),\n            all_pred,\n            len(bfrb_gestures)  # Single non-BFRB class\n        )\n        \n        # Macro F1 on collapsed classes\n        macro_f1 = f1_score(collapsed_true, collapsed_pred, average='macro')\n        final_metric = (binary_f1 + macro_f1) / 2\n        \n        print(f\"Epoch {epoch:02d}: Train Loss = {train_loss:.4f}, Val Loss = {val_loss:.4f}\")\n        print(f\"  Binary F1 = {binary_f1:.4f}, Macro F1 = {macro_f1:.4f}, Final Metric = {final_metric:.4f}\")\n        \n        if final_metric > best_metric:\n            best_metric = final_metric\n            best_binary_f1 = binary_f1\n            best_macro_f1 = macro_f1\n            epochs_no_improve = 0\n            best_model_state = model.state_dict()\n            print(f\"  New best metric! Saving model...\")\n        else:\n            epochs_no_improve += 1\n            if epochs_no_improve >= patience:\n                print(f\"Early stopping triggered at epoch {epoch}\")\n                model.load_state_dict(best_model_state)\n                break\n    \n    torch.save(best_model_state, f\"best_model_fold{fold}.pth\")\n    best_models.append(best_model_state)\n    \n    fold_metrics.append({\n        'binary_f1': binary_f1,\n        'macro_f1': macro_f1,\n        'final_metric': final_metric\n    })\n    \n    best_fold_metrics.append({\n        'binary_f1': best_binary_f1,\n        'macro_f1': best_macro_f1,\n        'final_metric': best_metric\n    })\n    \n    print(f\"\\nFold {fold + 1} completed.\")\n    print(f\"Final validation metrics - Binary F1: {binary_f1:.4f}, Macro F1: {macro_f1:.4f}, Final: {final_metric:.4f}\")\n    print(f\"Best validation metrics - Binary F1: {best_binary_f1:.4f}, Macro F1: {best_macro_f1:.4f}, Final: {best_metric:.4f}\")\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"Cross-Validation Results\")\nprint(\"=\"*50)\n\n# Statistiques pour les meilleures métriques\nbest_binary_f1 = [m['binary_f1'] for m in best_fold_metrics]\nbest_macro_f1 = [m['macro_f1'] for m in best_fold_metrics]\nbest_metrics = [m['final_metric'] for m in best_fold_metrics]\n\nprint(\"\\nBest Fold-wise Metrics:\")\nfor i, (bf1, mf1, fm) in enumerate(zip(best_binary_f1, best_macro_f1, best_metrics)):\n    print(f\"Fold {i+1}: Binary F1 = {bf1:.4f}, Macro F1 = {mf1:.4f}, Final = {fm:.4f}\")\n\nprint(\"\\nGlobal Statistics (Best Metrics):\")\nprint(f\"Mean Best Final Metric: {np.mean(best_metrics):.4f} ± {np.std(best_metrics):.4f}\")\nprint(f\"Mean Best Binary F1: {np.mean(best_binary_f1):.4f} ± {np.std(best_binary_f1):.4f}\")\nprint(f\"Mean Best Macro F1: {np.mean(best_macro_f1):.4f} ± {np.std(best_macro_f1):.4f}\")","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:46:09.236192Z","iopub.status.busy":"2025-06-14T09:46:09.235983Z","iopub.status.idle":"2025-06-14T09:53:30.935949Z","shell.execute_reply":"2025-06-14T09:53:30.934949Z"},"papermill":{"duration":441.705085,"end_time":"2025-06-14T09:53:30.937184","exception":false,"start_time":"2025-06-14T09:46:09.232099","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"54929651","cell_type":"markdown","source":"# Reloading best model","metadata":{"papermill":{"duration":0.013055,"end_time":"2025-06-14T09:53:30.964331","exception":false,"start_time":"2025-06-14T09:53:30.951276","status":"completed"},"tags":[]}},{"id":"33f14d5e","cell_type":"code","source":"model_ensemble = []\nfor fold in range(5):\n    model = IMU_HARModel(\n        total_features=len(feature_cols),\n        imu_dim=imu_dim,\n        pad_len=pad_len,\n        num_classes=num_classes,\n    ).to(device)\n    checkpoint = torch.load(f\"best_model_fold{fold}.pth\", map_location=device)\n    model.load_state_dict(checkpoint)\n    model.eval()\n    model_ensemble.append(model)","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:53:30.992535Z","iopub.status.busy":"2025-06-14T09:53:30.991762Z","iopub.status.idle":"2025-06-14T09:53:31.121870Z","shell.execute_reply":"2025-06-14T09:53:31.121278Z"},"papermill":{"duration":0.145456,"end_time":"2025-06-14T09:53:31.123179","exception":false,"start_time":"2025-06-14T09:53:30.977723","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e2c4ec6f","cell_type":"markdown","source":"# **Submission**","metadata":{"papermill":{"duration":0.012887,"end_time":"2025-06-14T09:53:31.149767","exception":false,"start_time":"2025-06-14T09:53:31.136880","status":"completed"},"tags":[]}},{"id":"750b79b6","cell_type":"code","source":"def preprocess_sequence(df_seq: pd.DataFrame):\n    \"\"\"\n    Process a single sequence DataFrame (pandas):\n    - Forward/backward fill missing\n    - Scale using loaded scaler\n    - Pad/truncate to pad_len\n    - Return torch.Tensor of shape (1, features, seq_len)\n    \"\"\"\n    data = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    # Pad/truncate\n    padded = keras_pad_sequences(\n        [data],\n        maxlen=pad_len,\n        dtype='float32',\n        padding='post',\n        truncating='post'\n    )[0]  # (pad_len, total_features)\n    # Transpose to (features, pad_len)\n    tensor = torch.from_numpy(padded.T).unsqueeze(0).float()  # (1, features, pad_len)\n    return tensor\n    \ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Kaggle evaluation API will call this for each sequence.\n    sequence: polars DataFrame for a single sequence\n    demographics: unused in this model\n    Returns: predicted gesture string\n    \"\"\"\n    df_seq = sequence.to_pandas()\n    df_demo = demographics.to_pandas()\n    \n    df_seq = df_seq.merge(\n    df_demo,\n    on='subject',\n    how='left',\n    validate='many_to_one',\n    )\n    right_handed_mask = df_seq['handedness'] == 1\n    df_seq.loc[right_handed_mask, imu_cols] = apply_symmetry(df_seq.loc[right_handed_mask, imu_cols])\n\n    x_tensor = preprocess_sequence(df_seq).to(device)\n    \n    all_outputs = []\n    with torch.no_grad():\n        for model in model_ensemble:\n            outputs = model(x_tensor).softmax(dim=-1)\n            all_outputs.append(outputs)\n\n    avg_outputs = torch.mean(torch.stack(all_outputs), dim=0)\n    pred_idx = torch.argmax(avg_outputs, dim=1).item()\n    \n    return str(gesture_classes[pred_idx])","metadata":{"execution":{"iopub.execute_input":"2025-06-14T09:53:31.177092Z","iopub.status.busy":"2025-06-14T09:53:31.176377Z","iopub.status.idle":"2025-06-14T09:53:31.183014Z","shell.execute_reply":"2025-06-14T09:53:31.182526Z"},"papermill":{"duration":0.02145,"end_time":"2025-06-14T09:53:31.184073","exception":false,"start_time":"2025-06-14T09:53:31.162623","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"6682940a","cell_type":"code","source":"inference_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":{"execution":{"iopub.execute_input":"2025-06-14T09:53:31.211322Z","iopub.status.busy":"2025-06-14T09:53:31.211029Z","iopub.status.idle":"2025-06-14T09:53:32.688114Z","shell.execute_reply":"2025-06-14T09:53:32.687302Z"},"papermill":{"duration":1.492266,"end_time":"2025-06-14T09:53:32.689615","exception":false,"start_time":"2025-06-14T09:53:31.197349","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b80be4e8","cell_type":"markdown","source":"Credit to [Tarun Mishra](https://www.kaggle.com/tarundirector) – this code is heavily based on his [notebook](https://www.kaggle.com/code/tarundirector/sensor-pulse-viz-eda-for-bfrb-detection?scriptVersionId=243465321).","metadata":{"papermill":{"duration":0.013803,"end_time":"2025-06-14T09:53:32.717502","exception":false,"start_time":"2025-06-14T09:53:32.703699","status":"completed"},"tags":[]}},{"id":"2330c2a6","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.062541,"end_time":"2025-06-14T09:53:32.793383","exception":false,"start_time":"2025-06-14T09:53:32.730842","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}