{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":3649,"databundleVersionId":46718,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# CIFAR-10 (Kaggle) — Custom Residual CNN (from scratch)\n# AdamW (+Native EMA), Cosine LR w/ Warmup, MixUp-or-CutMix, 10-crop TTA\n# Mixed precision, cached tf.data pipeline, class-labeled previews\n# ============================================================\n\n# ------------------------- FLAGS ----------------------------\nUSE_EMA             = True\nEMA_DECAY           = 0.999\nUSE_CUTOUT          = False       # off (MixUp/CutMix already strong, and this is slow)\nUSE_MIXUP           = True\nEPOCHS              = 260         \nWARMUP_EPOCHS       = 8\nBATCH               = 128\nBASE_LR             = 3e-3\nWEIGHT_DECAY        = 1e-4\nIMG_SIZE            = 32\nSEED                = 1337\nJIT_COMPILE         = False       # set True if your session is stable (can speed up)\n\nREFIT_EPOCHS        = 8\nSHOW_FIGS           = True\n# ------------------------------------------------------------\n\nimport os, math, random, numpy as np, pandas as pd, matplotlib.pyplot as plt, zipfile, shutil, time, sys\nfrom IPython.display import display, FileLink\nimport tensorflow as tf\nfrom tensorflow.keras import mixed_precision\n\n# ---- Setup & reproducibility\nmixed_precision.set_global_policy(\"mixed_float16\")\nrandom.seed(SEED); np.random.seed(SEED); tf.random.set_seed(SEED)\nprint(\"TF:\", tf.__version__)\nprint(\"GPU:\", tf.config.list_physical_devices('GPU'))\n\n# ---- Kaggle paths\nINPUT_DIR = \"/kaggle/input/cifar-10\"\nWORK_DIR  = \"/kaggle/working\"\nos.makedirs(WORK_DIR, exist_ok=True)\nprint(\"INPUT_DIR contents:\", os.listdir(INPUT_DIR))\n\n# ---- Extract train.7z / test.7z (dataset PNGs)\ntry:\n    import py7zr  # noqa\nexcept Exception:\n    print(\"Installing py7zr …\")\n    import subprocess, sys as _sys\n    subprocess.check_call([_sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"py7zr\"])\n    import py7zr\n\ndef extract_7z(src_7z, dst_dir):\n    import py7zr\n    with py7zr.SevenZipFile(src_7z, mode='r') as z:\n        z.extractall(path=dst_dir)\n\nif not os.path.exists(os.path.join(WORK_DIR, \"train\")):\n    extract_7z(os.path.join(INPUT_DIR, \"train.7z\"), WORK_DIR)\nif not os.path.exists(os.path.join(WORK_DIR, \"test\")):\n    extract_7z(os.path.join(INPUT_DIR, \"test.7z\"), WORK_DIR)\nprint(\"Extracted:\", [p for p in os.listdir(WORK_DIR) if p in [\"train\",\"test\"]])\n\n# ---- Labels & split\nCLASSES = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\nclass_to_id = {c:i for i,c in enumerate(CLASSES)}\n\ndf = pd.read_csv(os.path.join(INPUT_DIR, \"trainLabels.csv\"))\ndf[\"path\"] = df[\"id\"].astype(str).radd(os.path.join(WORK_DIR, \"train\") + \"/\") + \".png\"\ndf[\"label_id\"] = df[\"label\"].map(class_to_id).astype(int)\n\n# Stratified 45k/5k\ntry:\n    from sklearn.model_selection import StratifiedShuffleSplit\n    sss = StratifiedShuffleSplit(n_splits=1, test_size=5000, random_state=SEED)\n    idx_train, idx_valid = next(sss.split(df[\"id\"].values, df[\"label_id\"].values))\n    train_df = df.iloc[idx_train].reset_index(drop=True)\n    valid_df = df.iloc[idx_valid].reset_index(drop=True)\n    print(\"Using STRATIFIED 45k/5k split\")\nexcept Exception as e:\n    print(\"Stratified split unavailable, falling back to last-5k:\", e)\n    valid_ids = set(df.tail(5000)[\"id\"].tolist())\n    train_df = df[~df[\"id\"].isin(valid_ids)].reset_index(drop=True)\n    valid_df = df[df[\"id\"].isin(valid_ids)].reset_index(drop=True)\n\nprint(f\"Train: {len(train_df)}  Valid: {len(valid_df)}\")\n\n# ---- tf.data + MixUp/CutMix\nAUTO = tf.data.AUTOTUNE\nNUM_CLASSES = 10\nMEAN = tf.constant([0.4914, 0.4822, 0.4465], tf.float32)\nSTD  = tf.constant([0.2470, 0.2435, 0.2616], tf.float32)\n\ndef decode_image(path, label_id):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_png(img, channels=3)\n    img = tf.image.convert_image_dtype(img, tf.float32)     # [0,1]\n    img = tf.image.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = (img - MEAN) / STD\n    y = tf.one_hot(label_id, NUM_CLASSES, dtype=tf.float32)\n    return img, y\n\ndef color_jitter(img, y):\n    if tf.random.uniform([]) < 0.5:\n        img = tf.image.random_brightness(img, 0.15)\n        img = tf.image.random_contrast(img, 0.85, 1.15)\n        img = tf.image.random_saturation(img, 0.85, 1.15)\n    return img, y\n\ndef pad_random_crop_flip(img, y):\n    img = tf.pad(img, [[4,4],[4,4],[0,0]], mode=\"REFLECT\")\n    img = tf.image.random_crop(img, [IMG_SIZE, IMG_SIZE, 3])\n    img = tf.image.random_flip_left_right(img)\n    return img, y\n\ndef cutout(img, mask_size=16):\n    h = tf.shape(img)[0]; w = tf.shape(img)[1]\n    m = tf.cast(mask_size, tf.int32)\n    cy = tf.random.uniform([], 0, h, dtype=tf.int32)\n    cx = tf.random.uniform([], 0, w, dtype=tf.int32)\n    y1 = tf.clip_by_value(cy - m//2, 0, h); y2 = tf.clip_by_value(cy + m//2, 0, h)\n    x1 = tf.clip_by_value(cx - m//2, 0, w); x2 = tf.clip_by_value(cx + m//2, 0, w)\n    mask = tf.ones([y2-y1, x2-x1, 3], dtype=img.dtype)\n    mask = tf.pad(mask, [[y1, h-y2],[x1, w-x2],[0,0]])\n    return img * (1.0 - mask)\n\ndef sample_beta_tf(alpha=0.8):\n    u1 = tf.random.gamma([], alpha, dtype=tf.float32)\n    u2 = tf.random.gamma([], alpha, dtype=tf.float32)\n    lam = u1 / (u1 + u2)\n    return tf.maximum(lam, 1.0 - lam)\n\ndef rand_bbox(h, w, lam):\n    h = tf.cast(h, tf.int32); w = tf.cast(w, tf.int32)\n    cut_ratio = tf.sqrt(1.0 - lam)\n    ch = tf.cast(tf.cast(h, tf.float32) * cut_ratio, tf.int32)\n    cw = tf.cast(tf.cast(w, tf.float32) * cut_ratio, tf.int32)\n    cy = tf.random.uniform([], 0, h, dtype=tf.int32)\n    cx = tf.random.uniform([], 0, w, dtype=tf.int32)\n    y1 = tf.clip_by_value(cy - ch//2, 0, h); y2 = tf.clip_by_value(cy + ch//2, 0, h)\n    x1 = tf.clip_by_value(cx - cw//2, 0, w); x2 = tf.clip_by_value(cx + cw//2, 0, w)\n    return y1, y2, x1, x2\n\n@tf.function\ndef mixup_or_cutmix(images, labels, p_cutmix=0.5, alpha=0.8):\n    r = tf.random.uniform([])\n    bs = tf.shape(images)[0]\n    idx = tf.random.shuffle(tf.range(bs))\n    images2 = tf.gather(images, idx)\n    labels2 = tf.gather(labels, idx)\n\n    def cutmix_branch():\n        lam = sample_beta_tf(alpha)\n        h = tf.shape(images)[1]; w = tf.shape(images)[2]\n        y1, y2, x1, x2 = rand_bbox(h, w, lam)\n        mask = tf.ones([bs, y2-y1, x2-x1, 3], dtype=images.dtype)\n        mask = tf.pad(mask, [[0,0],[y1, h-y2],[x1, w-x2],[0,0]])\n        mixed = images * (1.0 - mask) + images2 * mask\n        lam_eff = 1.0 - tf.cast((y2-y1)*(x2-x1), tf.float32) / tf.cast(h*w, tf.float32)\n        out_lbl = lam_eff * labels + (1.0 - lam_eff) * labels2\n        return mixed, out_lbl\n\n    def mixup_branch():\n        lam = sample_beta_tf(alpha)\n        mixed = lam * images + (1.0 - lam) * images2\n        out_lbl = lam * labels + (1.0 - lam) * labels2\n        return mixed, out_lbl\n\n    return tf.cond(r < p_cutmix, cutmix_branch, mixup_branch)\n\nMIX_PROB = tf.Variable(0.6 if USE_MIXUP else 0.0, trainable=False, dtype=tf.float32)\n\ndef after_batch_aug(images, labels):\n    do_mix = tf.logical_and(tf.constant(USE_MIXUP), tf.less(tf.random.uniform([]), MIX_PROB))\n    def mix_branch():    return mixup_or_cutmix(images, labels, p_cutmix=0.5, alpha=0.8)\n    def no_mix_branch():\n        imgs = tf.map_fn(lambda im: cutout(im), images) if USE_CUTOUT else images\n        return imgs, labels\n    return tf.cond(do_mix, mix_branch, no_mix_branch)\n\ndef apply_ds_opts(ds, deterministic=False):\n    opts = tf.data.Options()\n    opts.deterministic = deterministic\n    exp = opts.experimental_optimization\n    for name, val in [\n        (\"autotune_buffers\", True),\n        (\"apply_default_optimizations\", True),\n        (\"map_parallelization\", True),\n    ]:\n        if hasattr(exp, name):\n            try: setattr(exp, name, val)\n            except Exception: pass\n    return ds.with_options(opts)\n\ndef make_base_ds(frame, shuffle=False, cache=True):\n    ds = tf.data.Dataset.from_tensor_slices((frame[\"path\"].values, frame[\"label_id\"].values))\n    ds = ds.map(decode_image, num_parallel_calls=AUTO)\n    if cache: ds = ds.cache()\n    if shuffle: ds = ds.shuffle(8192, reshuffle_each_iteration=True)\n    return ds\n\ndef make_train_ds(df_train):\n    ds = make_base_ds(df_train, shuffle=True, cache=True)\n    ds = ds.map(pad_random_crop_flip, num_parallel_calls=AUTO, deterministic=False)\n    ds = ds.map(color_jitter, num_parallel_calls=AUTO, deterministic=False)\n    ds = ds.batch(BATCH, drop_remainder=True)\n    ds = ds.map(after_batch_aug, num_parallel_calls=AUTO, deterministic=False)\n    ds = apply_ds_opts(ds, deterministic=False).prefetch(AUTO)\n    return ds\n\ndef make_valid_ds(df_valid):\n    ds = make_base_ds(df_valid, shuffle=False, cache=True)\n    ds = ds.batch(BATCH)\n    ds = apply_ds_opts(ds, deterministic=True).prefetch(AUTO)\n    return ds\n\ntrain_ds = make_train_ds(train_df)\nvalid_ds = make_valid_ds(valid_df)\n\n# ---- Augmented sample preview\nif SHOW_FIGS:\n    xb, yb = next(iter(train_ds.take(1)))\n    grid = min(16, xb.shape[0])\n    plt.figure(figsize=(8,8))\n    for i in range(grid):\n        ax = plt.subplot(4,4,i+1)\n        img = xb[i].numpy()\n        img_vis = (img * STD.numpy()) + MEAN.numpy()\n        img_vis = np.clip(img_vis, 0, 1)\n        top = int(np.argmax(yb[i].numpy()))\n        conf = float(np.max(yb[i].numpy()))\n        ax.imshow(img_vis); ax.axis('off')\n        ax.set_title(f\"{CLASSES[top]} ({conf:.2f})\", fontsize=9)\n    plt.suptitle(\"Augmented samples (MixUp/CutMix)\"); plt.tight_layout(); plt.show(); plt.close()\n\n# ---- Model (ResNet-ish + SE)\nfrom tensorflow.keras import layers, models, regularizers\n\ndef conv_bn_relu(x, f, k=3, s=1, wd=WEIGHT_DECAY):\n    x = layers.Conv2D(f, k, s, padding=\"same\", use_bias=False,\n                      kernel_initializer=\"he_normal\",\n                      kernel_regularizer=regularizers.l2(wd))(x)\n    x = layers.BatchNormalization()(x)\n    return layers.ReLU()(x)\n\ndef se_block(x, r=16):\n    c = x.shape[-1]\n    s = layers.GlobalAveragePooling2D()(x)\n    s = layers.Dense(max(c//r, 8), activation=\"relu\", kernel_initializer=\"he_normal\")(s)\n    s = layers.Dense(c, activation=\"sigmoid\", kernel_initializer=\"he_normal\")(s)\n    s = layers.Reshape((1,1,c))(s)\n    return layers.Multiply()([x, s])\n\ndef basic_block(x, f, down=False, wd=WEIGHT_DECAY, use_se=True, drop_path=0.0):\n    s = 2 if down else 1\n    shortcut = x\n    y = conv_bn_relu(x, f, 3, s, wd)\n    y = layers.Conv2D(f, 3, padding=\"same\", use_bias=False,\n                      kernel_initializer=\"he_normal\",\n                      kernel_regularizer=regularizers.l2(wd))(y)\n    y = layers.BatchNormalization()(y)\n    if use_se: y = se_block(y)\n    if drop_path > 0: y = layers.Dropout(drop_path)(y)\n    if down or x.shape[-1] != f:\n        shortcut = layers.Conv2D(f, 1, s, padding=\"same\", use_bias=False,\n                                 kernel_initializer=\"he_normal\",\n                                 kernel_regularizer=regularizers.l2(wd))(shortcut)\n        shortcut = layers.BatchNormalization()(shortcut)\n    out = layers.Add()([shortcut, y])\n    return layers.ReLU()(out)\n\ndef build_resnet_cifar(num_classes=10, wd=WEIGHT_DECAY, head_drop=0.15):\n    inp = layers.Input((IMG_SIZE, IMG_SIZE, 3))\n    x = layers.Activation(\"linear\", dtype=\"float32\", name=\"cast_to_f32\")(inp)\n    def stage(x, f, n, down_first, dp):\n        for i in range(n):\n            x = basic_block(x, f, down=(down_first and i==0), wd=wd, use_se=True, drop_path=dp)\n        return x\n    x = conv_bn_relu(x, 64, 3, 1, wd)\n    x = stage(x, 64,  3, False, 0.0)\n    x = stage(x, 128, 3, True,  0.0)\n    x = stage(x, 256, 3, True,  0.10)  # mild stochastic depth late\n    x = stage(x, 512, 3, True,  0.10)\n    x = layers.GlobalAveragePooling2D()(x)\n    if head_drop > 0: x = layers.Dropout(head_drop)(x)\n    out = layers.Dense(num_classes, activation=\"softmax\", dtype=\"float32\",\n                       kernel_initializer=\"he_normal\")(x)\n    return models.Model(inp, out, name=\"CIFAR10_CustomResNet_SE\")\n\nmodel = build_resnet_cifar()\nmodel.summary()\n\n# ---- Optimizer + cosine LR with warmup (AdamW + native EMA)\nfrom tensorflow.keras.optimizers import AdamW\noptimizer = AdamW(learning_rate=BASE_LR, weight_decay=WEIGHT_DECAY,\n                  use_ema=USE_EMA, ema_momentum=EMA_DECAY, clipnorm=1.0)\n\ndef lr_schedule(epoch):\n    if epoch < WARMUP_EPOCHS:\n        return BASE_LR * (epoch + 1) / WARMUP_EPOCHS\n    t = (epoch - WARMUP_EPOCHS) / max(1, (EPOCHS - WARMUP_EPOCHS))\n    min_lr = BASE_LR * 0.01\n    return min_lr + 0.5 * (BASE_LR - min_lr) * (1 + math.cos(math.pi * t))\n\nclass MixProbScheduler(tf.keras.callbacks.Callback):\n    def on_epoch_begin(self, epoch, logs=None):\n        # taper mixing sooner for sharper late training\n        if epoch < 150:   MIX_PROB.assign(0.6)\n        elif epoch < 200: MIX_PROB.assign(0.3)\n        else:             MIX_PROB.assign(0.1)\n\nlr_cb = tf.keras.callbacks.LearningRateScheduler(lr_schedule, verbose=1)\nckpt_cb = tf.keras.callbacks.ModelCheckpoint(\"best.keras\", monitor=\"val_accuracy\",\n                                             save_best_only=True, mode=\"max\", verbose=1)\nearly_cb = tf.keras.callbacks.EarlyStopping(monitor=\"val_loss\", patience=35,\n                                            restore_best_weights=True, verbose=1, start_from_epoch=120)\nnan_cb = tf.keras.callbacks.TerminateOnNaN()\nloss_fn = tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.02)\nmodel.compile(optimizer=optimizer, loss=loss_fn, metrics=[\"accuracy\"], jit_compile=JIT_COMPILE)\n\n# ---- Train (verbose=1 shows batch progress so it never looks frozen)\nhistory = model.fit(\n    train_ds,\n    validation_data=valid_ds,\n    epochs=EPOCHS,\n    callbacks=[MixProbScheduler(), lr_cb, ckpt_cb, early_cb, nan_cb],\n    verbose=1\n)\n\n# ---- Curves\nif SHOW_FIGS:\n    hist = history.history\n    plt.figure(figsize=(10,4))\n    plt.subplot(1,2,1); plt.plot(hist[\"loss\"]); plt.plot(hist[\"val_loss\"]); plt.title(\"Loss\"); plt.legend([\"train\",\"val\"]); plt.grid(True, ls=\"--\", alpha=.4)\n    plt.subplot(1,2,2); plt.plot(hist[\"accuracy\"]); plt.plot(hist[\"val_accuracy\"]); plt.title(\"Accuracy\"); plt.legend([\"train\",\"val\"]); plt.grid(True, ls=\"--\", alpha=.4)\n    plt.tight_layout(); plt.show(); plt.close()\n\nval_loss, val_acc = model.evaluate(valid_ds, verbose=0)\nprint(f\"Validation accuracy: {val_acc:.4f}\")\n\n# --- Ensure best.keras in /kaggle/working BEFORE next steps\nckpt_path = os.path.join(WORK_DIR, \"best.keras\")\nif os.path.exists(\"best.keras\") and (not os.path.samefile(\"best.keras\", ckpt_path)):\n    os.replace(\"best.keras\", ckpt_path)\nelif not os.path.exists(ckpt_path) and os.path.exists(\"best.keras\"):\n    os.replace(\"best.keras\", ckpt_path)\nelif not os.path.exists(ckpt_path):\n    model.save(ckpt_path)\nprint(\"Checkpoint at:\", ckpt_path, \" | Exists:\", os.path.exists(ckpt_path))\n\n# =========================\n# Refit on ALL 50k (train+valid) with tiny LR, then 10-crop TTA\n# =========================\n\n# 1) Refit on all labels\nall_df = pd.concat([train_df, valid_df], ignore_index=True)\ndef make_all_ds(df_all):\n    ds = make_base_ds(df_all, shuffle=True, cache=True)\n    ds = ds.map(pad_random_crop_flip, num_parallel_calls=AUTO, deterministic=False)\n    ds = ds.map(color_jitter, num_parallel_calls=AUTO, deterministic=False)\n    ds = ds.batch(BATCH, drop_remainder=True)\n    ds = ds.map(after_batch_aug, num_parallel_calls=AUTO, deterministic=False)\n    ds = apply_ds_opts(ds, deterministic=False).prefetch(AUTO)\n    return ds\nall_ds = make_all_ds(all_df)\n\nfrom tensorflow.keras.models import load_model\nbest = load_model(ckpt_path, compile=False)\nbest.compile(\n    optimizer=AdamW(learning_rate=1e-4, weight_decay=WEIGHT_DECAY, use_ema=USE_EMA, ema_momentum=EMA_DECAY),\n    loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.01),\n    metrics=[\"accuracy\"],\n    jit_compile=JIT_COMPILE\n)\nprint(\"\\n[Refit] Training on all 50k with tiny LR…\")\nbest.fit(all_ds, epochs=REFIT_EPOCHS, verbose=1)\nbest.save(ckpt_path)\ndel best\n\n# 2) Deterministic 10-crop TTA (5 crops × {no-flip, flip})\nprint(\"\\n[TTA] 10-crop (5 crops × flip) inference…\")\n\ndef decode_test_raw(path):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_png(img, channels=3)\n    img = tf.image.convert_image_dtype(img, tf.float32)\n    img = tf.image.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = (img - MEAN) / STD\n    return img\n\ndef pad_and_crop(img, top, left, size=IMG_SIZE, pad=4):\n    im = tf.pad(img, [[pad,pad],[pad,pad],[0,0]], mode=\"REFLECT\")\n    return tf.image.crop_to_bounding_box(im, top, left, size, size)\n\nCROPS = [(0,0), (0,8), (8,0), (8,8), (4,4)]\n\nsub_template = pd.read_csv(os.path.join(INPUT_DIR, \"sampleSubmission.csv\"))\ntest_paths = [os.path.join(WORK_DIR, \"test\", (str(fid) if str(fid).endswith(\".png\") else f\"{fid}.png\"))\n              for fid in sub_template[\"id\"].values]\npath_ds = tf.data.Dataset.from_tensor_slices(test_paths)\n\ndef make_variant_ds(flip=False, crop_idx=0):\n    def _map(p):\n        img = decode_test_raw(p)\n        top,left = CROPS[crop_idx]\n        img = pad_and_crop(img, top, left)\n        if flip: img = tf.image.flip_left_right(img)\n        return img\n    return (path_ds.map(_map, num_parallel_calls=AUTO)\n            .batch(BATCH).prefetch(AUTO))\n\nbest = tf.keras.models.load_model(ckpt_path, compile=False)\n\nprobs_sum = None\nvariants = []\nfor ci in range(5):\n    variants.append(make_variant_ds(flip=False, crop_idx=ci))\n    variants.append(make_variant_ds(flip=True,  crop_idx=ci))\n\nfor i, ds in enumerate(variants):\n    print(f\"Variant {i+1}/10\")\n    p = best.predict(ds, verbose=1)\n    probs_sum = p if probs_sum is None else probs_sum + p\n\nprobs = probs_sum / 10.0\npred_ids = np.argmax(probs, axis=1)\npred_labels = [CLASSES[i] for i in pred_ids]\n\n# ---- Write submissions visible in Output pane\nsub = sub_template.copy()\nsub[\"label\"] = pred_labels\n\nout_csv   = os.path.join(WORK_DIR, \"submission_tta.csv\")\nalias_csv = os.path.join(WORK_DIR, \"submission.csv\")\nzip_path  = os.path.join(WORK_DIR, \"submission_tta.zip\")\n\nsub.to_csv(out_csv, index=False)\nif os.path.exists(alias_csv): os.remove(alias_csv)\nshutil.copy2(out_csv, alias_csv)\n\nwith zipfile.ZipFile(zip_path, \"w\", compression=zipfile.ZIP_DEFLATED) as zf:\n    zf.write(out_csv,   arcname=os.path.basename(out_csv))\n    zf.write(alias_csv, arcname=os.path.basename(alias_csv))\n\n# ---- Sanity checks & sidebar-friendly links\ndef _size(p):\n    try: return os.path.getsize(p)\n    except: return 0\nprint(\"\\n=== OUTPUT CHECKS (post-refit + 10-crop TTA) ===\")\nprint(\"Dir:\", WORK_DIR)\nprint(\"CSV exists:\", os.path.exists(out_csv), \"  size:\", _size(out_csv))\nprint(\"Alias exists:\", os.path.exists(alias_csv), \"  size:\", _size(alias_csv))\nprint(\"ZIP exists:\", os.path.exists(zip_path), \"  size:\", _size(zip_path))\ntry:\n    display(pd.read_csv(out_csv).head())\nexcept Exception as e:\n    print(\"Preview failed:\", e)\n\nfor p in [out_csv, alias_csv, zip_path]:\n    if os.path.exists(p):\n        display(FileLink(p))\n\nsys.stdout.flush()\nprint(\"\\nReady to download from the Output panel:\")\nprint(\"  -\", out_csv)\nprint(\"  -\", alias_csv)\nprint(\"  -\", zip_path)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-12T16:17:42.580262Z","iopub.execute_input":"2025-10-12T16:17:42.580523Z","iopub.status.idle":"2025-10-12T21:37:10.541658Z","shell.execute_reply.started":"2025-10-12T16:17:42.580503Z","shell.execute_reply":"2025-10-12T21:37:10.541013Z"}},"outputs":[],"execution_count":null}]}