{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"databundleVersionId":46665,"sourceId":4117,"sourceType":"competition"},{"databundleVersionId":16509290,"datasetId":9966584,"sourceId":15577792,"sourceType":"datasetVersion"}],"dockerImageVersionId":31328,"isGpuEnabled":true,"isInternetEnabled":true,"language":"python","sourceType":"notebook"},"papermill":{"default_parameters":{},"duration":758.500972,"end_time":"2026-04-20T14:50:42.531284+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-04-20T14:38:04.030312+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"c03ea004-803f-41bc-a261-80aad7c2b788","cell_type":"markdown","source":"# Exp 02: Grayscale PNG Images with a Frozen MobileNetV2\n\nThis experiment evaluates an ImageNet-pretrained MobileNetV2 baseline for\nnine-class malware-family classification using grayscale PNG\nrepresentations stored as three-channel model inputs.\n\n| Component | Configuration |\n|---|---|\n| Training input | 10,868 labeled PNG images |\n| Test input | 10,873 unlabeled PNG images |\n| Image representation | Grayscale PNG represented with three channels |\n| Image resolution | 224 × 224 pixels |\n| Active architecture | MobileNetV2 |\n| Backbone initialization | ImageNet pretrained |\n| Backbone training | Frozen |\n| Input preprocessing | MobileNetV2 `preprocess_input` |\n| Data split | Stratified 80% training / 20% validation |\n| Batch size | 16 |\n| Class imbalance | Balanced class weights |\n| Training duration | 10 epochs |\n| Classifier head | Global average pooling, batch normalization, Dense-256, Dropout-0.5, Softmax-9 |\n| Final validation accuracy | 0.9669 |\n| Final validation loss | 0.2770 |\n\nThe notebook retains inactive ResNet50 and EfficientNetB0 branches for\ncontrolled architecture comparison. The active selection for this run is\n`my_model = 'mobilenetv2'`.\n","metadata":{}},{"id":"4a46467b-dc9f-472d-b8eb-4c0e9e0e9a08","cell_type":"markdown","source":"## 1. Dataset Path Verification\n","metadata":{}},{"id":"03b975f0","cell_type":"code","source":"import os\n\nprint(\n    \"Dataset directory contents:\",\n    os.listdir(\n        '/kaggle/input/datasets/kanedev/malware-datasets/'\n        'malware_png_train_224'\n    )\n)\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:06.456330Z","iopub.status.busy":"2026-04-20T14:38:06.455633Z","iopub.status.idle":"2026-04-20T14:38:06.472842Z","shell.execute_reply":"2026-04-20T14:38:06.471923Z"},"papermill":{"duration":0.022981,"end_time":"2026-04-20T14:38:06.474373+00:00","exception":false,"start_time":"2026-04-20T14:38:06.451392+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"c03a09b8-1f41-4750-ae2a-a46f3766c9e9","cell_type":"markdown","source":"## 2. Training Dataset Assembly\n","metadata":{}},{"id":"89d4d188","cell_type":"code","source":"import glob\nimport os\n\nimport pandas as pd\n\ncsv_path = (\n    '/kaggle/input/competitions/'\n    'malware-classification/trainLabels.csv'\n)\ndf = pd.read_csv(csv_path)\n\nimage_files = glob.glob(\n    '/kaggle/input/datasets/kanedev/malware-datasets/'\n    'malware_png_train_224/malware_png_train_224/*.png'\n)\n\ndf_files = pd.DataFrame({\n    'Id': [\n        os.path.basename(path).replace('.png', '')\n        for path in image_files\n    ],\n    'filepath': image_files\n})\n\ndf_final = pd.merge(df_files, df, on='Id', how='inner')\n\nprint(f\"Labeled training samples matched: {len(df_final):,}\")\ndisplay(df_final.head())\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:06.480784Z","iopub.status.busy":"2026-04-20T14:38:06.480055Z","iopub.status.idle":"2026-04-20T14:38:07.414395Z","shell.execute_reply":"2026-04-20T14:38:07.413639Z"},"papermill":{"duration":0.938981,"end_time":"2026-04-20T14:38:07.415844+00:00","exception":false,"start_time":"2026-04-20T14:38:06.476863+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8a65171e-4c87-4c78-b80b-17e4ad347805","cell_type":"markdown","source":"## 3. Image Loading and Custom Data Generator\n","metadata":{}},{"id":"e1c618c8","cell_type":"code","source":"import cv2\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\ndef load_png_image(filepath):\n    img = cv2.imread(filepath)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (224, 224))\n\n    return img\n\nclass MalwareDataGenerator(tf.keras.utils.Sequence):\n    def __init__(\n        self,\n        filepaths,\n        labels,\n        batch_size=32,\n        shuffle=True,\n        is_train=False,\n        **kwargs\n    ):\n        super().__init__(**kwargs)\n        self.filepaths = filepaths\n        self.labels = labels\n        self.batch_size = batch_size\n        self.indices = np.arange(len(self.filepaths))\n        self.shuffle = shuffle\n        self.is_train = is_train\n\n        if self.is_train:\n            self.augmenter = ImageDataGenerator(\n                width_shift_range=0.05,\n                height_shift_range=0.05,\n                fill_mode='constant',\n                cval=0.0\n            )\n\n        self.on_epoch_end()\n\n    def __len__(self):\n        return int(\n            np.ceil(len(self.filepaths) / float(self.batch_size))\n        )\n\n    def __getitem__(self, index):\n        batch_indices = self.indices[\n            index * self.batch_size:(index + 1) * self.batch_size\n        ]\n        X = []\n        y = []\n\n        for i in batch_indices:\n            img = load_png_image(self.filepaths[i])\n            X.append(img)\n\n            if self.is_train:\n                img = self.augmenter.random_transform(img)\n\n            y.append(\n                tf.keras.utils.to_categorical(\n                    self.labels[i],\n                    num_classes=9\n                )\n            )\n\n        return np.array(X), np.array(y)\n\n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.execute_input":"2026-04-20T14:38:07.423165Z","iopub.status.busy":"2026-04-20T14:38:07.422915Z","iopub.status.idle":"2026-04-20T14:38:33.842003Z","shell.execute_reply":"2026-04-20T14:38:33.841134Z"},"papermill":{"duration":26.425202,"end_time":"2026-04-20T14:38:33.844061+00:00","exception":false,"start_time":"2026-04-20T14:38:07.418859+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"d39bd8ec-2013-49e1-a378-bbbc89b86ca9","cell_type":"markdown","source":"## 4. Stratified Training–Validation Split\n","metadata":{}},{"id":"04601b2c","cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nX_paths = df_final['filepath'].values\ny_labels = df_final['Class'].values - 1\n\nX_train, X_val, y_train, y_val = train_test_split(\n    X_paths,\n    y_labels,\n    test_size=0.2,\n    random_state=42,\n    stratify=y_labels\n)\n\ntrain_gen = MalwareDataGenerator(\n    X_train,\n    y_train,\n    batch_size=16,\n    shuffle=True,\n    is_train=True,\n    workers=4,\n    use_multiprocessing=True\n)\nval_gen = MalwareDataGenerator(\n    X_val,\n    y_val,\n    batch_size=16,\n    shuffle=False,\n    is_train=False,\n    workers=4,\n    use_multiprocessing=True\n)\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:33.850939Z","iopub.status.busy":"2026-04-20T14:38:33.850539Z","iopub.status.idle":"2026-04-20T14:38:33.979588Z","shell.execute_reply":"2026-04-20T14:38:33.978729Z"},"papermill":{"duration":0.134328,"end_time":"2026-04-20T14:38:33.981212+00:00","exception":false,"start_time":"2026-04-20T14:38:33.846884+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"4edd5952-2b44-405c-b0ba-04198eda1bc5","cell_type":"markdown","source":"## 5. Training Class Distribution\n","metadata":{}},{"id":"08b918ca","cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\n\nplt.figure(figsize=(10, 5))\nsns.countplot(x='Class', data=df_final)\nplt.title('Malware Family Distribution in the Training Dataset')\nplt.show()\n\nprint(f\"Total labeled training files: {len(df_final):,}\")\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:33.988739Z","iopub.status.busy":"2026-04-20T14:38:33.987927Z","iopub.status.idle":"2026-04-20T14:38:34.679162Z","shell.execute_reply":"2026-04-20T14:38:34.678310Z"},"papermill":{"duration":0.696186,"end_time":"2026-04-20T14:38:34.680685+00:00","exception":false,"start_time":"2026-04-20T14:38:33.984499+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8398563c-efc3-4692-85d8-f8ac9687ea41","cell_type":"markdown","source":"## 6. Class-Imbalance Handling\n","metadata":{}},{"id":"846aad2e","cell_type":"code","source":"import numpy as np\nfrom sklearn.utils import class_weight\n\ny_train_labels = df_final['Class'].values - 1\n\nclass_weights = class_weight.compute_class_weight(\n    class_weight='balanced',\n    classes=np.unique(y_train_labels),\n    y=y_train_labels\n)\nclass_weight_dict = dict(enumerate(class_weights))\n\nprint(\n    \"Class weights \"\n    \"(higher values indicate underrepresented classes):\"\n)\nprint(class_weight_dict)\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:34.689851Z","iopub.status.busy":"2026-04-20T14:38:34.689229Z","iopub.status.idle":"2026-04-20T14:38:34.697894Z","shell.execute_reply":"2026-04-20T14:38:34.697090Z"},"papermill":{"duration":0.015185,"end_time":"2026-04-20T14:38:34.699177+00:00","exception":false,"start_time":"2026-04-20T14:38:34.683992+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"48ef5628-8655-4df2-b6a7-cb811af6d3e3","cell_type":"markdown","source":"## 7. Architecture Selection\n","metadata":{}},{"id":"52f7c3b2","cell_type":"code","source":"my_model = 'mobilenetv2'\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:34.706574Z","iopub.status.busy":"2026-04-20T14:38:34.706358Z","iopub.status.idle":"2026-04-20T14:38:34.709841Z","shell.execute_reply":"2026-04-20T14:38:34.709233Z"},"papermill":{"duration":0.008767,"end_time":"2026-04-20T14:38:34.711161+00:00","exception":false,"start_time":"2026-04-20T14:38:34.702394+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"7d7a49b8-ce59-4395-987f-6616dd175c5e","cell_type":"markdown","source":"## 8. Active Model: Frozen MobileNetV2\n","metadata":{}},{"id":"132b53d8","cell_type":"code","source":"from tensorflow.keras import layers, models\nfrom tensorflow.keras.applications import MobileNetV2\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input\n\nif my_model == 'mobilenetv2':\n    base_model = MobileNetV2(\n        weights='imagenet',\n        include_top=False,\n        input_shape=(224, 224, 3)\n    )\n    base_model.trainable = False\n\n    model = models.Sequential([\n        layers.Input(shape=(224, 224, 3)),\n        layers.Lambda(preprocess_input),\n        base_model,\n        layers.GlobalAveragePooling2D(),\n        layers.BatchNormalization(),\n        layers.Dense(256, activation='relu'),\n        layers.Dropout(0.5),\n        layers.Dense(9, activation='softmax')\n    ])\n\n    model.compile(\n        optimizer='adam',\n        loss='categorical_crossentropy',\n        metrics=['accuracy']\n    )\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:34.718391Z","iopub.status.busy":"2026-04-20T14:38:34.717968Z","iopub.status.idle":"2026-04-20T14:38:38.672381Z","shell.execute_reply":"2026-04-20T14:38:38.671751Z"},"papermill":{"duration":3.959822,"end_time":"2026-04-20T14:38:38.674120+00:00","exception":false,"start_time":"2026-04-20T14:38:34.714298+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"44950b31-b7ba-4c93-bd70-e0d52b7894d7","cell_type":"markdown","source":"## 9. Inactive Candidate: ResNet50\n","metadata":{}},{"id":"3a7b13bb","cell_type":"code","source":"from tensorflow.keras import layers, models\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.applications.resnet50 import preprocess_input\n\nif my_model == 'resnet50':\n    base_model = ResNet50(\n        weights='imagenet',\n        include_top=False,\n        input_shape=(224, 224, 3)\n    )\n    base_model.trainable = False\n\n    model = models.Sequential([\n        layers.Input(shape=(224, 224, 3)),\n        layers.Lambda(preprocess_input),\n        base_model,\n        layers.GlobalAveragePooling2D(),\n        layers.Dense(256, activation='relu'),\n        layers.Dropout(0.5),\n        layers.Dense(9, activation='softmax')\n    ])\n\n    model.compile(\n        optimizer='adam',\n        loss='categorical_crossentropy',\n        metrics=['accuracy']\n    )\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:38.681960Z","iopub.status.busy":"2026-04-20T14:38:38.681605Z","iopub.status.idle":"2026-04-20T14:38:38.688670Z","shell.execute_reply":"2026-04-20T14:38:38.688112Z"},"papermill":{"duration":0.012486,"end_time":"2026-04-20T14:38:38.690023+00:00","exception":false,"start_time":"2026-04-20T14:38:38.677537+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"9b7a6bca-583f-48b5-85e9-2c423d4d4e48","cell_type":"markdown","source":"## 10. Inactive Candidate: EfficientNetB0\n","metadata":{}},{"id":"bd026cb4","cell_type":"code","source":"from tensorflow.keras import layers, models\nfrom tensorflow.keras.applications import EfficientNetB0\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\n\nif my_model == 'efficientnetb0':\n    base_model = EfficientNetB0(\n        weights='imagenet',\n        include_top=False,\n        input_shape=(224, 224, 3)\n    )\n    base_model.trainable = False\n\n    model = models.Sequential([\n        layers.Input(shape=(224, 224, 3)),\n        layers.Lambda(preprocess_input),\n        base_model,\n        layers.GlobalAveragePooling2D(),\n        layers.BatchNormalization(),\n        layers.Dense(256, activation='relu'),\n        layers.Dropout(0.5),\n        layers.Dense(9, activation='softmax')\n    ])\n\n    model.compile(\n        optimizer='adam',\n        loss='categorical_crossentropy',\n        metrics=['accuracy']\n    )\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:38.697983Z","iopub.status.busy":"2026-04-20T14:38:38.697651Z","iopub.status.idle":"2026-04-20T14:38:38.704681Z","shell.execute_reply":"2026-04-20T14:38:38.704042Z"},"papermill":{"duration":0.012598,"end_time":"2026-04-20T14:38:38.706022+00:00","exception":false,"start_time":"2026-04-20T14:38:38.693424+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b7b43eab-12aa-47c0-86db-2a3072cd1c53","cell_type":"markdown","source":"## 11. Model Training\n","metadata":{}},{"id":"64cdba55","cell_type":"code","source":"history = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=10,\n    class_weight=class_weight_dict,\n    verbose=1\n)\n\nprint(\"Final training metrics\")\nprint(\n    f\"Training accuracy: \"\n    f\"{history.history['accuracy'][-1]:.4f}\"\n)\nprint(\n    f\"Validation accuracy: \"\n    f\"{history.history['val_accuracy'][-1]:.4f}\"\n)\nprint(\n    f\"Training loss: \"\n    f\"{history.history['loss'][-1]:.4f}\"\n)\nprint(\n    f\"Validation loss: \"\n    f\"{history.history['val_loss'][-1]:.4f}\"\n)\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:38:38.713665Z","iopub.status.busy":"2026-04-20T14:38:38.713302Z","iopub.status.idle":"2026-04-20T14:47:53.141272Z","shell.execute_reply":"2026-04-20T14:47:53.140069Z"},"papermill":{"duration":554.43374,"end_time":"2026-04-20T14:47:53.143097+00:00","exception":false,"start_time":"2026-04-20T14:38:38.709357+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"1da20948-842a-48b0-96ab-6f18467dfd4b","cell_type":"markdown","source":"## 12. Learning Curves\n","metadata":{}},{"id":"f7588aa3","cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(10, 4))\n\nplt.subplot(1, 2, 1)\nplt.plot(history.history['accuracy'], label='Training')\nplt.plot(history.history['val_accuracy'], label='Validation')\nplt.title('Accuracy')\nplt.legend()\n\nplt.subplot(1, 2, 2)\nplt.plot(history.history['loss'], label='Training')\nplt.plot(history.history['val_loss'], label='Validation')\nplt.title('Loss')\nplt.legend()\n\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:47:53.445850Z","iopub.status.busy":"2026-04-20T14:47:53.445561Z","iopub.status.idle":"2026-04-20T14:47:53.666994Z","shell.execute_reply":"2026-04-20T14:47:53.666191Z"},"papermill":{"duration":0.373286,"end_time":"2026-04-20T14:47:53.668751+00:00","exception":false,"start_time":"2026-04-20T14:47:53.295465+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"2884944a-de78-4ae3-8fc4-b7f40d610c23","cell_type":"markdown","source":"## 13. Model Artifact Export\n","metadata":{}},{"id":"1c262d27","cell_type":"code","source":"import pickle\n\nmodel.save('malware_mobilenetv2_final.keras')\n\nwith open('train_history.pkl', 'wb') as file:\n    pickle.dump(history.history, file)\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:47:53.972204Z","iopub.status.busy":"2026-04-20T14:47:53.971926Z","iopub.status.idle":"2026-04-20T14:47:54.447915Z","shell.execute_reply":"2026-04-20T14:47:54.447316Z"},"papermill":{"duration":0.623029,"end_time":"2026-04-20T14:47:54.449856+00:00","exception":false,"start_time":"2026-04-20T14:47:53.826827+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"daf5c858-2be8-4760-bfee-3e2d1abf4106","cell_type":"markdown","source":"## 14. Validation Evaluation\n","metadata":{}},{"id":"1f736630","cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, confusion_matrix\n\nprint(\"Generating detailed predictions for the validation set...\")\n\nval_predictions = model.predict(val_gen)\ny_pred = np.argmax(val_predictions, axis=1)\ny_true = y_val\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"VALIDATION CLASSIFICATION REPORT\")\nprint(\"=\" * 60)\n\ntarget_names = [f\"Class {i + 1}\" for i in range(9)]\nprint(\n    classification_report(\n        y_true,\n        y_pred,\n        target_names=target_names\n    )\n)\n\ncm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(10, 8))\nsns.heatmap(\n    cm,\n    annot=True,\n    fmt='d',\n    cmap='Blues',\n    xticklabels=target_names,\n    yticklabels=target_names\n)\nplt.title('Validation Confusion Matrix')\nplt.ylabel('True Label')\nplt.xlabel('Predicted Label')\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:47:54.745720Z","iopub.status.busy":"2026-04-20T14:47:54.745452Z","iopub.status.idle":"2026-04-20T14:48:06.813344Z","shell.execute_reply":"2026-04-20T14:48:06.812398Z"},"papermill":{"duration":12.216945,"end_time":"2026-04-20T14:48:06.815098+00:00","exception":false,"start_time":"2026-04-20T14:47:54.598153+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"2a5934c6-a8a0-4b28-baa9-c71721dba7fc","cell_type":"markdown","source":"## 15. Test Inference and Submission Generation\n","metadata":{}},{"id":"d8552074","cell_type":"code","source":"import glob\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\n\nTEST_PNG_DIR = (\n    '/kaggle/input/datasets/kanedev/malware-datasets/'\n    'malware_png_test_224/malware_png_test_224'\n)\ntest_image_paths = glob.glob(\n    os.path.join(TEST_PNG_DIR, '*.png')\n)\n\nprint(f\"Test PNG files discovered: {len(test_image_paths):,}\")\n\nif len(test_image_paths) > 0:\n    class MalwareTestGenerator(tf.keras.utils.Sequence):\n        def __init__(self, filepaths, batch_size=32):\n            self.filepaths = filepaths\n            self.batch_size = batch_size\n\n        def __len__(self):\n            return int(\n                np.ceil(len(self.filepaths) / float(self.batch_size))\n            )\n\n        def __getitem__(self, index):\n            batch_paths = self.filepaths[\n                index * self.batch_size:(index + 1) * self.batch_size\n            ]\n            X = []\n\n            for path in batch_paths:\n                img = load_png_image(path)\n                X.append(img)\n\n            return np.array(X)\n\n    test_gen = MalwareTestGenerator(\n        test_image_paths,\n        batch_size=32\n    )\n    predictions = model.predict(test_gen, verbose=1)\n\n    test_ids = [\n        os.path.basename(path).replace('.png', '')\n        for path in test_image_paths\n    ]\n\n    columns = [\n        'Prediction1',\n        'Prediction2',\n        'Prediction3',\n        'Prediction4',\n        'Prediction5',\n        'Prediction6',\n        'Prediction7',\n        'Prediction8',\n        'Prediction9'\n    ]\n\n    submission_df = pd.DataFrame(\n        predictions,\n        columns=columns\n    )\n    submission_df.insert(0, 'Id', test_ids)\n\n    submission_file = 'submission.csv'\n    submission_df.to_csv(submission_file, index=False)\n\n    print(f\"Submission file created: {submission_file}\")\n    display(submission_df.head())\nelse:\n    print(\n        \"No test PNG files were found. \"\n        \"Verify the test dataset path before generating predictions.\"\n    )\n","metadata":{"execution":{"iopub.execute_input":"2026-04-20T14:48:07.115503Z","iopub.status.busy":"2026-04-20T14:48:07.114235Z","iopub.status.idle":"2026-04-20T14:50:38.623083Z","shell.execute_reply":"2026-04-20T14:50:38.622275Z"},"papermill":{"duration":151.836633,"end_time":"2026-04-20T14:50:38.803555+00:00","exception":false,"start_time":"2026-04-20T14:48:06.966922+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}