{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":84555,"sourceType":"datasetVersion","datasetId":46490}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom torchvision import datasets, transforms\nfrom torch.utils.data import random_split\nfrom collections import Counter\nimport pandas as pd\n\n# -------------------------\n# 1. Reproducibility\n# -------------------------\nSEED = 42\ntorch.manual_seed(SEED)\nnp.random.seed(SEED)\n\n# -------------------------\n# 2. Transform\n# -------------------------\ntransform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor()\n])\n\n# -------------------------\n# 3. Load ONLY the train folder\n# -------------------------\ndataset_path = \"/kaggle/input/datasets/sriramr/fruits-fresh-and-rotten-for-classification/dataset/train\"\n\nfull_dataset = datasets.ImageFolder(\n    root=dataset_path,\n    transform=transform\n)\n\nprint(\"Class names:\", full_dataset.classes)\nprint(\"Total images:\", len(full_dataset))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:09.954703Z","iopub.execute_input":"2026-02-17T10:21:09.954979Z","iopub.status.idle":"2026-02-17T10:21:14.744707Z","shell.execute_reply.started":"2026-02-17T10:21:09.954947Z","shell.execute_reply":"2026-02-17T10:21:14.744004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_size = int(0.8 * len(full_dataset))\ntest_size = len(full_dataset) - train_size\n\ntrain_dataset, test_dataset = random_split(\n    full_dataset,\n    [train_size, test_size],\n    generator=torch.Generator().manual_seed(SEED)\n)\n\nprint(\"Train images:\", len(train_dataset))\nprint(\"Test images:\", len(test_dataset))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:14.751409Z","iopub.execute_input":"2026-02-17T10:21:14.751643Z","iopub.status.idle":"2026-02-17T10:21:14.75747Z","shell.execute_reply.started":"2026-02-17T10:21:14.751621Z","shell.execute_reply":"2026-02-17T10:21:14.756749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_class_distribution(dataset):\n    targets = [dataset.dataset.targets[i] for i in dataset.indices]\n    return Counter(targets)\n\ntrain_counts = get_class_distribution(train_dataset)\ntest_counts = get_class_distribution(test_dataset)\n\ndata = []\n\nfor idx, class_name in enumerate(full_dataset.classes):\n    data.append([\n        class_name,\n        train_counts[idx],\n        test_counts[idx]\n    ])\n\ndf = pd.DataFrame(data, columns=[\"Class\", \"Train Count\", \"Test Count\"])\nprint(df)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:14.758379Z","iopub.execute_input":"2026-02-17T10:21:14.758676Z","iopub.status.idle":"2026-02-17T10:21:14.777205Z","shell.execute_reply.started":"2026-02-17T10:21:14.758628Z","shell.execute_reply":"2026-02-17T10:21:14.776361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport torch\nimport random\n\n# Function to show images\ndef imshow(img):\n    img = img.permute(1, 2, 0)   # C,H,W → H,W,C\n    img = img.numpy()\n    plt.imshow(img)\n    plt.axis(\"off\")\n\n# Select 12 random indices from training set\nrandom.seed(42)\nindices = random.sample(range(len(train_dataset)), 12)\n\nplt.figure(figsize=(12, 10))\n\nfor i, idx in enumerate(indices):\n    image, label = train_dataset[idx]\n    plt.subplot(4, 3, i+1)\n    imshow(image)\n    plt.title(full_dataset.classes[label])\n    \nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:14.778201Z","iopub.execute_input":"2026-02-17T10:21:14.778864Z","iopub.status.idle":"2026-02-17T10:21:15.70112Z","shell.execute_reply.started":"2026-02-17T10:21:14.778842Z","shell.execute_reply":"2026-02-17T10:21:15.69903Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Several visual factors may confuse a CNN in this dataset. Poor lighting can reduce contrast and hide texture differences between fresh and rotten fruits. Occlusion and overlapping fruits may prevent the network from clearly observing surface defects. Background clutter introduces irrelevant features, which may lead to overfitting to background patterns instead of fruit characteristics. Additionally, subtle color differences between fresh and slightly rotten fruits can make classification challenging.","metadata":{}},{"cell_type":"code","source":"from torchvision import datasets, transforms\nfrom torch.utils.data import DataLoader\nimport torch\n\n# Temporary transform (ONLY Resize + ToTensor)\nstat_transform = transforms.Compose([\n    transforms.Resize((128, 128)),\n    transforms.ToTensor()\n])\ndataset_path = \"/kaggle/input/datasets/sriramr/fruits-fresh-and-rotten-for-classification/dataset/train\"\n# Reload dataset with this transform\ndataset_for_stats = datasets.ImageFolder(\n    root=dataset_path,\n    transform=stat_transform\n)\n\n# Same reproducible split\nSEED = 42\ntrain_size = int(0.8 * len(dataset_for_stats))\ntest_size = len(dataset_for_stats) - train_size\n\ntrain_dataset_stats, _ = torch.utils.data.random_split(\n    dataset_for_stats,\n    [train_size, test_size],\n    generator=torch.Generator().manual_seed(SEED)\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:15.702405Z","iopub.execute_input":"2026-02-17T10:21:15.702957Z","iopub.status.idle":"2026-02-17T10:21:15.777154Z","shell.execute_reply.started":"2026-02-17T10:21:15.702866Z","shell.execute_reply":"2026-02-17T10:21:15.776541Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(train_dataset_stats, batch_size=64, shuffle=False)\n\nmean = 0.\nstd = 0.\ntotal_images = 0\n\nfor images, _ in train_loader:\n    batch_size = images.size(0)\n    images = images.view(batch_size, images.size(1), -1)\n    \n    mean += images.mean(2).sum(0)\n    std += images.std(2).sum(0)\n    total_images += batch_size\n\nmean /= total_images\nstd /= total_images\n\nprint(\"Dataset Mean:\", mean)\nprint(\"Dataset Std:\", std)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:15.777968Z","iopub.execute_input":"2026-02-17T10:21:15.778223Z","iopub.status.idle":"2026-02-17T10:21:59.854106Z","shell.execute_reply.started":"2026-02-17T10:21:15.778202Z","shell.execute_reply":"2026-02-17T10:21:59.853438Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_transform = transforms.Compose([\n    transforms.Resize((128, 128)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(20),\n    transforms.ToTensor(),\n    transforms.Normalize(mean.tolist(), std.tolist())\n])\n\ntest_transform = transforms.Compose([\n    transforms.Resize((128, 128)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean.tolist(), std.tolist())\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:59.855058Z","iopub.execute_input":"2026-02-17T10:21:59.855333Z","iopub.status.idle":"2026-02-17T10:21:59.860224Z","shell.execute_reply.started":"2026-02-17T10:21:59.855273Z","shell.execute_reply":"2026-02-17T10:21:59.859587Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Data augmentation should not be applied to the test set because it artificially modifies evaluation data and changes its distribution. The test set must represent real unseen data to provide an unbiased estimate of model performance.","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass FruitCNN(nn.Module):\n    def __init__(self):\n        super(FruitCNN, self).__init__()\n        \n        self.features = nn.Sequential(\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2),\n            \n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2),\n            \n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2)\n        )\n        \n        # 🔥 This fixes everything\n        self.global_pool = nn.AdaptiveAvgPool2d((1, 1))\n        \n        self.classifier = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(128, 256),   # 128 comes from last conv channels\n            nn.ReLU(),\n            nn.Linear(256, 6)\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.global_pool(x)\n        x = self.classifier(x)\n        return x\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:59.861014Z","iopub.execute_input":"2026-02-17T10:21:59.861271Z","iopub.status.idle":"2026-02-17T10:21:59.875924Z","shell.execute_reply.started":"2026-02-17T10:21:59.861249Z","shell.execute_reply":"2026-02-17T10:21:59.875194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = FruitCNN()\nprint(model)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:59.876808Z","iopub.execute_input":"2026-02-17T10:21:59.877143Z","iopub.status.idle":"2026-02-17T10:21:59.890044Z","shell.execute_reply.started":"2026-02-17T10:21:59.877114Z","shell.execute_reply":"2026-02-17T10:21:59.889376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dummy_input = torch.randn(8, 3, 128, 128)\noutput = model(dummy_input)\n\nprint(\"Input shape:\", dummy_input.shape)\nprint(\"Output shape:\", output.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:21:59.891094Z","iopub.execute_input":"2026-02-17T10:21:59.891352Z","iopub.status.idle":"2026-02-17T10:21:59.976509Z","shell.execute_reply.started":"2026-02-17T10:21:59.89133Z","shell.execute_reply":"2026-02-17T10:21:59.975866Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport matplotlib.pyplot as plt\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel = FruitCNN().to(device)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001)\n\ntrain_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)\ntest_loader  = torch.utils.data.DataLoader(test_dataset, batch_size=32, shuffle=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:23:16.551381Z","iopub.execute_input":"2026-02-17T10:23:16.552262Z","iopub.status.idle":"2026-02-17T10:23:16.794237Z","shell.execute_reply.started":"2026-02-17T10:23:16.55223Z","shell.execute_reply":"2026-02-17T10:23:16.793451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_epochs = 5\n\ntrain_losses = []\ntrain_accuracies = []\ntest_accuracies = []\n\nfor epoch in range(num_epochs):\n    \n    # -------- TRAIN --------\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    for images, labels in train_loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        \n        _, predicted = torch.max(outputs, 1)\n        total += labels.size(0)\n        correct += (predicted == labels).sum().item()\n    \n    train_loss = running_loss / len(train_loader)\n    train_acc = 100 * correct / total\n    \n    # -------- TEST --------\n    model.eval()\n    correct = 0\n    total = 0\n    \n    with torch.no_grad():\n        for images, labels in test_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            _, predicted = torch.max(outputs, 1)\n            total += labels.size(0)\n            correct += (predicted == labels).sum().item()\n    \n    test_acc = 100 * correct / total\n    \n    train_losses.append(train_loss)\n    train_accuracies.append(train_acc)\n    test_accuracies.append(test_acc)\n    \n    print(f\"Epoch [{epoch+1}/{num_epochs}]\")\n    print(f\"Train Loss: {train_loss:.4f}\")\n    print(f\"Train Accuracy: {train_acc:.2f}%\")\n    print(f\"Test Accuracy: {test_acc:.2f}%\\n\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:23:22.186632Z","iopub.execute_input":"2026-02-17T10:23:22.186981Z","iopub.status.idle":"2026-02-17T10:30:07.508549Z","shell.execute_reply.started":"2026-02-17T10:23:22.186956Z","shell.execute_reply":"2026-02-17T10:30:07.507741Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nepochs = range(1, num_epochs + 1)\n\n# Plot Training Loss\nplt.figure()\nplt.plot(epochs, train_losses)\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Training Loss\")\nplt.title(\"Training Loss vs Epoch\")\nplt.show()\n\n# Plot Validation Accuracy\nplt.figure()\nplt.plot(epochs, test_accuracies)\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Validation Accuracy (%)\")\nplt.title(\"Validation Accuracy vs Epoch\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:31:18.602129Z","iopub.execute_input":"2026-02-17T10:31:18.602739Z","iopub.status.idle":"2026-02-17T10:31:18.840044Z","shell.execute_reply.started":"2026-02-17T10:31:18.602701Z","shell.execute_reply":"2026-02-17T10:31:18.839186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\nmodel.eval()\n\nall_preds = []\nall_labels = []\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n        \n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.numpy())\n\nall_preds = np.array(all_preds)\nall_labels = np.array(all_labels)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:31:25.25233Z","iopub.execute_input":"2026-02-17T10:31:25.253071Z","iopub.status.idle":"2026-02-17T10:31:39.894512Z","shell.execute_reply.started":"2026-02-17T10:31:25.25304Z","shell.execute_reply":"2026-02-17T10:31:39.893867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cm = confusion_matrix(all_labels, all_preds)\n\nplt.figure(figsize=(8,6))\nsns.heatmap(cm, annot=True, fmt='d',\n            xticklabels=full_dataset.classes,\n            yticklabels=full_dataset.classes)\n\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.title(\"Confusion Matrix\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:31:39.895988Z","iopub.execute_input":"2026-02-17T10:31:39.896536Z","iopub.status.idle":"2026-02-17T10:31:40.128815Z","shell.execute_reply.started":"2026-02-17T10:31:39.89651Z","shell.execute_reply":"2026-02-17T10:31:40.127971Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"report = classification_report(\n    all_labels,\n    all_preds,\n    target_names=full_dataset.classes\n)\n\nprint(report)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:31:40.129749Z","iopub.execute_input":"2026-02-17T10:31:40.130022Z","iopub.status.idle":"2026-02-17T10:31:40.143728Z","shell.execute_reply.started":"2026-02-17T10:31:40.129999Z","shell.execute_reply":"2026-02-17T10:31:40.143091Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"rotten_indices = [3, 4, 5]\n\nrotten_recall = cm[rotten_indices, :][:, rotten_indices].sum() / cm[rotten_indices, :].sum()\n\nprint(\"Rotten Recall:\", rotten_recall)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:31:40.145182Z","iopub.execute_input":"2026-02-17T10:31:40.145435Z","iopub.status.idle":"2026-02-17T10:31:40.151018Z","shell.execute_reply.started":"2026-02-17T10:31:40.145413Z","shell.execute_reply":"2026-02-17T10:31:40.150316Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fresh_indices = [0, 1, 2]\n\nfalse_negatives = 0\n\nfor r in rotten_indices:\n    for f in fresh_indices:\n        false_negatives += cm[r][f]\n\nprint(\"False Negatives (Rotten → Fresh):\", false_negatives)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:31:40.152018Z","iopub.execute_input":"2026-02-17T10:31:40.152343Z","iopub.status.idle":"2026-02-17T10:31:40.162391Z","shell.execute_reply.started":"2026-02-17T10:31:40.15232Z","shell.execute_reply":"2026-02-17T10:31:40.161726Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"False negatives are the most dangerous in this application because predicting rotten fruit as fresh can lead to health risks and customer dissatisfaction. While false positives cause minor economic loss, false negatives directly affect food safety. Therefore, maximizing recall for rotten classes is more critical than overall accuracy.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\n\nmodel.eval()\n\nmisclassified_images = []\nmisclassified_true = []\nmisclassified_pred = []\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n        \n        for i in range(len(labels)):\n            if preds[i] != labels[i]:\n                misclassified_images.append(images[i].cpu())\n                misclassified_true.append(labels[i].item())\n                misclassified_pred.append(preds[i].item())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:32:40.554602Z","iopub.execute_input":"2026-02-17T10:32:40.555257Z","iopub.status.idle":"2026-02-17T10:32:54.531301Z","shell.execute_reply.started":"2026-02-17T10:32:40.555225Z","shell.execute_reply":"2026-02-17T10:32:54.530655Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_to_show = min(10, len(misclassified_images))\n\nplt.figure(figsize=(15,8))\n\nfor i in range(num_to_show):\n    img = misclassified_images[i]\n    img = img.permute(1,2,0).numpy()\n    \n    # Undo normalization if needed\n    img = (img - img.min()) / (img.max() - img.min())\n    \n    true_label = full_dataset.classes[misclassified_true[i]]\n    pred_label = full_dataset.classes[misclassified_pred[i]]\n    \n    plt.subplot(2,5,i+1)\n    plt.imshow(img)\n    plt.title(f\"T: {true_label}\\nP: {pred_label}\")\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:32:54.532727Z","iopub.execute_input":"2026-02-17T10:32:54.533082Z","iopub.status.idle":"2026-02-17T10:32:55.403292Z","shell.execute_reply.started":"2026-02-17T10:32:54.533059Z","shell.execute_reply":"2026-02-17T10:32:55.402471Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fresh_indices = [0,1,2]\nrotten_indices = [3,4,5]\n\nrotten_to_fresh = 0\nfresh_to_rotten = 0\n\nfor t, p in zip(misclassified_true, misclassified_pred):\n    if t in rotten_indices and p in fresh_indices:\n        rotten_to_fresh += 1\n    if t in fresh_indices and p in rotten_indices:\n        fresh_to_rotten += 1\n\nprint(\"Rotten → Fresh:\", rotten_to_fresh)\nprint(\"Fresh → Rotten:\", fresh_to_rotten)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:32:55.404244Z","iopub.execute_input":"2026-02-17T10:32:55.404487Z","iopub.status.idle":"2026-02-17T10:32:55.410583Z","shell.execute_reply.started":"2026-02-17T10:32:55.404464Z","shell.execute_reply":"2026-02-17T10:32:55.410047Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Most misclassifications occur when mildly rotten fruits are predicted as fresh. This happens because early-stage rotting shows subtle discoloration that is difficult for the CNN to detect. Lighting variations and background similarities also confuse the model, as it may focus on global color rather than localized texture defects. Additionally, overlapping fruits and partial occlusion reduce visible surface detail, leading to incorrect predictions.\n\nIf Rotten → Fresh is dominant:\n\nThe model prioritizes overall fruit color instead of detecting small local rotten patches. Increasing data augmentation and adding deeper convolution layers could improve defect detection.\n\nIf Fresh → Rotten is dominant:\n\nThe model is oversensitive to minor surface variations and shadows, incorrectly treating them as decay patterns.","metadata":{}},{"cell_type":"code","source":"import torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nmodel.eval()\n\nall_images = []\nall_labels = []\nall_preds = []\nall_confidences = []\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        \n        probs = F.softmax(outputs, dim=1)\n        confidences, preds = torch.max(probs, 1)\n        \n        for i in range(len(labels)):\n            all_images.append(images[i].cpu())\n            all_labels.append(labels[i].item())\n            all_preds.append(preds[i].item())\n            all_confidences.append(confidences[i].item())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:33:15.725295Z","iopub.execute_input":"2026-02-17T10:33:15.725581Z","iopub.status.idle":"2026-02-17T10:33:29.510198Z","shell.execute_reply.started":"2026-02-17T10:33:15.725557Z","shell.execute_reply":"2026-02-17T10:33:29.509524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"high_conf_wrong = []\n\nfor img, true, pred, conf in zip(all_images, all_labels, all_preds, all_confidences):\n    if pred != true and conf > 0.80:\n        high_conf_wrong.append((img, true, pred, conf))\n\nhigh_conf_wrong = high_conf_wrong[:3]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:33:29.511518Z","iopub.execute_input":"2026-02-17T10:33:29.511902Z","iopub.status.idle":"2026-02-17T10:33:29.517353Z","shell.execute_reply.started":"2026-02-17T10:33:29.511871Z","shell.execute_reply":"2026-02-17T10:33:29.516575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"low_conf_correct = []\n\nfor img, true, pred, conf in zip(all_images, all_labels, all_preds, all_confidences):\n    if pred == true and conf < 0.55:\n        low_conf_correct.append((img, true, pred, conf))\n\nlow_conf_correct = low_conf_correct[:3]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:33:29.518262Z","iopub.execute_input":"2026-02-17T10:33:29.51869Z","iopub.status.idle":"2026-02-17T10:33:29.530264Z","shell.execute_reply.started":"2026-02-17T10:33:29.518644Z","shell.execute_reply":"2026-02-17T10:33:29.529468Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_examples(examples, title):\n    plt.figure(figsize=(12,4))\n    plt.suptitle(title)\n    \n    for i, (img, true, pred, conf) in enumerate(examples):\n        img = img.permute(1,2,0).numpy()\n        img = (img - img.min()) / (img.max() - img.min())\n        \n        true_label = full_dataset.classes[true]\n        pred_label = full_dataset.classes[pred]\n        \n        plt.subplot(1,3,i+1)\n        plt.imshow(img)\n        plt.title(f\"T: {true_label}\\nP: {pred_label}\\nConf: {conf:.2f}\")\n        plt.axis(\"off\")\n    \n    plt.show()\n\n# Show both groups\nshow_examples(high_conf_wrong, \"High-Confidence WRONG Predictions (>0.80)\")\nshow_examples(low_conf_correct, \"Low-Confidence CORRECT Predictions (<0.55)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:33:29.53193Z","iopub.execute_input":"2026-02-17T10:33:29.532488Z","iopub.status.idle":"2026-02-17T10:33:30.05346Z","shell.execute_reply.started":"2026-02-17T10:33:29.532464Z","shell.execute_reply":"2026-02-17T10:33:30.052638Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"High-confidence wrong predictions indicate that the model is confidently making incorrect decisions, which is dangerous in real-world deployment. This suggests overconfidence and poor calibration of probabilities. In safety-critical systems like food quality detection, such errors reduce trust in the model and may require techniques like probability calibration or uncertainty estimation.","metadata":{}},{"cell_type":"code","source":"import torch.nn.functional as F\nimport numpy as np\n\nmodel.eval()\n\nall_rotten_probs = []\nall_true_binary = []\n\nrotten_indices = [3,4,5]\nfresh_indices = [0,1,2]\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        probs = F.softmax(outputs, dim=1)\n        \n        for i in range(len(labels)):\n            # Sum rotten probabilities\n            rotten_prob = probs[i][rotten_indices].sum().item()\n            all_rotten_probs.append(rotten_prob)\n            \n            # Convert true label to binary\n            if labels[i].item() in rotten_indices:\n                all_true_binary.append(1)   # Rotten\n            else:\n                all_true_binary.append(0)   # Fresh\n\nall_rotten_probs = np.array(all_rotten_probs)\nall_true_binary = np.array(all_true_binary)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:34:42.465726Z","iopub.execute_input":"2026-02-17T10:34:42.466546Z","iopub.status.idle":"2026-02-17T10:34:55.864522Z","shell.execute_reply.started":"2026-02-17T10:34:42.466515Z","shell.execute_reply":"2026-02-17T10:34:55.863729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nthresholds = [0.50, 0.40, 0.30]\n\nresults = []\n\nfor t in thresholds:\n    \n    preds_binary = (all_rotten_probs >= t).astype(int)\n    \n    TP = ((preds_binary == 1) & (all_true_binary == 1)).sum()\n    TN = ((preds_binary == 0) & (all_true_binary == 0)).sum()\n    FP = ((preds_binary == 1) & (all_true_binary == 0)).sum()\n    FN = ((preds_binary == 0) & (all_true_binary == 1)).sum()\n    \n    recall = TP / (TP + FN + 1e-8)\n    precision = TP / (TP + FP + 1e-8)\n    \n    results.append([\n        t,\n        TP,\n        FP,\n        TN,\n        FN,\n        recall,\n        precision\n    ])\n\n# Create DataFrame\ndf = pd.DataFrame(results, columns=[\n    \"Threshold\",\n    \"TP\",\n    \"FP\",\n    \"TN\",\n    \"FN\",\n    \"Recall\",\n    \"Precision\"\n])\n\nprint(df)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:35:48.844038Z","iopub.execute_input":"2026-02-17T10:35:48.844724Z","iopub.status.idle":"2026-02-17T10:35:48.854924Z","shell.execute_reply.started":"2026-02-17T10:35:48.844697Z","shell.execute_reply":"2026-02-17T10:35:48.85414Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As the classification threshold decreases from 0.50 → 0.30, Rotten Recall steadily increases from 0.8418 to 0.9205, while False Negatives significantly decrease from 195 to 98. This means the model becomes better at correctly identifying rotten fruits. Although Rotten Precision slightly decreases (from 0.9411 to 0.9080), it remains high across all thresholds.\n\nFor grocery delivery deployment, threshold = 0.30 is the best choice because minimizing False Negatives is critical — delivering rotten fruit poses health risks and damages customer trust. The slight drop in precision is acceptable compared to the substantial reduction in missed rotten cases. Therefore, prioritizing higher recall at threshold 0.30 ensures safer and more reliable deployment.","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nmodel.eval()\n\ncorrect_samples = []\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n        \n        for i in range(len(labels)):\n            if preds[i] == labels[i]:\n                correct_samples.append((images[i].cpu(), labels[i].item()))\n                \n        if len(correct_samples) >= 5:\n            break\n\ncorrect_samples = correct_samples[:5]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:37:55.529052Z","iopub.execute_input":"2026-02-17T10:37:55.529358Z","iopub.status.idle":"2026-02-17T10:37:55.753738Z","shell.execute_reply.started":"2026-02-17T10:37:55.529334Z","shell.execute_reply":"2026-02-17T10:37:55.752893Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_gaussian_noise(img, mean=0.0, std=0.15):\n    noise = torch.randn_like(img) * std + mean\n    corrupted = img + noise\n    corrupted = torch.clamp(corrupted, 0, 1)\n    return corrupted\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:37:56.01552Z","iopub.execute_input":"2026-02-17T10:37:56.016243Z","iopub.status.idle":"2026-02-17T10:37:56.020082Z","shell.execute_reply.started":"2026-02-17T10:37:56.016217Z","shell.execute_reply":"2026-02-17T10:37:56.019404Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(15,6))\n\nfor i, (img, true_label) in enumerate(correct_samples):\n    \n    corrupted = add_gaussian_noise(img)\n    \n    # Get predictions\n    with torch.no_grad():\n        orig_out = model(img.unsqueeze(0).to(device))\n        corr_out = model(corrupted.unsqueeze(0).to(device))\n        \n        orig_pred = torch.argmax(orig_out, 1).item()\n        corr_pred = torch.argmax(corr_out, 1).item()\n    \n    true_name = full_dataset.classes[true_label]\n    orig_name = full_dataset.classes[orig_pred]\n    corr_name = full_dataset.classes[corr_pred]\n    \n    # Original image\n    plt.subplot(2,5,i+1)\n    img_disp = img.permute(1,2,0).numpy()\n    img_disp = (img_disp - img_disp.min())/(img_disp.max()-img_disp.min())\n    plt.imshow(img_disp)\n    plt.title(f\"Orig\\nT:{true_name}\\nP:{orig_name}\")\n    plt.axis(\"off\")\n    \n    # Corrupted image\n    plt.subplot(2,5,i+6)\n    corr_disp = corrupted.permute(1,2,0).numpy()\n    corr_disp = (corr_disp - corr_disp.min())/(corr_disp.max()-corr_disp.min())\n    plt.imshow(corr_disp)\n    plt.title(f\"Corrupt\\nP:{corr_name}\")\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:37:57.956203Z","iopub.execute_input":"2026-02-17T10:37:57.956588Z","iopub.status.idle":"2026-02-17T10:37:58.86949Z","shell.execute_reply.started":"2026-02-17T10:37:57.956561Z","shell.execute_reply":"2026-02-17T10:37:58.868507Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The model shows limited robustness, as several correct predictions change after adding noise. This indicates sensitivity to small pixel perturbations and suggests that the model may rely heavily on fine-grained color or texture patterns. Additional data augmentation during training could improve robustness.","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass ImprovedCNN(nn.Module):\n    def __init__(self):\n        super(ImprovedCNN, self).__init__()\n        \n        self.features = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(),\n            nn.MaxPool2d(2,2),\n            \n            nn.Conv2d(32, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.MaxPool2d(2,2),\n            \n            nn.Conv2d(64, 128, 3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(),\n            nn.MaxPool2d(2,2)\n        )\n        \n        self.global_pool = nn.AdaptiveAvgPool2d((1,1))\n        \n        self.classifier = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(128, 256),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(256, 6)\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.global_pool(x)\n        x = self.classifier(x)\n        return x\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:41:15.47722Z","iopub.execute_input":"2026-02-17T10:41:15.478022Z","iopub.status.idle":"2026-02-17T10:41:15.48583Z","shell.execute_reply.started":"2026-02-17T10:41:15.477993Z","shell.execute_reply":"2026-02-17T10:41:15.485058Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = ImprovedCNN().to(device)\n\ncriterion = nn.CrossEntropyLoss()\n\noptimizer = torch.optim.Adam(\n    model.parameters(),\n    lr=0.001,\n    weight_decay=1e-4   # 🔥 improvement\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:41:25.999937Z","iopub.execute_input":"2026-02-17T10:41:26.000225Z","iopub.status.idle":"2026-02-17T10:41:26.01007Z","shell.execute_reply.started":"2026-02-17T10:41:26.000201Z","shell.execute_reply":"2026-02-17T10:41:26.009425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_transform = transforms.Compose([\n    transforms.Resize((128,128)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(25),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean, std)\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:41:41.512562Z","iopub.execute_input":"2026-02-17T10:41:41.512888Z","iopub.status.idle":"2026-02-17T10:41:41.51822Z","shell.execute_reply.started":"2026-02-17T10:41:41.512861Z","shell.execute_reply":"2026-02-17T10:41:41.517357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom sklearn.metrics import classification_report, f1_score\nimport numpy as np\n\n# Initialize improved model\nmodel = ImprovedCNN().to(device)\n\ncriterion = nn.CrossEntropyLoss()\n\noptimizer = torch.optim.Adam(\n    model.parameters(),\n    lr=0.001,\n    weight_decay=1e-4   # L2 Regularization\n)\n\nnum_epochs = 5\n\nfor epoch in range(num_epochs):\n    model.train()\n    \n    running_loss = 0\n    correct = 0\n    total = 0\n    \n    for images, labels in train_loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        \n        _, preds = torch.max(outputs, 1)\n        total += labels.size(0)\n        correct += (preds == labels).sum().item()\n    \n    train_loss = running_loss / len(train_loader)\n    train_acc = 100 * correct / total\n    \n    print(f\"Epoch [{epoch+1}/{num_epochs}]\")\n    print(f\"Train Loss: {train_loss:.4f}\")\n    print(f\"Train Accuracy: {train_acc:.2f}%\\n\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:42:37.446193Z","iopub.execute_input":"2026-02-17T10:42:37.446557Z","iopub.status.idle":"2026-02-17T10:48:24.64997Z","shell.execute_reply.started":"2026-02-17T10:42:37.446533Z","shell.execute_reply":"2026-02-17T10:48:24.64926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\nall_preds = []\nall_labels = []\nall_rotten_probs = []\n\nrotten_indices = [3,4,5]\nfresh_indices = [0,1,2]\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        probs = F.softmax(outputs, dim=1)\n        \n        _, preds = torch.max(outputs, 1)\n        \n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.numpy())\n        \n        # Rotten probability (sum of rotten classes)\n        for i in range(len(labels)):\n            rotten_prob = probs[i][rotten_indices].sum().item()\n            all_rotten_probs.append(rotten_prob)\n\nall_preds = np.array(all_preds)\nall_labels = np.array(all_labels)\nall_rotten_probs = np.array(all_rotten_probs)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:48:45.66591Z","iopub.execute_input":"2026-02-17T10:48:45.666241Z","iopub.status.idle":"2026-02-17T10:48:59.404922Z","shell.execute_reply.started":"2026-02-17T10:48:45.666213Z","shell.execute_reply":"2026-02-17T10:48:59.404306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch.nn.functional as F\nfrom sklearn.metrics import f1_score\nimport pandas as pd\n\nmodel.eval()\n\nall_preds = []\nall_labels = []\nall_rotten_probs = []\n\nrotten_indices = [3,4,5]\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        probs = F.softmax(outputs, dim=1)\n        \n        _, preds = torch.max(outputs, 1)\n        \n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.numpy())\n        \n        for i in range(len(labels)):\n            rotten_prob = probs[i][rotten_indices].sum().item()\n            all_rotten_probs.append(rotten_prob)\n\nall_preds = np.array(all_preds)\nall_labels = np.array(all_labels)\nall_rotten_probs = np.array(all_rotten_probs)\n\n# -----------------------------\n# 1. Accuracy\n# -----------------------------\naccuracy = (all_preds == all_labels).mean()\n\n# -----------------------------\n# 2. Macro F1\n# -----------------------------\nf1 = f1_score(all_labels, all_preds, average='macro')\n\n# -----------------------------\n# 3. Rotten Recall + Precision + FN\n# -----------------------------\nthreshold = 0.30\n\ntrue_binary = np.isin(all_labels, rotten_indices).astype(int)\npred_binary = (all_rotten_probs >= threshold).astype(int)\n\nTP = ((pred_binary == 1) & (true_binary == 1)).sum()\nFP = ((pred_binary == 1) & (true_binary == 0)).sum()\nFN = ((pred_binary == 0) & (true_binary == 1)).sum()\n\nrotten_recall = TP / (TP + FN + 1e-8)\nrotten_precision = TP / (TP + FP + 1e-8)\n\n# -----------------------------\n# Print Clean Table\n# -----------------------------\nresults = pd.DataFrame({\n    \"Metric\": [\"Accuracy\", \"Macro F1\", \"Rotten Recall\", \"Rotten Precision\", \"False Negatives\"],\n    \"Improved CNN\": [\n        round(accuracy, 4),\n        round(f1, 4),\n        round(rotten_recall, 4),\n        round(rotten_precision, 4),\n        FN\n    ]\n})\n\nprint(results)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:49:51.218106Z","iopub.execute_input":"2026-02-17T10:49:51.218389Z","iopub.status.idle":"2026-02-17T10:50:04.82241Z","shell.execute_reply.started":"2026-02-17T10:49:51.218367Z","shell.execute_reply":"2026-02-17T10:50:04.821516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"report = classification_report(\n    all_labels,\n    all_preds,\n    target_names=full_dataset.classes\n)\n\nprint(report)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:49:43.332488Z","iopub.execute_input":"2026-02-17T10:49:43.332912Z","iopub.status.idle":"2026-02-17T10:49:43.345836Z","shell.execute_reply.started":"2026-02-17T10:49:43.332883Z","shell.execute_reply":"2026-02-17T10:49:43.345088Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"| Model        | Accuracy | Macro F1 | Rotten Recall | FN Count |\n| ------------ | -------- | -------- | ------------- | -------- |\n| Baseline CNN | 0.81     | 0.81     | 0.8305        | 209      |\n| Improved CNN | 0.8854   | 0.8879   | 0.9400        | 74       |\n","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torchvision.models as models\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Load pretrained ResNet18\nresnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n\n# Freeze backbone\nfor param in resnet.parameters():\n    param.requires_grad = False\n\n# Replace final layer\nnum_features = resnet.fc.in_features\n\nresnet.fc = nn.Sequential(\n    nn.Linear(num_features, 256),\n    nn.ReLU(),\n    nn.Dropout(0.5),\n    nn.Linear(256, 6)  # 6 classes\n)\n\nresnet = resnet.to(device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:55:38.639516Z","iopub.execute_input":"2026-02-17T10:55:38.640416Z","iopub.status.idle":"2026-02-17T10:55:39.126818Z","shell.execute_reply.started":"2026-02-17T10:55:38.640377Z","shell.execute_reply":"2026-02-17T10:55:39.125902Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\n\noptimizer = torch.optim.Adam(\n    resnet.fc.parameters(),   # 🔥 only head\n    lr=0.001\n)\n\nnum_epochs = 3\n\nfor epoch in range(num_epochs):\n    resnet.train()\n    \n    running_loss = 0\n    correct = 0\n    total = 0\n    \n    for images, labels in train_loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = resnet(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        \n        _, preds = torch.max(outputs, 1)\n        total += labels.size(0)\n        correct += (preds == labels).sum().item()\n    \n    train_acc = 100 * correct / total\n    \n    print(f\"Epoch [{epoch+1}/3]\")\n    print(f\"Train Loss: {running_loss/len(train_loader):.4f}\")\n    print(f\"Train Accuracy: {train_acc:.2f}%\\n\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:55:49.349901Z","iopub.execute_input":"2026-02-17T10:55:49.350489Z","iopub.status.idle":"2026-02-17T10:58:46.700065Z","shell.execute_reply.started":"2026-02-17T10:55:49.350459Z","shell.execute_reply":"2026-02-17T10:58:46.699343Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transfer_accuracy, transfer_f1, transfer_recall, transfer_fn = evaluate_model(resnet)\n\nprint(\"\\nTransfer Learning Metrics:\")\nprint(\"Accuracy:\", round(transfer_accuracy,4))\nprint(\"Macro F1:\", round(transfer_f1,4))\nprint(\"Rotten Recall:\", round(transfer_recall,4))\nprint(\"FN Count:\", transfer_fn)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T10:59:01.138232Z","iopub.execute_input":"2026-02-17T10:59:01.138519Z","iopub.status.idle":"2026-02-17T10:59:15.654892Z","shell.execute_reply.started":"2026-02-17T10:59:01.138498Z","shell.execute_reply":"2026-02-17T10:59:15.654023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save improved model metrics for later comparison\nimproved_accuracy = accuracy\nimproved_f1 = f1\nimproved_recall = rotten_recall\nimproved_fn = FN\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T11:00:30.836828Z","iopub.execute_input":"2026-02-17T11:00:30.837485Z","iopub.status.idle":"2026-02-17T11:00:30.841223Z","shell.execute_reply.started":"2026-02-17T11:00:30.837455Z","shell.execute_reply":"2026-02-17T11:00:30.840408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"final_df = pd.DataFrame({\n    \"Model\": [\"Best CNN\", \"Transfer ResNet18\"],\n    \"Accuracy\": [round(improved_accuracy,4), round(transfer_accuracy,4)],\n    \"Macro F1\": [round(improved_f1,4), round(transfer_f1,4)],\n    \"Rotten Recall\": [round(improved_recall,4), round(transfer_recall,4)],\n    \"FN Count\": [improved_fn, transfer_fn]\n})\n\nfinal_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-17T11:00:49.550811Z","iopub.execute_input":"2026-02-17T11:00:49.551376Z","iopub.status.idle":"2026-02-17T11:00:49.568798Z","shell.execute_reply.started":"2026-02-17T11:00:49.551348Z","shell.execute_reply":"2026-02-17T11:00:49.567817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}