{"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":"none","dataSources":[{"sourceId":59093,"databundleVersionId":7469972,"sourceType":"competition"},{"sourceId":235484897,"sourceType":"kernelVersion"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport warnings\nimport pickle\nimport gc\n\nfrom scipy.stats import entropy\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score\nfrom sklearn.svm import SVC\n\nwarnings.filterwarnings('ignore')\n\n# ------------------- CONFIGURATION -------------------\nTEST_PATH            = \"/kaggle/working/\"\nBASE_PATH            = \"/kaggle/input/hms-harmful-brain-activity-classification\"\nPREPROCESSED_PATH    = \"/kaggle/input/preprocessing/preprocessed/eeg\"\nTRAIN_LABELS_PATH    = os.path.join(BASE_PATH, \"train.csv\")\nSUCCESS_FILE_PATH    = os.path.join(os.path.dirname(PREPROCESSED_PATH), \"success.csv\")\n\nCLASSES    = ['Seizure', 'LPD', 'GPD', 'LRDA', 'GRDA', 'Other']\nN_CLASSES  = len(CLASSES)\nBATCH_SIZE = 500\n\nprint(\"Starting SVM-based EEG classification (handling NaNs via median imputation)\\n\")\n\n# ------------------- KL DIVERGENCE FUNCTION -------------------\ndef kl_divergence_numpy(y_true_onehot, y_pred_proba, epsilon=1e-7):\n    \"\"\"\n    Compute mean KL divergence between true one-hot and predicted probability vectors.\n    \"\"\"\n    y_pred = np.clip(y_pred_proba, epsilon, 1.0 - epsilon)\n    y_true = np.clip(y_true_onehot,   epsilon, 1.0       )\n    kl = np.sum(y_true * np.log(y_true / y_pred), axis=1)\n    return np.mean(kl)\n\n\n# ------------------- FEATURE EXTRACTION -------------------\ndef extract_features(eeg_data: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Given a 2D array (channels × timepoints), extract per-channel\n    statistical features + cross-channel statistics.\n    Returns a 1D array of length 251.\n    \"\"\"\n    features = []\n\n    # 1) Per-channel (19 channels):\n    for ch in range(eeg_data.shape[0]):\n        channel_data = eeg_data[ch, :]\n\n        # Basic stats\n        features.extend([\n            np.mean(channel_data),\n            np.std(channel_data),\n            np.var(channel_data),\n            np.median(channel_data),\n            np.min(channel_data),\n            np.max(channel_data),\n            np.percentile(channel_data, 25),\n            np.percentile(channel_data, 75),\n            np.sum(np.abs(channel_data)),\n            np.sum(channel_data ** 2),\n        ])\n\n        # Additional measures\n        features.extend([\n            np.abs(np.mean(channel_data)),\n            np.sqrt(np.mean(channel_data ** 2)),\n            entropy(np.abs(channel_data) + 1e-10),\n        ])\n\n    # 2) Cross-channel features (global)\n    features.extend([\n        np.mean(eeg_data),\n        np.std(eeg_data),\n        np.var(eeg_data),\n        np.corrcoef(eeg_data).mean(),\n    ])\n\n    return np.array(features, dtype=np.float32)\n\n\ndef process_eeg_file(eeg_id: str) -> np.ndarray:\n    \"\"\"\n    Load the preprocessed .npy file, handle NaNs/Infs, z‐normalize each channel,\n    then return the 251‐dim feature vector. On error, returns None.\n    \"\"\"\n    eeg_path = os.path.join(PREPROCESSED_PATH, f\"{eeg_id}.npy\")\n    try:\n        eeg_data = np.load(eeg_path).astype(np.float32)\n\n        # Replace NaN/Inf per channel with that channel's mean\n        if np.any(np.isnan(eeg_data)) or np.any(np.isinf(eeg_data)):\n            for ch in range(eeg_data.shape[0]):\n                channel = eeg_data[ch, :]\n                mask = np.isnan(channel) | np.isinf(channel)\n                if np.any(mask):\n                    channel[mask] = np.nanmean(channel)\n\n        # Z‐normalize each channel independently\n        eeg_data = (eeg_data - np.mean(eeg_data, axis=1, keepdims=True)) / (\n            np.std(eeg_data, axis=1, keepdims=True) + 1e-7\n        )\n\n        # Extract features\n        feats = extract_features(eeg_data)\n        return feats\n\n    except Exception as e:\n        print(f\"Error loading/processing EEG {eeg_id}: {e}\")\n        return None\n\n\nclass BatchDataGenerator:\n    \"\"\"\n    Yields (X_batch, y_batch) in chunks of batch_size. Uses process_eeg_file().\n    \"\"\"\n    def __init__(self, eeg_ids: list, labels: np.ndarray, batch_size: int = BATCH_SIZE):\n        self.eeg_ids    = eeg_ids\n        self.labels     = labels\n        self.batch_size = batch_size\n\n    def __iter__(self):\n        for i in range(0, len(self.eeg_ids), self.batch_size):\n            batch_ids    = self.eeg_ids[i : i + self.batch_size]\n            batch_labels = self.labels[i : i + self.batch_size]\n\n            batch_features = []\n            batch_y        = []\n\n            for eeg_id, lbl in zip(batch_ids, batch_labels):\n                feats = process_eeg_file(eeg_id)\n                if feats is not None:\n                    batch_features.append(feats)\n                    batch_y.append(lbl)\n\n            if batch_features:\n                yield np.vstack(batch_features), np.array(batch_y, dtype=np.int32)\n\n\n# ------------------- LOAD & PREPROCESS LABELS -------------------\ntry:\n    train_df = pd.read_csv(TRAIN_LABELS_PATH)\n    print(f\"Loaded {len(train_df)} annotations, {train_df['eeg_id'].nunique()} unique EEG IDs\")\nexcept Exception as e:\n    print(f\"Error loading train.csv: {e}\")\n    raise SystemExit\n\n# Encode expert_consensus → integer labels [0..5]\nlabel_encoder = LabelEncoder()\nlabel_encoder.fit(CLASSES)\ntrain_df['label'] = label_encoder.transform(train_df['expert_consensus'])\n\n# Determine which EEG IDs successfully preprocessed\nif os.path.exists(SUCCESS_FILE_PATH):\n    try:\n        success_df  = pd.read_csv(SUCCESS_FILE_PATH)\n        success_ids = set(success_df['eeg_id'].astype(str).tolist())\n        print(f\"Found success.csv with {len(success_ids)} successful IDs\")\n    except Exception as e:\n        print(f\"Error loading success.csv: {e}\")\n        success_ids = set(train_df['eeg_id'].astype(str).tolist())\nelse:\n    print(\"No success.csv found → using all EEG IDs from train.csv\")\n    success_ids = set(train_df['eeg_id'].astype(str).tolist())\n\n# Filter to only those that were successfully preprocessed\nvalid_df = train_df[train_df['eeg_id'].astype(str).isin(success_ids)].copy()\neeg_ids = valid_df['eeg_id'].astype(str).tolist()\nlabels  = valid_df['label'].values\nprint(f\"Processing {len(eeg_ids)} valid training samples\\n\")\n\n# ------------------- TRAIN/VALIDATION SPLIT -------------------\ntrain_ids, val_ids, train_labels, val_labels = train_test_split(\n    eeg_ids,\n    labels,\n    test_size=0.20,\n    stratify=labels,\n    random_state=42\n)\nprint(f\"-> Train IDs: {len(train_ids)},  Val IDs: {len(val_ids)}\\n\")\n\n# ------------------- EXTRACT FEATURES FOR TRAIN & VAL -------------------\nprint(\"Extracting features for TRAIN set in batches...\")\nall_train_feats = []\nall_train_lbls  = []\n\nfor Xb, yb in BatchDataGenerator(train_ids, train_labels, batch_size=BATCH_SIZE):\n    all_train_feats.append(Xb)\n    all_train_lbls.append(yb)\n    gc.collect()\n\nX_train = np.vstack(all_train_feats)\ny_train = np.hstack(all_train_lbls)\nprint(f\"  Completed: X_train.shape = {X_train.shape}, y_train.shape = {y_train.shape}\")\n\ndel all_train_feats, all_train_lbls\ngc.collect()\n\nprint(\"\\nExtracting features for VALID set in batches...\")\nall_val_feats = []\nall_val_lbls  = []\n\nfor Xb, yb in BatchDataGenerator(val_ids, val_labels, batch_size=BATCH_SIZE):\n    all_val_feats.append(Xb)\n    all_val_lbls.append(yb)\n    gc.collect()\n\nX_val = np.vstack(all_val_feats)\ny_val = np.hstack(all_val_lbls)\nprint(f\"  Completed: X_val.shape = {X_val.shape}, y_val.shape = {y_val.shape}\\n\")\n\ndel all_val_feats, all_val_lbls\ngc.collect()\n\n# ------------------- IMPUTE MISSING VALUES -------------------\nprint(\"Imputing NaNs (if any) with median of each feature...\")\nimputer = SimpleImputer(strategy='median')\n\n# Fit on X_train and transform both train & val\nX_train_imputed = imputer.fit_transform(X_train)\nX_val_imputed   = imputer.transform(X_val)\n\n# (Optional) Save the imputer for later inference\nwith open(os.path.join(TEST_PATH, \"svm_imputer.pkl\"), \"wb\") as f:\n    pickle.dump(imputer, f)\n\n# ------------------- STANDARD SCALING -------------------\nscaler = StandardScaler()\nX_train_scaled = scaler.fit_transform(X_train_imputed)\nX_val_scaled   = scaler.transform(X_val_imputed)\n\n# Save the scaler for later inference\nwith open(os.path.join(TEST_PATH, \"svm_scaler.pkl\"), \"wb\") as f:\n    pickle.dump(scaler, f)\n\n# ------------------- TRAIN SVM WITH PROBABILITIES -------------------\nprint(\"Training SVM (RBF kernel, probability=True, class_weight='balanced')...\\n\")\nsvm_clf = SVC(\n    kernel=\"rbf\",\n    C=1.0,\n    probability=True,\n    class_weight=\"balanced\",\n    random_state=42\n)\n\nsvm_clf.fit(X_train_scaled, y_train)\n\n# ------------------- PREDICTIONS & METRICS -------------------\ntrain_proba = svm_clf.predict_proba(X_train_scaled)  # shape = (n_train, 6)\nval_proba   = svm_clf.predict_proba(X_val_scaled)    # shape = (n_val, 6)\n\ntrain_pred = np.argmax(train_proba, axis=1)\nval_pred   = np.argmax(val_proba,   axis=1)\n\ntrain_acc = accuracy_score(y_train, train_pred)\nval_acc   = accuracy_score(y_val,   val_pred)\n\ntrain_true_onehot = np.eye(N_CLASSES)[y_train]\nval_true_onehot   = np.eye(N_CLASSES)[y_val]\n\ntrain_kl = kl_divergence_numpy(train_true_onehot, train_proba)\nval_kl   = kl_divergence_numpy(val_true_onehot,   val_proba)\n\nprint(\"=== FINAL RESULTS ===\")\nprint(f\"Train Accuracy     → {train_acc * 100:.2f}%\")\nprint(f\"Validation Accuracy→ {val_acc   * 100:.2f}%\\n\")\n\nprint(f\"Train KL Divergence→ {train_kl:.6f}\")\nprint(f\"Val   KL Divergence→ {val_kl:.6f}\")\nprint(\"=====================\\n\")\n\nif train_acc >= 0.75 and val_acc >= 0.75:\n    print(\"✔ Both TRAIN and VALIDATION accuracy ≥ 75%\")\nelse:\n    print(\"⚠ Did not reach ≥ 75% on train and/or validation. Consider hyperparameter tuning.\")\n\n# ------------------- SAVE SVM MODEL & ARTIFACTS -------------------\nos.makedirs(os.path.join(TEST_PATH, \"models_svm\"), exist_ok=True)\nwith open(os.path.join(TEST_PATH, \"models_svm\", \"svm_model.pkl\"), \"wb\") as f:\n    pickle.dump(svm_clf, f)\n\nwith open(os.path.join(TEST_PATH, \"models_svm\", \"label_encoder.pkl\"), \"wb\") as f:\n    pickle.dump(label_encoder, f)\n\nprint(\"\\nSaved:\")\nprint(f\"  • SVM model   → {os.path.join(TEST_PATH, 'models_svm', 'svm_model.pkl')}\")\nprint(f\"  • LabelEncoder→ {os.path.join(TEST_PATH, 'models_svm', 'label_encoder.pkl')}\")\nprint(f\"  • Imputer     → {os.path.join(TEST_PATH, 'svm_imputer.pkl')}\")\nprint(f\"  • Scaler      → {os.path.join(TEST_PATH, 'svm_scaler.pkl')}\\n\")\n\nprint(\"🟢 SVM training + evaluation complete!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:44:22.972559Z","iopub.execute_input":"2025-06-02T14:44:22.973008Z"}},"outputs":[],"execution_count":null}]}