{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":3649}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CIFAR-10 Kaggle Competition — ResNet-9 from scratch\n**DSEG660 Applied Deep Learning**\n\n**Before running:** in the notebook settings (right panel), set **Accelerator → GPU (P100 or T4)**, turn **Internet → On**,\nand make sure the *CIFAR-10 - Object Recognition in Images* competition data is attached (Add Input → Competitions → cifar-10).\n\n**Plan**\n1. Train a ResNet-9 on the official CIFAR-10 training set (Keras). **All weights are randomly initialized; no pre-trained weights.**\n2. Evaluate on the official 10,000-image test set, which estimates the Kaggle score.\n3. Extract Kaggle's `test.7z` (300,000 images), predict, and write `submission.csv`.","metadata":{}},{"cell_type":"markdown","source":"## 1. Setup and configuration","metadata":{}},{"cell_type":"code","source":"import os, glob, time, shutil, subprocess, math\nfrom concurrent.futures import ThreadPoolExecutor\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, regularizers\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nprint(\"TensorFlow\", tf.__version__, \"| Keras\", keras.__version__)\ngpus = tf.config.list_physical_devices(\"GPU\")\nprint(\"GPUs:\", gpus)\n\nSEED = 42\nkeras.utils.set_random_seed(SEED)\n\n# ---- Hyperparameters ----\nEPOCHS          = 40\nBATCH_SIZE      = 512     # global batch size (split across GPUs if there are 2)\nPEAK_LR         = 0.2     # one-cycle schedule: warm up to this, then cosine-decay to ~0\nWARMUP_FRAC     = 0.15    # fraction of training spent warming up\nWEIGHT_DECAY    = 5e-4\nLABEL_SMOOTHING = 0.1\nCUTOUT_SIZE     = 8       # 8x8 Cutout patch; set 0 to disable\n\nCLASSES = ['airplane', 'automobile', 'bird', 'cat', 'deer',\n           'dog', 'frog', 'horse', 'ship', 'truck']   # same order as Keras labels 0-9\n\n# Mixed precision: faster on GPU, same accuracy\nif gpus:\n    keras.mixed_precision.set_global_policy(\"mixed_float16\")\n\n# Use both GPUs if the runtime has 2 (e.g. T4 x2), otherwise the default single-device strategy\nstrategy = tf.distribute.MirroredStrategy() if len(gpus) > 1 else tf.distribute.get_strategy()\nprint(\"Replicas in sync:\", strategy.num_replicas_in_sync)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T13:50:17.521921Z","iopub.execute_input":"2026-09-23T13:50:17.522189Z","iopub.status.idle":"2026-09-23T13:50:36.953302Z","shell.execute_reply.started":"2026-09-23T13:50:17.522165Z","shell.execute_reply":"2026-09-23T13:50:36.952575Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load the official CIFAR-10 data and build the input pipeline","metadata":{}},{"cell_type":"code","source":"(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()\ny_train = y_train.flatten().astype(np.int32)\ny_test  = y_test.flatten().astype(np.int32)\nprint(\"Train:\", x_train.shape, \"Test:\", x_test.shape)\n\n# Per-channel mean/std computed on the training set only\n_flat = x_train.reshape(-1, 3).astype(np.float32) / 255.0\nMEAN = _flat.mean(axis=0)\nSTD  = _flat.std(axis=0)\ndel _flat\nprint(\"mean:\", MEAN, \"std:\", STD)\n\ndef normalize_np(x_uint8):\n    return ((x_uint8.astype(np.float32) / 255.0) - MEAN) / STD\n\ndef cutout(img, size):\n    # Zero out a random size x size square (0 = channel mean after normalization)\n    cy = tf.random.uniform([], 0, 32, dtype=tf.int32)\n    cx = tf.random.uniform([], 0, 32, dtype=tf.int32)\n    ys = tf.range(32)[:, None]\n    xs = tf.range(32)[None, :]\n    inside = ((ys >= cy - size // 2) & (ys < cy + size // 2) &\n              (xs >= cx - size // 2) & (xs < cx + size // 2))\n    mask = tf.cast(tf.logical_not(inside), img.dtype)[..., None]\n    return img * mask\n\ndef augment(img, label):\n    img = (tf.cast(img, tf.float32) / 255.0 - MEAN) / STD\n    img = tf.pad(img, [[4, 4], [4, 4], [0, 0]], mode=\"REFLECT\")   # pad 4 px...\n    img = tf.image.random_crop(img, [32, 32, 3])                  # ...then random 32x32 crop\n    img = tf.image.random_flip_left_right(img)\n    if CUTOUT_SIZE:\n        img = cutout(img, CUTOUT_SIZE)\n    return img, tf.one_hot(label, 10)\n\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = (tf.data.Dataset.from_tensor_slices((x_train, y_train))\n            .shuffle(50000, seed=SEED, reshuffle_each_iteration=True)\n            .map(augment, num_parallel_calls=AUTOTUNE)\n            .batch(BATCH_SIZE, drop_remainder=True)\n            .prefetch(AUTOTUNE))\n\nx_test_norm = normalize_np(x_test)\ntest_ds = (tf.data.Dataset.from_tensor_slices((x_test_norm, tf.one_hot(y_test, 10)))\n           .batch(1024).prefetch(AUTOTUNE))\n\nsteps_per_epoch = len(x_train) // BATCH_SIZE\nprint(\"Steps per epoch:\", steps_per_epoch)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T13:50:36.954103Z","iopub.execute_input":"2026-09-23T13:50:36.954687Z","iopub.status.idle":"2026-09-23T14:23:52.295911Z","shell.execute_reply.started":"2026-09-23T13:50:36.954612Z","shell.execute_reply":"2026-09-23T14:23:52.29516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Quick look at augmented training images (sanity check)\nimgs, labels = next(iter(train_ds))\nimgs = np.clip(imgs[:12].numpy() * STD + MEAN, 0, 1)\nplt.figure(figsize=(12, 3))\nfor i in range(12):\n    plt.subplot(2, 6, i + 1); plt.imshow(imgs[i]); plt.axis(\"off\")\n    plt.title(CLASSES[int(np.argmax(labels[i]))], fontsize=9)\nplt.suptitle(\"Augmented training samples (crop + flip + cutout)\"); plt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:23:52.29779Z","iopub.execute_input":"2026-09-23T14:23:52.298542Z","iopub.status.idle":"2026-09-23T14:23:53.207337Z","shell.execute_reply.started":"2026-09-23T14:23:52.298504Z","shell.execute_reply":"2026-09-23T14:23:53.206608Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Model: ResNet-9 (randomly initialized)\nEach **ConvBN** block is Conv 3×3 → BatchNorm → ReLU. A **residual block** is two ConvBN blocks plus a skip connection.\n\n`Conv(64) → Conv(128)+Pool → Res(128) → Conv(256)+Pool → Conv(512)+Pool → Res(512) → GlobalMaxPool → Dense(10) → ×0.125 → softmax`\n\nWeights use He-normal random initialization. **No pre-trained weights are loaded anywhere.**","metadata":{}},{"cell_type":"code","source":"# Keras' L2 adds wd * sum(w^2) to the loss; using wd/2 makes the gradient equal standard weight decay\nreg = regularizers.l2(WEIGHT_DECAY / 2)\n\ndef conv_bn(x, filters):\n    x = layers.Conv2D(filters, 3, padding=\"same\", use_bias=False,\n                      kernel_initializer=\"he_normal\", kernel_regularizer=reg)(x)\n    x = layers.BatchNormalization(momentum=0.9, epsilon=1e-5)(x)\n    return layers.ReLU()(x)\n\ndef res_block(x, filters):\n    y = conv_bn(x, filters)\n    y = conv_bn(y, filters)\n    return layers.Add()([x, y])\n\ndef build_resnet9():\n    inp = keras.Input(shape=(32, 32, 3))\n    x = conv_bn(inp, 64)\n    x = conv_bn(x, 128); x = layers.MaxPooling2D()(x); x = res_block(x, 128)\n    x = conv_bn(x, 256); x = layers.MaxPooling2D()(x)\n    x = conv_bn(x, 512); x = layers.MaxPooling2D()(x); x = res_block(x, 512)\n    x = layers.GlobalMaxPooling2D()(x)\n    x = layers.Dense(10, kernel_initializer=\"he_normal\", kernel_regularizer=reg)(x)\n    x = layers.Rescaling(0.125)(x)   # scale logits down (ResNet-9 trick) for stable training at a high LR\n    out = layers.Activation(\"softmax\", dtype=\"float32\")(x)   # float32 output for mixed precision\n    return keras.Model(inp, out, name=\"resnet9\")\n\nclass OneCycleCosine(keras.optimizers.schedules.LearningRateSchedule):\n    # Linear warm-up to peak_lr, then cosine decay to ~0 (updated every step)\n    def __init__(self, peak_lr, total_steps, warmup_steps):\n        self.peak_lr, self.total_steps, self.warmup_steps = peak_lr, total_steps, warmup_steps\n    def __call__(self, step):\n        step  = tf.cast(step, tf.float32)\n        warm  = tf.cast(self.warmup_steps, tf.float32)\n        total = tf.cast(self.total_steps, tf.float32)\n        up    = self.peak_lr * step / warm\n        prog  = tf.clip_by_value((step - warm) / (total - warm), 0.0, 1.0)\n        down  = self.peak_lr * 0.5 * (1.0 + tf.cos(math.pi * prog))\n        return tf.where(step < warm, up, down)\n    def get_config(self):\n        return {\"peak_lr\": self.peak_lr, \"total_steps\": self.total_steps, \"warmup_steps\": self.warmup_steps}\n\ntotal_steps  = EPOCHS * steps_per_epoch\nwarmup_steps = int(WARMUP_FRAC * total_steps)\n\nwith strategy.scope():\n    model = build_resnet9()\n    optimizer = keras.optimizers.SGD(learning_rate=OneCycleCosine(PEAK_LR, total_steps, warmup_steps),\n                                     momentum=0.9, nesterov=True)\n    model.compile(optimizer=optimizer,\n                  loss=keras.losses.CategoricalCrossentropy(label_smoothing=LABEL_SMOOTHING),\n                  metrics=[\"accuracy\"])\n\nmodel.summary()\nN_PARAMS = model.count_params()\nprint(f\"Total parameters: {N_PARAMS:,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:23:53.208247Z","iopub.execute_input":"2026-09-23T14:23:53.208503Z","iopub.status.idle":"2026-09-23T14:23:54.276966Z","shell.execute_reply.started":"2026-09-23T14:23:53.208481Z","shell.execute_reply":"2026-09-23T14:23:54.276368Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Train\nThe official test set is shown as `val_accuracy` **for monitoring only**. The schedule is fixed (no early stopping or checkpoint selection on it),\nso the final test accuracy is an honest estimate of the Kaggle score.","metadata":{}},{"cell_type":"code","source":"t0 = time.time()\nhistory = model.fit(train_ds, epochs=EPOCHS, validation_data=test_ds, verbose=2)\nTRAIN_MIN = (time.time() - t0) / 60\nprint(f\"Training time: {TRAIN_MIN:.1f} min\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:23:54.277749Z","iopub.execute_input":"2026-09-23T14:23:54.278032Z","iopub.status.idle":"2026-09-23T14:33:02.947305Z","shell.execute_reply.started":"2026-09-23T14:23:54.27801Z","shell.execute_reply":"2026-09-23T14:33:02.946719Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"h = history.history\nfig, ax = plt.subplots(1, 2, figsize=(12, 4))\nax[0].plot(h[\"loss\"], label=\"train\"); ax[0].plot(h[\"val_loss\"], label=\"test\")\nax[0].set_title(\"Loss\"); ax[0].set_xlabel(\"epoch\"); ax[0].legend()\nax[1].plot(h[\"accuracy\"], label=\"train (augmented)\"); ax[1].plot(h[\"val_accuracy\"], label=\"test\")\nax[1].set_title(\"Accuracy\"); ax[1].set_xlabel(\"epoch\"); ax[1].legend()\nplt.tight_layout(); plt.savefig(\"/kaggle/working/training_curves.png\", dpi=120); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:33:02.948279Z","iopub.execute_input":"2026-09-23T14:33:02.948561Z","iopub.status.idle":"2026-09-23T14:33:03.455037Z","shell.execute_reply.started":"2026-09-23T14:33:02.948537Z","shell.execute_reply":"2026-09-23T14:33:03.454132Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Evaluate on the official CIFAR-10 test set (estimates the Kaggle score)","metadata":{}},{"cell_type":"code","source":"def predict_tta(x_uint8, batch_size=1024):\n    # Test-time augmentation: average the prediction on the image and its horizontal flip\n    x = normalize_np(x_uint8)\n    p  = model.predict(x, batch_size=batch_size, verbose=0)\n    p += model.predict(np.ascontiguousarray(x[:, :, ::-1, :]), batch_size=batch_size, verbose=0)\n    return p / 2.0\n\np_plain = model.predict(x_test_norm, batch_size=1024, verbose=0)\nACC_PLAIN = float(np.mean(np.argmax(p_plain, 1) == y_test))\np_tta = predict_tta(x_test)\nACC_TTA = float(np.mean(np.argmax(p_tta, 1) == y_test))\nprint(f\"Official test accuracy (no TTA):   {ACC_PLAIN:.4f}\")\nprint(f\"Official test accuracy (flip TTA): {ACC_TTA:.4f}   <-- expected Kaggle score\")\n\nmodel.save(\"/kaggle/working/resnet9_cifar10.keras\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:33:03.455952Z","iopub.execute_input":"2026-09-23T14:33:03.456502Z","iopub.status.idle":"2026-09-23T14:33:08.546954Z","shell.execute_reply.started":"2026-09-23T14:33:03.456477Z","shell.execute_reply":"2026-09-23T14:33:08.546133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Extract the Kaggle test set (300,000 images)","metadata":{}},{"cell_type":"code","source":"# Stream test.7z into a numpy array - no files on disk, constant memory\nimport io, glob, time\nimport numpy as np\nfrom PIL import Image\nimport py7zr\nfrom py7zr.io import Py7zIO, WriterFactory\n\nTEST_7Z = glob.glob(\"/kaggle/input/**/test.7z\", recursive=True)[0]\nN_TEST = 300000\nX_kaggle = np.empty((N_TEST, 32, 32, 3), dtype=np.uint8)\nstate = {\"seen\": 0, \"t0\": time.time()}\n\nclass PngIntoArray(Py7zIO):\n    \"\"\"py7zr writes one member's bytes here; we decode it on close and keep nothing.\"\"\"\n    def __init__(self, filename):\n        self.filename, self.buf = filename, io.BytesIO()\n    def write(self, s):  return self.buf.write(s)\n    def read(self, size=None): return self.buf.read(size)\n    def seek(self, offset, whence=0): return self.buf.seek(offset, whence)\n    def flush(self): pass\n    def size(self):  return self.buf.tell()\n    def close(self):\n        base = self.filename.replace(\"\\\\\", \"/\").split(\"/\")[-1]\n        if base.lower().endswith(\".png\"):\n            idx = int(base[:-4]) - 1            # \"12345.png\" -> row 12344\n            self.buf.seek(0)\n            X_kaggle[idx] = np.asarray(Image.open(self.buf).convert(\"RGB\"), dtype=np.uint8)\n            state[\"seen\"] += 1\n            if state[\"seen\"] % 50000 == 0:\n                print(f\"  {state['seen']:,} / {N_TEST:,}  ({time.time() - state['t0']:.0f}s)\")\n        self.buf = io.BytesIO()\n\nclass Factory(WriterFactory):\n    def create(self, filename): return PngIntoArray(filename)\n\nstate[\"t0\"] = time.time()\nwith py7zr.SevenZipFile(TEST_7Z, \"r\") as z:\n    z.extractall(factory=Factory())\n\nassert state[\"seen\"] == N_TEST, state[\"seen\"]\nprint(f\"Loaded {X_kaggle.shape} in {time.time() - state['t0']:.0f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:36:59.694912Z","iopub.execute_input":"2026-09-23T14:36:59.695334Z","iopub.status.idle":"2026-09-23T14:40:12.170149Z","shell.execute_reply.started":"2026-09-23T14:36:59.695302Z","shell.execute_reply":"2026-09-23T14:40:12.169468Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Locate test.7z wherever Kaggle mounted the competition data\n# cands = glob.glob(\"/kaggle/input/**/test.7z\", recursive=True)\n# assert cands, \"test.7z not found - attach the cifar-10 competition data to this notebook\"\n# TEST_7Z = cands[0]\n# TEST_DIR = \"/tmp/test\"   # extract to /tmp, not /kaggle/working (300k files would slow down saving)\n# print(\"Archive:\", TEST_7Z)\n\n# if not (os.path.isdir(TEST_DIR) and len(os.listdir(TEST_DIR)) == 300000):\n#     t0 = time.time()\n#     if shutil.which(\"7z\") is None:\n#         subprocess.run(\"apt-get update -qq && apt-get install -y -qq p7zip-full > /dev/null\",\n#                        shell=True, check=False)\n#     if shutil.which(\"7z\"):\n#         subprocess.run([\"7z\", \"x\", \"-y\", \"-o/tmp\", TEST_7Z], check=True, stdout=subprocess.DEVNULL)\n#     else:  # fallback: pure-Python extractor\n#         subprocess.run(\"pip install -q py7zr\", shell=True, check=True)\n#         import py7zr\n#         with py7zr.SevenZipFile(TEST_7Z, \"r\") as z:\n#             z.extractall(\"/tmp\")\n#     print(f\"Extracted in {time.time() - t0:.0f}s\")\n\n# n_files = len(glob.glob(TEST_DIR + \"/*\"))\n# print(\"Number of test images:\", n_files)\n# assert n_files == 300000","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:33:18.675278Z","iopub.status.idle":"2026-09-23T14:33:18.675575Z","shell.execute_reply.started":"2026-09-23T14:33:18.675447Z","shell.execute_reply":"2026-09-23T14:33:18.675462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Load images in NUMERIC order 1.png ... 300000.png (a plain sorted() would give 1, 10, 100, ... which is wrong)\n# N_TEST = 300000\n# def load_png(i):\n#     return np.asarray(Image.open(f\"{TEST_DIR}/{i}.png\").convert(\"RGB\"), dtype=np.uint8)\n\n# t0 = time.time()\n# X_kaggle = np.empty((N_TEST, 32, 32, 3), dtype=np.uint8)   # kept as uint8 (~0.9 GB) to save memory\n# with ThreadPoolExecutor(max_workers=8) as ex:\n#     for idx, img in enumerate(ex.map(load_png, range(1, N_TEST + 1))):\n#         X_kaggle[idx] = img\n# print(f\"Loaded {X_kaggle.shape} in {time.time() - t0:.0f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:33:18.676961Z","iopub.status.idle":"2026-09-23T14:33:18.677189Z","shell.execute_reply.started":"2026-09-23T14:33:18.677079Z","shell.execute_reply":"2026-09-23T14:33:18.677092Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Predict and write `submission.csv`","metadata":{}},{"cell_type":"code","source":"CHUNK = 25000   # predict in chunks so float32 copies stay small\npreds = np.empty(N_TEST, dtype=np.int64)\nt0 = time.time()\nfor s in range(0, N_TEST, CHUNK):\n    preds[s:s + CHUNK] = np.argmax(predict_tta(X_kaggle[s:s + CHUNK]), axis=1)\n    print(f\"  {min(s + CHUNK, N_TEST):,} / {N_TEST:,}\")\nprint(f\"Prediction time: {time.time() - t0:.0f}s\")\n\nsubmission = pd.DataFrame({\"id\": np.arange(1, N_TEST + 1),\n                           \"label\": [CLASSES[k] for k in preds]})\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(submission.shape)\nprint(submission.head())\nprint(submission[\"label\"].value_counts())   # should be roughly balanced across the 10 classes","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:42:35.983583Z","iopub.execute_input":"2026-09-23T14:42:35.984458Z","iopub.status.idle":"2026-09-23T14:43:52.83023Z","shell.execute_reply.started":"2026-09-23T14:42:35.984415Z","shell.execute_reply":"2026-09-23T14:43:52.829485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visual sanity check: first 12 Kaggle test images with predicted labels\nplt.figure(figsize=(12, 3))\nfor i in range(12):\n    plt.subplot(2, 6, i + 1); plt.imshow(X_kaggle[i]); plt.axis(\"off\")\n    plt.title(f\"{i + 1}: {submission.label[i]}\", fontsize=9)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:43:53.231744Z","iopub.execute_input":"2026-09-23T14:43:53.232017Z","iopub.status.idle":"2026-09-23T14:43:53.628457Z","shell.execute_reply.started":"2026-09-23T14:43:53.231993Z","shell.execute_reply":"2026-09-23T14:43:53.627807Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Summary (for the report)","metadata":{}},{"cell_type":"code","source":"print(\"Model:               ResNet-9 (random He-normal init, no pre-trained weights)\")\nprint(f\"Parameters:          {N_PARAMS:,}\")\nprint(f\"Epochs / batch:      {EPOCHS} / {BATCH_SIZE}\")\nprint(f\"Optimizer:           SGD (Nesterov, momentum 0.9), weight decay {WEIGHT_DECAY}\")\nprint(f\"LR schedule:         one-cycle, peak {PEAK_LR}, {int(WARMUP_FRAC*100)}% warm-up then cosine decay\")\nprint(f\"Augmentation:        pad-4 random crop, horizontal flip, {CUTOUT_SIZE}x{CUTOUT_SIZE} cutout\")\nprint(f\"Label smoothing:     {LABEL_SMOOTHING}\")\nprint(f\"Mixed precision:     {bool(gpus)}   | GPUs used: {strategy.num_replicas_in_sync}\")\nprint(f\"Training time:       {TRAIN_MIN:.1f} min\")\nprint(f\"Official test acc:   {ACC_PLAIN:.4f} (no TTA), {ACC_TTA:.4f} (flip TTA)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T14:45:27.73538Z","iopub.execute_input":"2026-09-23T14:45:27.736163Z","iopub.status.idle":"2026-09-23T14:45:27.742087Z","shell.execute_reply.started":"2026-09-23T14:45:27.73613Z","shell.execute_reply":"2026-09-23T14:45:27.741361Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## How to submit\n1. Click **Save Version → Save & Run All (Commit)** and wait for it to finish.\n2. Open the saved version → **Output** tab → select `submission.csv` → **Submit**.\n   (Or download `submission.csv` and upload it on the competition page → *Late Submission*.)\n3. Screenshot the score on the competition's **Submissions** tab.\n4. Set the notebook to **Public** (Share → Public) and copy its link for the report.","metadata":{}}]}