{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":41880,"databundleVersionId":5677426}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport time\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import average_precision_score, recall_score, confusion_matrix, roc_auc_score\nfrom sklearn.manifold import TSNE\nimport matplotlib.pyplot as plt\nimport matplotlib.colors as mcolors\nimport seaborn as sns\n\n# ==========================================\n# 1. CONFIGURATION & PATHS\n# ==========================================\nCONF = {\n    'root_dir': \"/kaggle/input/tlvmc-parkinsons-freezing-gait-prediction/train/tdcsfog\",\n    'window_size': 384,\n    'step_size': 128,\n    'batch_size': 64,\n    'lr': 5e-4,\n    'epochs': 30,\n    'device': 'cuda' if torch.cuda.is_available() else 'cpu',\n    'output_dir': \"/kaggle/working/\",\n}\n\n# ==========================================\n# 2. ROBUST DATASET CLASS\n# ==========================================\nclass FoGDataset(Dataset):\n    def __init__(self, root_dir, window_size=384, step_size=128):\n        self.windows, self.labels, self.subjects = [], [], []\n        csv_files = [f for f in sorted(os.listdir(root_dir)) if f.endswith('.csv')]\n        \n        print(f\"Loading and Normalizing {len(csv_files[:100])} patient files...\")\n        for file in csv_files[:100]:\n            df = pd.read_csv(os.path.join(root_dir, file))\n            cols = ['AccV', 'AccML', 'AccAP']\n            data = df[cols].values\n            data = (data - np.mean(data, axis=0)) / (np.std(data, axis=0) + 1e-6)\n            labels = df['StartHesitation'].values\n            for start in range(0, len(df) - window_size + 1, step_size):\n                self.windows.append(data[start:start+window_size].T)\n                self.labels.append(labels[start:start+window_size].max())\n                self.subjects.append(file.split('_')[0])\n        self.windows = np.array(self.windows, dtype=np.float32)\n        self.labels  = np.array(self.labels,  dtype=np.float32)\n        self.subjects = np.array(self.subjects)\n\n    def __len__(self): return len(self.windows)\n    def __getitem__(self, idx):\n        return torch.tensor(self.windows[idx]), torch.tensor(self.labels[idx])\n\n# ==========================================\n# 3. THE THREE MODELS (FOR ABLATION)\n# ==========================================\nclass CNN_Only(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.cnn = nn.Sequential(\n            nn.Conv1d(3, 64, 7, padding=3), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(64, 128, 5, padding=2), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(128, 256, 3, padding=1), nn.BatchNorm1d(256), nn.ReLU(), nn.AdaptiveAvgPool1d(1)\n        )\n        self.classifier = nn.Linear(256, 1)\n\n    def forward(self, x, return_feats=False):\n        feat = self.cnn(x).squeeze(-1)\n        if return_feats: return feat\n        return self.classifier(feat)\n\nclass CNN_LSTM_NoAttn(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.cnn = nn.Sequential(\n            nn.Conv1d(3, 64, 7, padding=3), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(64, 128, 5, padding=2), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(128, 256, 3, padding=1), nn.BatchNorm1d(256), nn.ReLU(), nn.MaxPool1d(2)\n        )\n        self.lstm = nn.LSTM(256, 128, batch_first=True, bidirectional=True)\n        self.classifier = nn.Linear(256, 1)\n\n    def forward(self, x, return_feats=False):\n        x = self.cnn(x).transpose(1, 2)\n        x, _ = self.lstm(x)\n        feat = x[:, -1, :]\n        if return_feats: return feat\n        return self.classifier(feat)\n\nclass FoG1DLSTM(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.cnn = nn.Sequential(\n            nn.Conv1d(3, 64, 7, padding=3), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(64, 128, 5, padding=2), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(128, 256, 3, padding=1), nn.BatchNorm1d(256), nn.ReLU(), nn.MaxPool1d(2)\n        )\n        self.lstm = nn.LSTM(256, 128, batch_first=True, bidirectional=True)\n        self.attention = nn.Sequential(nn.Linear(256, 64), nn.Tanh(), nn.Linear(64, 1))\n        self.classifier = nn.Linear(256, 1)\n\n    def forward(self, x, return_feats=False):\n        x = self.cnn(x).transpose(1, 2)\n        x, _ = self.lstm(x)\n        weights = F.softmax(self.attention(x), dim=1)\n        feat = (x * weights).sum(dim=1)\n        if return_feats: return feat\n        return self.classifier(feat)\n\n# ==========================================\n# 4. METRICS & TRAINING LOGIC\n# ==========================================\ndef get_metrics(y_true, y_probs):\n    y_pred = (y_probs > 0.5).astype(int)\n    cm = confusion_matrix(y_true, y_pred, labels=[0, 1])\n    tn, fp, fn, tp = cm.ravel()\n    mAP = average_precision_score(y_true, y_probs) if len(np.unique(y_true)) > 1 else 0.0\n    sens = tp / (tp + fn) if (tp + fn) > 0 else 0.0\n    spec = tn / (tn + fp) if (tn + fp) > 0 else 0.0\n    try: auc = roc_auc_score(y_true, y_probs)\n    except: auc = 0.5\n    return mAP, sens, spec, auc\n\n# ==========================================\n# 5. CONFUSION MATRIX PLOT (BLUE)\n# ==========================================\ndef plot_confusion_matrices(cm_data: dict, output_dir: str):\n    \"\"\"\n    cm_data: { model_name: (y_true np.array, y_pred np.array) }\n    Saves one PNG per model confusion matrix.\n    \"\"\"\n    blue_cmap = mcolors.LinearSegmentedColormap.from_list(\n        'fog_blue', ['#eef4fc', '#b8d4ee', '#5b9bd5', '#1a5ea8', '#0a2d6e']\n    )\n    label_names = ['Normal', 'FoG']\n\n    for name, (y_true, y_pred) in cm_data.items():\n        fig, ax = plt.subplots(figsize=(6, 5))\n\n        cm = confusion_matrix(y_true, y_pred, labels=[0, 1])\n        row_sums = cm.sum(axis=1, keepdims=True)\n        cm_norm = np.where(row_sums > 0, cm / row_sums, 0.0)\n\n        annot = np.array([\n            [f\"{cm[r, c]:,}\\n({cm_norm[r, c]*100:.1f}%)\" for c in range(2)]\n            for r in range(2)\n        ])\n\n        sns.heatmap(\n            cm_norm,\n            ax=ax,\n            cmap=blue_cmap,\n            annot=annot,\n            fmt='',\n            linewidths=2,\n            linecolor='white',\n            cbar=True,\n            vmin=0, vmax=1,\n            annot_kws={'size': 13, 'weight': 'bold'},\n            xticklabels=label_names,\n            yticklabels=label_names,\n        )\n\n        # Auto-contrast text\n        for text_obj, (r, c) in zip(ax.texts, [(r, c) for r in range(2) for c in range(2)]):\n            text_obj.set_color('white' if cm_norm[r, c] > 0.45 else '#0a2d6e')\n\n        title_color = '#0a2d6e' if 'Proposed' in name else '#444444'\n        ax.set_title(f'{name}\\nConfusion Matrix', fontsize=13, fontweight='bold',\n                     pad=12, color=title_color)\n        ax.set_xlabel('Predicted Label', fontsize=11, labelpad=8)\n        ax.set_ylabel('True Label',      fontsize=11, labelpad=8)\n        ax.tick_params(axis='both', labelsize=10)\n\n        for spine in ax.spines.values():\n            spine.set_visible(True)\n            spine.set_linewidth(1.8)\n            spine.set_edgecolor('#1a5ea8')\n\n        plt.tight_layout()\n\n        clean = name.replace(\" \", \"_\").replace(\"(\", \"\").replace(\")\", \"\").replace(\"+\", \"plus\")\n        save_path = os.path.join(output_dir, f\"{clean}_confusion_matrix.png\")\n        plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white')\n        print(f\"Saved confusion matrix → {save_path}\")\n        plt.show()\n        plt.close()\n\n# ==========================================\n# 6. MAIN COMPARISON STUDY\n# ==========================================\ndef run_comparison_study():\n    os.makedirs(CONF['output_dir'], exist_ok=True)\n\n    ds = FoGDataset(CONF['root_dir'])\n\n    gkf = GroupKFold(n_splits=5)\n    for train_idx, val_idx in gkf.split(ds.windows, ds.labels, ds.subjects):\n        if ds.labels[val_idx].sum() > 20: break\n\n    train_loader = DataLoader(torch.utils.data.Subset(ds, train_idx),\n                              batch_size=CONF['batch_size'], shuffle=True)\n    val_loader   = DataLoader(torch.utils.data.Subset(ds, val_idx),\n                              batch_size=CONF['batch_size'], shuffle=False)\n\n    models = {\n        \"CNN Only\":              CNN_Only(),\n        \"CNN + LSTM\":            CNN_LSTM_NoAttn(),\n        \"FoG1DLSTM (Proposed)\":  FoG1DLSTM(),\n    }\n\n    results  = {}\n    tsne_data = {}\n    cm_data   = {}   # ← collects (y_true, y_pred) per model for conf matrices\n\n    for name, model in models.items():\n        print(f\"\\n{'='*50}\\nTraining: {name}\\n{'='*50}\")\n        model.to(CONF['device'])\n        optimizer = optim.Adam(model.parameters(), lr=CONF['lr'])\n        criterion = nn.BCEWithLogitsLoss(\n            pos_weight=torch.tensor([35.0]).to(CONF['device'])\n        )\n\n        for epoch in range(CONF['epochs']):\n            model.train()\n            epoch_loss = 0.0\n            for X, y in train_loader:\n                X, y = X.to(CONF['device']), y.to(CONF['device'])\n                optimizer.zero_grad()\n                loss = criterion(model(X).squeeze(), y)\n                loss.backward()\n                optimizer.step()\n                epoch_loss += loss.item()\n            if (epoch + 1) % 5 == 0:\n                print(f\"  Epoch {epoch+1:02d}/{CONF['epochs']} | Loss: {epoch_loss/len(train_loader):.4f}\")\n\n        # ── Evaluation ──────────────────────────────────────────────────────\n        model.eval()\n\n        # Latency benchmark\n        start_time = time.time()\n        with torch.no_grad():\n            for _ in range(50):\n                _ = model(torch.randn(1, 3, 384).to(CONF['device']))\n        latency = (time.time() - start_time) / 50 * 1000\n\n        probs, targets, feats = [], [], []\n        with torch.no_grad():\n            for X, y in val_loader:\n                f = model(X.to(CONF['device']), return_feats=True)\n                p = torch.sigmoid(model.classifier(f).squeeze())\n                probs.extend(p.cpu().numpy())\n                targets.extend(y.numpy())\n                feats.extend(f.cpu().numpy())\n\n        probs_arr   = np.array(probs)\n        targets_arr = np.array(targets)\n        preds_arr   = (probs_arr > 0.5).astype(int)\n\n        mAP, sens, spec, auc = get_metrics(targets_arr, probs_arr)\n        results[name]  = [mAP, sens, spec, auc, latency]\n        tsne_data[name] = (np.array(feats), targets_arr)\n        cm_data[name]   = (targets_arr, preds_arr)   # ← store for conf matrix\n\n        print(f\"  AUC: {auc:.4f} | Sensitivity: {sens:.4f} | Specificity: {spec:.4f} | mAP: {mAP:.4f}\")\n\n    # ── Results Table ────────────────────────────────────────────────────────\n    df_results = pd.DataFrame(\n        results, index=['mAP', 'Sensitivity', 'Specificity', 'AUC-ROC', 'Latency (ms)']\n    ).T\n    print(\"\\n\" + \"=\"*50 + \"\\nFINAL ABLATION TABLE\\n\" + \"=\"*50)\n    print(df_results.to_string())\n\n    # ── t-SNE Plots ──────────────────────────────────────────────────────────\n    for name, (f, t) in tsne_data.items():\n        plt.figure(figsize=(8, 6))\n        pos_idx = np.where(t == 1)[0]\n        neg_idx = np.where(t == 0)[0][:len(pos_idx) * 2]\n        plot_idx = np.concatenate([pos_idx, neg_idx])\n        embed = TSNE(n_components=2, perplexity=30, random_state=42).fit_transform(f[plot_idx])\n        sns.scatterplot(x=embed[:, 0], y=embed[:, 1], hue=t[plot_idx],\n                        palette='rocket', alpha=0.7)\n        plt.title(f\"{name} — Feature Clustering (t-SNE)\")\n        clean = name.replace(\" \", \"_\").replace(\"(\", \"\").replace(\")\", \"\")\n        save_path = os.path.join(CONF['output_dir'], f\"{clean}_tSNE.svg\")\n        plt.savefig(save_path, format='svg', bbox_inches='tight')\n        print(f\"Saved t-SNE → {save_path}\")\n        plt.show()\n        plt.close()\n\n    # ── Confusion Matrices ───────────────────────────────────────────────────\n    plot_confusion_matrices(cm_data, CONF['output_dir'])\n\n\nif __name__ == \"__main__\":\n    run_comparison_study()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-18T13:05:18.385495Z","iopub.execute_input":"2026-02-18T13:05:18.385762Z","iopub.status.idle":"2026-02-18T13:05:19.897912Z","shell.execute_reply.started":"2026-02-18T13:05:18.385739Z","shell.execute_reply":"2026-02-18T13:05:19.897192Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}