{"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":129.256572,"end_time":"2025-06-05T05:45:30.375955","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-06-05T05:43:21.119383","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"92c970b9","cell_type":"code","source":"import os\nimport joblib\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm import tqdm\n\n# CONFIG\nTRAIN = True\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nEXPORT_DIR = Path(\"./\")\nBATCH_SIZE = 128\nPAD_PERCENTILE = 90\nLR_INIT = 1e-3\nWD = 1e-4\nMIXUP_ALPHA = 0.2\nEPOCHS = 160\nPATIENCE = 10\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list, scaler: StandardScaler):\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(mat).astype('float32')\n\ndef pad_sequences(X_list, pad_len):\n    X_pad = np.zeros((len(X_list), pad_len, X_list[0].shape[1]), dtype=np.float32)\n    for i, x in enumerate(X_list):\n        l = min(len(x), pad_len)\n        X_pad[i, :l, :] = x[:l]\n    return X_pad\n\nclass SensorDataset(Dataset):\n    def __init__(self, X, y):\n        self.X = X\n        self.y = y\n\n    def __len__(self):\n        return len(self.X)\n\n    def __getitem__(self, idx):\n        return (torch.tensor(self.X[idx], dtype=torch.float32),\n                torch.tensor(self.y[idx], dtype=torch.float32))\n\nclass MixupSensorDataset(Dataset):\n    def __init__(self, X, y, alpha=0.2):\n        self.X, self.y = X, y\n        self.alpha = alpha\n\n    def __len__(self):\n        return len(self.X)\n\n    def __getitem__(self, i):\n        lam = np.random.beta(self.alpha, self.alpha)\n        j = np.random.randint(0, len(self.X))\n        X1, y1 = self.X[i], self.y[i]\n        X2, y2 = self.X[j], self.y[j]\n        X = lam * X1 + (1 - lam) * X2\n        y = lam * y1 + (1 - lam) * y2\n        return (torch.tensor(X, dtype=torch.float32),\n                torch.tensor(y, dtype=torch.float32))\n\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        self.fc1 = nn.Linear(channels, channels // reduction)\n        self.fc2 = nn.Linear(channels // reduction, channels)\n\n    def forward(self, x):\n        y = self.pool(x).squeeze(-1)\n        y = F.relu(self.fc1(y))\n        y = torch.sigmoid(self.fc2(y)).unsqueeze(-1)\n        return x * y\n\nclass ResidualSEBlock(nn.Module):\n    def __init__(self, in_ch, out_ch, kernel_size, pool_size=2, drop=0.3):\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_ch, out_ch, kernel_size, padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_ch)\n        self.conv2 = nn.Conv1d(out_ch, out_ch, kernel_size, padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_ch)\n        self.se = SEBlock(out_ch)\n        self.pool = nn.MaxPool1d(pool_size)\n        self.drop = nn.Dropout(drop)\n        self.shortcut = nn.Sequential()\n        if in_ch != out_ch:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_ch, out_ch, 1, bias=False),\n                nn.BatchNorm1d(out_ch)\n            )\n\n    def forward(self, x):\n        shortcut = self.shortcut(x)\n        x = F.relu(self.bn1(self.conv1(x)))\n        x = F.relu(self.bn2(self.conv2(x)))\n        x = self.se(x)\n        x = x + shortcut\n        x = F.relu(x)\n        x = self.pool(x)\n        x = self.drop(x)\n        return x\n\nclass AttentionLayer(nn.Module):\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.fc = nn.Linear(hidden_dim, 1)\n\n    def forward(self, x):\n        score = torch.tanh(self.fc(x)).squeeze(-1)\n        weights = F.softmax(score, dim=1).unsqueeze(-1)\n        context = (x * weights).sum(dim=1)\n        return context\n\nclass IMUOnlyHAR(nn.Module):\n    def __init__(self, pad_len, imu_dim, n_classes):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.imu_res1 = ResidualSEBlock(imu_dim, 64, 3)\n        self.imu_res2 = ResidualSEBlock(64, 128, 5)\n        self.lstm = nn.LSTM(128, 128, num_layers=1, batch_first=True, bidirectional=True)\n        self.attn = AttentionLayer(256)\n        self.fc1 = nn.Linear(256, 256, bias=False)\n        self.bn1 = nn.BatchNorm1d(256)\n        self.drop1 = nn.Dropout(0.5)\n        self.fc2 = nn.Linear(256, 128, bias=False)\n        self.bn2 = nn.BatchNorm1d(128)\n        self.drop2 = nn.Dropout(0.3)\n        self.out = nn.Linear(128, n_classes)\n\n    def forward(self, x):\n        imu = x.permute(0,2,1)\n        imu = self.imu_res1(imu)\n        imu = self.imu_res2(imu)\n        imu = imu.permute(0,2,1)\n        lstm_out, _ = self.lstm(imu)\n        attn_out = self.attn(lstm_out)\n        x = F.relu(self.bn1(self.fc1(attn_out)))\n        x = self.drop1(x)\n        x = F.relu(self.bn2(self.fc2(x)))\n        x = self.drop2(x)\n        out = self.out(x)\n        return out\n\nif TRAIN:\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n    df_demo = pd.read_csv(RAW_DIR / \"train_demographics.csv\")\n    df = pd.merge(df, df_demo, how='left', on='subject')\n    le = LabelEncoder(); df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n\n    meta_cols = {'gesture', 'gesture_int', 'sequence_type', 'behavior', 'orientation',\n                 'row_id', 'subject', 'phase', 'sequence_id', 'sequence_counter'}\n    demo_cols = {'adult_child', 'age', 'sex', 'handedness', 'height_cm', 'shoulder_to_wrist_cm', 'elbow_to_wrist_cm'}\n    # Remove demographics and other unwanted columns!\n    feature_cols = [c for c in df.columns if c not in meta_cols and c not in demo_cols\n                    and not c.startswith('thm_') and not c.startswith('tof_')]\n    imu_cols = feature_cols\n\n    scaler = StandardScaler().fit(df[imu_cols].ffill().bfill().fillna(0).values)\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(imu_cols))\n\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, imu_cols, scaler)\n        X_list.append(mat)\n        y_list.append(seq['gesture_int'].iloc[0])\n        lens.append(len(mat))\n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    X = pad_sequences(X_list, pad_len)\n    y = np.eye(len(le.classes_))[y_list]\n\n    # Split\n    X_tr, X_val, y_tr, y_val = train_test_split(\n        X, y, test_size=0.2, random_state=42, stratify=y_list\n    )\n    train_data = MixupSensorDataset(X_tr, y_tr, alpha=MIXUP_ALPHA)\n    val_data = SensorDataset(X_val, y_val)\n    train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)\n    val_loader = DataLoader(val_data, batch_size=BATCH_SIZE, shuffle=False)\n\n    # Class weights\n    cw_vals = compute_class_weight('balanced', classes=np.arange(len(le.classes_)), y=y_list)\n    class_weight = torch.tensor(cw_vals, dtype=torch.float32).to(DEVICE)\n\n    model = IMUOnlyHAR(pad_len, len(imu_cols), len(le.classes_)).to(DEVICE)\n    optimizer = torch.optim.Adam(model.parameters(), lr=LR_INIT, weight_decay=WD)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=5*len(train_loader))\n    criterion = nn.CrossEntropyLoss(weight=class_weight)\n\n    best_acc, bad_epochs = 0, 0\n    for epoch in range(EPOCHS):\n        model.train()\n        tr_loss, tr_count = 0, 0\n        for xb, yb in tqdm(train_loader, desc=f\"Epoch {epoch+1}\"):\n            xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n            optimizer.zero_grad()\n            out = model(xb)\n            loss = criterion(out, yb.argmax(1))\n            loss.backward()\n            optimizer.step()\n            scheduler.step(epoch + tr_count / len(train_loader))\n            tr_loss += loss.item() * xb.size(0)\n            tr_count += xb.size(0)\n        tr_loss /= tr_count\n\n        model.eval()\n        val_loss, val_acc, val_count = 0, 0, 0\n        with torch.no_grad():\n            for xb, yb in val_loader:\n                xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n                out = model(xb)\n                loss = criterion(out, yb.argmax(1))\n                val_loss += loss.item() * xb.size(0)\n                preds = out.argmax(1).cpu().numpy()\n                labels = yb.argmax(1).cpu().numpy()\n                val_acc += (preds == labels).sum()\n                val_count += xb.size(0)\n        val_loss /= val_count\n        val_acc /= val_count\n        print(f\"Epoch {epoch+1}, Loss: {tr_loss:.4f}, Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}\")\n\n        if val_acc > best_acc:\n            best_acc = val_acc\n            bad_epochs = 0\n            torch.save(model.state_dict(), EXPORT_DIR / \"gesture_imu_only.pt\")\n        else:\n            bad_epochs += 1\n            if bad_epochs >= PATIENCE:\n                print(\"Early stopping.\")\n                break\n\n    print(\"✔ Training done – model saved in\", EXPORT_DIR)\n","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.execute_input":"2025-06-05T05:43:25.124846Z","iopub.status.busy":"2025-06-05T05:43:25.124625Z","iopub.status.idle":"2025-06-05T05:45:25.690029Z","shell.execute_reply":"2025-06-05T05:45:25.689227Z"},"papermill":{"duration":120.569921,"end_time":"2025-06-05T05:45:25.691180","exception":false,"start_time":"2025-06-05T05:43:25.121259","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"24ff67cc","cell_type":"code","source":"import joblib\nimport numpy as np\nimport torch\nfrom pathlib import Path\nimport polars as pl\n\nPRETRAINED_DIR = Path(\"/kaggle/working/\")\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n\nfeature_cols = np.load(PRETRAINED_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\npad_len = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\nscaler = joblib.load(PRETRAINED_DIR / \"scaler.pkl\")\ngesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n\nclass IMUOnlyHAR(nn.Module):\n    def __init__(self, pad_len, imu_dim, n_classes):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.imu_res1 = ResidualSEBlock(imu_dim, 64, 3)\n        self.imu_res2 = ResidualSEBlock(64, 128, 5)\n        self.lstm = nn.LSTM(128, 128, num_layers=1, batch_first=True, bidirectional=True)\n        self.attn = AttentionLayer(256)\n        self.fc1 = nn.Linear(256, 256, bias=False)\n        self.bn1 = nn.BatchNorm1d(256)\n        self.drop1 = nn.Dropout(0.5)\n        self.fc2 = nn.Linear(256, 128, bias=False)\n        self.bn2 = nn.BatchNorm1d(128)\n        self.drop2 = nn.Dropout(0.3)\n        self.out = nn.Linear(128, n_classes)\n\n    def forward(self, x):\n        imu = x.permute(0,2,1)\n        imu = self.imu_res1(imu)\n        imu = self.imu_res2(imu)\n        imu = imu.permute(0,2,1)\n        lstm_out, _ = self.lstm(imu)\n        attn_out = self.attn(lstm_out)\n        x = F.relu(self.bn1(self.fc1(attn_out)))\n        x = self.drop1(x)\n        x = F.relu(self.bn2(self.fc2(x)))\n        x = self.drop2(x)\n        out = self.out(x)\n        return out\n\nmodel = IMUOnlyHAR(pad_len, len(feature_cols), len(gesture_classes)).to(DEVICE)\nmodel.load_state_dict(torch.load(PRETRAINED_DIR / \"gesture_imu_only.pt\", map_location=DEVICE))\nmodel.eval()\n\ndef preprocess_sequence(df_seq, feature_cols, scaler):\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(mat).astype('float32')\n\ndef pad_sequences(X_list, pad_len):\n    X_pad = np.zeros((len(X_list), pad_len, X_list[0].shape[1]), dtype=np.float32)\n    for i, x in enumerate(X_list):\n        l = min(len(x), pad_len)\n        X_pad[i, :l, :] = x[:l]\n    return X_pad\n\n# Accept demographics for Kaggle compatibility but IGNORE it\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_seq = sequence.to_pandas()\n    mat = preprocess_sequence(df_seq, feature_cols, scaler)\n    pad = pad_sequences([mat], pad_len)\n    x = torch.tensor(pad, dtype=torch.float32).to(DEVICE)\n    with torch.no_grad():\n        out = model(x)\n        idx = int(out.argmax(1).cpu().numpy()[0])\n    return gesture_classes[idx]\n","metadata":{"execution":{"iopub.execute_input":"2025-06-05T05:45:25.760562Z","iopub.status.busy":"2025-06-05T05:45:25.759866Z","iopub.status.idle":"2025-06-05T05:45:26.262998Z","shell.execute_reply":"2025-06-05T05:45:26.262465Z"},"papermill":{"duration":0.538249,"end_time":"2025-06-05T05:45:26.264277","exception":false,"start_time":"2025-06-05T05:45:25.726028","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"50dc63dd","cell_type":"code","source":"import 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":{"execution":{"iopub.execute_input":"2025-06-05T05:45:26.331941Z","iopub.status.busy":"2025-06-05T05:45:26.331706Z","iopub.status.idle":"2025-06-05T05:45:27.926589Z","shell.execute_reply":"2025-06-05T05:45:27.925875Z"},"papermill":{"duration":1.629938,"end_time":"2025-06-05T05:45:27.927977","exception":false,"start_time":"2025-06-05T05:45:26.298039","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}