{"metadata":{"kernelspec":{"display_name":"Python 3","name":"python3"},"language_info":{"name":""},"colab":{"gpuType":"T4","provenance":[]},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"b77b7e4e","cell_type":"markdown","source":"# CIFAR-10 Image Classification: Improved Model (Before vs After)\n\n**Your original notebook** trained two models on CIFAR-10: a small dense network (~39% validation accuracy) and a ResNet50 (8x up-sampled to 256x256, flattened 8x8x2048 features) that reached **93.86% test accuracy** in about **80 minutes**.\n\n**This notebook keeps the same data and the same 80/20 split (`random_state=2`)**, so the test set is identical, and builds a better model.\n\n| Area | Original ResNet50 | Improved |\n|---|---|---|\n| Backbone | ResNet50 (2015) | **ConvNeXt-Tiny** (2022), ImageNet pre-trained |\n| Input | 32px up-sampled to 256px | 32px up-sampled to 128px (about 4x cheaper per image) |\n| Pixel preprocessing | `/255` (ResNet50 expects different ImageNet scaling) | model's own built-in preprocessing, fed raw 0-255 pixels |\n| Classifier head | `Flatten` of 131,072 features then 3 Dense layers (about 17M head params, overfits: train 97.6% vs val 94.6%) | Global Average Pooling then Dropout then Dense(10) |\n| Data augmentation | none | flip, shift, rotation, zoom |\n| Optimiser | RMSprop, fixed LR 2e-5 | AdamW + warm-up + cosine learning-rate decay |\n| Loss | cross-entropy | cross-entropy + label smoothing (0.1) |\n| Precision | float32 | mixed precision (float16) on GPU, faster |\n| Model selection | last epoch | best validation epoch restored |\n| Inference | single pass | + horizontal-flip test-time augmentation (TTA) |\n\n**Runtime:** Colab with a **GPU** (Runtime > Change runtime type > T4 GPU).","metadata":{}},{"id":"479baedd","cell_type":"markdown","source":"## 1. Download and extract CIFAR-10 (same as original)","metadata":{}},{"id":"2b18f80d","cell_type":"markdown","source":"## 2. Imports and settings","metadata":{}},{"id":"737ff3ba","cell_type":"code","source":"import os, time, json\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom tqdm.auto import tqdm\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\n\n# ---------------- settings ----------------\nSEED = 2\nIMG = 128                 # up-sampled input size for the backbone\nBATCH = 64\nEPOCHS = 8\nPEAK_LR = 1e-4\nWEIGHT_DECAY = 0.01\nLABEL_SMOOTHING = 0.1\nWEIGHTS = 'imagenet'      # pre-trained weights\nDENSE_EPOCHS = 10         # the original small dense network\nRETRAIN_RESNET_BASELINE = False   # True = re-train your original ResNet50 here (about 80 min) instead of using the recorded numbers\nCLASS_NAMES = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\n\nnp.random.seed(SEED); tf.random.set_seed(SEED)\nGPU = len(tf.config.list_physical_devices('GPU')) > 0\nprint('TensorFlow', tf.__version__, '| GPU available:', GPU)\nif not GPU: print('WARNING: no GPU found. Training will be very slow. Enable a GPU runtime.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:39:21.274527Z","iopub.execute_input":"2026-10-10T16:39:21.275105Z","iopub.status.idle":"2026-10-10T16:39:21.282704Z","shell.execute_reply.started":"2026-10-10T16:39:21.275078Z","shell.execute_reply":"2026-10-10T16:39:21.281974Z"}},"outputs":[],"execution_count":null},{"id":"d05e812a","cell_type":"markdown","source":"## 3. Load images and labels (same as original)","metadata":{}},{"id":"0f7e082b","cell_type":"code","source":"labels_df = pd.read_csv('/kaggle/input/competitions/cifar-10/trainLabels.csv')\nlabel_map = {n: i for i, n in enumerate(CLASS_NAMES)}\nlabels = [label_map[l] for l in labels_df['label']]\nid_list = list(labels_df['id'])\n\ndata = [np.array(Image.open(f'/kaggle/working/train/train/{i}.png').convert('RGB')) for i in tqdm(id_list)]\nX = np.array(data, dtype=np.uint8); Y = np.array(labels); del data\nprint(X.shape, Y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:39:21.28428Z","iopub.execute_input":"2026-10-10T16:39:21.284929Z","iopub.status.idle":"2026-10-10T16:39:29.679625Z","shell.execute_reply.started":"2026-10-10T16:39:21.284899Z","shell.execute_reply":"2026-10-10T16:39:29.67883Z"}},"outputs":[],"execution_count":null},{"id":"dd3222a8","cell_type":"markdown","source":"## 4. Train / validation / test split (identical to original)\nSame call as before: 80/20 with `random_state=2`. The last 10% of the training part is the validation set, exactly what `validation_split=0.1` used.","metadata":{}},{"id":"dfc06280","cell_type":"code","source":"X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=2)\nn_val = int(len(X_train) * 0.1)\nX_tr, Y_tr = X_train[:-n_val], Y_train[:-n_val]\nX_val, Y_val = X_train[-n_val:], Y_train[-n_val:]\nprint('train', X_tr.shape, '| val', X_val.shape, '| test', X_test.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:39:29.680816Z","iopub.execute_input":"2026-10-10T16:39:29.681145Z","iopub.status.idle":"2026-10-10T16:39:29.739819Z","shell.execute_reply.started":"2026-10-10T16:39:29.681113Z","shell.execute_reply":"2026-10-10T16:39:29.739114Z"}},"outputs":[],"execution_count":null},{"id":"c1262a37","cell_type":"markdown","source":"## 5. BEFORE: your original models\n### 5a. ResNet50 numbers recorded from your original notebook run\nThese are copied from the saved outputs of your own notebook (10 epochs), so no re-training is needed. Set `RETRAIN_RESNET_BASELINE = True` above if you prefer to re-run it.","metadata":{}},{"id":"9f3e5bfc","cell_type":"code","source":"resnet_rec = {\n    'loss':    [1.6400,0.9631,0.7167,0.5456,0.4263,0.3387,0.2800,0.2276,0.1899,0.1634],\n    'acc':     [0.4631,0.7197,0.8133,0.8719,0.9108,0.9365,0.9500,0.9626,0.9711,0.9757],\n    'val_loss':[0.8222,0.4171,0.3444,0.2669,0.2324,0.2353,0.2237,0.2111,0.2091,0.2015],\n    'val_acc': [0.7667,0.8953,0.9195,0.9335,0.9383,0.9385,0.9413,0.9392,0.9427,0.9460],\n    'epoch_sec':[503,482,477,476,476,482,477,482,476,476],\n    'test_acc': 0.9386,\n}\nresnet_pred = None   # per-class predictions only exist if the baseline is re-trained below\nprint(f\"Recorded ResNet50 test accuracy: {resnet_rec['test_acc']*100:.2f}%  | total training time: {sum(resnet_rec['epoch_sec'])/60:.0f} min\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:39:29.741461Z","iopub.execute_input":"2026-10-10T16:39:29.741859Z","iopub.status.idle":"2026-10-10T16:39:29.752371Z","shell.execute_reply.started":"2026-10-10T16:39:29.741834Z","shell.execute_reply":"2026-10-10T16:39:29.751648Z"}},"outputs":[],"execution_count":null},{"id":"73d88878","cell_type":"markdown","source":"### 5b. Small dense network (original first model), re-trained here for a test-set score\nYour notebook only reported its validation accuracy; training takes about 1 minute so it is re-run to get a test accuracy.","metadata":{}},{"id":"b53e6865","cell_type":"code","source":"dense = keras.Sequential([\n    keras.Input(shape=(32, 32, 3)),\n    layers.Rescaling(1./255),\n    layers.Flatten(),\n    layers.Dense(64, activation='relu'),\n    layers.Dense(10, activation='softmax'),\n])\ndense.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['acc'])\nhist_dense = dense.fit(X_tr, Y_tr, validation_data=(X_val, Y_val), epochs=DENSE_EPOCHS, batch_size=32, verbose=2)\ndense_loss, dense_acc = dense.evaluate(X_test, Y_test, verbose=0)\nprint(f'Dense network test accuracy: {dense_acc*100:.2f}%')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:39:29.753216Z","iopub.execute_input":"2026-10-10T16:39:29.75402Z","iopub.status.idle":"2026-10-10T16:40:00.993086Z","shell.execute_reply.started":"2026-10-10T16:39:29.753997Z","shell.execute_reply":"2026-10-10T16:40:00.992149Z"}},"outputs":[],"execution_count":null},{"id":"fa2d783a","cell_type":"code","source":"# Optional: re-train the original ResNet50 pipeline (about 80 min on a T4). Skipped by default.\nif RETRAIN_RESNET_BASELINE:\n    from tensorflow.keras.applications.resnet50 import ResNet50\n    base50 = ResNet50(weights='imagenet', include_top=False, input_shape=(256, 256, 3))\n    r50 = keras.Sequential([\n        keras.Input(shape=(32, 32, 3)),\n        layers.UpSampling2D((2,2)), layers.UpSampling2D((2,2)), layers.UpSampling2D((2,2)),\n        base50, layers.Flatten(), layers.BatchNormalization(),\n        layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.BatchNormalization(),\n        layers.Dense(64, activation='relu'), layers.Dropout(0.5), layers.BatchNormalization(),\n        layers.Dense(10, activation='softmax')])\n    r50.compile(optimizer=keras.optimizers.RMSprop(learning_rate=2e-5), loss='sparse_categorical_crossentropy', metrics=['acc'])\n    t = []\n    class _T(keras.callbacks.Callback):\n        def on_epoch_begin(self, e, logs=None): self.s = time.time()\n        def on_epoch_end(self, e, logs=None): t.append(time.time() - self.s)\n    h = r50.fit((X_tr/255.0).astype('float32'), Y_tr, validation_data=((X_val/255.0).astype('float32'), Y_val),\n                epochs=10, batch_size=32, callbacks=[_T()])\n    resnet_pred = r50.predict((X_test/255.0).astype('float32'), batch_size=128, verbose=0).argmax(1)\n    resnet_rec = {**{k: h.history[k] for k in ['loss','acc','val_loss','val_acc']}, 'epoch_sec': t,\n                  'test_acc': float((resnet_pred == Y_test).mean())}\n    print('Re-trained ResNet50 test accuracy:', resnet_rec['test_acc'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:40:00.994439Z","iopub.execute_input":"2026-10-10T16:40:00.994853Z","iopub.status.idle":"2026-10-10T16:40:01.009219Z","shell.execute_reply.started":"2026-10-10T16:40:00.99482Z","shell.execute_reply":"2026-10-10T16:40:01.008309Z"}},"outputs":[],"execution_count":null},{"id":"33ed8189","cell_type":"markdown","source":"## 6. AFTER: improved model (ConvNeXt-Tiny + augmentation + better training recipe)","metadata":{}},{"id":"036751ee","cell_type":"code","source":"if GPU:\n    keras.mixed_precision.set_global_policy('mixed_float16')   # faster on T4/L4/A100\nprint('compute dtype:', keras.mixed_precision.global_policy().compute_dtype)\n\naugment = keras.Sequential([\n    layers.RandomFlip('horizontal'),\n    layers.RandomTranslation(0.1, 0.1, fill_mode='reflect'),\n    layers.RandomRotation(0.05, fill_mode='reflect'),\n    layers.RandomZoom(0.1, fill_mode='reflect'),\n], name='augmentation')\n\nbackbone = keras.applications.ConvNeXtTiny(include_top=False, weights=WEIGHTS, pooling='avg',\n                                          include_preprocessing=True,       # expects raw 0-255 pixels\n                                          input_shape=(IMG, IMG, 3))\n\ninputs = keras.Input(shape=(32, 32, 3))\nx = augment(inputs)                                   # only active during training\nx = layers.Resizing(IMG, IMG, interpolation='bilinear')(x)   # 32px to 128px\nx = backbone(x)\nx = layers.Dropout(0.3)(x)\noutputs = layers.Dense(10, activation='softmax', dtype='float32')(x)\nmodel = keras.Model(inputs, outputs, name='convnext_cifar10')\nprint(f'parameters: {model.count_params()/1e6:.1f}M')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:40:01.010314Z","iopub.execute_input":"2026-10-10T16:40:01.010625Z","iopub.status.idle":"2026-10-10T16:40:02.7587Z","shell.execute_reply.started":"2026-10-10T16:40:01.010594Z","shell.execute_reply":"2026-10-10T16:40:02.757822Z"}},"outputs":[],"execution_count":null},{"id":"1fca5c73","cell_type":"code","source":"def make_ds(X, Y, training):\n    ds = tf.data.Dataset.from_tensor_slices((X.astype('float32'), tf.one_hot(Y, 10)))\n    if training: ds = ds.shuffle(10000, seed=SEED, reshuffle_each_iteration=True)\n    return ds.batch(BATCH).prefetch(tf.data.AUTOTUNE)\n\ntrain_ds, val_ds = make_ds(X_tr, Y_tr, True), make_ds(X_val, Y_val, False)\n\nsteps_per_epoch = int(np.ceil(len(X_tr) / BATCH))\nwarmup = steps_per_epoch                      # 1 epoch warm-up\nschedule = keras.optimizers.schedules.CosineDecay(\n    initial_learning_rate=1e-6, decay_steps=EPOCHS * steps_per_epoch - warmup,\n    alpha=0.01, warmup_target=PEAK_LR, warmup_steps=warmup)\n\nmodel.compile(\n    optimizer=keras.optimizers.AdamW(learning_rate=schedule, weight_decay=WEIGHT_DECAY),\n    loss=keras.losses.CategoricalCrossentropy(label_smoothing=LABEL_SMOOTHING),\n    metrics=[keras.metrics.CategoricalAccuracy(name='acc'),\n             keras.metrics.CategoricalCrossentropy(name='ce')])   # plain CE, comparable with the original val_loss\n\nclass EpochTimer(keras.callbacks.Callback):\n    def __init__(self): super().__init__(); self.times = []\n    def on_epoch_begin(self, e, logs=None): self._s = time.time()\n    def on_epoch_end(self, e, logs=None): self.times.append(time.time() - self._s)\n\ntimer = EpochTimer()\nckpt = keras.callbacks.ModelCheckpoint('best_cifar10.weights.h5', monitor='val_acc', mode='max',\n                                       save_best_only=True, save_weights_only=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:40:02.759746Z","iopub.execute_input":"2026-10-10T16:40:02.760131Z","iopub.status.idle":"2026-10-10T16:40:05.121828Z","shell.execute_reply.started":"2026-10-10T16:40:02.760108Z","shell.execute_reply":"2026-10-10T16:40:05.121244Z"}},"outputs":[],"execution_count":null},{"id":"98ad979f","cell_type":"code","source":"history = model.fit(train_ds, validation_data=val_ds, epochs=EPOCHS, callbacks=[timer, ckpt], verbose=2)\nmodel.load_weights('best_cifar10.weights.h5')     # restore the best validation epoch\nprint(f'total training time: {sum(timer.times)/60:.1f} min')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T16:40:05.122694Z","iopub.execute_input":"2026-10-10T16:40:05.123013Z","iopub.status.idle":"2026-10-10T17:31:29.131416Z","shell.execute_reply.started":"2026-10-10T16:40:05.122978Z","shell.execute_reply":"2026-10-10T17:31:29.130574Z"}},"outputs":[],"execution_count":null},{"id":"e9b41932","cell_type":"markdown","source":"## 7. Evaluation on the identical test set","metadata":{}},{"id":"0668d6bc","cell_type":"code","source":"def probs(m, X, flip_tta=False, bs=256):\n    Xf = X.astype('float32')\n    p = m.predict(Xf, batch_size=bs, verbose=0)\n    if flip_tta:\n        p = (p + m.predict(Xf[:, :, ::-1, :], batch_size=bs, verbose=0)) / 2\n    return p\n\np_plain = probs(model, X_test)\np_tta   = probs(model, X_test, flip_tta=True)\npred_plain, pred_tta = p_plain.argmax(1), p_tta.argmax(1)\n\nacc_plain, acc_tta = float((pred_plain == Y_test).mean()), float((pred_tta == Y_test).mean())\nacc_before = resnet_rec['test_acc']\nprint(f'DENSE NET   : {dense_acc*100:.2f}%')\nprint(f'BEFORE (ResNet50)         : {acc_before*100:.2f}%')\nprint(f'AFTER  (ConvNeXt-Tiny)    : {acc_plain*100:.2f}%')\nprint(f'AFTER  + flip TTA         : {acc_tta*100:.2f}%')\nprint(f'Improvement vs ResNet50   : {(acc_tta-acc_before)*100:+.2f} points')\n\nprint(); print(classification_report(Y_test, pred_tta, target_names=CLASS_NAMES, digits=4))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T17:31:29.133741Z","iopub.execute_input":"2026-10-10T17:31:29.13412Z","iopub.status.idle":"2026-10-10T17:33:33.194516Z","shell.execute_reply.started":"2026-10-10T17:31:29.134098Z","shell.execute_reply":"2026-10-10T17:33:33.193836Z"}},"outputs":[],"execution_count":null},{"id":"fd6fdba2","cell_type":"markdown","source":"## 8. Before vs. After: graphs","metadata":{}},{"id":"56079cac","cell_type":"code","source":"RED, GRAY, GREEN, LGREEN = '#d9534f', '#8a8f98', '#2e8b57', '#7fc99a'\nep_r = range(1, len(resnet_rec['val_acc']) + 1)\nep_d = range(1, len(hist_dense.history['val_acc']) + 1)\nep_i = range(1, len(history.history['val_acc']) + 1)\nerr_before = int(round((1 - acc_before) * len(Y_test)))\nerr_after, err_after_tta = int((pred_plain != Y_test).sum()), int((pred_tta != Y_test).sum())\n\nfig, ax = plt.subplots(2, 3, figsize=(19, 10.5))\n\n# (1) headline test accuracy\nnames = ['Dense net\\n(original #1)', 'ResNet50\\n(original #2)', 'ConvNeXt-T\\n(improved)', 'Improved\\n+ flip TTA']\nvals = [dense_acc*100, acc_before*100, acc_plain*100, acc_tta*100]\nb = ax[0,0].bar(names, vals, color=[GRAY, RED, LGREEN, GREEN], width=0.6)\nlo = max(0, min(vals) - 8) if min(vals) > 60 else 0\nax[0,0].set_ylim(lo, 100.5); ax[0,0].set_ylabel('Test accuracy (%)'); ax[0,0].set_title('Test Accuracy: Before vs After')\nfor r, v in zip(b, vals): ax[0,0].text(r.get_x()+r.get_width()/2, v+0.3, f'{v:.2f}%', ha='center', fontweight='bold')\nax[0,0].grid(axis='y', alpha=.3)\n\n# (2) validation accuracy per epoch\nax[0,1].plot(ep_d, hist_dense.history['val_acc'], 'o-', color=GRAY, label='Dense net (val)')\nax[0,1].plot(ep_r, resnet_rec['val_acc'], 'o-', color=RED, label='ResNet50 original (val)')\nax[0,1].plot(ep_i, history.history['val_acc'], 'o-', color=GREEN, label='ConvNeXt-T improved (val)')\nax[0,1].set_xlabel('Epoch'); ax[0,1].set_ylabel('Validation accuracy'); ax[0,1].set_title('Validation Accuracy per Epoch')\nax[0,1].legend(loc='lower right'); ax[0,1].grid(alpha=.3)\n\n# (3) validation loss (plain cross-entropy for all models)\nax[0,2].plot(ep_d, hist_dense.history['val_loss'], 'o-', color=GRAY, label='Dense net')\nax[0,2].plot(ep_r, resnet_rec['val_loss'], 'o-', color=RED, label='ResNet50 original')\nax[0,2].plot(ep_i, history.history['val_ce'], 'o-', color=GREEN, label='ConvNeXt-T improved')\nax[0,2].set_xlabel('Epoch'); ax[0,2].set_ylabel('Validation cross-entropy'); ax[0,2].set_title('Validation Loss per Epoch (lower is better)')\nax[0,2].legend(); ax[0,2].grid(alpha=.3)\n\n# (4) misclassified test images\nev = [int(round((1-dense_acc)*len(Y_test))), err_before, err_after, err_after_tta]\nb = ax[1,0].bar(names, ev, color=[GRAY, RED, LGREEN, GREEN], width=0.6)\nfor r, v in zip(b, ev): ax[1,0].text(r.get_x()+r.get_width()/2, v+max(ev)*0.01, f'{v:,}', ha='center', fontweight='bold')\nax[1,0].set_title(f'Misclassified test images (of {len(Y_test):,})'); ax[1,0].grid(axis='y', alpha=.3)\n\n# (5) training time per epoch\nt_before, t_after = float(np.mean(resnet_rec['epoch_sec'])), float(np.mean(timer.times))\nb = ax[1,1].bar(['ResNet50\\n(original)', 'ConvNeXt-T\\n(improved)'], [t_before, t_after], color=[RED, GREEN], width=0.5)\nfor r, v in zip(b, [t_before, t_after]): ax[1,1].text(r.get_x()+r.get_width()/2, v+t_before*0.01, f'{v:.0f}s', ha='center', fontweight='bold')\nax[1,1].set_ylabel('Seconds per epoch'); ax[1,1].set_title('Training Time per Epoch (lower is better)'); ax[1,1].grid(axis='y', alpha=.3)\n\n# (6) summary table\nax[1,2].axis('off')\nrows = [['Test accuracy', f'{acc_before*100:.2f}%', f'{acc_tta*100:.2f}%'],\n        [f'Errors / {len(Y_test):,}', f'{err_before:,}', f'{err_after_tta:,}'],\n        ['Epochs', str(len(resnet_rec['val_acc'])), str(len(history.history['val_acc']))],\n        ['Total train time', f\"{sum(resnet_rec['epoch_sec'])/60:.0f} min\", f'{sum(timer.times)/60:.0f} min'],\n        ['Final train acc', f\"{resnet_rec['acc'][-1]*100:.1f}%\", f\"{history.history['acc'][-1]*100:.1f}%*\"],\n        ['Best val acc', f\"{max(resnet_rec['val_acc'])*100:.2f}%\", f\"{max(history.history['val_acc'])*100:.2f}%\"]]\ntbl = ax[1,2].table(cellText=rows, colLabels=['', 'Before\\n(ResNet50)', 'After\\n(ConvNeXt-T + TTA)'], loc='center', cellLoc='center')\ntbl.auto_set_font_size(False); tbl.set_fontsize(11); tbl.scale(1, 2.0)\nax[1,2].set_title('Summary'); ax[1,2].text(0.5, 0.02, '*measured on augmented images with label smoothing', ha='center', fontsize=8, color='gray', transform=ax[1,2].transAxes)\n\nplt.suptitle('CIFAR-10: Original vs Improved Model', fontsize=16, fontweight='bold')\nplt.tight_layout(rect=[0, 0, 1, 0.97]); plt.savefig('cifar10_before_after.png', dpi=150); plt.show()\nprint(f'Before: {acc_before*100:.2f}%  ->  After: {acc_tta*100:.2f}%   (improvement: {(acc_tta-acc_before)*100:+.2f} points, '\n      f'{err_before - err_after_tta:+,} fewer errors)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T17:33:33.19557Z","iopub.execute_input":"2026-10-10T17:33:33.19637Z","iopub.status.idle":"2026-10-10T17:33:34.900312Z","shell.execute_reply.started":"2026-10-10T17:33:33.196346Z","shell.execute_reply":"2026-10-10T17:33:34.899607Z"}},"outputs":[],"execution_count":null},{"id":"d05ba507","cell_type":"markdown","source":"## 9. Where does the improved model still make mistakes?","metadata":{}},{"id":"931300c5","cell_type":"code","source":"cm = confusion_matrix(Y_test, pred_tta)\nper_class = cm.diagonal() / cm.sum(1) * 100\n\nfig, ax = plt.subplots(1, 2, figsize=(16, 6.5))\nax[0].imshow(cm, cmap='Blues'); ax[0].set_xticks(range(10)); ax[0].set_yticks(range(10))\nax[0].set_xticklabels(CLASS_NAMES, rotation=45, ha='right'); ax[0].set_yticklabels(CLASS_NAMES)\nax[0].set_xlabel('Predicted'); ax[0].set_ylabel('Actual'); ax[0].set_title('Confusion Matrix (improved model + TTA)')\nfor i in range(10):\n    for j in range(10):\n        ax[0].text(j, i, cm[i, j], ha='center', va='center', fontsize=8, color='white' if cm[i, j] > cm.max()/2 else 'black')\n\norder = np.argsort(per_class)\nif resnet_pred is not None:   # per-class before/after only when the baseline was re-trained\n    cm_b = confusion_matrix(Y_test, resnet_pred); pc_b = cm_b.diagonal() / cm_b.sum(1) * 100\n    y = np.arange(10); ax[1].barh(y-0.2, pc_b[order], 0.4, color=RED, label='Before (ResNet50)')\n    ax[1].barh(y+0.2, per_class[order], 0.4, color=GREEN, label='After'); ax[1].legend()\nelse:\n    ax[1].barh(range(10), per_class[order], color=GREEN)\n    for i, v in enumerate(per_class[order]): ax[1].text(v+0.1, i, f'{v:.1f}%', va='center', fontsize=9)\nax[1].set_yticks(range(10)); ax[1].set_yticklabels([CLASS_NAMES[i] for i in order])\nax[1].set_xlim(max(0, per_class.min()-5), 101); ax[1].set_xlabel('Accuracy (%)'); ax[1].set_title('Per-class accuracy'); ax[1].grid(axis='x', alpha=.3)\nplt.tight_layout(); plt.savefig('cifar10_confusion_perclass.png', dpi=150); plt.show()\n\n# sample of misclassified images\nwrong = np.where(pred_tta != Y_test)[0][:12]\nfig, axs = plt.subplots(2, 6, figsize=(14, 5))\nfor a, i in zip(axs.ravel(), wrong):\n    a.imshow(X_test[i]); a.axis('off'); a.set_title(f'true: {CLASS_NAMES[Y_test[i]]}\\npred: {CLASS_NAMES[pred_tta[i]]}', fontsize=9, color='#b02a2a')\nplt.suptitle('Some remaining mistakes'); plt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T17:33:34.901252Z","iopub.execute_input":"2026-10-10T17:33:34.901498Z","iopub.status.idle":"2026-10-10T17:33:36.194098Z","shell.execute_reply.started":"2026-10-10T17:33:34.901479Z","shell.execute_reply":"2026-10-10T17:33:36.193501Z"}},"outputs":[],"execution_count":null},{"id":"d208be50","cell_type":"markdown","source":"## 10. Save the model and the results","metadata":{}},{"id":"479f0f7d","cell_type":"code","source":"model.save('cifar10_convnext_improved.keras')\nresults = {'dense_test_acc': float(dense_acc), 'resnet50_test_acc_before': float(acc_before),\n           'improved_test_acc': acc_plain, 'improved_test_acc_flip_tta': acc_tta,\n           'improvement_points': float((acc_tta - acc_before) * 100),\n           'resnet_total_train_min': sum(resnet_rec['epoch_sec'])/60, 'improved_total_train_min': sum(timer.times)/60}\njson.dump(results, open('results.json', 'w'), indent=2); print(json.dumps(results, indent=2))\nfrom google.colab import files\nfiles.download('cifar10_before_after.png'); files.download('cifar10_confusion_perclass.png')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-10T17:33:36.194922Z","iopub.execute_input":"2026-10-10T17:33:36.195129Z","iopub.status.idle":"2026-10-10T17:33:38.127329Z","shell.execute_reply.started":"2026-10-10T17:33:36.195111Z","shell.execute_reply":"2026-10-10T17:33:38.126717Z"}},"outputs":[],"execution_count":null}]}