{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":59093,"databundleVersionId":7469972,"sourceType":"competition"},{"sourceId":6127,"sourceType":"modelInstanceVersion","modelInstanceId":4598,"modelId":2797}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\n#import os\n#for dirname, _, filenames in os.walk('/kaggle/input/hms-harmful-brain-activity-classification'):\n#    for filename in filenames:\n#        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:38:41.562892Z","iopub.execute_input":"2025-07-31T08:38:41.563240Z","iopub.status.idle":"2025-07-31T08:38:41.567484Z","shell.execute_reply.started":"2025-07-31T08:38:41.563215Z","shell.execute_reply":"2025-07-31T08:38:41.566576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install --quiet lime\n\nfrom lime import lime_image\nfrom skimage.segmentation import mark_boundaries\nimport matplotlib.pyplot as plt\nimport numpy as np","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:38:45.699847Z","iopub.execute_input":"2025-07-31T08:38:45.700179Z","iopub.status.idle":"2025-07-31T08:38:50.399604Z","shell.execute_reply.started":"2025-07-31T08:38:45.700152Z","shell.execute_reply":"2025-07-31T08:38:50.398674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport os\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.metrics import accuracy_score, classification_report, confusion_matrix\nfrom tensorflow.keras.utils import to_categorical\nimport pyarrow.parquet as pq\nimport io\nimport scipy.signal\nfrom PIL import Image\n\n# Define dataset directories\ndata_csv = \"/kaggle/input/hms-harmful-brain-activity-classification/train.csv\"\nparquet_dir = \"/kaggle/input/hms-harmful-brain-activity-classification/train_spectrograms\"\n\n# Load metadata\ndf = pd.read_csv(data_csv)\n\n# Filter rows that have corresponding parquet files\nexisting_files = set(os.listdir(parquet_dir))\ndf = df[df[\"spectrogram_id\"].apply(lambda x: f\"{x}.parquet\" in existing_files)]\n\n# Limit to first 5000 samples\ndf = df.head(5000)\n\nif df.empty:\n    raise ValueError(\"No matching parquet files found. Check your directory and filenames.\")\n\n# Encode labels\ny = df[\"expert_consensus\"].dropna()\ndf = df.loc[y.index]\nle = LabelEncoder()\ny_encoded = le.fit_transform(y)\ny_categorical = to_categorical(y_encoded)\n\n# Metadata features\nX_meta = df[['seizure_vote', 'lpd_vote', 'gpd_vote', 'lrda_vote', 'grda_vote', 'other_vote']].values\nscaler = StandardScaler()\nX_meta = scaler.fit_transform(X_meta)\n\n# Helper to convert spectrogram parquet to image array\ndef load_spectrogram_as_image(spectrogram_id, size=(224, 224)):\n    file_path = os.path.join(parquet_dir, f\"{spectrogram_id}.parquet\")\n    table = pq.read_table(file_path)\n    data = table.to_pandas().values.T  # Transpose to get (freq, time)\n    data = np.log1p(data)\n    data = (data - np.min(data)) / (np.max(data) - np.min(data))\n    img = Image.fromarray(np.uint8(data * 255)).convert(\"RGB\").resize(size)\n    return np.array(img) / 255.0\n\n# Load spectrogram images\nX_images = np.stack([load_spectrogram_as_image(sid) for sid in df[\"spectrogram_id\"]])\n\n# Split dataset\nX_img_train, X_img_temp, X_meta_train, X_meta_temp, y_train, y_temp = train_test_split(\n    X_images, X_meta, y_categorical, test_size=0.3, random_state=42, stratify=y_categorical)\nX_img_val, X_img_test, X_meta_val, X_meta_test, y_val, y_test = train_test_split(\n    X_img_temp, X_meta_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp)\n\n# Define RetNet-inspired image model \nfrom tensorflow.keras.applications import ResNet50\n\nimage_input = keras.Input(shape=(224, 224, 3), name=\"image_input\")\nx = layers.Conv2D(32, (3, 3), activation='relu', padding='same')(image_input)\nx = layers.MaxPooling2D((2, 2))(x)\nx = layers.Conv2D(64, (3, 3), activation='relu', padding='same')(x)\nx = layers.MaxPooling2D((2, 2))(x)\nx = layers.Conv2D(128, (3, 3), activation='relu', padding='same')(x)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dense(128, activation='relu')(x)\nimage_features = layers.Dropout(0.3)(x)\n\n# Metadata model\nmeta_input = keras.Input(shape=(X_meta.shape[1],), name=\"meta_input\")\ny_meta = layers.Dense(32, activation='relu')(meta_input)\nmeta_features = layers.Dense(16, activation='relu')(y_meta)\n\n# Merge models\nmerged = layers.concatenate([image_features, meta_features])\nz = layers.Dense(64, activation='relu')(merged)\nout = layers.Dense(y_categorical.shape[1], activation='softmax')(z)\n\nmodel = keras.Model(inputs=[image_input, meta_input], outputs=out)\nmodel.compile(optimizer=keras.optimizers.Adam(learning_rate=1e-4),\n              loss='categorical_crossentropy', metrics=['accuracy'])\n\n# Train model\nmodel.fit(\n    x=[X_img_train, X_meta_train],\n    y=y_train,\n    epochs=10,\n    batch_size=32,\n    validation_data=([X_img_val, X_meta_val], y_val)\n)\n\n# Evaluate model\ny_pred = model.predict([X_img_test, X_meta_test])\ny_pred_classes = np.argmax(y_pred, axis=1)\ny_true_classes = np.argmax(y_test, axis=1)\n\nprint(\"Test Accuracy:\", accuracy_score(y_true_classes, y_pred_classes))\nprint(\"Test Report:\\n\", classification_report(y_true_classes, y_pred_classes))\n\n# Confusion Matrix\nplt.figure(figsize=(6, 4))\nplt.imshow(confusion_matrix(y_true_classes, y_pred_classes), cmap='Blues', interpolation='nearest')\nplt.colorbar()\nplt.title(\"Confusion Matrix - Test Set\")\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:21:23.326316Z","iopub.execute_input":"2025-07-31T08:21:23.326676Z","iopub.status.idle":"2025-07-31T08:26:00.655925Z","shell.execute_reply.started":"2025-07-31T08:21:23.326647Z","shell.execute_reply":"2025-07-31T08:26:00.655177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow.keras.backend as K\nimport matplotlib.cm as cm\n\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name=\"conv2d_2\", pred_index=None):\n    # Build a model that maps the inputs to the activations of the last conv layer and output\n    grad_model = tf.keras.models.Model(\n        inputs=[model.inputs],\n        outputs=[\n            model.get_layer(last_conv_layer_name).output,\n            model.output\n        ]\n    )\n\n    # Dummy metadata input of correct shape (1, 6)\n    dummy_meta = np.zeros((1, 6))\n\n    # Compute the gradient of the top predicted class\n    with tf.GradientTape() as tape:\n        conv_outputs, predictions = grad_model([img_array, dummy_meta])\n        if pred_index is None:\n            pred_index = tf.argmax(predictions[0])\n        class_channel = predictions[:, pred_index]\n\n    # Gradient of the output neuron w.r.t. the conv layer output\n    grads = tape.gradient(class_channel, conv_outputs)\n\n    # Global average pooling on the gradients\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n\n    # Weight the conv layer outputs with the pooled gradients\n    conv_outputs = conv_outputs[0]\n    heatmap = conv_outputs @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n\n    # Normalize between 0 and 1\n    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)\n    return heatmap.numpy()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:29:04.281323Z","iopub.execute_input":"2025-07-31T08:29:04.281653Z","iopub.status.idle":"2025-07-31T08:29:04.287806Z","shell.execute_reply.started":"2025-07-31T08:29:04.281629Z","shell.execute_reply":"2025-07-31T08:29:04.286925Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_gradcam(image, heatmap, alpha=0.4):\n    image_uint8 = np.uint8(255 * image)\n    heatmap_uint8 = np.uint8(255 * heatmap)\n\n    jet = cm.get_cmap(\"jet\")\n    jet_colors = jet(np.arange(256))[:, :3]\n    jet_heatmap = jet_colors[heatmap_uint8]\n\n    jet_heatmap = tf.keras.utils.array_to_img(jet_heatmap)\n    jet_heatmap = jet_heatmap.resize((image.shape[1], image.shape[0]))\n    jet_heatmap = np.array(jet_heatmap)\n\n    superimposed_img = jet_heatmap * alpha + image_uint8\n    superimposed_img = tf.keras.utils.array_to_img(superimposed_img)\n\n    plt.imshow(superimposed_img)\n    plt.axis('off')\n    plt.title(\"Grad-CAM Visualization\")\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:29:12.946779Z","iopub.execute_input":"2025-07-31T08:29:12.947106Z","iopub.status.idle":"2025-07-31T08:29:12.952260Z","shell.execute_reply.started":"2025-07-31T08:29:12.947077Z","shell.execute_reply":"2025-07-31T08:29:12.951462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Choose a sample from the test set\nsample_index = 0\nsample_img = X_img_test[sample_index:sample_index+1]     # shape: (1, 224, 224, 3)\nsample_meta = X_meta_test[sample_index:sample_index+1]   # shape: (1, 6)\n\n# Predict the class\npredicted_class = np.argmax(model.predict([sample_img, sample_meta]))\n\n# Generate Grad-CAM heatmap\nheatmap = make_gradcam_heatmap(sample_img, model, last_conv_layer_name=\"conv2d_1\", pred_index=predicted_class)\n\nplt.imshow(heatmap, cmap='viridis')\nplt.title(\"Raw Grad-CAM Heatmap\")\nplt.colorbar()\nplt.show()\n\n# Display the Grad-CAM result\ndisplay_gradcam(sample_img[0], heatmap)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:32:21.788353Z","iopub.execute_input":"2025-07-31T08:32:21.788657Z","iopub.status.idle":"2025-07-31T08:32:22.210892Z","shell.execute_reply.started":"2025-07-31T08:32:21.788634Z","shell.execute_reply":"2025-07-31T08:32:22.209854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_fn(imgs):\n    meta_dummy = np.zeros((imgs.shape[0], 6))  # Replace 6 with number of metadata features\n    return model.predict([imgs, meta_dummy])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:57:28.918581Z","iopub.execute_input":"2025-07-31T08:57:28.918987Z","iopub.status.idle":"2025-07-31T08:57:28.923238Z","shell.execute_reply.started":"2025-07-31T08:57:28.918955Z","shell.execute_reply":"2025-07-31T08:57:28.922346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from lime import lime_image\nexplainer = lime_image.LimeImageExplainer()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:57:37.066746Z","iopub.execute_input":"2025-07-31T08:57:37.067119Z","iopub.status.idle":"2025-07-31T08:57:37.071303Z","shell.execute_reply.started":"2025-07-31T08:57:37.067087Z","shell.execute_reply":"2025-07-31T08:57:37.070426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from skimage.segmentation import quickshift\nsample_index = 0\nsample_img = X_img_test[sample_index:sample_index+1]\n\nexplanation = explainer.explain_instance(\n    image=np.array(sample_img[0]),\n    classifier_fn=predict_fn,\n    top_labels=1,\n    hide_color=0,\n    num_samples=1000,\n    segmentation_fn=lambda x: quickshift(x, kernel_size=2, max_dist=10, ratio=0.2)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:58:50.070032Z","iopub.execute_input":"2025-07-31T08:58:50.070418Z","iopub.status.idle":"2025-07-31T08:59:05.083542Z","shell.execute_reply.started":"2025-07-31T08:58:50.070385Z","shell.execute_reply":"2025-07-31T08:59:05.082726Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from skimage.segmentation import mark_boundaries\nimport matplotlib.pyplot as plt\n\npredicted_class = explanation.top_labels[0]\ntemp, mask = explanation.get_image_and_mask(\n    label=predicted_class,\n    positive_only=True,\n    num_features=8,\n    hide_rest=False\n)\n\nfig, ax = plt.subplots(figsize=(6, 6))\nax.imshow(mark_boundaries(temp / 255.0, mask))\nax.set_title(f\"LIME Explanation - Class {predicted_class}\")\nax.axis('off')\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T08:59:10.899786Z","iopub.execute_input":"2025-07-31T08:59:10.900155Z","iopub.status.idle":"2025-07-31T08:59:11.090721Z","shell.execute_reply.started":"2025-07-31T08:59:10.900124Z","shell.execute_reply":"2025-07-31T08:59:11.089460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from lime import lime_image\nfrom skimage.segmentation import quickshift, mark_boundaries\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# Step 1: Pick test sample and original image\nsample_index = 0\nsample_img = X_img_test[sample_index:sample_index+1]  # (1, 224, 224, 3)\noriginal_img = sample_img[0]\n\n# Step 2: Ensure image is in uint8 format (0–255)\nif original_img.max() <= 1.0:\n    display_img = (original_img * 255).astype(np.uint8)\nelse:\n    display_img = original_img.astype(np.uint8)\n\n# Step 3: LIME explainer\nexplainer = lime_image.LimeImageExplainer()\nexplanation = explainer.explain_instance(\n    image=display_img,\n    classifier_fn=predict_fn,\n    top_labels=1,\n    hide_color=0,\n    num_samples=1000,\n    segmentation_fn=lambda x: quickshift(x, kernel_size=2, max_dist=10, ratio=0.2)\n)\n\n# Step 4: Get mask for most important regions\ntemp, mask = explanation.get_image_and_mask(\n    label=explanation.top_labels[0],\n    positive_only=True,\n    hide_rest=False,\n    num_features=5,\n    min_weight=0.0\n)\n\n# Step 5: Plot overlay of original image + LIME boundary\nplt.figure(figsize=(6, 6))\nplt.imshow(mark_boundaries(display_img, mask))  # NOT temp — we show original image with mask\nplt.title(f\"LIME Explanation - Class {explanation.top_labels[0]}\")\nplt.axis('off')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T09:02:58.758134Z","iopub.execute_input":"2025-07-31T09:02:58.758479Z","iopub.status.idle":"2025-07-31T09:03:12.979219Z","shell.execute_reply.started":"2025-07-31T09:02:58.758454Z","shell.execute_reply":"2025-07-31T09:03:12.978144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}