{"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":11614671,"sourceType":"datasetVersion","datasetId":7285696}],"dockerImageVersionId":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom glob import glob\nfrom tqdm.notebook import tqdm\nimport joblib\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom sklearn.model_selection import StratifiedGroupKFold, GroupKFold\nimport keras\nimport keras_cv\nimport matplotlib.pyplot as plt\nimport math\nimport warnings\nimport seaborn as sns\nfrom sklearn.metrics import confusion_matrix, classification_report, roc_auc_score\nimport tensorflow.keras.backend as K, gc\nfrom sklearn.utils import class_weight\nimport albumentations as albu\nfrom sklearn.utils.class_weight import compute_class_weight\nwarnings.filterwarnings('ignore')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:16.177097Z","iopub.execute_input":"2025-05-08T13:33:16.177412Z","iopub.status.idle":"2025-05-08T13:33:45.649371Z","shell.execute_reply.started":"2025-05-08T13:33:16.177388Z","shell.execute_reply":"2025-05-08T13:33:45.648073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gpus = tf.config.list_physical_devices('GPU')\nif len(gpus)<=1: \n    strategy = tf.distribute.OneDeviceStrategy(device=\"/gpu:0\")\n    print(f'Using {len(gpus)} GPU')\nelse: \n    strategy = tf.distribute.MirroredStrategy()\n    print(f'Using {len(gpus)} GPUs')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:45.650804Z","iopub.execute_input":"2025-05-08T13:33:45.651387Z","iopub.status.idle":"2025-05-08T13:33:45.663519Z","shell.execute_reply.started":"2025-05-08T13:33:45.651363Z","shell.execute_reply":"2025-05-08T13:33:45.662439Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    verbose = 1  # Verbosity\n    seed = 42  # Random seed\n    input_shape = (19, 1000, 1)\n    epochs = 10  # Training epochs\n    n_splits = 5 # Number of folds\n    segments_per_eeg = 8\n\n    sample_batch_size = 64\n\n    num_classes = 6  # Number of classes in the dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:45.664710Z","iopub.execute_input":"2025-05-08T13:33:45.665070Z","iopub.status.idle":"2025-05-08T13:33:45.690266Z","shell.execute_reply.started":"2025-05-08T13:33:45.665019Z","shell.execute_reply":"2025-05-08T13:33:45.689086Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NPY_DIR = '/kaggle/input/eeg-npy-unfiltered-90/eeg_npy_unfiltered/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:45.692777Z","iopub.execute_input":"2025-05-08T13:33:45.693124Z","iopub.status.idle":"2025-05-08T13:33:45.709106Z","shell.execute_reply.started":"2025-05-08T13:33:45.693100Z","shell.execute_reply":"2025-05-08T13:33:45.708235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"Load and preprocess data with patient-aware grouping\"\"\"\n# Load and group data as per your preprocessing\ndf = pd.read_csv('/kaggle/input/hms-harmful-brain-activity-classification/train.csv')\nTARGETS = df.columns[-6:]\n\n# Create grouped dataframe\ntrain = df.groupby('eeg_id')[['spectrogram_id','spectrogram_label_offset_seconds']].agg(\n    {'spectrogram_id':'first','spectrogram_label_offset_seconds':'min'})\ntrain.columns = ['spec_id','min']\n\ntmp = df.groupby('eeg_id')[['spectrogram_id','spectrogram_label_offset_seconds']].agg(\n    {'spectrogram_label_offset_seconds':'max'})\ntrain['max'] = tmp\n\ntmp = df.groupby('eeg_id')[['patient_id']].agg('first') \ntrain['patient_id'] = tmp\n\ntmp = df.groupby('eeg_id')[TARGETS].agg('sum')\nsum_targets = tmp.sum(axis=1)\nmax_vote_percentage = tmp.max(axis=1) / sum_targets\ntrain['max_vote_percentage'] = max_vote_percentage\n\ntmp = df.groupby('eeg_id')[TARGETS].agg('sum')\nfor t in TARGETS:\n    train[t] = tmp[t].values\n    \ny_data = train[TARGETS].values\ny_data = y_data / y_data.sum(axis=1,keepdims=True)\ntrain[TARGETS] = y_data\n\ntmp = df.groupby('eeg_id')[['expert_consensus']].agg('first') \ntrain['target'] = tmp\ntrain = train.reset_index()\ntrain = train[train['max_vote_percentage']>=.9]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:45.710139Z","iopub.execute_input":"2025-05-08T13:33:45.710463Z","iopub.status.idle":"2025-05-08T13:33:46.122784Z","shell.execute_reply.started":"2025-05-08T13:33:45.710436Z","shell.execute_reply":"2025-05-08T13:33:46.121738Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"frequency = train['target'].value_counts()\nprint(frequency)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:46.123945Z","iopub.execute_input":"2025-05-08T13:33:46.124656Z","iopub.status.idle":"2025-05-08T13:33:46.135698Z","shell.execute_reply.started":"2025-05-08T13:33:46.124625Z","shell.execute_reply":"2025-05-08T13:33:46.134576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_names = ['GPD', 'GRDA', 'LPD', 'LRDA', 'Other', 'Seizure']\nclass_to_idx = {name: i for i, name in enumerate(class_names)}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:46.136779Z","iopub.execute_input":"2025-05-08T13:33:46.137031Z","iopub.status.idle":"2025-05-08T13:33:46.158067Z","shell.execute_reply.started":"2025-05-08T13:33:46.137013Z","shell.execute_reply":"2025-05-08T13:33:46.156905Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"classes = np.unique(train['target'])\nclass_weights = class_weight.compute_class_weight(class_weight='balanced',\n                                                  classes=classes,\n                                                  y=train['target'])\nclass_weight_dict = dict(zip(classes, class_weights))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:46.159194Z","iopub.execute_input":"2025-05-08T13:33:46.159643Z","iopub.status.idle":"2025-05-08T13:33:46.189196Z","shell.execute_reply.started":"2025-05-08T13:33:46.159619Z","shell.execute_reply":"2025-05-08T13:33:46.187563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"all_eegs = {}\nfor eeg_id in train.eeg_id.unique():    \n    for i in range(1, CFG.segments_per_eeg + 1):  # segments 1 to 10\n        x = np.load(f'{NPY_DIR}{eeg_id}_{i}.npy')\n        all_eegs[(eeg_id, i)] = x  # key is tuple (eeg_id, segment_index)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:33:46.190399Z","iopub.execute_input":"2025-05-08T13:33:46.190701Z","iopub.status.idle":"2025-05-08T13:42:28.791662Z","shell.execute_reply.started":"2025-05-08T13:33:46.190679Z","shell.execute_reply":"2025-05-08T13:42:28.790251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DataGenerator(tf.keras.utils.Sequence):\n    'Generates data for Keras'\n    def __init__(self, data, batch_size=32, shuffle=False, augment=False, mode='train',\n                 eeg_specs=all_eegs, segments_per_eeg=CFG.segments_per_eeg, target_map=class_to_idx):\n\n        self.data = data\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.augment = augment\n        self.mode = mode\n        self.eeg_specs = eeg_specs\n        self.segments_per_eeg = segments_per_eeg\n        self.target_map = target_map\n\n        # Create a map from sample index to (original_data_index, segment_index)\n        self.sample_map = []\n        for i in range(len(self.data)): # Iterate through original eeg_ids\n            eeg_id = self.data.iloc[i].eeg_id\n            for seg in range(1, self.segments_per_eeg + 1):\n                # Check if this specific segment exists in our loaded data\n                if (eeg_id, seg) in self.eeg_specs:\n                     self.sample_map.append({'data_idx': i, 'segment_idx': seg})\n\n        self.on_epoch_end()\n\n    def __len__(self):\n        ct = math.ceil(len(self.sample_map) / self.batch_size)\n        print('len ', ct)\n        return ct\n\n    def __getitem__(self, index):\n        'Generate one batch of data'\n        # Get the indices for the *samples* in this batch\n        start_idx = index * self.batch_size\n        end_idx = (index + 1) * self.batch_size\n        print('start & end index ', start_idx, end_idx)\n        # Get the sample map indices for this batch from the shuffled list\n        batch_sample_map_indices = self.indexes[start_idx : end_idx]\n\n        # Generate data based on these sample map indices\n        X, y = self.__data_generation(batch_sample_map_indices)\n\n        if self.augment:\n            X = self.__augment_batch(X)\n\n        return X, y\n\n    def on_epoch_end(self):\n        'Updates indexes after each epoch'\n        # Indexes now refer to the positions in self.sample_map\n        self.indexes = np.arange(len(self.sample_map))\n        if self.shuffle:\n            np.random.shuffle(self.indexes)\n\n    def __data_generation(self, batch_sample_map_indices):\n        'Generates data containing batch_size samples'\n\n        # The size of X and y is determined by how many sample indices we got\n        num_samples_in_batch = len(batch_sample_map_indices)\n        print('num_samples_in batch ', num_samples_in_batch)\n        X = np.zeros((num_samples_in_batch, CFG.input_shape[0], CFG.input_shape[1], CFG.input_shape[2]), dtype='float32')\n        y = np.zeros(num_samples_in_batch, dtype='int32')\n\n        for j, map_idx in enumerate(batch_sample_map_indices):\n            # Get the actual mapping info {data_idx, segment_idx}\n            sample_info = self.sample_map[map_idx]\n            original_data_idx = sample_info['data_idx']\n            segment_idx = sample_info['segment_idx']\n\n            row = self.data.iloc[original_data_idx]\n            eeg_id = row.eeg_id\n            target_val = row['target'] \n            target_idx = self.target_map[target_val]\n\n            x = self.eeg_specs.get((eeg_id, segment_idx))\n\n            if x is not None:\n                if x.shape == (1000, 19) and CFG.input_shape[0] == 19 and CFG.input_shape[1] == 1000:\n                     X[j, :, :, 0] = np.transpose(x) # shape (19, 1000)\n                elif x.shape == (19, 1000) and CFG.input_shape[0] == 19 and CFG.input_shape[1] == 1000:\n                     X[j, :, :, 0] = x # shape (19, 1000)\n                else:\n                    print(f\"Warning: Shape mismatch for eeg {eeg_id}, seg {segment_idx}. Got {x.shape}, expected (1000, 19) or (19, 1000). Skipping.\")\n                    y[j] = -1\n                    continue \n\n                y[j] = target_idx\n            else:\n                 print(f\"Warning: Data for eeg {eeg_id}, segment {segment_idx} not found in eeg_specs dict. Skipping sample.\")\n                 y[j] = -1\n\n        valid_indices = np.where(y != -1)[0]\n        X = X[valid_indices]\n        y = y[valid_indices]\n\n        return X, y\n\n\n    def __random_transform(self, img):        \n        composition = albu.Compose([            \n            albu.GaussNoise(var_limit=(1.0, 10.0), p=0.3),\n            albu.CoarseDropout(max_holes=8, max_height=4, max_width=100, fill_value=0, p=0.5),\n        ])\n        \n        return composition(image=img)['image']\n\n    def __augment_batch(self, img_batch):\n        # Input img_batch shape: (batch_size, 19, 1000, 1)\n        for i in range(img_batch.shape[0]):\n            img_batch[i,] = self.__random_transform(img_batch[i,])\n        return img_batch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:42:28.795411Z","iopub.execute_input":"2025-05-08T13:42:28.795804Z","iopub.status.idle":"2025-05-08T13:42:28.814545Z","shell.execute_reply.started":"2025-05-08T13:42:28.795762Z","shell.execute_reply":"2025-05-08T13:42:28.813647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_eeg_cnn(input_shape=(19, 1000, 1), num_classes=6):\n    inputs = tf.keras.Input(shape=input_shape)\n    \n    l2_lambda = 1e-5\n    \n    x = layers.Conv2D(32, (3, 11), activation='relu', padding='same',\n                      kernel_regularizer=keras.regularizers.l2(l2_lambda))(inputs)\n    x = layers.BatchNormalization()(x)\n    x = layers.MaxPooling2D((1, 2))(x)\n\n    x = layers.Conv2D(64, (3, 9), activation='relu', padding='same',\n                      kernel_regularizer=keras.regularizers.l2(l2_lambda))(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.MaxPooling2D((1, 2))(x)\n\n    x = layers.Conv2D(128, (3, 7), activation='relu', padding='same',\n                      kernel_regularizer=keras.regularizers.l2(l2_lambda))(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.MaxPooling2D((1, 2))(x)\n\n    x = layers.Conv2D(256, (3, 5), activation='relu', padding='same',\n                      kernel_regularizer=keras.regularizers.l2(l2_lambda))(x)\n    x = layers.BatchNormalization()(x)\n    # x = layers.MaxPooling2D((1, 2))(x)\n    \n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dense(128, activation='relu', \n                     kernel_regularizer=keras.regularizers.l2(l2_lambda))(x)\n    x = layers.Dropout(0.6)(x)\n\n    outputs = layers.Dense(num_classes, activation='softmax')(x)\n\n    model = models.Model(inputs, outputs)\n\n    initial_learning_rate = 5e-4    \n    decay_steps = 5000  \n    alpha = 1e-6\n\n    cosine_decay_schedule = tf.keras.optimizers.schedules.CosineDecay(\n        initial_learning_rate=initial_learning_rate,\n        decay_steps=decay_steps,\n        alpha=alpha\n    )\n\n    optimizer = tf.keras.optimizers.AdamW(\n        learning_rate=cosine_decay_schedule,\n        weight_decay=1e-5\n    )\n\n    model.compile(\n        optimizer=optimizer,\n        loss=tf.keras.losses.SparseCategoricalCrossentropy(),\n        metrics=[\n            'accuracy'\n        ]\n    )\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:42:28.815685Z","iopub.execute_input":"2025-05-08T13:42:28.816028Z","iopub.status.idle":"2025-05-08T13:42:28.841020Z","shell.execute_reply.started":"2025-05-08T13:42:28.816000Z","shell.execute_reply":"2025-05-08T13:42:28.839963Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def main():\n    sgkf = StratifiedGroupKFold(n_splits=CFG.n_splits, shuffle=True, random_state=CFG.seed)\n\n    all_fold_preds = []\n    all_fold_trues = []\n    fold_metrics = []\n    \n    target_for_split = train['target'] \n    \n    for i, (train_index, valid_index) in enumerate(sgkf.split(train, target_for_split, train.patient_id)):\n        print('#' * 25)\n        print(f'### Fold {i+1}')\n    \n        # StratifiedGroupKFold splits based on original train DataFrame\n        train_data = train.iloc[train_index].reset_index(drop=True) # Reset index for clean iloc access later\n        valid_data = train.iloc[valid_index].reset_index(drop=True) # Reset index\n    \n        # Double-check leakage\n        assert len(set(train_data['patient_id']) & set(valid_data['patient_id'])) == 0, \"Patient leakage detected!\"\n    \n        train_ds = DataGenerator(train_data, shuffle=True, batch_size=CFG.sample_batch_size, augment=True,\n                                 eeg_specs=all_eegs, segments_per_eeg=CFG.segments_per_eeg, target_map=class_to_idx)\n        valid_ds = DataGenerator(valid_data, shuffle=False, batch_size=CFG.sample_batch_size, mode='valid',\n                                 eeg_specs=all_eegs, segments_per_eeg=CFG.segments_per_eeg, target_map=class_to_idx)\n    \n        keras.backend.clear_session()\n        model = build_eeg_cnn()\n        model.summary()\n    \n        # Callbacks\n        ckpt_path = f\"best_model_fold{i+1}.keras\"\n        ckpt_cb = keras.callbacks.ModelCheckpoint(\n            ckpt_path,\n            monitor='val_loss',\n            save_best_only=True,\n            save_weights_only=False, # Save entire model\n            mode='min'\n        )\n    \n        early_stopping_cb = keras.callbacks.EarlyStopping(\n            monitor='val_loss',\n            patience=5, # Increased patience slightly\n            restore_best_weights=True # Restore weights from best epoch\n        )\n    \n        # --- Train ---\n        print(f\"\\nTraining Fold {i+1}...\")\n        history = model.fit(\n            train_ds,\n            epochs=CFG.epochs,\n            validation_data=valid_ds,\n            callbacks=[ckpt_cb, early_stopping_cb],\n            verbose=CFG.verbose,\n            class_weight=class_weight_dict\n        )\n    \n        print(f\"\\nLoading best weights for Fold {i+1} from {ckpt_path}...\")\n        \n        best_model = keras.models.load_model(ckpt_path)\n    \n        print(f\"Evaluating Fold {i+1} on validation data...\")\n        # Evaluate using the generator\n        results = best_model.evaluate(valid_ds, verbose=0)\n        fold_metrics.append(results)\n        print(f\"Fold {i+1} Validation Metrics (Loss, Acc): {results}\")\n    \n    \n        print(f'### Fold {i+1} Finished ###\\n')\n        if i == 1:\n            break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:42:28.842225Z","iopub.execute_input":"2025-05-08T13:42:28.842629Z","iopub.status.idle":"2025-05-08T13:42:28.872673Z","shell.execute_reply.started":"2025-05-08T13:42:28.842601Z","shell.execute_reply":"2025-05-08T13:42:28.871007Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-08T13:42:28.873661Z","iopub.execute_input":"2025-05-08T13:42:28.874070Z"}},"outputs":[],"execution_count":null}]}