{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"notebook setup","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nimport pandas as pd\nimport tensorflow as tf\n\n# 1. Check TensorFlow and available GPUs\nprint(\"TensorFlow version:\", tf.__version__)\n\ngpus = tf.config.list_physical_devices(\"GPU\")\nprint(\"Number of GPUs detected:\", len(gpus))\n\nfor gpu in gpus:\n    print(\"GPU:\", gpu)\n\n# 2. Locate the attached CIFAR-10 competition data\nlabel_files = list(Path(\"/kaggle/input\").rglob(\"trainLabels.csv\"))\n\nif len(label_files) != 1:\n    raise RuntimeError(\n        f\"Expected one trainLabels.csv file, found {len(label_files)}.\"\n    )\n\nDATA_DIR = label_files[0].parent\nprint(\"\\nData folder:\", DATA_DIR)\nprint(\"Available files:\", sorted(p.name for p in DATA_DIR.iterdir()))\n\n# 3. Read the training labels\nlabels = pd.read_csv(DATA_DIR / \"trainLabels.csv\")\n\nprint(\"\\nNumber of training labels:\", len(labels))\nprint(\"Number of classes:\", labels[\"label\"].nunique())\nprint(\"Classes:\", sorted(labels[\"label\"].unique()))\n\ndisplay(labels.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:30:02.736361Z","iopub.execute_input":"2026-09-21T10:30:02.736816Z","iopub.status.idle":"2026-09-21T10:30:21.30889Z","shell.execute_reply.started":"2026-09-21T10:30:02.736788Z","shell.execute_reply":"2026-09-21T10:30:21.308042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"now we’ll prepare:\n- 45,000 training images for the model to learn from\n- 5,000 validation images to check its accuracy during development","metadata":{}},{"cell_type":"code","source":"%pip install -q py7zr\n\nfrom pathlib import Path\nimport py7zr\n\nEXTRACT_DIR = Path(\"/kaggle/working/cifar10_data\")\nTRAIN_DIR = EXTRACT_DIR / \"train\"\nEXTRACT_DIR.mkdir(parents=True, exist_ok=True)\n\n# Match each image to its row in the label table.\nimage_paths = [\n    TRAIN_DIR / f\"{image_id}.png\"\n    for image_id in labels[\"id\"]\n]\n\n# Skip extraction on later runs if all expected images exist.\nif not all(path.is_file() for path in image_paths):\n    print(\"Extracting training images...\")\n\n    with py7zr.SevenZipFile(DATA_DIR / \"train.7z\", mode=\"r\") as archive:\n        archive.extractall(path=EXTRACT_DIR)\n\nmissing = [path for path in image_paths if not path.is_file()]\nassert not missing, f\"{len(missing)} training images are missing.\"\n\nprint(\"Training images found:\", len(image_paths))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:30:21.310206Z","iopub.execute_input":"2026-09-21T10:30:21.310539Z","iopub.status.idle":"2026-09-21T10:31:06.298615Z","shell.execute_reply.started":"2026-09-21T10:30:21.310503Z","shell.execute_reply":"2026-09-21T10:31:06.297744Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.auto import tqdm\n\nSEED = 42\n\n# Convert category names into numbers from 0 to 9.\n# Keep this mapping: we will need it for the Kaggle submission.\nclass_names = sorted(labels[\"label\"].unique())\nclass_to_index = {\n    name: index for index, name in enumerate(class_names)\n}\n\ny = labels[\"label\"].map(class_to_index).to_numpy(dtype=np.int64)\n\n# Load images in exactly the same order as their labels.\nX = np.empty((len(image_paths), 32, 32, 3), dtype=np.uint8)\n\nfor index, path in enumerate(tqdm(image_paths, desc=\"Loading images\")):\n    with Image.open(path) as img:\n        assert img.size == (32, 32), f\"Unexpected image size: {path}\"\n        X[index] = np.asarray(img.convert(\"RGB\"))\n\n# Reserve 10% of the images for validation.\nX_train, X_val, y_train, y_val = train_test_split(\n    X,\n    y,\n    test_size=0.10,\n    random_state=SEED,\n    stratify=y\n)\n\nprint(\"Training images:\", X_train.shape)\nprint(\"Validation images:\", X_val.shape)\nprint(\"Training labels:\", y_train.shape)\nprint(\"Validation labels:\", y_val.shape)\nprint(\"Validation images per class:\", np.bincount(y_val))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:31:06.299662Z","iopub.execute_input":"2026-09-21T10:31:06.300373Z","iopub.status.idle":"2026-09-21T10:31:15.729Z","shell.execute_reply.started":"2026-09-21T10:31:06.300341Z","shell.execute_reply":"2026-09-21T10:31:15.728028Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now, we’ll inspect some images and build the CNN\n","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nfig, axes = plt.subplots(2, 5, figsize=(12, 5))\n\nfor class_id, ax in enumerate(axes.flat):\n    index = np.flatnonzero(y_train == class_id)[0]\n\n    ax.imshow(X_train[index])\n    ax.set_title(class_names[class_id])\n    ax.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:31:15.731001Z","iopub.execute_input":"2026-09-21T10:31:15.731821Z","iopub.status.idle":"2026-09-21T10:31:16.213017Z","shell.execute_reply.started":"2026-09-21T10:31:15.731791Z","shell.execute_reply":"2026-09-21T10:31:16.212282Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow import keras\nfrom tensorflow.keras import layers, regularizers\n\nkeras.backend.clear_session()\nkeras.utils.set_random_seed(SEED)\n\n# A convolution followed by batch normalization and optional ReLU.\ndef conv_bn(x, filters, kernel_size=3, stride=1, activate=True):\n    x = layers.Conv2D(\n        filters,\n        kernel_size,\n        strides=stride,\n        padding=\"same\",\n        use_bias=False,\n        kernel_initializer=\"he_normal\",\n        kernel_regularizer=regularizers.l2(1e-4)\n    )(x)\n\n    x = layers.BatchNormalization(momentum=0.9)(x)\n\n    if activate:\n        x = layers.ReLU()(x)\n\n    return x\n\n\n# A residual block combines new features with its input.\ndef residual_block(x, filters, stride=1):\n    shortcut = x\n\n    x = conv_bn(x, filters, stride=stride)\n    x = conv_bn(x, filters, activate=False)\n\n    # Match the shortcut's dimensions when the image size\n    # or number of feature channels changes.\n    if stride != 1 or shortcut.shape[-1] != filters:\n        shortcut = conv_bn(\n            shortcut,\n            filters,\n            kernel_size=1,\n            stride=stride,\n            activate=False\n        )\n\n    x = layers.Add()([x, shortcut])\n    return layers.ReLU()(x)\n\n\ninputs = keras.Input(shape=(32, 32, 3), name=\"image\")\n\n# Training augmentation: horizontal flips and shifts of up to 4 pixels.\nx = layers.RandomFlip(\"horizontal\", seed=SEED)(inputs)\nx = layers.RandomTranslation(\n    height_factor=0.125,\n    width_factor=0.125,\n    fill_mode=\"reflect\",\n    interpolation=\"nearest\",\n    seed=SEED + 1\n)(x)\n\n# Convert pixel values from [0, 255] to [-1, 1].\nx = layers.Rescaling(scale=1.0 / 127.5, offset=-1.0)(x)\n\n# Initial feature extraction.\nx = conv_bn(x, filters=32)\n\n# Three stages, each containing three residual blocks.\nfor stage, filters in enumerate([32, 64, 128]):\n    for block in range(3):\n        stride = 2 if stage > 0 and block == 0 else 1\n        x = residual_block(x, filters, stride=stride)\n\n# Convert the learned features into probabilities for the 10 classes.\nx = layers.GlobalAveragePooling2D()(x)\noutputs = layers.Dense(\n    len(class_names),\n    activation=\"softmax\",\n    kernel_regularizer=regularizers.l2(1e-4)\n)(x)\n\nmodel = keras.Model(inputs, outputs, name=\"cifar10_resnet\")\n\n# Verify that four images produce four valid probability vectors.\nprobabilities = model(X_train[:4], training=False).numpy()\n\nassert probabilities.shape == (4, 10)\nassert np.isfinite(probabilities).all()\nnp.testing.assert_allclose(\n    probabilities.sum(axis=1),\n    np.ones(4),\n    atol=1e-5\n)\n\nprint(\"Model parameters:\", f\"{model.count_params():,}\")\nprint(\"Model output shape:\", model.output_shape)\nprint(\"Forward pass successful:\", probabilities.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:31:16.214141Z","iopub.execute_input":"2026-09-21T10:31:16.214541Z","iopub.status.idle":"2026-09-21T10:31:20.149324Z","shell.execute_reply.started":"2026-09-21T10:31:16.214504Z","shell.execute_reply":"2026-09-21T10:31:20.148414Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"now we’ll configure training and save the model","metadata":{}},{"cell_type":"code","source":"import math\nimport json\n\nEPOCHS = 150\nBATCH_SIZE = 128\nWARMUP_EPOCHS = 5\nMAX_LR = 0.1\nMIN_LR = 0.0001\n\nRUN_DIR = Path(\"/kaggle/working/cifar10_run\")\nRUN_DIR.mkdir(parents=True, exist_ok=True)\n\n# Save the category order for interpreting predictions later.\n(RUN_DIR / \"class_names.json\").write_text(\n    json.dumps(class_names, indent=2)\n)\n\n# Prepare batches. Pixel scaling is already inside the model.\nwith tf.device(\"/CPU:0\"):\n    train_ds = (\n        tf.data.Dataset.from_tensor_slices((X_train, y_train))\n        .shuffle(\n            buffer_size=len(X_train),\n            seed=SEED,\n            reshuffle_each_iteration=True\n        )\n        .batch(BATCH_SIZE)\n        .prefetch(tf.data.AUTOTUNE)\n    )\n\n    val_ds = (\n        tf.data.Dataset.from_tensor_slices((X_val, y_val))\n        .batch(256)\n        .prefetch(tf.data.AUTOTUNE)\n    )\n\n\ndef learning_rate_schedule(epoch, current_lr):\n    if epoch < WARMUP_EPOCHS:\n        return float(MAX_LR * (epoch + 1) / WARMUP_EPOCHS)\n\n    progress = (\n        (epoch - WARMUP_EPOCHS)\n        / (EPOCHS - WARMUP_EPOCHS - 1)\n    )\n\n    return float(\n        MIN_LR\n        + 0.5 * (MAX_LR - MIN_LR)\n        * (1 + math.cos(math.pi * progress))\n    )\n\n\nmodel.compile(\n    optimizer=keras.optimizers.SGD(\n        learning_rate=MAX_LR / WARMUP_EPOCHS,\n        momentum=0.9,\n        nesterov=True\n    ),\n    loss=keras.losses.SparseCategoricalCrossentropy(),\n    metrics=[\n        keras.metrics.SparseCategoricalAccuracy(name=\"accuracy\")\n    ],\n    jit_compile=False\n)\n\ncallbacks = [\n    keras.callbacks.LearningRateScheduler(learning_rate_schedule),\n\n    # Keep the model with the highest validation accuracy.\n    keras.callbacks.ModelCheckpoint(\n        filepath=str(RUN_DIR / \"best.keras\"),\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        save_best_only=True,\n        verbose=1\n    ),\n\n    # Also save the latest completed epoch for possible continuation.\n    keras.callbacks.ModelCheckpoint(\n        filepath=str(RUN_DIR / \"latest.keras\"),\n        save_best_only=False,\n        verbose=0\n    ),\n\n    # Record each epoch's results for our analysis and report.\n    keras.callbacks.CSVLogger(str(RUN_DIR / \"training_log.csv\")),\n\n    keras.callbacks.TerminateOnNaN()\n]\n\nprint(\"Training configuration is ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:31:20.150568Z","iopub.execute_input":"2026-09-21T10:31:20.151502Z","iopub.status.idle":"2026-09-21T10:31:20.376102Z","shell.execute_reply.started":"2026-09-21T10:31:20.151425Z","shell.execute_reply":"2026-09-21T10:31:20.375402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCHS,\n    callbacks=callbacks,\n    verbose=2\n)\n\nprint(\n    \"\\nBest validation accuracy:\",\n    f\"{max(history.history['val_accuracy']):.4f}\"\n)\nprint(\"Saved files:\", RUN_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T10:31:20.377015Z","iopub.execute_input":"2026-09-21T10:31:20.377328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## save training results\nimport shutil\nfrom pathlib import Path\n\nRUN_DIR = Path(\"/kaggle/working/cifar10_run\")\n\nassert (RUN_DIR / \"best.keras\").is_file(), \"Saved model was not found.\"\n\nbackup_path = shutil.make_archive(\n    \"/kaggle/working/cifar10_training_backup\",\n    \"zip\",\n    root_dir=str(RUN_DIR)\n)\n\nprint(\"Training backup saved:\", backup_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T11:57:28.942113Z","iopub.execute_input":"2026-09-21T11:57:28.94246Z","iopub.status.idle":"2026-09-21T11:57:29.824275Z","shell.execute_reply.started":"2026-09-21T11:57:28.942408Z","shell.execute_reply":"2026-09-21T11:57:29.823618Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"now, we will restored the weights from the epoch with the highest validation accuracy","metadata":{}},{"cell_type":"code","source":"import json\n\nbest_model = keras.models.load_model(\n    str(RUN_DIR / \"best.keras\")\n)\n\n# Restore the exact category order used during training.\nclass_names = json.loads(\n    (RUN_DIR / \"class_names.json\").read_text()\n)\n\nresults = best_model.evaluate(\n    val_ds,\n    verbose=0,\n    return_dict=True\n)\n\ntraining_log = pd.read_csv(RUN_DIR / \"training_log.csv\")\nbest_row = training_log.loc[training_log[\"val_accuracy\"].idxmax()]\n\nprint(\"Best training epoch:\", int(best_row[\"epoch\"]) + 1)\nprint(f\"Restored validation accuracy: {results['accuracy']:.4f}\")\nprint(\"Class order:\", class_names)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T11:57:42.292836Z","iopub.execute_input":"2026-09-21T11:57:42.293244Z","iopub.status.idle":"2026-09-21T11:57:44.359857Z","shell.execute_reply.started":"2026-09-21T11:57:42.293214Z","shell.execute_reply":"2026-09-21T11:57:44.359213Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\nimport pandas as pd\n\nEXTRACT_DIR = Path(\"/kaggle/working/cifar10_data\")\nTEST_DIR = EXTRACT_DIR / \"test\"\nTEST_DIR.mkdir(parents=True, exist_ok=True)\n\nsample_submission = pd.read_csv(\n    DATA_DIR / \"sampleSubmission.csv\"\n)\n\ntest_paths = [\n    TEST_DIR / f\"{image_id}.png\"\n    for image_id in sample_submission[\"id\"]\n]\n\nprint(\"Expected test images:\", len(test_paths))\n   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T11:57:47.676996Z","iopub.execute_input":"2026-09-21T11:57:47.677694Z","iopub.status.idle":"2026-09-21T11:57:49.537422Z","shell.execute_reply.started":"2026-09-21T11:57:47.677662Z","shell.execute_reply":"2026-09-21T11:57:49.536484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\nimport subprocess\nfrom pathlib import Path\n\ntest_dir = Path(\"/kaggle/working/cifar10_data/test\")\n\ncount = 0\nif test_dir.is_dir():\n    with os.scandir(test_dir) as entries:\n        count = sum(\n            entry.name.endswith(\".png\") and entry.is_file()\n            for entry in entries\n        )\n\nprint(f\"Test image files found: {count:,} / 300,000\")\n\nfree_gb = shutil.disk_usage(\"/kaggle/working\").free / 1024**3\nprint(f\"Free disk space: {free_gb:.1f} GB\")\n\nmodel_path = Path(\"/kaggle/working/cifar10_run/best.keras\")\nprint(\"Saved model exists:\", model_path.is_file())\n\n# Check whether an extraction process remains running.\nprocesses = subprocess.run(\n    [\"ps\", \"-eo\", \"pid,comm,stat,etime\"],\n    capture_output=True,\n    text=True,\n    check=True\n).stdout.splitlines()\n\nextractors = [\n    line for line in processes[1:]\n    if len(line.split()) >= 2\n    and line.split()[1] in {\"7z\", \"7zz\", \"7za\"}\n]\n\nprint(\"\\nRemaining extraction processes:\")\nprint(\"\\n\".join(extractors) or \"None\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T11:57:52.36994Z","iopub.execute_input":"2026-09-21T11:57:52.370862Z","iopub.status.idle":"2026-09-21T11:57:52.409697Z","shell.execute_reply.started":"2026-09-21T11:57:52.370814Z","shell.execute_reply":"2026-09-21T11:57:52.408853Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Test Image Extraction and Verification\ntwo progress bars:\n1. Checking existing files: checks for incomplete images\n2. Test image files present: tracks extraction toward 300,000\n\n","metadata":{}},{"cell_type":"code","source":"#model = keras.models.load_model(\n#    \"/kaggle/working/cifar10_run/best.keras\"\n#)\n\n#training_log = pd.read_csv(\n#    \"/kaggle/working/cifar10_run/training_log.csv\"\n#)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport time\nimport shutil\nimport subprocess\nfrom pathlib import Path\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\n# Check existing images so an incomplete file is not skipped.\nvalid_existing = 0\ndamaged = 0\n\nfor path in tqdm(test_paths, desc=\"Checking existing files\"):\n    if not path.is_file():\n        continue\n\n    try:\n        with Image.open(path) as img:\n            if img.size != (32, 32):\n                raise ValueError(\"Unexpected image dimensions\")\n            img.verify()\n\n    except (OSError, SyntaxError, ValueError):\n        path.rename(\n            path.with_name(\n                path.name + f\".incomplete-{time.time_ns()}\"\n            )\n        )\n        damaged += 1\n\n    else:\n        valid_existing += 1\n\nprint(f\"Valid existing images: {valid_existing:,}\")\nprint(f\"Images needing re-extraction: {damaged}\")\n\n\ndef count_test_images():\n    with os.scandir(TEST_DIR) as entries:\n        return sum(\n            entry.name.endswith(\".png\") and entry.is_file()\n            for entry in entries\n        )\n\n\nif valid_existing < len(test_paths):\n    extractor = shutil.which(\"7zz\") or shutil.which(\"7z\")\n    assert extractor, \"7-Zip was not found.\"\n\n    log_path = Path(\"/kaggle/working/test_extraction.log\")\n\n    command = [\n        extractor,\n        \"x\",\n        str(DATA_DIR / \"test.7z\"),\n        f\"-o{EXTRACT_DIR}\",\n        \"-aos\",  # Preserve existing complete images.\n        \"-y\"\n    ]\n\n    with log_path.open(\"w\") as log_file:\n        process = subprocess.Popen(\n            command,\n            stdout=log_file,\n            stderr=subprocess.STDOUT\n        )\n\n        try:\n            with tqdm(\n                total=len(test_paths),\n                initial=valid_existing,\n                desc=\"Test image files present\",\n                unit=\"image\"\n            ) as progress:\n                while process.poll() is None:\n                    time.sleep(10)\n                    current_count = count_test_images()\n                    progress.update(current_count - progress.n)\n                    progress.refresh()\n\n                progress.update(count_test_images() - progress.n)\n\n        except BaseException:\n            if process.poll() is None:\n                process.kill()\n            process.wait()\n            raise\n\n        exit_code = process.wait()\n\n    if exit_code != 0:\n        print(log_path.read_text(errors=\"replace\")[-3000:])\n        raise RuntimeError(\"Extraction failed. Please share this output.\")\n\n# Confirm every image requested by the submission template exists.\nmissing = [path for path in test_paths if not path.is_file()]\nassert not missing, f\"{len(missing)} test images are still missing.\"\n\nprint(\"\\nTest images ready:\", len(test_paths))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T11:57:58.2173Z","iopub.execute_input":"2026-09-21T11:57:58.217635Z","iopub.status.idle":"2026-09-21T15:43:08.580122Z","shell.execute_reply.started":"2026-09-21T11:57:58.217609Z","shell.execute_reply":"2026-09-21T15:43:08.579122Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## save\nfrom pathlib import Path\nfrom zipfile import ZipFile, ZIP_STORED\nfrom tqdm.auto import tqdm\n\nassert len(test_paths) == 300_000\nmissing = [path for path in test_paths if not path.is_file()]\nassert not missing, f\"{len(missing)} test images are missing.\"\n\nbackup_path = Path(\"/kaggle/working/cifar10_test_images_backup.zip\")\n\nwith ZipFile(backup_path, \"w\", compression=ZIP_STORED) as archive:\n    for path in tqdm(test_paths, desc=\"Backing up test images\"):\n        archive.write(path, arcname=f\"test/{path.name}\")\n\nprint(\"Backup ready:\", backup_path)\nprint(f\"Size: {backup_path.stat().st_size / 1024**3:.2f} GB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T15:43:08.870173Z","iopub.execute_input":"2026-09-21T15:43:08.870704Z","iopub.status.idle":"2026-09-21T15:43:29.17076Z","shell.execute_reply.started":"2026-09-21T15:43:08.870679Z","shell.execute_reply":"2026-09-21T15:43:29.169918Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Generate Predictions and Submission","metadata":{}},{"cell_type":"code","source":"import json\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom pathlib import Path\n\n# Load the trained model and its exact class order.\nRUN_DIR = Path(\"/kaggle/working/cifar10_run\")\n\nbest_model = keras.models.load_model(\n    RUN_DIR / \"best.keras\",\n    compile=False\n)\n\nclass_names = json.loads(\n    (RUN_DIR / \"class_names.json\").read_text()\n)\n\n# Keep images in the same order as the submission template.\nsample_submission = pd.read_csv(DATA_DIR / \"sampleSubmission.csv\")\ntest_paths = [\n    TEST_DIR / f\"{image_id}.png\"\n    for image_id in sample_submission[\"id\"]\n]\n\nassert len(test_paths) == 300_000\nassert all(path.is_file() for path in test_paths)\n\ndef load_test_image(path):\n    image = tf.io.decode_png(tf.io.read_file(path), channels=3)\n    image = tf.ensure_shape(image, (32, 32, 3))\n\n    # The model already includes rescaling: keep pixels in 0–255.\n    return tf.cast(image, tf.float32)\n\ntest_ds = (\n    tf.data.Dataset.from_tensor_slices([str(p) for p in test_paths])\n    .map(\n        load_test_image,\n        num_parallel_calls=tf.data.AUTOTUNE,\n        deterministic=True\n    )\n    .batch(256)\n    .prefetch(tf.data.AUTOTUNE)\n)\n\n# Predict without shuffling or training.\nprobabilities = best_model.predict(test_ds, verbose=1)\n\nassert probabilities.shape == (300_000, len(class_names))\nassert np.isfinite(probabilities).all()\n\npredicted_labels = np.asarray(class_names)[\n    probabilities.argmax(axis=1)\n]\n\nsubmission = sample_submission.copy()\nsubmission[\"label\"] = predicted_labels\n\nassert submission[\"id\"].equals(sample_submission[\"id\"])\nassert submission[\"id\"].is_unique\nassert submission[\"label\"].isin(class_names).all()\nassert not submission.isna().any().any()\n\nsubmission_path = Path(\"/kaggle/working/submission.csv\")\nsubmission.to_csv(submission_path, index=False)\n\nprint(\"\\nSubmission saved:\", submission_path)\nprint(\"Submission shape:\", submission.shape)\ndisplay(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-21T15:45:19.023786Z","iopub.execute_input":"2026-09-21T15:45:19.024084Z","iopub.status.idle":"2026-09-21T15:46:09.823362Z","shell.execute_reply.started":"2026-09-21T15:45:19.024055Z","shell.execute_reply":"2026-09-21T15:46:09.822522Z"}},"outputs":[],"execution_count":null}]}