{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "dd39fcd5-10e4-44e2-8b7b-bd0cf57a6156",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ مكتبات مثبتة وجاهزة.\n",
      "torch: 2.9.0+cpu\n",
      "torchvision: 0.24.0+cpu\n"
     ]
    }
   ],
   "source": [
    "import torch, torchvision, numpy, pandas, matplotlib\n",
    "print(\"✅ مكتبات مثبتة وجاهزة.\")\n",
    "print(\"torch:\", torch.__version__)\n",
    "print(\"torchvision:\", torchvision.__version__)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "588fe413-68fb-4e71-b06c-befe80b8e34c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ تم استيراد جميع المكتبات بنجاح\n",
      "torch: 2.9.0+cpu\n",
      "torchvision: torchvision.models\n",
      "CUDA available: False\n"
     ]
    }
   ],
   "source": [
    "# Step 1 — Import required libraries\n",
    "\n",
    "import os\n",
    "import random\n",
    "from pathlib import Path\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torch.utils.data import DataLoader, random_split\n",
    "\n",
    "from torchvision import datasets, transforms, models\n",
    "\n",
    "from sklearn.metrics import classification_report, confusion_matrix\n",
    "\n",
    "print(\"✅ تم استيراد جميع المكتبات بنجاح\")\n",
    "print(\"torch:\", torch.__version__)\n",
    "print(\"torchvision:\", models.__name__)\n",
    "print(\"CUDA available:\", torch.cuda.is_available())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "58163cf1-dc7d-4161-a75e-ac25ddacf06f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Dataset path: C:\\Users\\hp\\Downloads\\flowers\n",
      "Exists? True\n",
      "Class folders found: ['daisy', 'dandelion', 'rose', 'sunflower', 'tulip']\n"
     ]
    }
   ],
   "source": [
    "# Step 2 — Verify correct dataset path\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "# ⚠️ اكتبي هنا المسار الصحيح اللي نسختيه من مستكشف الملفات\n",
    "DATASET_DIR = Path(r\"C:\\Users\\hp\\Downloads\\flowers\")\n",
    "\n",
    "print(\"Dataset path:\", DATASET_DIR)\n",
    "print(\"Exists?\", DATASET_DIR.exists())\n",
    "\n",
    "if DATASET_DIR.exists():\n",
    "    class_dirs = [p.name for p in DATASET_DIR.iterdir() if p.is_dir()]\n",
    "    print(\"Class folders found:\", class_dirs)\n",
    "    if not class_dirs:\n",
    "        raise RuntimeError(\"No class folders found inside the dataset directory. Expected structure: root/class_x/*.jpg\")\n",
    "else:\n",
    "    raise FileNotFoundError(\"Dataset path not found. Please correct DATASET_DIR.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "d3a14b6d-bb37-487c-a974-03bb8e93d2ad",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Classes: ['daisy', 'dandelion', 'rose', 'sunflower', 'tulip']\n",
      "Train batch: torch.Size([32, 3, 224, 224]) torch.Size([32]) | total images: 4317\n"
     ]
    }
   ],
   "source": [
    "# Step 3 — Load dataset, split into train/val/test, and create DataLoaders\n",
    "\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader, random_split\n",
    "import torch\n",
    "\n",
    "# Reuse DATASET_DIR from previous step\n",
    "IMG_SIZE    = 224\n",
    "BATCH_SIZE  = 32\n",
    "VAL_SPLIT   = 0.1\n",
    "TEST_SPLIT  = 0.1\n",
    "RANDOM_SEED = 42\n",
    "NUM_WORKERS = 0  # Windows-safe\n",
    "\n",
    "# Transforms (augmentation for train, simple resize for val/test)\n",
    "train_transform = transforms.Compose([\n",
    "    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
    "    transforms.RandomHorizontalFlip(p=0.5),\n",
    "    transforms.ColorJitter(0.2, 0.2, 0.2),\n",
    "    transforms.ToTensor()\n",
    "])\n",
    "eval_transform = transforms.Compose([\n",
    "    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
    "    transforms.ToTensor()\n",
    "])\n",
    "\n",
    "# Load all images (class-per-folder)\n",
    "full_dataset = datasets.ImageFolder(DATASET_DIR.as_posix(), transform=train_transform)\n",
    "class_names = full_dataset.classes\n",
    "print(\"Classes:\", class_names)\n",
    "\n",
    "# Split into train/val/test\n",
    "g = torch.Generator().manual_seed(RANDOM_SEED)\n",
    "n_total = len(full_dataset)\n",
    "n_test  = int(TEST_SPLIT * n_total)\n",
    "n_val   = int(VAL_SPLIT * n_total)\n",
    "n_train = n_total - n_val - n_test\n",
    "\n",
    "train_ds, val_ds, test_ds = random_split(full_dataset, [n_train, n_val, n_test], generator=g)\n",
    "\n",
    "# Use eval transforms for val/test\n",
    "val_ds.dataset.transform  = eval_transform\n",
    "test_ds.dataset.transform = eval_transform\n",
    "\n",
    "# DataLoaders\n",
    "train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True,  num_workers=NUM_WORKERS)\n",
    "val_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n",
    "test_loader  = DataLoader(test_ds,  batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n",
    "\n",
    "# Quick sanity check\n",
    "xb, yb = next(iter(train_loader))\n",
    "print(\"Train batch:\", xb.shape, yb.shape, \"| total images:\", n_total)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "144f8673-7bce-4a1d-9e89-5eca9de60000",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ Model and helpers are ready. Device: cpu\n"
     ]
    }
   ],
   "source": [
    "# Step 4 — Define model (ResNet18) + helpers\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torchvision import models\n",
    "\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "num_classes = len(class_names)\n",
    "\n",
    "# ResNet18 from scratch (no internet needed)\n",
    "model = models.resnet18(weights=None)\n",
    "model.fc = nn.Linear(model.fc.in_features, num_classes)\n",
    "model = model.to(device)\n",
    "\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = optim.Adam(model.parameters(), lr=1e-3)\n",
    "\n",
    "def train_one_epoch(model, loader, optimizer, criterion, device):\n",
    "    model.train()\n",
    "    total_loss, correct, n = 0.0, 0, 0\n",
    "    for xb, yb in loader:\n",
    "        xb, yb = xb.to(device), yb.to(device)\n",
    "        optimizer.zero_grad()\n",
    "        out = model(xb)\n",
    "        loss = criterion(out, yb)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        total_loss += loss.item() * xb.size(0)\n",
    "        correct += (out.argmax(1) == yb).sum().item()\n",
    "        n += xb.size(0)\n",
    "    return total_loss / n, correct / n\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluate(model, loader, criterion, device):\n",
    "    model.eval()\n",
    "    total_loss, correct, n = 0.0, 0, 0\n",
    "    for xb, yb in loader:\n",
    "        xb, yb = xb.to(device), yb.to(device)\n",
    "        out = model(xb)\n",
    "        loss = criterion(out, yb)\n",
    "        total_loss += loss.item() * xb.size(0)\n",
    "        correct += (out.argmax(1) == yb).sum().item()\n",
    "        n += xb.size(0)\n",
    "    return total_loss / n, correct / n\n",
    "\n",
    "print(\"✅ Model and helpers are ready. Device:\", device)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5331b1fc-a2ab-4946-9ced-45fd0f86e20b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "STATUS: starting diagnostic\n",
      "STATUS: python: C:\\Users\\hp\\anaconda3 1\\envs\\image\\python.exe\n",
      "STATUS: platform: Windows-11-10.0.26200-SP0\n",
      "STATUS: imports ok\n",
      "STATUS: torch: 2.9.0+cpu torchvision: 0.24.0+cpu\n",
      "STATUS: dataset path: C:\\Users\\hp\\Downloads\\flowers\n",
      "STATUS: classes: ['daisy', 'dandelion', 'rose', 'sunflower', 'tulip']\n",
      "STATUS: dataset sizes -> train: 3455 val: 431 test: 431\n",
      "STATUS: one batch -> (16, 3, 224, 224) (16,)\n",
      "STATUS: device: cpu\n",
      "STATUS: one training step ok, loss: 1.599729299545288\n",
      "STATUS: diagnostic finished successfully\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\hp\\AppData\\Local\\Temp\\ipykernel_7924\\2673844871.py:115: UserWarning: Converting a tensor with requires_grad=True to a scalar may lead to unexpected behavior.\n",
      "Consider using tensor.detach() first. (Triggered internally at C:\\actions-runner\\_work\\pytorch\\pytorch\\pytorch\\torch\\csrc\\autograd\\generated\\python_variable_methods.cpp:837.)\n",
      "  print(\"STATUS: one training step ok, loss:\", float(loss))\n"
     ]
    }
   ],
   "source": [
    "# DIAG STEP — single-cell end-to-end check (Arabic explanation, English-only code)\n",
    "\n",
    "import os, sys, traceback, platform, math, random\n",
    "from pathlib import Path\n",
    "\n",
    "print(\"STATUS: starting diagnostic\")\n",
    "print(\"STATUS: python:\", sys.executable)\n",
    "print(\"STATUS: platform:\", platform.platform())\n",
    "\n",
    "# 1) Imports\n",
    "try:\n",
    "    import numpy as np, pandas as pd, matplotlib\n",
    "    import torch, torch.nn as nn, torch.optim as optim\n",
    "    from torch.utils.data import DataLoader, random_split\n",
    "    import torchvision\n",
    "    from torchvision import datasets, transforms\n",
    "    print(\"STATUS: imports ok\")\n",
    "    print(\"STATUS: torch:\", torch.__version__, \"torchvision:\", torchvision.__version__)\n",
    "except Exception as e:\n",
    "    err = traceback.format_exc()\n",
    "    Path(\"debug_log.txt\").write_text(err, encoding=\"utf-8\")\n",
    "    print(\"STATUS: import error -> see debug_log.txt\")\n",
    "    raise\n",
    "\n",
    "# 2) Dataset path (edit this line if needed)\n",
    "DATASET_DIR = Path(r\"C:\\Users\\hp\\Downloads\\flowers\")  # <-- change if your folder is elsewhere\n",
    "print(\"STATUS: dataset path:\", DATASET_DIR)\n",
    "\n",
    "try:\n",
    "    assert DATASET_DIR.exists(), f\"Dataset path not found: {DATASET_DIR}\"\n",
    "    classes = [p.name for p in DATASET_DIR.iterdir() if p.is_dir()]\n",
    "    assert classes, \"No class subfolders found in dataset root.\"\n",
    "    print(\"STATUS: classes:\", classes)\n",
    "except Exception:\n",
    "    err = traceback.format_exc()\n",
    "    Path(\"debug_log.txt\").write_text(err, encoding=\"utf-8\")\n",
    "    print(\"STATUS: dataset error -> see debug_log.txt\")\n",
    "    raise\n",
    "\n",
    "# 3) Transforms + ImageFolder + split\n",
    "IMG_SIZE = 224\n",
    "BATCH_SIZE = 16\n",
    "VAL_SPLIT = 0.1\n",
    "TEST_SPLIT = 0.1\n",
    "RANDOM_SEED = 42\n",
    "NUM_WORKERS = 0  # Windows-safe\n",
    "\n",
    "train_tf = transforms.Compose([\n",
    "    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
    "    transforms.RandomHorizontalFlip(0.5),\n",
    "    transforms.ToTensor()\n",
    "])\n",
    "eval_tf  = transforms.Compose([\n",
    "    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
    "    transforms.ToTensor()\n",
    "])\n",
    "\n",
    "try:\n",
    "    full_ds = datasets.ImageFolder(DATASET_DIR.as_posix(), transform=train_tf)\n",
    "    class_names = full_ds.classes\n",
    "    g = torch.Generator().manual_seed(RANDOM_SEED)\n",
    "    n_total = len(full_ds)\n",
    "    n_test  = int(TEST_SPLIT * n_total)\n",
    "    n_val   = int(VAL_SPLIT * n_total)\n",
    "    n_train = n_total - n_val - n_test\n",
    "    train_ds, val_ds, test_ds = random_split(full_ds, [n_train, n_val, n_test], generator=g)\n",
    "    val_ds.dataset.transform  = eval_tf\n",
    "    test_ds.dataset.transform = eval_tf\n",
    "    print(\"STATUS: dataset sizes -> train:\", len(train_ds), \"val:\", len(val_ds), \"test:\", len(test_ds))\n",
    "except Exception:\n",
    "    err = traceback.format_exc()\n",
    "    Path(\"debug_log.txt\").write_text(err, encoding=\"utf-8\")\n",
    "    print(\"STATUS: dataset/split error -> see debug_log.txt\")\n",
    "    raise\n",
    "\n",
    "# 4) DataLoaders\n",
    "try:\n",
    "    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True,  num_workers=NUM_WORKERS)\n",
    "    val_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n",
    "    test_loader  = DataLoader(test_ds,  batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n",
    "    xb, yb = next(iter(train_loader))\n",
    "    print(\"STATUS: one batch ->\", tuple(xb.shape), tuple(yb.shape))\n",
    "except Exception:\n",
    "    err = traceback.format_exc()\n",
    "    Path(\"debug_log.txt\").write_text(err, encoding=\"utf-8\")\n",
    "    print(\"STATUS: dataloader error -> see debug_log.txt\")\n",
    "    raise\n",
    "\n",
    "# 5) Tiny model (faster than ResNet) + one forward/backward step\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "print(\"STATUS: device:\", device)\n",
    "\n",
    "class TinyCNN(nn.Module):\n",
    "    def __init__(self, num_classes):\n",
    "        super().__init__()\n",
    "        self.net = nn.Sequential(\n",
    "            nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n",
    "            nn.Conv2d(32,64,3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n",
    "            nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(),\n",
    "            nn.Linear(64, num_classes)\n",
    "        )\n",
    "    def forward(self, x): return self.net(x)\n",
    "\n",
    "try:\n",
    "    model = TinyCNN(num_classes=len(class_names)).to(device)\n",
    "    criterion = nn.CrossEntropyLoss()\n",
    "    optimizer = optim.Adam(model.parameters(), lr=1e-3)\n",
    "\n",
    "    xb, yb = xb.to(device), yb.to(device)\n",
    "    optimizer.zero_grad(set_to_none=True)\n",
    "    out = model(xb)\n",
    "    loss = criterion(out, yb)\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "    print(\"STATUS: one training step ok, loss:\", float(loss))\n",
    "except Exception:\n",
    "    err = traceback.format_exc()\n",
    "    Path(\"debug_log.txt\").write_text(err, encoding=\"utf-8\")\n",
    "    print(\"STATUS: model/step error -> see debug_log.txt\")\n",
    "    raise\n",
    "\n",
    "print(\"STATUS: diagnostic finished successfully\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f52594c8-8077-40d0-a428-91ee93051f01",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ">> Training started\n",
      "\n",
      "== Epoch 1/5 ==\n",
      "[train] step 0020 loss=1.6233\n",
      "[train] step 0040 loss=1.5892\n",
      "[train] step 0060 loss=1.4894\n",
      "[train] step 0080 loss=1.5817\n",
      "[train] step 0100 loss=1.4473\n",
      "[train] step 0120 loss=1.4265\n",
      "[train] step 0140 loss=1.3810\n",
      "[train] step 0160 loss=1.3037\n",
      "[train] step 0180 loss=1.5330\n",
      "[train] step 0200 loss=1.3712\n",
      "[summary] train_loss=1.4392 acc=0.344 | val_loss=1.3614 acc=0.378\n",
      "  saved new best -> C:\\Users\\hp\\outputs\\best_model_tiny.pt\n",
      "\n",
      "== Epoch 2/5 ==\n",
      "[train] step 0020 loss=1.5059\n",
      "[train] step 0040 loss=0.9831\n",
      "[train] step 0060 loss=1.3872\n",
      "[train] step 0080 loss=1.2088\n",
      "[train] step 0100 loss=1.3680\n",
      "[train] step 0120 loss=1.5445\n",
      "[train] step 0140 loss=1.3158\n",
      "[train] step 0160 loss=1.2516\n",
      "[train] step 0180 loss=1.1849\n",
      "[train] step 0200 loss=1.0706\n",
      "[summary] train_loss=1.2694 acc=0.424 | val_loss=1.2751 acc=0.478\n",
      "  saved new best -> C:\\Users\\hp\\outputs\\best_model_tiny.pt\n",
      "\n",
      "== Epoch 3/5 ==\n",
      "[train] step 0020 loss=1.3754\n",
      "[train] step 0040 loss=1.1396\n",
      "[train] step 0060 loss=1.2378\n",
      "[train] step 0080 loss=1.1311\n",
      "[train] step 0100 loss=1.3048\n",
      "[train] step 0120 loss=1.0211\n",
      "[train] step 0140 loss=0.8952\n",
      "[train] step 0160 loss=1.2015\n",
      "[train] step 0180 loss=1.3926\n",
      "[train] step 0200 loss=0.9893\n",
      "[summary] train_loss=1.2362 acc=0.453 | val_loss=1.2386 acc=0.471\n",
      "\n",
      "== Epoch 4/5 ==\n",
      "[train] step 0020 loss=1.0799\n",
      "[train] step 0040 loss=1.3315\n",
      "[train] step 0060 loss=1.0495\n",
      "[train] step 0080 loss=1.1945\n",
      "[train] step 0100 loss=1.3548\n",
      "[train] step 0120 loss=1.0847\n",
      "[train] step 0140 loss=1.3479\n",
      "[train] step 0160 loss=2.3432\n",
      "[train] step 0180 loss=1.5872\n",
      "[train] step 0200 loss=0.9974\n",
      "[summary] train_loss=1.2240 acc=0.466 | val_loss=1.2626 acc=0.434\n",
      "\n",
      "== Epoch 5/5 ==\n",
      "[train] step 0020 loss=1.1657\n",
      "[train] step 0040 loss=1.1390\n",
      "[train] step 0060 loss=1.2358\n",
      "[train] step 0080 loss=1.0375\n",
      "[train] step 0100 loss=1.1660\n",
      "[train] step 0120 loss=0.9429\n",
      "[train] step 0140 loss=1.4672\n",
      "[train] step 0160 loss=1.0671\n",
      "[train] step 0180 loss=1.3826\n",
      "[train] step 0200 loss=1.1179\n",
      "[summary] train_loss=1.1998 acc=0.490 | val_loss=1.1944 acc=0.524\n",
      "  saved new best -> C:\\Users\\hp\\outputs\\best_model_tiny.pt\n",
      "\n",
      ">> Training finished.\n",
      "Best weights: C:\\Users\\hp\\outputs\\best_model_tiny.pt\n",
      "Saved curves: C:\\Users\\hp\\outputs\\loss_tiny.png | C:\\Users\\hp\\outputs\\acc_tiny.png\n"
     ]
    }
   ],
   "source": [
    "# Step 6 — Train TinyCNN for a few epochs, save best weights, save curves (no interactive plots)\n",
    "\n",
    "import torch, matplotlib\n",
    "matplotlib.use(\"Agg\")  # safe backend (no GUI)\n",
    "import matplotlib.pyplot as plt\n",
    "from pathlib import Path\n",
    "\n",
    "# --- sanity: ensure required objects exist (from previous steps/diagnostic) ---\n",
    "assert 'model' in globals(), \"model is not defined (run the diagnostic cell again).\"\n",
    "assert 'criterion' in globals(), \"criterion is not defined (run the diagnostic cell again).\"\n",
    "assert 'optimizer' in globals(), \"optimizer is not defined (run the diagnostic cell again).\"\n",
    "assert 'train_loader' in globals() and 'val_loader' in globals(), \"DataLoaders missing (run Step 3/diagnostic).\"\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "model.to(device)\n",
    "\n",
    "OUT_DIR = Path(\"outputs\"); OUT_DIR.mkdir(parents=True, exist_ok=True)\n",
    "NUM_EPOCHS = 5  # you can increase later (e.g., 10)\n",
    "history = {\"train_loss\": [], \"train_acc\": [], \"val_loss\": [], \"val_acc\": []}\n",
    "best_val_acc = -1.0\n",
    "\n",
    "def epoch_train():\n",
    "    model.train()\n",
    "    total_loss, correct, n = 0.0, 0, 0\n",
    "    for i, (xb, yb) in enumerate(train_loader, 1):\n",
    "        xb, yb = xb.to(device), yb.to(device)\n",
    "        optimizer.zero_grad(set_to_none=True)\n",
    "        out = model(xb)\n",
    "        loss = criterion(out, yb)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        total_loss += loss.item() * xb.size(0)\n",
    "        correct += (out.argmax(1) == yb).sum().item()\n",
    "        n += xb.size(0)\n",
    "        if i % 20 == 0:\n",
    "            print(f\"[train] step {i:04d} loss={loss.item():.4f}\", flush=True)\n",
    "    return total_loss / n, correct / n\n",
    "\n",
    "@torch.no_grad()\n",
    "def epoch_eval(loader):\n",
    "    model.eval()\n",
    "    total_loss, correct, n = 0.0, 0, 0\n",
    "    for xb, yb in loader:\n",
    "        xb, yb = xb.to(device), yb.to(device)\n",
    "        out = model(xb)\n",
    "        loss = criterion(out, yb)\n",
    "        total_loss += loss.item() * xb.size(0)\n",
    "        correct += (out.argmax(1) == yb).sum().item()\n",
    "        n += xb.size(0)\n",
    "    return total_loss / n, correct / n\n",
    "\n",
    "print(\">> Training started\")\n",
    "for epoch in range(1, NUM_EPOCHS + 1):\n",
    "    print(f\"\\n== Epoch {epoch}/{NUM_EPOCHS} ==\")\n",
    "    tr_loss, tr_acc = epoch_train()\n",
    "    val_loss, val_acc = epoch_eval(val_loader)\n",
    "\n",
    "    history[\"train_loss\"].append(tr_loss); history[\"train_acc\"].append(tr_acc)\n",
    "    history[\"val_loss\"].append(val_loss);   history[\"val_acc\"].append(val_acc)\n",
    "\n",
    "    print(f\"[summary] train_loss={tr_loss:.4f} acc={tr_acc:.3f} | val_loss={val_loss:.4f} acc={val_acc:.3f}\", flush=True)\n",
    "\n",
    "    if val_acc > best_val_acc:\n",
    "        best_val_acc = val_acc\n",
    "        torch.save(model.state_dict(), OUT_DIR / \"best_model_tiny.pt\")\n",
    "        print(\"  saved new best ->\", (OUT_DIR / \"best_model_tiny.pt\").resolve())\n",
    "\n",
    "# save curves\n",
    "plt.figure(); plt.plot(history[\"train_loss\"], label=\"train_loss\"); plt.plot(history[\"val_loss\"], label=\"val_loss\")\n",
    "plt.xlabel(\"epoch\"); plt.ylabel(\"loss\"); plt.title(\"Loss\"); plt.legend()\n",
    "plt.savefig(OUT_DIR / \"loss_tiny.png\", dpi=150)\n",
    "\n",
    "plt.figure(); plt.plot(history[\"train_acc\"], label=\"train_acc\"); plt.plot(history[\"val_acc\"], label=\"val_acc\")\n",
    "plt.xlabel(\"epoch\"); plt.ylabel(\"acc\"); plt.title(\"Accuracy\"); plt.legend()\n",
    "plt.savefig(OUT_DIR / \"acc_tiny.png\", dpi=150)\n",
    "\n",
    "print(\"\\n>> Training finished.\")\n",
    "print(\"Best weights:\", (OUT_DIR / \"best_model_tiny.pt\").resolve())\n",
    "print(\"Saved curves:\", (OUT_DIR / \"loss_tiny.png\").resolve(), \"|\", (OUT_DIR / \"acc_tiny.png\").resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e71ef7d0-9ac5-4e3d-9847-41373febf13c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TEST  loss=1.2223  acc=0.476\n",
      "Saved: C:\\Users\\hp\\outputs\\classification_report_tiny.csv\n",
      "Saved: C:\\Users\\hp\\outputs\\confusion_matrix_tiny.csv\n",
      "Saved: C:\\Users\\hp\\outputs\\confusion_matrix_tiny.png\n",
      "Saved: C:\\Users\\hp\\outputs\\predictions_tiny.csv\n"
     ]
    }
   ],
   "source": [
    "# Step 7 — Evaluate on test set + save reports (CSV/PNG)\n",
    "\n",
    "import torch\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "from sklearn.metrics import classification_report, confusion_matrix\n",
    "import matplotlib\n",
    "matplotlib.use(\"Agg\")  # safe backend\n",
    "import matplotlib.pyplot as plt\n",
    "from pathlib import Path\n",
    "\n",
    "# sanity checks\n",
    "assert 'test_loader' in globals(), \"test_loader is missing. Run Step 3.\"\n",
    "assert 'class_names' in globals(), \"class_names is missing. Run Step 3.\"\n",
    "assert 'model' in globals(), \"model is missing. Run Step 6 (training) first.\"\n",
    "\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "OUT_DIR = Path(\"outputs\"); OUT_DIR.mkdir(parents=True, exist_ok=True)\n",
    "\n",
    "# load best weights (from Step 6)\n",
    "best_w = OUT_DIR / \"best_model_tiny.pt\"\n",
    "model.load_state_dict(torch.load(best_w, map_location=device))\n",
    "model.to(device)\n",
    "model.eval()\n",
    "\n",
    "criterion = torch.nn.CrossEntropyLoss()\n",
    "all_preds, all_labels = [], []\n",
    "test_loss, correct, n = 0.0, 0, 0\n",
    "\n",
    "with torch.no_grad():\n",
    "    for xb, yb in test_loader:\n",
    "        xb, yb = xb.to(device), yb.to(device)\n",
    "        out = model(xb)\n",
    "        loss = criterion(out, yb)\n",
    "        test_loss += loss.item() * xb.size(0)\n",
    "        pred = out.argmax(1)\n",
    "        correct += (pred == yb).sum().item()\n",
    "        n += xb.size(0)\n",
    "        all_preds.append(pred.cpu())\n",
    "        all_labels.append(yb.cpu())\n",
    "\n",
    "y_pred = torch.cat(all_preds).numpy()\n",
    "y_true = torch.cat(all_labels).numpy()\n",
    "\n",
    "test_loss /= n\n",
    "test_acc  = correct / n\n",
    "print(f\"TEST  loss={test_loss:.4f}  acc={test_acc:.3f}\")\n",
    "\n",
    "# classification report CSV\n",
    "rep = classification_report(y_true, y_pred, target_names=class_names, digits=3, output_dict=True)\n",
    "rep_df = pd.DataFrame(rep).T\n",
    "rep_df.to_csv(OUT_DIR / \"classification_report_tiny.csv\")\n",
    "print(\"Saved:\", (OUT_DIR / \"classification_report_tiny.csv\").resolve())\n",
    "\n",
    "# confusion matrix PNG + CSV\n",
    "cm = confusion_matrix(y_true, y_pred)\n",
    "cm_df = pd.DataFrame(cm, index=class_names, columns=class_names)\n",
    "cm_df.to_csv(OUT_DIR / \"confusion_matrix_tiny.csv\")\n",
    "\n",
    "plt.figure(figsize=(6,6))\n",
    "plt.imshow(cm, interpolation='nearest')\n",
    "plt.title(\"Confusion Matrix (TinyCNN)\")\n",
    "plt.xlabel(\"Predicted\"); plt.ylabel(\"True\")\n",
    "plt.colorbar(); plt.tight_layout()\n",
    "plt.savefig(OUT_DIR / \"confusion_matrix_tiny.png\", dpi=150)\n",
    "plt.close()\n",
    "print(\"Saved:\", (OUT_DIR / \"confusion_matrix_tiny.csv\").resolve())\n",
    "print(\"Saved:\", (OUT_DIR / \"confusion_matrix_tiny.png\").resolve())\n",
    "\n",
    "# optional: save raw predictions as CSV (label ids)\n",
    "pd.DataFrame({\n",
    "    \"y_true\": y_true,\n",
    "    \"y_pred\": y_pred\n",
    "}).to_csv(OUT_DIR / \"predictions_tiny.csv\", index=False)\n",
    "print(\"Saved:\", (OUT_DIR / \"predictions_tiny.csv\").resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2ffd8fba-5cd2-401f-b82f-ab6a71c6386c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Notebook detected: flower.ipynb\n",
      "Converting to PDF via nbconvert...\n",
      "⚠️ PDF via LaTeX failed. Falling back to HTML.\n",
      "✅ HTML saved at: C:\\Users\\hp\\flower.html\n",
      "Next: open the HTML in your browser and use Print → Save as PDF.\n"
     ]
    }
   ],
   "source": [
    "# Auto-convert the latest .ipynb in this folder to PDF (fallback to HTML if LaTeX is missing)\n",
    "\n",
    "import sys, subprocess, glob, os\n",
    "from pathlib import Path\n",
    "\n",
    "# 1) pick the most recently modified .ipynb in current directory\n",
    "nb_files = sorted(glob.glob(\"*.ipynb\"), key=lambda p: os.path.getmtime(p), reverse=True)\n",
    "assert nb_files, \"No .ipynb files found in the current directory.\"\n",
    "nb = nb_files[0]\n",
    "print(\"Notebook detected:\", nb)\n",
    "\n",
    "# 2) ensure nbconvert/nbformat are available\n",
    "try:\n",
    "    import nbconvert, nbformat  # noqa\n",
    "except Exception:\n",
    "    print(\"Installing nbconvert/nbformat...\")\n",
    "    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"nbconvert>=7\", \"nbformat>=5\"])\n",
    "\n",
    "pdf_path = Path(nb).with_suffix(\".pdf\")\n",
    "html_path = Path(nb).with_suffix(\".html\")\n",
    "\n",
    "# 3) try PDF via LaTeX\n",
    "try:\n",
    "    print(\"Converting to PDF via nbconvert...\")\n",
    "    subprocess.check_call([sys.executable, \"-m\", \"jupyter\", \"nbconvert\", \"--to\", \"pdf\", nb])\n",
    "    print(\"✅ PDF saved at:\", pdf_path.resolve())\n",
    "except subprocess.CalledProcessError:\n",
    "    # 4) fallback to HTML if LaTeX not installed\n",
    "    print(\"⚠️ PDF via LaTeX failed. Falling back to HTML.\")\n",
    "    subprocess.check_call([sys.executable, \"-m\", \"jupyter\", \"nbconvert\", \"--to\", \"html\", nb])\n",
    "    print(\"✅ HTML saved at:\", html_path.resolve())\n",
    "    print(\"Next: open the HTML in your browser and use Print → Save as PDF.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0215375d-8600-4194-a649-6dde1de3b76b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Notebook: BIOBERT.ipynb\n",
      "✅ HTML saved at: C:\\Users\\hp\\outputs\\BIOBERT.html\n",
      "\n",
      "Next:\n",
      "1) Open the HTML file in your browser.\n",
      "2) Press Ctrl+P → Destination: Save as PDF → Save.\n"
     ]
    }
   ],
   "source": [
    "# Export current notebook to HTML using nbconvert API (no LaTeX, no subprocess)\n",
    "\n",
    "import os, glob, os.path as osp\n",
    "from pathlib import Path\n",
    "\n",
    "# 1) pick the target notebook:\n",
    "NB_NAME = None  # set to e.g. \"flower_classification.ipynb\" if you want\n",
    "if NB_NAME is None:\n",
    "    # auto-pick the most recently modified .ipynb in current folder\n",
    "    ipynbs = sorted(glob.glob(\"*.ipynb\"), key=lambda p: osp.getmtime(p), reverse=True)\n",
    "    assert ipynbs, \"No .ipynb files found in this folder.\"\n",
    "    NB_NAME = ipynbs[0]\n",
    "\n",
    "print(\"Notebook:\", NB_NAME)\n",
    "\n",
    "# 2) ensure deps\n",
    "import sys, importlib\n",
    "def ensure(pkg, pipname=None):\n",
    "    try:\n",
    "        importlib.import_module(pkg)\n",
    "    except Exception:\n",
    "        import subprocess\n",
    "        subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", pipname or pkg])\n",
    "\n",
    "ensure(\"nbformat\", \"nbformat>=5\")\n",
    "ensure(\"nbconvert\", \"nbconvert>=7\")\n",
    "ensure(\"jinja2\")\n",
    "ensure(\"mistune\")\n",
    "ensure(\"pygments\")\n",
    "ensure(\"tinycss2\")\n",
    "\n",
    "# 3) export to HTML (pure Python, no external binaries)\n",
    "import nbformat\n",
    "from nbconvert import HTMLExporter\n",
    "\n",
    "nb = nbformat.read(NB_NAME, as_version=4)\n",
    "exporter = HTMLExporter()\n",
    "exporter.exclude_output_prompt = False\n",
    "exporter.exclude_input_prompt  = False\n",
    "\n",
    "(body, resources) = exporter.from_notebook_node(nb)\n",
    "\n",
    "OUT_DIR = Path(\"outputs\"); OUT_DIR.mkdir(parents=True, exist_ok=True)\n",
    "html_path = OUT_DIR / (Path(NB_NAME).stem + \".html\")\n",
    "html_path.write_text(body, encoding=\"utf-8\")\n",
    "print(\"✅ HTML saved at:\", html_path.resolve())\n",
    "\n",
    "print(\"\\nNext:\")\n",
    "print(\"1) Open the HTML file in your browser.\")\n",
    "print(\"2) Press Ctrl+P → Destination: Save as PDF → Save.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "26c5daad-b79f-4b11-a233-ec047139b6d9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ Final report saved at: C:\\Users\\hp\\outputs\\final_report.pdf\n"
     ]
    }
   ],
   "source": [
    "# Generate final_report.pdf inside outputs folder\n",
    "from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table\n",
    "from reportlab.lib.styles import getSampleStyleSheet\n",
    "from reportlab.lib.pagesizes import A4\n",
    "from reportlab.lib import colors\n",
    "import pandas as pd\n",
    "from pathlib import Path\n",
    "\n",
    "out_dir = Path(\"outputs\")\n",
    "pdf_path = out_dir / \"final_report.pdf\"\n",
    "\n",
    "# Load data\n",
    "rep_csv = out_dir / \"classification_report_tiny.csv\"\n",
    "loss_img = out_dir / \"loss_tiny.png\"\n",
    "acc_img = out_dir / \"acc_tiny.png\"\n",
    "cm_img = out_dir / \"confusion_matrix_tiny.png\"\n",
    "\n",
    "report_df = pd.read_csv(rep_csv)\n",
    "\n",
    "# PDF structure\n",
    "doc = SimpleDocTemplate(str(pdf_path), pagesize=A4)\n",
    "styles = getSampleStyleSheet()\n",
    "elements = []\n",
    "\n",
    "# Title\n",
    "elements.append(Paragraph(\"<b>Flower Image Classification Report</b>\", styles[\"Title\"]))\n",
    "elements.append(Spacer(1, 12))\n",
    "\n",
    "# Section 1 — Overview\n",
    "text = \"\"\"\n",
    "<b>Project:</b> Flower Classification using CNN (Tiny Model)<br/>\n",
    "<b>Dataset:</b> 5 Flower Categories (Daisy, Dandelion, Rose, Sunflower, Tulip)<br/>\n",
    "<b>Framework:</b> PyTorch<br/>\n",
    "<b>Device:</b> CPU<br/>\n",
    "<b>Epochs:</b> 5<br/>\n",
    "<b>Batch size:</b> 32<br/>\n",
    "\"\"\"\n",
    "elements.append(Paragraph(text, styles[\"Normal\"]))\n",
    "elements.append(Spacer(1, 12))\n",
    "\n",
    "# Section 2 — Images\n",
    "if loss_img.exists():\n",
    "    elements.append(Paragraph(\"<b>Training Loss Curve</b>\", styles[\"Heading2\"]))\n",
    "    elements.append(Image(str(loss_img), width=400, height=250))\n",
    "    elements.append(Spacer(1, 12))\n",
    "\n",
    "if acc_img.exists():\n",
    "    elements.append(Paragraph(\"<b>Training Accuracy Curve</b>\", styles[\"Heading2\"]))\n",
    "    elements.append(Image(str(acc_img), width=400, height=250))\n",
    "    elements.append(Spacer(1, 12))\n",
    "\n",
    "if cm_img.exists():\n",
    "    elements.append(Paragraph(\"<b>Confusion Matrix</b>\", styles[\"Heading2\"]))\n",
    "    elements.append(Image(str(cm_img), width=400, height=300))\n",
    "    elements.append(Spacer(1, 12))\n",
    "\n",
    "# Section 3 — Classification Report Table\n",
    "elements.append(Paragraph(\"<b>Classification Report</b>\", styles[\"Heading2\"]))\n",
    "data = [report_df.columns.to_list()] + report_df.values.tolist()\n",
    "table = Table(data)\n",
    "table.setStyle([\n",
    "    ('BACKGROUND', (0,0), (-1,0), colors.lightblue),\n",
    "    ('GRID', (0,0), (-1,-1), 0.5, colors.grey),\n",
    "    ('ALIGN', (1,1), (-1,-1), 'CENTER')\n",
    "])\n",
    "elements.append(table)\n",
    "elements.append(Spacer(1, 20))\n",
    "\n",
    "# Footer\n",
    "elements.append(Paragraph(\"<b>Generated automatically in Jupyter Notebook</b>\", styles[\"Italic\"]))\n",
    "\n",
    "# Build PDF\n",
    "doc.build(elements)\n",
    "print(\"✅ Final report saved at:\", pdf_path.resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a518353-8535-4c7b-960f-263d5c234560",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
