{"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":"gpu","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# import ","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"# Standard library\nfrom io import BytesIO\nimport time\nfrom pathlib import Path\nfrom typing import Callable, Dict, List, Tuple\n\n# Third-party\nimport cv2\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport requests\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\n\n# PyTorch\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch import Tensor\nfrom torch.amp import autocast, GradScaler\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom torch.utils.data import DataLoader, Dataset\n\n# TorchVision\nfrom torchvision.transforms import Compose\nfrom torchvision import transforms, models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.004632Z","iopub.execute_input":"2026-02-12T22:54:58.005082Z","iopub.status.idle":"2026-02-12T22:54:58.013497Z","shell.execute_reply.started":"2026-02-12T22:54:58.005021Z","shell.execute_reply":"2026-02-12T22:54:58.012520Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# config","metadata":{}},{"cell_type":"code","source":"config = {\n    \"image_size\": 224,\n    \"batch_size\": 32,\n    \"epochs\": 10,\n    \"learning_rate\": 1e-4,    # 0.0001\n    \"seed\": 42,\n    \"image_mean\": [0.485, 0.456, 0.406],\n    \"image_std\": [0.229, 0.224, 0.225],\n    \"num_workers\": 2,\n    \"lr_patience\": 3,\n    \"lr_factor\": 0.5,\n    \"lr_mode\": \"min\",\n    \"num_classes\": 5,\n    \"class_names\" : [\n               \"CBB (Cassava Bacterial Blight)\",\n               \"CBSD (Cassava Brown Streak Disease)\",\n               \"CGM (Cassava Green Mottle)\",\n               \"CMD (Cassava Mosaic Disease)\",\n               \"Healthy\"\n]\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.015319Z","iopub.execute_input":"2026-02-12T22:54:58.015651Z","iopub.status.idle":"2026-02-12T22:54:58.029789Z","shell.execute_reply.started":"2026-02-12T22:54:58.015621Z","shell.execute_reply":"2026-02-12T22:54:58.028961Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# prepare data","metadata":{}},{"cell_type":"code","source":"DATA_DIR = Path(\"/kaggle/input/cassava-leaf-disease-classification\")\n\ntrain_csv = DATA_DIR / \"train.csv\"\ntrain_dir = DATA_DIR / \"train_images\"\n\ndf = pd.read_csv(train_csv)\ndf[\"image_path\"] = train_dir / df[\"image_id\"]\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.030730Z","iopub.execute_input":"2026-02-12T22:54:58.031065Z","iopub.status.idle":"2026-02-12T22:54:58.368278Z","shell.execute_reply.started":"2026-02-12T22:54:58.031031Z","shell.execute_reply":"2026-02-12T22:54:58.367547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# dataframe\ntrain_df, holdout_df = train_test_split(\n        df,\n        test_size=0.3,\n        stratify=df[\"label\"],\n        random_state=config[\"seed\"]\n    )\nval_df, test_df = train_test_split(\n    holdout_df,\n    test_size=1/3,\n    stratify=holdout_df[\"label\"],\n    random_state=config[\"seed\"]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.369170Z","iopub.execute_input":"2026-02-12T22:54:58.369613Z","iopub.status.idle":"2026-02-12T22:54:58.395734Z","shell.execute_reply.started":"2026-02-12T22:54:58.369588Z","shell.execute_reply":"2026-02-12T22:54:58.395117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# transforms\ntrain_tf = Compose([\n        transforms.RandomResizedCrop(config[\"image_size\"], scale=(0.8, 1.0)),\n        transforms.RandomHorizontalFlip(),\n        transforms.ColorJitter(0.2, 0.2, 0.2, 0.05),\n        transforms.ToTensor(),\n        transforms.Normalize(config[\"image_mean\"], config[\"image_std\"]),\n    ])\n\nval_tf = Compose([\n    transforms.Resize((config[\"image_size\"], config[\"image_size\"])),\n    transforms.ToTensor(),\n    transforms.Normalize(config[\"image_mean\"],  config[\"image_std\"]),\n])\n\ntest_tf = Compose([\n    transforms.Resize((config[\"image_size\"], config[\"image_size\"])),\n    transforms.ToTensor(),\n    transforms.Normalize(config[\"image_mean\"],  config[\"image_std\"]),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.398234Z","iopub.execute_input":"2026-02-12T22:54:58.398497Z","iopub.status.idle":"2026-02-12T22:54:58.404470Z","shell.execute_reply.started":"2026-02-12T22:54:58.398474Z","shell.execute_reply":"2026-02-12T22:54:58.403800Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# dataset \nclass CassavaDataset(Dataset):\n    def __init__(\n            self, \n            df: pd.DataFrame, \n            transform: Callable[[Image.Image], Tensor]\n    ) -> None:\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self) -> int:\n        return len(self.df)\n\n    def __getitem__(self, index: int) -> Tuple[Tensor, int]:\n        row = self.df.iloc[index]\n\n        image_path = Path(row[\"image_path\"])\n        label = int(row[\"label\"])\n\n        img_bgr = cv2.imread(str(image_path))\n        img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n        pil_img = Image.fromarray(img_rgb)\n\n        image = self.transform(pil_img)\n\n        return image, label\n\ntrain_dataset = CassavaDataset(train_df, transform=train_tf)\nval_dataset = CassavaDataset(val_df, transform=val_tf)\ntest_dataset = CassavaDataset(test_df, transform=test_tf)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.405354Z","iopub.execute_input":"2026-02-12T22:54:58.405665Z","iopub.status.idle":"2026-02-12T22:54:58.423606Z","shell.execute_reply.started":"2026-02-12T22:54:58.405632Z","shell.execute_reply":"2026-02-12T22:54:58.422805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# dataloader\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=config[\"batch_size\"],\n    shuffle=True,\n    num_workers=config[\"num_workers\"],\n    drop_last=True\n)\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=config[\"batch_size\"],\n    shuffle=False,\n    num_workers=config[\"num_workers\"]\n)\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=config[\"batch_size\"],\n    shuffle=False,\n    num_workers=config[\"num_workers\"]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.424592Z","iopub.execute_input":"2026-02-12T22:54:58.424909Z","iopub.status.idle":"2026-02-12T22:54:58.434201Z","shell.execute_reply.started":"2026-02-12T22:54:58.424879Z","shell.execute_reply":"2026-02-12T22:54:58.433584Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# models","metadata":{}},{"cell_type":"code","source":"class SimpleCNN(nn.Module):\n    def __init__(self, num_classes=5):\n        super().__init__()\n\n        self.features = nn.Sequential(\n            # Block1: (B,32,H/2,W/2)\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n\n            # Block2: (B,64,H/4,W/4)\n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n\n            # Block3: (B,128,H/8,W/8)\n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n\n            # Block4: (B,256,H/16,W/16)\n            nn.Conv2d(128, 256, kernel_size=3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n\n            # GAP: (B,256,1,1)\n            nn.AdaptiveAvgPool2d(1),\n        )\n\n        self.classifier = nn.Sequential(\n            # (B,256)\n            nn.Flatten(1),\n\n            # (B,512)\n            nn.Linear(256, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n\n            # (B,num_classes)\n            nn.Linear(512, num_classes)\n        )\n    def forward(self, x: Tensor) -> Tensor:\n        x = self.features(x)\n        x = self.classifier(x)\n\n        return x\n\ndef build_mobilenet_v2(num_classes: int):\n    weights = models.MobileNet_V2_Weights.DEFAULT\n    model = models.mobilenet_v2(weights=weights)\n    model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes)\n    return model\n\ndef build_resnet18(num_classes: int):\n    weights = models.ResNet18_Weights.DEFAULT\n    model = models.resnet18(weights=weights)\n    model.fc = nn.Linear(model.fc.in_features, num_classes)\n    return model\n\ndef build_model(\n        model_name: str,\n        num_classes: int\n) -> nn.Module:\n    model_name = model_name.lower()\n\n    if model_name in {\"simple_cnn\", \"simplecnn\"}:\n        return SimpleCNN(num_classes=num_classes)\n\n    if model_name in {\"mobilenet_v2\", \"mobilenetv2\"}:\n        return build_mobilenet_v2(num_classes=num_classes)\n\n    if model_name in {\"resnet18\"}:\n        return build_resnet18(num_classes=num_classes)\n\n    raise ValueError(f\"Unknown model_name: {model_name}. Use one of: simple_cnn, mobilenet_v2, resnet18\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.435415Z","iopub.execute_input":"2026-02-12T22:54:58.435739Z","iopub.status.idle":"2026-02-12T22:54:58.456277Z","shell.execute_reply.started":"2026-02-12T22:54:58.435704Z","shell.execute_reply":"2026-02-12T22:54:58.455517Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# train_loop","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(\n    model: nn.Module,\n    train_loader: DataLoader,\n    loss_fn: nn.Module,\n    device: torch.device,\n    optimizer: Optimizer,\n    scaler: GradScaler\n) -> Tuple[float, float]:\n    \n    running_loss = 0.0\n    num_correct = 0\n    num_samples = 0\n\n    use_amp = (device.type == \"cuda\")\n\n    for images, labels in train_loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad(set_to_none=True)\n\n        with autocast(device_type=device.type, enabled=use_amp):\n            logits = model(images)\n            loss = loss_fn(logits, labels)\n\n        if use_amp:\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            loss.backward()\n            optimizer.step()\n\n        batch_size = labels.size(dim=0)\n        running_loss += loss.item() * batch_size\n\n        preds = logits.argmax(dim=1)\n        num_correct += preds.eq(labels).sum().item()\n        num_samples += batch_size\n\n    train_loss = running_loss / max(num_samples, 1)\n    train_acc = 100.0 * num_correct / max(num_samples, 1)\n\n    return train_loss, train_acc\n\n@torch.no_grad()\ndef evaluate_one_epoch(\n        model: nn.Module,\n        val_loader: DataLoader,\n        loss_fn: nn.Module,\n        device: torch.device\n) -> Tuple[float, float]:\n\n    running_loss = 0.0\n    num_correct = 0\n    num_samples = 0\n\n    use_amp = (device.type == \"cuda\")\n\n    for images, labels in val_loader:\n        images, labels = images.to(device), labels.to(device)\n\n        with autocast(device_type=device.type, enabled=use_amp):\n            logits = model(images)\n            loss = loss_fn(logits, labels)\n\n        batch_size = labels.size(dim=0)\n        running_loss += loss.item() * batch_size\n\n        preds = logits.argmax(dim=1)\n        num_correct += preds.eq(labels).sum().item()\n        num_samples += batch_size\n\n    val_loss = running_loss / max(num_samples, 1)\n    val_acc = 100.0 * num_correct / max(num_samples, 1)\n\n    return val_loss, val_acc\n\ndef fit(\n        run_name: str,\n        run_dir: Path,\n        epochs: int,\n        model: nn.Module,\n        train_loader: DataLoader,\n        val_loader: DataLoader,\n        loss_fn: nn.Module,\n        device: torch.device,\n        optimizer: Optimizer,\n        scaler: GradScaler,\n        scheduler: ReduceLROnPlateau,\n):\n    run_dir.mkdir(parents=True, exist_ok=True)\n\n    history = {\n        \"train_loss\": [], \"train_acc\": [],\n        \"val_loss\": [], \"val_acc\": []\n    }\n    best_val_acc = 0.0\n    print(f\"{run_name} 모델 훈련 시작\")\n    start_time = time.time()\n    for epoch in range(epochs):\n        model.train()\n        train_loss, train_acc = train_one_epoch(\n            model=model,\n            train_loader=train_loader,\n            loss_fn=loss_fn,\n            device=device,\n            optimizer=optimizer,\n            scaler=scaler\n        )\n        model.eval()\n        val_loss, val_acc = evaluate_one_epoch(\n            model=model,\n            val_loader=val_loader,\n            loss_fn=loss_fn,\n            device=device\n        )\n        scheduler.step(val_loss)\n\n        print(\n            f\"\\n[Epoch {epoch + 1:02d}/{epochs}] \"\n            f\"{run_name} | \"\n            f\"Train: Loss {train_loss:.4f}, Acc {train_acc:6.2f}% | \"\n            f\"Val: Loss {val_loss:.4f}, Acc {val_acc:6.2f}%\"\n        )\n\n        history[\"train_loss\"].append(train_loss)\n        history[\"train_acc\"].append(train_acc)\n        history[\"val_loss\"].append(val_loss)\n        history[\"val_acc\"].append(val_acc)\n\n        if val_acc > best_val_acc:\n            best_val_acc = val_acc\n            best = {\n                \"model_state_dict\": model.state_dict(),\n                \"epoch\": epoch + 1,\n                \"optimizer_state_dict\": optimizer.state_dict(),\n                \"scheduler_state_dict\": scheduler.state_dict(),\n                \"scaler_state_dict\": scaler.state_dict(),\n                \"best_val_acc\": best_val_acc\n            }\n            torch.save(best, run_dir / \"best.pt\")\n\n    train_time = time.time() - start_time\n    history = {\n        \"history\": history,\n        \"train_time\": train_time,\n        \"best_val_acc\": best_val_acc\n    }\n    torch.save(history, run_dir / \"history.pt\")\n    print(f\"{run_name} 모델 훈련 완료, train_time: {train_time:.1f}초, best_val_acc: {best_val_acc:.2f}\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.457375Z","iopub.execute_input":"2026-02-12T22:54:58.457693Z","iopub.status.idle":"2026-02-12T22:54:58.476492Z","shell.execute_reply.started":"2026-02-12T22:54:58.457670Z","shell.execute_reply":"2026-02-12T22:54:58.475710Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# train","metadata":{}},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"train device: {device}\")\nloss_fn = nn.CrossEntropyLoss()\nruns_dir = Path(\"/kaggle/working/runs\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.477687Z","iopub.execute_input":"2026-02-12T22:54:58.477990Z","iopub.status.idle":"2026-02-12T22:54:58.491278Z","shell.execute_reply.started":"2026-02-12T22:54:58.477967Z","shell.execute_reply":"2026-02-12T22:54:58.490522Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# simple_cnn\nsimple_cnn_model = build_model(\"simple_cnn\", num_classes=config[\"num_classes\"]).to(device)\nsimple_cnn_optimizer = optim.Adam(simple_cnn_model.parameters(), lr=config[\"learning_rate\"])\nsimple_cnn_scaler = GradScaler(enabled=(device.type == \"cuda\"))\nsimple_cnn_scheduler = ReduceLROnPlateau(\n    optimizer = simple_cnn_optimizer,\n    mode = config[\"lr_mode\"],\n    factor = config[\"lr_factor\"],\n    patience = config[\"lr_patience\"],\n)\n\nsimple_cnn_dir = runs_dir / \"simple_cnn\"\nfit(\n    run_name=\"simple_cnn\",\n    run_dir=simple_cnn_dir,\n    epochs=config[\"epochs\"],\n    model=simple_cnn_model,\n    train_loader=train_loader,\n    val_loader=val_loader,\n    loss_fn=loss_fn,\n    device=device,\n    optimizer=simple_cnn_optimizer,\n    scaler=simple_cnn_scaler,\n    scheduler=simple_cnn_scheduler\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T22:54:58.492193Z","iopub.execute_input":"2026-02-12T22:54:58.492438Z","iopub.status.idle":"2026-02-12T23:26:40.042947Z","shell.execute_reply.started":"2026-02-12T22:54:58.492399Z","shell.execute_reply":"2026-02-12T23:26:40.041927Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# mobilenet_v2\nmobilenet_v2_model = build_model(\"mobilenet_v2\", num_classes=config[\"num_classes\"]).to(device)\nmobilenet_v2_optimizer = optim.Adam(mobilenet_v2_model.parameters(), lr=config[\"learning_rate\"])\nmobilenet_v2_scaler = GradScaler(enabled=(device.type == \"cuda\"))\nmobilenet_v2_scheduler = ReduceLROnPlateau(\n    optimizer = mobilenet_v2_optimizer,\n    mode = config[\"lr_mode\"],\n    factor = config[\"lr_factor\"],\n    patience = config[\"lr_patience\"],\n)\n\nmobilenet_v2_dir = runs_dir / \"mobilenet_v2\"\nfit(\n    run_name=\"mobilenet_v2\",\n    run_dir=mobilenet_v2_dir,\n    epochs=config[\"epochs\"],\n    model=mobilenet_v2_model,\n    train_loader=train_loader,\n    val_loader=val_loader,\n    loss_fn=loss_fn,\n    device=device,\n    optimizer=mobilenet_v2_optimizer,\n    scaler=mobilenet_v2_scaler,\n    scheduler=mobilenet_v2_scheduler\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T23:26:40.044488Z","iopub.execute_input":"2026-02-12T23:26:40.044776Z","iopub.status.idle":"2026-02-12T23:58:37.251854Z","shell.execute_reply.started":"2026-02-12T23:26:40.044745Z","shell.execute_reply":"2026-02-12T23:58:37.250921Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# resnet18\nresnet18_model = build_model(\"resnet18\", num_classes=config[\"num_classes\"]).to(device)\nresnet18_optimizer = optim.Adam(resnet18_model.parameters(), lr=config[\"learning_rate\"])\nresnet18_scaler = GradScaler(enabled=(device.type == \"cuda\"))\nresnet18_scheduler = ReduceLROnPlateau(\n    optimizer = resnet18_optimizer,\n    mode = config[\"lr_mode\"],\n    factor = config[\"lr_factor\"],\n    patience = config[\"lr_patience\"],\n)\n\nresnet18_dir = runs_dir / \"resnet18\"\nfit(\n    run_name=\"resnet18\",\n    run_dir=resnet18_dir,\n    epochs=config[\"epochs\"],\n    model=resnet18_model,\n    train_loader=train_loader,\n    val_loader=val_loader,\n    loss_fn=loss_fn,\n    device=device,\n    optimizer=resnet18_optimizer,\n    scaler=resnet18_scaler,\n    scheduler=resnet18_scheduler\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-12T23:58:37.253343Z","iopub.execute_input":"2026-02-12T23:58:37.253634Z","iopub.status.idle":"2026-02-13T00:29:26.743532Z","shell.execute_reply.started":"2026-02-12T23:58:37.253603Z","shell.execute_reply":"2026-02-13T00:29:26.742560Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# test","metadata":{}},{"cell_type":"code","source":"model_list = [\n    (\"simple_cnn\", runs_dir / \"simple_cnn/best.pt\"),\n    (\"mobilenet_v2\", runs_dir / \"mobilenet_v2/best.pt\"),\n    (\"resnet18\", runs_dir / \"resnet18/best.pt\"),\n]\n\nfor name, path in model_list:\n    ckpt = torch.load(path)\n    model = build_model(name, num_classes=config[\"num_classes\"]).to(device)\n    model.load_state_dict(ckpt[\"model_state_dict\"])\n    model.eval()\n    test_loss, test_acc = evaluate_one_epoch(\n        model=model,\n        val_loader=test_loader,\n        loss_fn=loss_fn,\n        device=device\n    )\n    print(f\"{name:<12} | test loss: {test_loss:.4f} | test acc: {test_acc:.2f}%\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:29:26.747069Z","iopub.execute_input":"2026-02-13T00:29:26.747430Z","iopub.status.idle":"2026-02-13T00:30:12.215017Z","shell.execute_reply.started":"2026-02-13T00:29:26.747400Z","shell.execute_reply":"2026-02-13T00:30:12.214022Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# predict","metadata":{}},{"cell_type":"code","source":"def pil_from_url(url: str) -> Image.Image:\n    resp = requests.get(url, timeout=10)\n    resp.raise_for_status()\n    return Image.open(BytesIO(resp.content)).convert(\"RGB\")\n\n\ndef show_image_from_url(url: str) -> None:\n    img_pil = pil_from_url(url) # (H,W,C)\n    plt.imshow(img_pil)\n    plt.axis(\"off\")\n    plt.show()\n\n\ndef make_batch_image_from_url(url: str, transform=None) -> Tensor:\n    img_pil = pil_from_url(url) # (H,W,C)\n\n    if transform is None:\n        transform = transforms.ToTensor()\n\n    img = transform(img_pil)          # (C,H,W)\n    batch_img = img.unsqueeze(0)          # (1,C,H,W)\n\n    return batch_img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:12.216625Z","iopub.execute_input":"2026-02-13T00:30:12.217101Z","iopub.status.idle":"2026-02-13T00:30:12.223601Z","shell.execute_reply.started":"2026-02-13T00:30:12.217034Z","shell.execute_reply":"2026-02-13T00:30:12.222981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"url = \"http://www.iita.org/wp-content/uploads/2017/09/1024_CBSD-cassava-root-1024x683.jpg\"\nshow_image_from_url(url)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:12.224509Z","iopub.execute_input":"2026-02-13T00:30:12.224772Z","iopub.status.idle":"2026-02-13T00:30:12.738077Z","shell.execute_reply.started":"2026-02-13T00:30:12.224752Z","shell.execute_reply":"2026-02-13T00:30:12.737157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"img = make_batch_image_from_url(url=url, transform=test_tf)\nimg = img.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:12.739077Z","iopub.execute_input":"2026-02-13T00:30:12.739332Z","iopub.status.idle":"2026-02-13T00:30:12.924101Z","shell.execute_reply.started":"2026-02-13T00:30:12.739309Z","shell.execute_reply":"2026-02-13T00:30:12.923439Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_mobilenet_v2 = build_model(\"mobilenet_v2\", num_classes=config[\"num_classes\"]).to(device)\nckpt = torch.load(runs_dir / \"mobilenet_v2/best.pt\")\nbest_mobilenet_v2.load_state_dict(ckpt[\"model_state_dict\"])\nbest_mobilenet_v2.eval();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:12.924982Z","iopub.execute_input":"2026-02-13T00:30:12.925237Z","iopub.status.idle":"2026-02-13T00:30:13.212609Z","shell.execute_reply.started":"2026-02-13T00:30:12.925212Z","shell.execute_reply":"2026-02-13T00:30:13.211728Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with torch.no_grad():\n    logits = best_mobilenet_v2(img)\n    pred = logits.argmax(dim=1)\n    pred_idx = pred.item()\n\nprint(f\"pred_idx={pred_idx} | class={config[\"class_names\"][pred_idx]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:13.213613Z","iopub.execute_input":"2026-02-13T00:30:13.213895Z","iopub.status.idle":"2026-02-13T00:30:13.227976Z","shell.execute_reply.started":"2026-02-13T00:30:13.213862Z","shell.execute_reply":"2026-02-13T00:30:13.227195Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# visualize","metadata":{}},{"cell_type":"code","source":"History = Dict[str, List[float]]\n\n\ndef get_epochs(history: History) -> List[int]:\n    n = len(history.get(\"val_loss\", []))\n    return list(range(1, n + 1))\n\n\ndef plot_loss_curves(\n    history: History,\n    title: str = \"Loss Curves\"\n) -> None:\n    epochs = get_epochs(history)\n    train_loss = history[\"train_loss\"]\n    val_loss = history[\"val_loss\"]\n\n    plt.figure()\n    plt.plot(epochs, train_loss, label=\"train_loss\")\n    plt.plot(epochs, val_loss, label=\"val_loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.title(title)\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.show()\n\n\ndef plot_acc_curves(\n    history: History,\n    title: str = \"Accuracy Curves (%)\"\n) -> None:\n    epochs = get_epochs(history)\n    train_acc = history[\"train_acc\"]\n    val_acc = history[\"val_acc\"]\n\n    plt.figure()\n    plt.plot(epochs, train_acc, label=\"train_acc\")\n    plt.plot(epochs, val_acc, label=\"val_acc\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy (%)\")\n    plt.title(title)\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.show()\n\n\ndef plot_best_val_acc(\n    history: History,\n    title: str\n) -> None:\n    epochs = get_epochs(history)\n    val_acc = history[\"val_acc\"]\n\n    best_idx = max(range(len(val_acc)), key=lambda i: val_acc[i])\n    best_epoch = best_idx + 1\n    best_val = val_acc[best_idx]\n\n    plt.figure()\n    plt.plot(epochs, val_acc, label=\"val_acc\")\n    plt.scatter([best_epoch], [best_val])\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy (%)\")\n    plt.title(title)\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.show()\n\n\ndef plot_multi_model_loss(\n    histories: dict,\n    title: str\n) -> None:\n    plt.figure()\n\n    for name, history in histories.items():\n        epochs = get_epochs(history)\n        plt.plot(epochs, history[\"val_loss\"], label=f\"{name}_val\")\n\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.title(title)\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.show()\n\n\ndef plot_multi_model_acc(\n    histories: dict,\n    title: str\n) -> None:\n    plt.figure()\n\n    for name, history in histories.items():\n        epochs = get_epochs(history)\n        plt.plot(epochs, history[\"val_acc\"], label=f\"{name}_val\")\n\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy (%)\")\n    plt.title(title)\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:13.228986Z","iopub.execute_input":"2026-02-13T00:30:13.229372Z","iopub.status.idle":"2026-02-13T00:30:13.243534Z","shell.execute_reply.started":"2026-02-13T00:30:13.229349Z","shell.execute_reply":"2026-02-13T00:30:13.242636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ckpt = torch.load(runs_dir / \"mobilenet_v2/history.pt\")\nhistory = ckpt[\"history\"]\n\nplot_loss_curves(history, title=\"mobilenet_v2 Loss\")\nplot_acc_curves(history, title=\"mobilenet_v2 Acc\")\n\nplot_best_val_acc(history, title=\"mobilenet_v2 best_val_acc\")\n\nhistories = {\n    \"simple_cnn\": torch.load(runs_dir / \"simple_cnn/history.pt\")[\"history\"],\n    \"mobilenet_v2\": torch.load(runs_dir / \"mobilenet_v2/history.pt\")[\"history\"],\n    \"resnet18\": torch.load(runs_dir / \"resnet18/history.pt\")[\"history\"],\n}\n\nplot_multi_model_loss(histories, \"Loss Comparison\")\nplot_multi_model_acc(histories, \"Accuracy Comparison\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T00:30:13.244458Z","iopub.execute_input":"2026-02-13T00:30:13.244907Z","iopub.status.idle":"2026-02-13T00:30:14.141520Z","shell.execute_reply.started":"2026-02-13T00:30:13.244884Z","shell.execute_reply":"2026-02-13T00:30:14.140747Z"}},"outputs":[],"execution_count":null}]}