{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","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":"# Step 1 - Preparing the environment\n\nSettings → Accelerator → GPU T4 ×2","metadata":{}},{"cell_type":"code","source":"# 1. Environment setup\n\nfrom pathlib import Path\nimport shutil\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow import keras\n\nSEED = 42\n\ngpus = tf.config.list_physical_devices(\"GPU\")\n\nif not gpus:\n    raise RuntimeError(\n        \"No GPU detected. Select GPU T4 x2 in Settings > Accelerator.\"\n    )\n\nkeras.utils.set_random_seed(SEED)\n\nprint(\"TensorFlow version:\", tf.__version__)\nprint(\"GPUs detected:\", len(gpus))\n\nfor gpu in gpus:\n    print(\"GPU:\", gpu.name)\n\nprint(\"AdamW available:\", hasattr(keras.optimizers, \"AdamW\"))\n\n\n# 2. Data verification\n\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\n\nrequired_files = [\n    \"trainLabels.csv\",\n    \"train.7z\",\n    \"test.7z\",\n    \"sampleSubmission.csv\",\n]\n\nmissing_files = [\n    name for name in required_files\n    if not (DATA_DIR / name).is_file()\n]\n\nif missing_files:\n    raise FileNotFoundError(f\"Missing files: {missing_files}\")\n\nlabels = pd.read_csv(DATA_DIR / \"trainLabels.csv\")\n\nassert {\"id\", \"label\"}.issubset(labels.columns)\nassert len(labels) == 50_000\nassert labels[\"id\"].is_unique\nassert not labels[[\"id\", \"label\"]].isna().any().any()\nassert labels[\"label\"].nunique() == 10\n\nprint(\"\\nData folder:\", DATA_DIR)\nprint(\"Training images:\", len(labels))\nprint(\"Classes:\", labels[\"label\"].nunique())\nprint(\"\\nImages per class:\")\nprint(labels[\"label\"].value_counts().sort_index())\n\n\n# 3. Output directory\n\nRUN_DIR = Path(\"/kaggle/working/advanced_convnet_run\")\nRUN_DIR.mkdir(parents=True, exist_ok=True)\n\nfree_gb = shutil.disk_usage(\"/kaggle/working\").free / (1024 ** 3)\n\nprint(\"\\nOutput folder:\", RUN_DIR)\nprint(f\"Free disk space: {free_gb:.1f} GB\")\nprint(\"\\nSetup completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T15:59:16.489666Z","iopub.execute_input":"2026-10-06T15:59:16.489928Z","iopub.status.idle":"2026-10-06T15:59:56.973429Z","shell.execute_reply.started":"2026-10-06T15:59:16.489904Z","shell.execute_reply":"2026-10-06T15:59:56.972299Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 2 - Data preparation","metadata":{}},{"cell_type":"code","source":"%pip install -q py7zr","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T16:04:47.661008Z","iopub.execute_input":"2026-10-06T16:04:47.661843Z","iopub.status.idle":"2026-10-06T16:04:54.455344Z","shell.execute_reply.started":"2026-10-06T16:04:47.661808Z","shell.execute_reply":"2026-10-06T16:04:54.454469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 4. Extract training images\n\nimport json\nimport numpy as np\nimport py7zr\n\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.auto import tqdm\n\nEXTRACT_DIR = Path(\"/kaggle/working/cifar10_data\")\nTRAIN_DIR = EXTRACT_DIR / \"train\"\n\nEXTRACT_DIR.mkdir(parents=True, exist_ok=True)\n\nimage_paths = [\n    TRAIN_DIR / f\"{image_id}.png\"\n    for image_id in labels[\"id\"]\n]\n\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_images = [\n    path for path in image_paths\n    if not path.is_file()\n]\n\nif missing_images:\n    raise FileNotFoundError(\n        f\"{len(missing_images)} training images are missing.\"\n    )\n\nprint(\"Training images found:\", len(image_paths))\n\n\n# 5. Load images and labels\n\nclass_names = sorted(labels[\"label\"].unique())\n\nclass_to_index = {\n    name: index\n    for index, name in enumerate(class_names)\n}\n\ny = labels[\"label\"].map(class_to_index).to_numpy(dtype=np.int64)\n\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 image:\n        if image.size != (32, 32):\n            raise ValueError(f\"Unexpected image size: {path}\")\n\n        X[index] = np.asarray(image.convert(\"RGB\"))\n\n\n# 6. Create training and validation sets\n\ntrain_indices, val_indices = train_test_split(\n    np.arange(len(labels)),\n    test_size=0.10,\n    random_state=SEED,\n    stratify=y\n)\n\nX_train = X[train_indices]\nX_val = X[val_indices]\n\ny_train = y[train_indices]\ny_val = y[val_indices]\n\nassert len(np.intersect1d(train_indices, val_indices)) == 0\nassert len(train_indices) + len(val_indices) == len(labels)\n\nassert np.all(np.bincount(y_train, minlength=10) == 4500)\nassert np.all(np.bincount(y_val, minlength=10) == 500)\n\n\n# 7. Save data preparation settings\n\n(RUN_DIR / \"class_names.json\").write_text(\n    json.dumps(class_names, indent=2),\n    encoding=\"utf-8\"\n)\n\nnp.savez_compressed(\n    RUN_DIR / \"data_split.npz\",\n    train_indices=train_indices,\n    val_indices=val_indices,\n    train_ids=labels[\"id\"].to_numpy()[train_indices],\n    val_ids=labels[\"id\"].to_numpy()[val_indices]\n)\n\ndata_config = {\n    \"seed\": SEED,\n    \"validation_fraction\": 0.10,\n    \"training_images\": len(train_indices),\n    \"validation_images\": len(val_indices),\n    \"image_shape\": [32, 32, 3],\n    \"pixel_range\": [0, 255]\n}\n\n(RUN_DIR / \"data_config.json\").write_text(\n    json.dumps(data_config, indent=2),\n    encoding=\"utf-8\"\n)\n\nprint(\"\\nTraining images:\", X_train.shape)\nprint(\"Validation images:\", X_val.shape)\nprint(\"Training images per class:\", np.bincount(y_train))\nprint(\"Validation images per class:\", np.bincount(y_val))\nprint(\"\\nData preparation settings saved:\", RUN_DIR)\nprint(\"Data preparation completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T16:05:14.479574Z","iopub.execute_input":"2026-10-06T16:05:14.480078Z","iopub.status.idle":"2026-10-06T16:06:08.499231Z","shell.execute_reply.started":"2026-10-06T16:05:14.480048Z","shell.execute_reply":"2026-10-06T16:06:08.498231Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 3 - Modeling","metadata":{}},{"cell_type":"code","source":"# 8. Model setup\n\nfrom tensorflow.keras import layers\n\nkeras.backend.clear_session()\nkeras.utils.set_random_seed(SEED)\n\nstrategy = tf.distribute.MirroredStrategy()\n\nprint(\"GPUs used:\", strategy.num_replicas_in_sync)\n\n\n# 9. Model architecture\n\ndef conv_bn(x, filters, kernel_size=3, stride=1, activate=True):\n    x = layers.Conv2D(\n        filters=filters,\n        kernel_size=kernel_size,\n        strides=stride,\n        padding=\"same\",\n        use_bias=False,\n        kernel_initializer=\"he_normal\"\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\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    if stride != 1 or int(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    x = layers.ReLU()(x)\n\n    return x\n\n\ndef build_model():\n    inputs = keras.Input(\n        shape=(32, 32, 3),\n        dtype=\"float32\",\n        name=\"image\"\n    )\n\n    x = layers.Rescaling(\n        scale=1.0 / 127.5,\n        offset=-1.0,\n        name=\"pixel_scaling\"\n    )(inputs)\n\n    x = conv_bn(x, filters=32)\n\n    for 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    x = layers.GlobalAveragePooling2D(name=\"global_pool\")(x)\n\n    outputs = layers.Dense(\n        len(class_names),\n        activation=\"softmax\",\n        name=\"class_probabilities\"\n    )(x)\n\n    return keras.Model(\n        inputs=inputs,\n        outputs=outputs,\n        name=\"advanced_cifar10_convnet\"\n    )\n\n\nwith strategy.scope():\n    model = build_model()\n\n\n# 10. Model verification\n\nsample_images = tf.convert_to_tensor(\n    X_train[:4],\n    dtype=tf.float32\n)\n\nprobabilities = model(sample_images, training=False).numpy()\n\nassert probabilities.shape == (4, len(class_names))\nassert np.isfinite(probabilities).all()\n\nnp.testing.assert_allclose(\n    probabilities.sum(axis=1),\n    np.ones(4),\n    atol=1e-5\n)\n\nprint(\"\\nModel parameters:\", f\"{model.count_params():,}\")\nprint(\"Model input shape:\", model.input_shape)\nprint(\"Model output shape:\", model.output_shape)\nprint(\"Forward pass successful:\", probabilities.shape)\n\n\n# 11. Save model architecture\n\n(RUN_DIR / \"model_architecture.json\").write_text(\n    model.to_json(),\n    encoding=\"utf-8\"\n)\n\nprint(\"\\nModel architecture saved.\")\nprint(\"Model setup completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T16:08:28.411581Z","iopub.execute_input":"2026-10-06T16:08:28.412807Z","iopub.status.idle":"2026-10-06T16:08:35.740805Z","shell.execute_reply.started":"2026-10-06T16:08:28.412775Z","shell.execute_reply":"2026-10-06T16:08:35.739983Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 4 - Cut Out and data batches preparation","metadata":{}},{"cell_type":"code","source":"# 12. Data augmentation\n\nimport matplotlib.pyplot as plt\n\nBATCH_SIZE = 128\nCUTOUT_SIZE = 16\nPADDING = 4\nCUTOUT_FILL = 127.5\n\n\ndef augment_image(image, label):\n    image = tf.cast(image, tf.float32)\n\n    image = tf.pad(\n        image,\n        [[PADDING, PADDING], [PADDING, PADDING], [0, 0]],\n        mode=\"REFLECT\"\n    )\n\n    image = tf.image.random_crop(image, size=[32, 32, 3])\n    image = tf.image.random_flip_left_right(image)\n\n    center_y = tf.random.uniform(\n        [], minval=0, maxval=32, dtype=tf.int32\n    )\n    center_x = tf.random.uniform(\n        [], minval=0, maxval=32, dtype=tf.int32\n    )\n\n    y_start = center_y - CUTOUT_SIZE // 2\n    x_start = center_x - CUTOUT_SIZE // 2\n\n    rows = tf.range(32)[:, None]\n    columns = tf.range(32)[None, :]\n\n    cutout_mask = (\n        (rows >= y_start)\n        & (rows < y_start + CUTOUT_SIZE)\n        & (columns >= x_start)\n        & (columns < x_start + CUTOUT_SIZE)\n    )\n\n    image = tf.where(\n        cutout_mask[..., None],\n        tf.constant(CUTOUT_FILL, dtype=tf.float32),\n        image\n    )\n\n    return tf.ensure_shape(image, [32, 32, 3]), label\n\n\ndef prepare_validation(image, label):\n    return tf.cast(image, tf.float32), label\n\n\n# 13. Training and validation datasets\n\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        .map(\n            augment_image,\n            num_parallel_calls=tf.data.AUTOTUNE\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        .map(\n            prepare_validation,\n            num_parallel_calls=tf.data.AUTOTUNE\n        )\n        .batch(BATCH_SIZE)\n        .prefetch(tf.data.AUTOTUNE)\n    )\n\n\n# 14. Dataset verification and preview\n\npreview_images, preview_labels = next(iter(train_ds))\nval_images, val_labels = next(iter(val_ds))\n\nassert tuple(preview_images.shape) == (BATCH_SIZE, 32, 32, 3)\nassert bool(tf.reduce_all(tf.math.is_finite(preview_images)).numpy())\nassert float(tf.reduce_min(preview_images)) >= 0.0\nassert float(tf.reduce_max(preview_images)) <= 255.0\n\nnp.testing.assert_array_equal(\n    val_images.numpy(),\n    X_val[:BATCH_SIZE].astype(np.float32)\n)\nnp.testing.assert_array_equal(\n    val_labels.numpy(),\n    y_val[:BATCH_SIZE]\n)\n\nfig, axes = plt.subplots(2, 4, figsize=(10, 5))\n\nfor index, ax in enumerate(axes.flat):\n    ax.imshow(preview_images[index].numpy() / 255.0)\n    ax.set_title(class_names[int(preview_labels[index].numpy())])\n    ax.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n\n\n# 15. Save augmentation settings\n\naugmentation_config = {\n    \"batch_size_total\": BATCH_SIZE,\n    \"horizontal_flip\": True,\n    \"random_crop_padding\": PADDING,\n    \"padding_mode\": \"REFLECT\",\n    \"cutout_size\": CUTOUT_SIZE,\n    \"cutout_probability\": 1.0,\n    \"cutout_fill_before_scaling\": CUTOUT_FILL,\n    \"cutout_clipped_at_image_edges\": True,\n    \"validation_augmentation\": False\n}\n\n(RUN_DIR / \"augmentation_config.json\").write_text(\n    json.dumps(augmentation_config, indent=2),\n    encoding=\"utf-8\"\n)\n\nprint(\"Training batches:\", int(train_ds.cardinality().numpy()))\nprint(\"Validation batches:\", int(val_ds.cardinality().numpy()))\nprint(\"Total batch size:\", BATCH_SIZE)\nprint(\"CutOut size:\", CUTOUT_SIZE)\nprint(\"Validation images unchanged: verified\")\nprint(\"Dataset setup completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T16:11:59.684247Z","iopub.execute_input":"2026-10-06T16:11:59.684647Z","iopub.status.idle":"2026-10-06T16:12:01.169968Z","shell.execute_reply.started":"2026-10-06T16:11:59.684619Z","shell.execute_reply":"2026-10-06T16:12:01.169186Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 5 - training preparation","metadata":{}},{"cell_type":"code","source":"# 16. Training configuration\n\nimport math\n\nEPOCHS = 200\nWARMUP_EPOCHS = 5\nMAX_LR = 0.001\nMIN_LR = 0.00001\nWEIGHT_DECAY = 0.0005\n\nexisting_outputs = [\n    name\n    for name in [\"best.keras\", \"latest.keras\", \"training_log.csv\"]\n    if (RUN_DIR / name).exists()\n]\n\nif existing_outputs:\n    raise RuntimeError(\n        \"Existing training results found. \"\n        \"Load the saved run before continuing: \"\n        + \", \".join(existing_outputs)\n    )\n\n\n# 17. Warmup and cosine learning rate\n\ndef learning_rate_schedule(epoch, current_lr=None):\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    progress = min(max(progress, 0.0), 1.0)\n\n    return float(\n        MIN_LR\n        + 0.5 * (MAX_LR - MIN_LR)\n        * (1.0 + math.cos(math.pi * progress))\n    )\n\n\n# 18. Optimizer and loss\n\nwith strategy.scope():\n    optimizer = keras.optimizers.AdamW(\n        learning_rate=learning_rate_schedule(0),\n        weight_decay=WEIGHT_DECAY\n    )\n\n    optimizer.exclude_from_weight_decay(\n        var_names=[\"bias\", \"beta\", \"gamma\"]\n    )\n\n    model.compile(\n        optimizer=optimizer,\n        loss=keras.losses.SparseCategoricalCrossentropy(),\n        metrics=[\n            keras.metrics.SparseCategoricalAccuracy(name=\"accuracy\")\n        ],\n        jit_compile=False\n    )\n\n\n# 19. Training state and backup\n\nBACKUP_BASE = Path(\"/kaggle/working/advanced_convnet_backup\")\n\n\ndef create_training_backup():\n    backup_path = shutil.make_archive(\n        base_name=str(BACKUP_BASE),\n        format=\"zip\",\n        root_dir=str(RUN_DIR)\n    )\n\n    print(\"\\nTraining backup created:\", backup_path)\n    return backup_path\n\n\nclass SaveTrainingState(keras.callbacks.Callback):\n    def on_epoch_end(self, epoch, logs=None):\n        state = {\n            \"last_finished_epoch\": int(epoch + 1),\n            \"optimizer_iterations\": int(\n                self.model.optimizer.iterations.numpy()\n            ),\n            \"learning_rate\": float(\n                self.model.optimizer.learning_rate.numpy()\n            )\n        }\n\n        temporary_path = RUN_DIR / \"training_state.tmp\"\n        temporary_path.write_text(\n            json.dumps(state, indent=2),\n            encoding=\"utf-8\"\n        )\n        temporary_path.replace(RUN_DIR / \"training_state.json\")\n\n    def on_train_end(self, logs=None):\n        create_training_backup()\n\n\n# 20. Automatic checkpoints and training log\n\ncallbacks = [\n    keras.callbacks.LearningRateScheduler(\n        learning_rate_schedule,\n        verbose=0\n    ),\n\n    keras.callbacks.ModelCheckpoint(\n        filepath=str(RUN_DIR / \"best.keras\"),\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        save_best_only=True,\n        save_weights_only=False,\n        save_freq=\"epoch\",\n        verbose=1\n    ),\n\n    keras.callbacks.ModelCheckpoint(\n        filepath=str(RUN_DIR / \"latest.keras\"),\n        save_best_only=False,\n        save_weights_only=False,\n        save_freq=\"epoch\",\n        verbose=0\n    ),\n\n    keras.callbacks.CSVLogger(\n        filename=str(RUN_DIR / \"training_log.csv\"),\n        append=False\n    ),\n\n    keras.callbacks.TerminateOnNaN(),\n\n    SaveTrainingState()\n]\n\n\n# 21. Save training settings\n\ntraining_config = {\n    \"epochs\": EPOCHS,\n    \"batch_size_total\": BATCH_SIZE,\n    \"warmup_epochs\": WARMUP_EPOCHS,\n    \"max_learning_rate\": MAX_LR,\n    \"min_learning_rate\": MIN_LR,\n    \"learning_rate_schedule\": \"linear_warmup_then_cosine\",\n    \"optimizer\": \"AdamW\",\n    \"weight_decay\": WEIGHT_DECAY,\n    \"weight_decay_exclusions\": [\"bias\", \"beta\", \"gamma\"],\n    \"seed\": SEED,\n    \"tensorflow_version\": tf.__version__,\n    \"gpu_count\": strategy.num_replicas_in_sync,\n    \"checkpoint_metric\": \"val_accuracy\"\n}\n\n(RUN_DIR / \"training_config.json\").write_text(\n    json.dumps(training_config, indent=2),\n    encoding=\"utf-8\"\n)\n\nprint(\"Optimizer:\", type(model.optimizer).__name__)\nprint(\"Epochs:\", EPOCHS)\nprint(\"Warmup epochs:\", WARMUP_EPOCHS)\n\nprint(\n    \"Warmup learning rates:\",\n    [round(learning_rate_schedule(i), 7) for i in range(WARMUP_EPOCHS)]\n)\n\nprint(\"Final learning rate:\", learning_rate_schedule(EPOCHS - 1))\nprint(\"Checkpoint folder:\", RUN_DIR)\nprint(\"Automatic backup:\", str(BACKUP_BASE) + \".zip\")\nprint(\"\\nTraining configuration ready. Training has not started.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T16:16:07.102821Z","iopub.execute_input":"2026-10-06T16:16:07.103292Z","iopub.status.idle":"2026-10-06T16:16:07.139054Z","shell.execute_reply.started":"2026-10-06T16:16:07.103263Z","shell.execute_reply":"2026-10-06T16:16:07.137965Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 6 - training ","metadata":{}},{"cell_type":"code","source":"# 22. Training safeguards\n\nfrom IPython.display import display, FileLink\nfrom zipfile import ZipFile\nimport os\n\nexisting_results = [\n    name\n    for name in [\"best.keras\", \"latest.keras\", \"training_log.csv\"]\n    if (RUN_DIR / name).exists()\n]\n\nif existing_results or int(model.optimizer.iterations.numpy()) > 0:\n    raise RuntimeError(\n        \"A training run has already started. \"\n        \"Resume the existing run instead of starting again.\"\n    )\n\n\n# 23. Model training\n\ntry:\n    history = model.fit(\n        train_ds,\n        validation_data=val_ds,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    training_log = pd.read_csv(RUN_DIR / \"training_log.csv\")\n\n    valid_results = training_log[\n        np.isfinite(training_log[\"val_accuracy\"])\n        & np.isfinite(training_log[\"val_loss\"])\n        & np.isfinite(training_log[\"loss\"])\n    ]\n\n    if valid_results.empty:\n        raise RuntimeError(\"No valid validation results were recorded.\")\n\n    best_row = valid_results.loc[\n        valid_results[\"val_accuracy\"].idxmax()\n    ]\n\n    completed_epochs = len(training_log)\n\n    summary = {\n        \"status\": (\n            \"completed\"\n            if completed_epochs == EPOCHS\n            and len(valid_results) == EPOCHS\n            else \"stopped_early_or_invalid_metrics\"\n        ),\n        \"epochs_recorded\": completed_epochs,\n        \"best_epoch\": int(best_row[\"epoch\"]) + 1,\n        \"best_validation_accuracy\": float(best_row[\"val_accuracy\"]),\n        \"best_validation_loss\": float(best_row[\"val_loss\"])\n    }\n\n    (RUN_DIR / \"training_summary.json\").write_text(\n        json.dumps(summary, indent=2),\n        encoding=\"utf-8\"\n    )\n\n    print(\"\\nTraining status:\", summary[\"status\"])\n    print(\"Epochs recorded:\", completed_epochs)\n    print(\"Best epoch:\", summary[\"best_epoch\"])\n    print(\n        \"Best validation accuracy:\",\n        f\"{summary['best_validation_accuracy']:.4f}\"\n    )\n\n\n    # 24. Saved model verification\n\n    with tf.device(\"/CPU:0\"):\n        saved_model_check = keras.models.load_model(\n            RUN_DIR / \"best.keras\",\n            compile=False\n        )\n\n        saved_predictions = saved_model_check(\n            tf.convert_to_tensor(X_val[:4], dtype=tf.float32),\n            training=False\n        ).numpy()\n\n    assert saved_predictions.shape == (4, len(class_names))\n    assert np.isfinite(saved_predictions).all()\n\n    np.testing.assert_allclose(\n        saved_predictions.sum(axis=1),\n        np.ones(4),\n        atol=1e-5\n    )\n\n    del saved_model_check\n\n    print(\"Saved best model loaded successfully.\")\n\nexcept BaseException as error:\n    print(\"\\nTraining or verification stopped:\", str(error))\n    print(\"Existing checkpoints remain in:\", RUN_DIR)\n    raise\n\nfinally:\n    # 25. Backup and download\n\n    if any(\n        (RUN_DIR / name).is_file()\n        for name in [\"best.keras\", \"latest.keras\"]\n    ):\n        try:\n            backup_path = create_training_backup()\n\n            with ZipFile(backup_path, \"r\") as archive:\n                damaged_file = archive.testzip()\n\n            if damaged_file is not None:\n                raise RuntimeError(\n                    f\"Backup verification failed: {damaged_file}\"\n                )\n\n            print(\"Backup ZIP verified.\")\n            print(\"Download the backup before ending the session.\")\n\n            display(\n                FileLink(\n                    os.path.relpath(backup_path, start=Path.cwd())\n                )\n            )\n\n        except Exception as backup_error:\n            print(\"Backup could not be completed:\", str(backup_error))\n            print(\"Saved model files are in:\", RUN_DIR)\n\n    else:\n        print(\"No model checkpoint has been saved yet.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T16:18:31.674976Z","iopub.execute_input":"2026-10-06T16:18:31.675358Z","iopub.status.idle":"2026-10-06T17:48:15.348505Z","shell.execute_reply.started":"2026-10-06T16:18:31.675334Z","shell.execute_reply":"2026-10-06T17:48:15.347421Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 7 - Test images readiness","metadata":{}},{"cell_type":"code","source":"# 26. Test data preparation\n\nimport subprocess\n\nTEST_DIR = EXTRACT_DIR / \"test\"\n\nsample_submission = pd.read_csv(\n    DATA_DIR / \"sampleSubmission.csv\"\n)\n\nassert list(sample_submission.columns) == [\"id\", \"label\"]\nassert sample_submission[\"id\"].is_unique\nassert not sample_submission[\"id\"].isna().any()\nassert len(sample_submission) == 300_000\n\ntest_paths = [\n    TEST_DIR / f\"{image_id}.png\"\n    for image_id in sample_submission[\"id\"]\n]\n\nassert (RUN_DIR / \"best.keras\").is_file()\n\nfree_gb = shutil.disk_usage(\"/kaggle/working\").free / (1024 ** 3)\n\nprint(\"Expected test images:\", len(test_paths))\nprint(f\"Free disk space: {free_gb:.1f} GB\")\n\n\n# 27. Extract test images\n\nif not all(path.is_file() for path in test_paths):\n    print(\"Extracting test images. This may take several minutes...\")\n\n    extractor = (\n        shutil.which(\"7zz\")\n        or shutil.which(\"7z\")\n        or shutil.which(\"7za\")\n    )\n\n    if extractor:\n        subprocess.run(\n            [\n                extractor,\n                \"x\",\n                str(DATA_DIR / \"test.7z\"),\n                f\"-o{EXTRACT_DIR}\",\n                \"-y\",\n                \"-aoa\",\n                \"-bsp0\",\n                \"-bso0\"\n            ],\n            check=True\n        )\n\n    else:\n        with py7zr.SevenZipFile(\n            DATA_DIR / \"test.7z\",\n            mode=\"r\"\n        ) as archive:\n            archive.extractall(path=EXTRACT_DIR)\n\n\n# 28. Verify test files\n\nmissing_test_images = [\n    path\n    for path in tqdm(test_paths, desc=\"Checking test files\")\n    if not path.is_file()\n]\n\nif missing_test_images:\n    raise FileNotFoundError(\n        f\"{len(missing_test_images)} test images are missing.\"\n    )\n\nprint(\"\\nTest images ready:\", len(test_paths))\nprint(\"Saved model:\", RUN_DIR / \"best.keras\")\nprint(\"Test data preparation completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T17:50:34.694672Z","iopub.execute_input":"2026-10-06T17:50:34.695009Z","iopub.status.idle":"2026-10-06T17:51:59.097698Z","shell.execute_reply.started":"2026-10-06T17:50:34.694985Z","shell.execute_reply":"2026-10-06T17:51:59.096704Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 8 - ","metadata":{}},{"cell_type":"code","source":"# 29. Load the saved model\n\nclass_names = json.loads(\n    (RUN_DIR / \"class_names.json\").read_text(encoding=\"utf-8\")\n)\n\nwith strategy.scope():\n    best_model = keras.models.load_model(\n        RUN_DIR / \"best.keras\",\n        compile=False\n    )\n\nassert best_model.output_shape[-1] == len(class_names)\n\nprint(\"Best model loaded.\")\nprint(\"Classes:\", class_names)\n\n\n# 30. Prepare test batches\n\ndef load_test_image(path):\n    image = tf.io.decode_png(\n        tf.io.read_file(path),\n        channels=3\n    )\n    image = tf.ensure_shape(image, [32, 32, 3])\n    return tf.cast(image, tf.float32)\n\n\nwith tf.device(\"/CPU:0\"):\n    test_ds = (\n        tf.data.Dataset.from_tensor_slices(\n            [str(path) for path in test_paths]\n        )\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\n# 31. Generate and save predictions\n\nprobabilities = best_model.predict(\n    test_ds,\n    verbose=1\n)\n\nassert probabilities.shape == (\n    len(sample_submission),\n    len(class_names)\n)\nassert np.isfinite(probabilities).all()\nassert (probabilities >= 0).all()\nassert (probabilities <= 1).all()\n\nnp.testing.assert_allclose(\n    probabilities.sum(axis=1),\n    np.ones(len(sample_submission)),\n    atol=1e-5\n)\n\nnp.savez_compressed(\n    RUN_DIR / \"test_predictions.npz\",\n    ids=sample_submission[\"id\"].to_numpy(),\n    probabilities=probabilities,\n    class_names=np.asarray(class_names)\n)\n\nprint(\"\\nPredictions saved.\")\n\n\n# 32. Create submission file\n\npredicted_labels = np.asarray(class_names)[\n    probabilities.argmax(axis=1)\n]\n\nsubmission = sample_submission.copy()\nsubmission[\"label\"] = predicted_labels\n\nassert list(submission.columns) == [\"id\", \"label\"]\nassert len(submission) == 300_000\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\")\n\nsubmission.to_csv(submission_path, index=False)\nshutil.copy2(submission_path, RUN_DIR / \"submission.csv\")\n\nprint(\"Submission saved:\", submission_path)\nprint(\"Submission shape:\", submission.shape)\n\ndisplay(submission.head(10))\n\n\n# 33. Update backup and download files\n\nbackup_path = create_training_backup()\n\nwith ZipFile(backup_path, \"r\") as archive:\n    damaged_file = archive.testzip()\n\nif damaged_file is not None:\n    raise RuntimeError(\n        f\"Backup verification failed: {damaged_file}\"\n    )\n\nprint(\"\\nSubmission file:\")\ndisplay(\n    FileLink(\n        os.path.relpath(submission_path, start=Path.cwd())\n    )\n)\n\nprint(\"Updated backup with model, results, and predictions:\")\ndisplay(\n    FileLink(\n        os.path.relpath(backup_path, start=Path.cwd())\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-06T17:54:50.666157Z","iopub.execute_input":"2026-10-06T17:54:50.666794Z","iopub.status.idle":"2026-10-06T17:55:37.128234Z","shell.execute_reply.started":"2026-10-06T17:54:50.666746Z","shell.execute_reply":"2026-10-06T17:55:37.127468Z"}},"outputs":[],"execution_count":null}]}