{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.14","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"},{"sourceId":274457,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":235005,"modelId":256700},{"sourceId":274459,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":235007,"modelId":256702}],"dockerImageVersionId":30762,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Data Loading, Preprocessing, Labeling, & Augmentation","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport pydicom\nfrom sklearn.model_selection import train_test_split\n\nBASE_PATH = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\n# Load CSVs\ntrain_df = pd.read_csv(os.path.join(BASE_PATH, \"train.csv\"))\nseries_df = pd.read_csv(os.path.join(BASE_PATH, \"train_series_descriptions.csv\"))\ncoords_df = pd.read_csv(os.path.join(BASE_PATH, \"train_label_coordinates.csv\"))\n\n# Map labels & fill missing values\nseverity_map = {\"Normal/Mild\": 0, \"Moderate\": 1, \"Severe\": 2}\nlabel_cols = [col for col in train_df.columns if col != \"study_id\"]\nfor col in label_cols:\n    train_df[col] = train_df[col].fillna(\"Normal/Mild\").map(severity_map)\n\n# Select Sagittal T2 series\nmask = series_df[\"series_description\"].str.contains(\"Sag\", case=False) & series_df[\"series_description\"].str.contains(\"T2\", case=False)\nsag_df = series_df[mask].copy()\n\n# Choose one instance per study based on coordinate data\ncoords_merged = pd.merge(sag_df, coords_df, on=[\"study_id\", \"series_id\"], how=\"inner\")\nslice_df = coords_merged.groupby(\"study_id\", as_index=False)[\"instance_number\"].min()\nsag_slice_df = pd.merge(sag_df, slice_df, on=\"study_id\", how=\"inner\")\n\n# Merge with labels and build file paths\ndata_df = pd.merge(sag_slice_df, train_df, on=\"study_id\", how=\"inner\")\ndef build_path(row):\n    return os.path.join(BASE_PATH, \"train_images\", str(row[\"study_id\"]), str(row[\"series_id\"]), f\"{row['instance_number']}.dcm\")\ndata_df[\"dicom_path\"] = data_df.apply(build_path, axis=1)\nprint(\"Total samples:\", len(data_df))\n\n# Process labels: (N,25) -> (N,5,5) -> one-hot encode -> (N,5,5,3) -> reshape to (N,5,15)\nlabels = data_df[label_cols].values\nlabels = labels.reshape((-1, 5, 5))\nlabels_onehot = tf.one_hot(labels, depth=3).numpy()\nlabels_onehot = labels_onehot.reshape((-1, 5, 15))\nlabels_tuple = tuple(labels_onehot[:, i, :] for i in range(5))\n\npaths = data_df[\"dicom_path\"].tolist()\nprint(\"Paths:\", len(paths), \"Labels:\", labels_onehot.shape[0])\nassert len(paths) == labels_onehot.shape[0], \"Mismatch in paths and labels!\"\n\n# Train/Validation split\ntrain_paths, val_paths, train_labels_np, val_labels_np = train_test_split(paths, labels_onehot, test_size=0.2, random_state=42)\ntrain_labels_tuple = tuple(train_labels_np[:, i, :] for i in range(5))\nval_labels_tuple = tuple(val_labels_np[:, i, :] for i in range(5))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_dicom(path):\n    dcm = pydicom.dcmread(path.numpy().decode())\n    image = dcm.pixel_array.astype(np.float32)\n    image -= np.min(image)\n    image /= np.ptp(image)\n    if image.ndim == 2:\n        image = np.expand_dims(image, axis=-1)\n    elif image.shape[-1] != 1:\n        image = image[..., :1]\n    image = np.repeat(image, 3, axis=-1)\n    image = tf.image.resize(image, (224, 224))\n    return image\n\ndef preprocess_image(path, labels):\n    image = tf.py_function(load_dicom, [path], Tout=tf.float32)\n    image.set_shape([224, 224, 3])\n    return image, labels","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ROI Cropping (using center crop as a placeholder)\ndef roi_crop(image, crop_size=(160, 160)):\n    shape = tf.shape(image)\n    h, w = shape[0], shape[1]\n    ch, cw = crop_size\n    start_h = (h - ch) // 2\n    start_w = (w - cw) // 2\n    cropped = tf.image.crop_to_bounding_box(image, start_h, start_w, ch, cw)\n    return tf.image.resize(cropped, (224, 224))\n\ndef preprocess_cropped_ensemble(path, labels):\n    img, labels = preprocess_image(path, labels)\n    cropped_img = roi_crop(img)\n    return {\"cnn_input\": cropped_img, \"vit_input\": cropped_img}, labels","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Augmentation and dataset construction (using ROI-cropped images)\ndef augment_image(image, labels):\n    image = tf.image.random_flip_left_right(image)\n    image = tf.image.random_brightness(image, max_delta=0.1)\n    image = tf.image.random_contrast(image, lower=0.9, upper=1.1)\n    return image, labels\n\n# Training dataset with cropping\ntrain_ds_ensemble = tf.data.Dataset.from_tensor_slices((train_paths, train_labels_tuple))\ntrain_ds_ensemble = train_ds_ensemble.map(preprocess_cropped_ensemble, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds_ensemble = train_ds_ensemble.map(lambda inp, lbl: (\n    {\"cnn_input\": augment_image(inp[\"cnn_input\"], lbl)[0],\n     \"vit_input\": augment_image(inp[\"vit_input\"], lbl)[0]}, lbl),\n    num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds_ensemble = train_ds_ensemble.shuffle(512).batch(16).prefetch(tf.data.AUTOTUNE)\n\n# Validation dataset with cropping\nval_ds_ensemble = tf.data.Dataset.from_tensor_slices((val_paths, val_labels_tuple))\nval_ds_ensemble = val_ds_ensemble.map(preprocess_cropped_ensemble, num_parallel_calls=tf.data.AUTOTUNE)\nval_ds_ensemble = val_ds_ensemble.batch(16).prefetch(tf.data.AUTOTUNE)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Definition, Initial Training, and Fine-Tuning","metadata":{}},{"cell_type":"code","source":"from transformers import ViTConfig, TFViTModel\nimport tensorflow as tf\nfrom tensorflow.keras.layers import Layer, Input, GlobalAveragePooling2D, Dense, Dropout, Concatenate\nfrom tensorflow.keras.models import Model\n\n# Create a configuration dictionary\nconfig_dict = {\n    \"_name_or_path\": \"google/vit-base-patch16-224-in21k\",\n    \"architectures\": [\"ViTModel\"],\n    \"attention_probs_dropout_prob\": 0.0,\n    \"hidden_act\": \"gelu\",\n    \"hidden_dropout_prob\": 0.0,\n    \"hidden_size\": 768,\n    \"image_size\": 224,\n    \"initializer_range\": 0.02,\n    \"intermediate_size\": 3072,\n    \"layer_norm_eps\": 1e-12,\n    \"model_type\": \"vit\",\n    \"num_attention_heads\": 12,\n    \"num_channels\": 3,\n    \"num_hidden_layers\": 12,\n    \"patch_size\": 16,\n    \"qkv_bias\": True,\n    \"transformers_version\": \"4.13.0.dev0\"\n}\nconfig = ViTConfig.from_dict(config_dict)\n\n# Updated ViTLayer that loads model weights from a local cache directory using the config\nclass ViTLayer(Layer):\n    def __init__(self, model_name='google/vit-base-patch16-224-in21k',\n                 local_path='/kaggle/input/googlevit-base-patch16-224-in21k/tensorflow2/default/1', **kwargs):\n        super(ViTLayer, self).__init__(**kwargs)\n        # Load the pretrained model using the provided config\n        self.vit = TFViTModel.from_pretrained(local_path, config=config)\n        self.vit.trainable = False\n    def call(self, inputs):\n        inputs_nchw = tf.transpose(inputs, perm=[0, 3, 1, 2])\n        outputs = self.vit(pixel_values=inputs_nchw, training=False)\n        return outputs.last_hidden_state[:, 0, :]\n    def compute_output_shape(self, input_shape):\n        return (input_shape[0], self.vit.config.hidden_size)\n\n# CNN branch: load EfficientNetB0 weights from local path\ncnn_base = tf.keras.applications.EfficientNetB0(\n    include_top=False, \n    weights='/kaggle/input/efficientnetb0/keras/default/1/efficientnetb0_notop.h5', \n    input_shape=(224,224,3)\n)\ncnn_base.trainable = False\ncnn_input = Input(shape=(224,224,3), name='cnn_input')\ncnn_x = cnn_base(cnn_input, training=False)\ncnn_x = GlobalAveragePooling2D()(cnn_x)\n\n# ViT branch\nvit_input = Input(shape=(224,224,3), name='vit_input')\nvit_x = ViTLayer()(vit_input)\n\n# Combine features and create multi-head outputs\ncombined = Concatenate()([cnn_x, vit_x])\ncombined = Dropout(0.3)(combined)\ncondition_names = ['LNFN','RNFN','LSS','RSS','SCS']\noutputs = []\nfor cond in condition_names:\n    x = Dense(128, activation='relu')(combined)\n    out = Dense(15, activation='softmax', name=f\"{cond}_output\")(x)\n    outputs.append(out)\n    \nensemble_model = Model(inputs=[cnn_input, vit_input], outputs=outputs)\nensemble_model.summary()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def focal_loss(gamma=2., alpha=.5):\n    def focal_loss_fixed(y_true, y_pred):\n        epsilon = 1e-7\n        y_pred = tf.clip_by_value(y_pred, epsilon, 1. - epsilon)\n        cross_entropy = -y_true * tf.math.log(y_pred)\n        weight = alpha * tf.math.pow(1 - y_pred, gamma)\n        loss = weight * cross_entropy\n        return tf.reduce_mean(tf.reduce_sum(loss, axis=-1))\n    return focal_loss_fixed\n\nlosses = {f\"{name}_output\": focal_loss(gamma=2., alpha=0.25) for name in condition_names}\nmetrics = {f\"{name}_output\": [\"accuracy\"] for name in condition_names}\n\n# Initial training with Adam (LR=1e-4)\nensemble_model.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n    loss=losses,\n    metrics=metrics\n)\n\nhistory_initial = ensemble_model.fit(\n    train_ds_ensemble,\n    validation_data=val_ds_ensemble,\n    epochs=5\n)\n\n# Fine-tuning: unfreeze CNN base, lower learning rate to 1e-5\ncnn_base.trainable = True\nensemble_model.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),\n    loss=losses,\n    metrics=metrics\n)\n\nhistory_finetune = ensemble_model.fit(\n    train_ds_ensemble,\n    validation_data=val_ds_ensemble,\n    epochs=3\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submission File","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport tensorflow as tf\nimport numpy as np\n\n# Load test series descriptions\ntest_series_df = pd.read_csv(os.path.join(BASE_PATH, \"test_series_descriptions.csv\"))\nprint(\"Test Series DataFrame Head:\")\nprint(test_series_df.head())\nprint(\"Unique study_ids:\", test_series_df[\"study_id\"].nunique())\n\n# Get unique study_ids (all studies need predictions, not just Sagittal T2 series)\nstudy_ids = test_series_df[\"study_id\"].unique()\nnum_studies = len(study_ids)\nprint(\"Number of studies:\", num_studies)\n\n# Filter for Sagittal T2 images (for spinal canal stenosis, adjust as needed)\nmask = test_series_df[\"series_description\"].str.contains(\"Sag\", case=False) & \\\n       test_series_df[\"series_description\"].str.contains(\"T2\", case=False)\ntest_df = test_series_df[mask].copy()\nprint(\"Filtered Test Series:\", len(test_df))\n\n# Build dicom path\ndef build_test_path(row):\n    return os.path.join(BASE_PATH, \"test_images\", str(row[\"study_id\"]), str(row[\"series_id\"]), \"1.dcm\")\n    \ntest_df[\"dicom_path\"] = test_df.apply(build_test_path, axis=1)\ntest_paths = test_df[\"dicom_path\"].tolist()\n\n# Preprocess\ndef preprocess_test_ensemble(path):\n    image, _ = preprocess_image(path, 0)\n    cropped_image = roi_crop(image)\n    return {\"cnn_input\": cropped_image, \"vit_input\": cropped_image}\n\ntest_ds = tf.data.Dataset.from_tensor_slices(test_paths)\ntest_ds = test_ds.map(lambda path: preprocess_test_ensemble(path), num_parallel_calls=tf.data.AUTOTUNE)\ntest_ds = test_ds.batch(16).prefetch(tf.data.AUTOTUNE)\n\n# Generate predictions\npredictions = ensemble_model.predict(test_ds)\n\n# Reshape predictions (assuming model outputs predictions for all studies)\npreds_by_condition = [pred.reshape(-1, 5, 3) for pred in predictions]\n\n# Map conditions and disc levels\ncond_mapping = {\n    \"LNFN\": \"left_neural_foraminal_narrowing\",\n    \"RNFN\": \"right_neural_foraminal_narrowing\",\n    \"LSS\": \"left_subarticular_stenosis\",\n    \"RSS\": \"right_subarticular_stenosis\",\n    \"SCS\": \"spinal_canal_stenosis\"\n}\ndisc_levels = [\"l1_l2\", \"l2_l3\", \"l3_l4\", \"l4_l5\", \"l5_s1\"]\n\n# Build submission for all studies\nrows = []\nfor i, study_id in enumerate(study_ids):\n    for j, cond in enumerate([\"LNFN\", \"RNFN\", \"LSS\", \"RSS\", \"SCS\"]):\n        for k in range(5):\n            row_id = f\"{study_id}_{cond_mapping[cond]}_{disc_levels[k]}\"\n            # Adjust index based on how predictions align with studies\n            pred_idx = min(i, len(preds_by_condition[j]) - 1)  # Fallback if predictions < studies\n            raw_prob = preds_by_condition[j][pred_idx, k, :]\n            norm_prob = tf.nn.softmax(raw_prob).numpy()\n            rows.append({\n                \"row_id\": row_id,\n                \"normal_mild\": norm_prob[0],\n                \"moderate\": norm_prob[1],\n                \"severe\": norm_prob[2]\n            })\n\nsubmission_df = pd.DataFrame(rows)\n\n# Normalize probabilities\nprobs = submission_df[['normal_mild', 'moderate', 'severe']].values\nprobs = probs / probs.sum(axis=1, keepdims=True)\nsubmission_df[['normal_mild', 'moderate', 'severe']] = probs\n\n# Save and verify\nsubmission_df.to_csv(\"/kaggle/working/submission.csv\", index=False)\ndf = pd.read_csv(\"/kaggle/working/submission.csv\")\nprint(\"Submission Shape:\", df.shape)  # Should be (num_studies * 25, 4)\nprint(\"Submission Columns:\", df.columns.tolist())\nprint(\"Submission Head:\")\nprint(df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}