Upload folder using huggingface_hub
Browse files- .virtual_documents/__notebook_source__.ipynb +668 -0
- Gradio.py +402 -0
- checkpoints/best_model.pth +3 -0
- checkpoints/densenet121.pth +3 -0
- checkpoints/efficientnet_b0.pth +3 -0
- checkpoints/resnet50.pth +3 -0
- checkpoints/scratch_cnn.pth +3 -0
- notebooks/oral-disseases-image-classification.ipynb +0 -0
- outputs/densenet121_confusion_matrix.png +0 -0
- outputs/densenet121_history.png +0 -0
- outputs/efficientnet_b0_confusion_matrix.png +0 -0
- outputs/efficientnet_b0_history.png +0 -0
- outputs/models_comparison.csv +5 -0
- outputs/resnet50_confusion_matrix.png +0 -0
- outputs/resnet50_history.png +0 -0
- outputs/scratch_cnn_confusion_matrix.png +0 -0
- outputs/scratch_cnn_history.png +0 -0
.virtual_documents/__notebook_source__.ipynb
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This Python 3 environment comes with many helpful analytics libraries installed
|
| 2 |
+
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
|
| 3 |
+
# For example, here's several helpful packages to load
|
| 4 |
+
|
| 5 |
+
import numpy as np # linear algebra
|
| 6 |
+
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
|
| 7 |
+
|
| 8 |
+
# Input data files are available in the read-only "../input/" directory
|
| 9 |
+
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
for dirname, _, filenames in os.walk('/kaggle/input'):
|
| 13 |
+
for filename in filenames:
|
| 14 |
+
print(os.path.join(dirname, filename))
|
| 15 |
+
|
| 16 |
+
# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All"
|
| 17 |
+
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
|
| 18 |
+
|
| 19 |
+
# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session
|
| 20 |
+
# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md
|
| 21 |
+
|
| 22 |
+
import kagglehub
|
| 23 |
+
# kagglehub.dataset_download('<owner>/<dataset-slug>')
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
get_ipython().getoutput("pip install git+https://github.com/jacobgil/pytorch-grad-cam.git -q")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
import matplotlib.pyplot as plt
|
| 30 |
+
import seaborn as sns
|
| 31 |
+
from PIL import Image
|
| 32 |
+
import copy
|
| 33 |
+
import os
|
| 34 |
+
import shutil
|
| 35 |
+
import random
|
| 36 |
+
|
| 37 |
+
from sklearn.model_selection import train_test_split
|
| 38 |
+
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix , f1_score
|
| 39 |
+
|
| 40 |
+
import torch
|
| 41 |
+
import torch.nn as nn
|
| 42 |
+
import torch.optim as optim
|
| 43 |
+
import torch.cuda.amp as amp
|
| 44 |
+
import torchvision.models as models
|
| 45 |
+
import torchvision.transforms as transforms
|
| 46 |
+
import torchvision.datasets as datasets
|
| 47 |
+
from torchvision.datasets import ImageFolder
|
| 48 |
+
from torch.utils.data import DataLoader ,Dataset
|
| 49 |
+
from torch.optim.lr_scheduler import ReduceLROnPlateau
|
| 50 |
+
from tqdm import tqdm
|
| 51 |
+
from tqdm.auto import tqdm
|
| 52 |
+
from torchvision.models import efficientnet_b3
|
| 53 |
+
from pytorch_grad_cam import GradCAM
|
| 54 |
+
from pytorch_grad_cam.utils.image import show_cam_on_image
|
| 55 |
+
|
| 56 |
+
import warnings
|
| 57 |
+
warnings.filterwarnings("ignore")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
original_dirs = {
|
| 61 |
+
'Calculus': '/kaggle/input/datasets/salmansajid05/oral-diseases/Calculus/Calculus',
|
| 62 |
+
'Caries': '/kaggle/input/datasets/salmansajid05/oral-diseases/Data caries/Data caries/caries augmented data set/preview',
|
| 63 |
+
'Gingivitis': '/kaggle/input/datasets/salmansajid05/oral-diseases/Gingivitis/Gingivitis',
|
| 64 |
+
'Ulcers': '/kaggle/input/datasets/salmansajid05/oral-diseases/Mouth Ulcer/Mouth Ulcer/Mouth_Ulcer_augmented_DataSet/preview',
|
| 65 |
+
'Tooth Discoloration': '/kaggle/input/datasets/salmansajid05/oral-diseases/Tooth Discoloration/Tooth Discoloration /Tooth_discoloration_augmented_dataser/preview',
|
| 66 |
+
'Hypodontia': '/kaggle/input/datasets/salmansajid05/oral-diseases/hypodontia/hypodontia'
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
DATA_DIR = "/kaggle/working/oral_dataset"
|
| 71 |
+
splits = ['train', 'val', 'test']
|
| 72 |
+
classes = list(original_dirs.keys())
|
| 73 |
+
classes
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
import os
|
| 77 |
+
import shutil
|
| 78 |
+
|
| 79 |
+
NEW_DATASET = "/kaggle/working/oral_dataset"
|
| 80 |
+
|
| 81 |
+
os.makedirs(NEW_DATASET, exist_ok=True)
|
| 82 |
+
|
| 83 |
+
for class_name, source_dir in original_dirs.items():
|
| 84 |
+
|
| 85 |
+
target_dir = os.path.join(NEW_DATASET, class_name)
|
| 86 |
+
os.makedirs(target_dir, exist_ok=True)
|
| 87 |
+
|
| 88 |
+
for file in os.listdir(source_dir):
|
| 89 |
+
if file.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 90 |
+
shutil.copy(
|
| 91 |
+
os.path.join(source_dir, file),
|
| 92 |
+
os.path.join(target_dir, file)
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
print("Done")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
full_dataset = datasets.ImageFolder(root=DATA_DIR)
|
| 99 |
+
|
| 100 |
+
CLASS_NAMES = full_dataset.classes
|
| 101 |
+
NUM_CLASSES = len(CLASS_NAMES)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
DATA_DIR = original_dirs
|
| 105 |
+
|
| 106 |
+
CHECKPOINT_DIR = "./checkpoints"
|
| 107 |
+
OUTPUT_DIR = "./outputs"
|
| 108 |
+
os.makedirs(CHECKPOINT_DIR, exist_ok=True)
|
| 109 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 110 |
+
|
| 111 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 112 |
+
|
| 113 |
+
IMG_SIZE = 224
|
| 114 |
+
BATCH_SIZE = 32
|
| 115 |
+
NUM_WORKERS = 4
|
| 116 |
+
TRAIN_RATIO, VAL_RATIO, TEST_RATIO = 0.8, 0.1, 0.1
|
| 117 |
+
SEED = 42
|
| 118 |
+
|
| 119 |
+
EPOCHS = 30
|
| 120 |
+
LR_SCRATCH = 1e-3
|
| 121 |
+
LR_PRETRAINED_HEAD = 1e-3
|
| 122 |
+
LR_PRETRAINED_FINETUNE = 1e-5
|
| 123 |
+
|
| 124 |
+
WEIGHT_DECAY = 1e-4
|
| 125 |
+
LABEL_SMOOTHING = 0.1
|
| 126 |
+
DROPOUT = 0.4
|
| 127 |
+
|
| 128 |
+
EARLY_STOPPING_PATIENCE = 2
|
| 129 |
+
FREEZE_EPOCHS = 5
|
| 130 |
+
|
| 131 |
+
torch.manual_seed(SEED)
|
| 132 |
+
print("Done")
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
| 136 |
+
IMAGENET_STD = [0.229, 0.224, 0.225]
|
| 137 |
+
|
| 138 |
+
train_transform = transforms.Compose([
|
| 139 |
+
transforms.Resize((IMG_SIZE + 20, IMG_SIZE + 20)),
|
| 140 |
+
transforms.RandomResizedCrop(IMG_SIZE, scale=(0.8, 1.0)),
|
| 141 |
+
transforms.RandomHorizontalFlip(p=0.5),
|
| 142 |
+
transforms.RandomRotation(degrees=15),
|
| 143 |
+
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
|
| 144 |
+
transforms.ToTensor(),
|
| 145 |
+
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
|
| 146 |
+
transforms.RandomErasing(p=0.2),
|
| 147 |
+
])
|
| 148 |
+
|
| 149 |
+
eval_transform = transforms.Compose([
|
| 150 |
+
transforms.Resize((IMG_SIZE, IMG_SIZE)),
|
| 151 |
+
transforms.ToTensor(),
|
| 152 |
+
transforms.CenterCrop(224),
|
| 153 |
+
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
|
| 154 |
+
])
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class SubsetWithTransform(Dataset):
|
| 159 |
+
def __init__(self, base_dataset, indices, transform):
|
| 160 |
+
self.base_dataset = base_dataset
|
| 161 |
+
self.indices = indices
|
| 162 |
+
self.transform = transform
|
| 163 |
+
|
| 164 |
+
def __len__(self):
|
| 165 |
+
return len(self.indices)
|
| 166 |
+
|
| 167 |
+
def __getitem__(self, idx):
|
| 168 |
+
real_idx = self.indices[idx]
|
| 169 |
+
path, label = self.base_dataset.samples[real_idx]
|
| 170 |
+
image = self.base_dataset.loader(path)
|
| 171 |
+
image = self.transform(image)
|
| 172 |
+
return image, label
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
targets = np.array([label for _, label in full_dataset.samples])
|
| 176 |
+
indices = np.arange(len(full_dataset))
|
| 177 |
+
|
| 178 |
+
train_idx, temp_idx = train_test_split(
|
| 179 |
+
indices, test_size=(1 - TRAIN_RATIO), stratify=targets, random_state=SEED
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
val_ratio_of_temp = VAL_RATIO / (VAL_RATIO + TEST_RATIO)
|
| 183 |
+
val_idx, test_idx = train_test_split(
|
| 184 |
+
temp_idx, test_size=(1 - val_ratio_of_temp),
|
| 185 |
+
stratify=targets[temp_idx], random_state=SEED
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
train_ds = SubsetWithTransform(full_dataset, train_idx, train_transform)
|
| 189 |
+
val_ds = SubsetWithTransform(full_dataset, val_idx, eval_transform)
|
| 190 |
+
test_ds = SubsetWithTransform(full_dataset, test_idx, eval_transform)
|
| 191 |
+
|
| 192 |
+
train_loader = DataLoader(
|
| 193 |
+
train_ds,
|
| 194 |
+
batch_size=BATCH_SIZE,
|
| 195 |
+
shuffle=True,
|
| 196 |
+
num_workers=0,
|
| 197 |
+
pin_memory=True,
|
| 198 |
+
drop_last=True
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
val_loader = DataLoader(
|
| 202 |
+
val_ds,
|
| 203 |
+
batch_size=BATCH_SIZE,
|
| 204 |
+
shuffle=False,
|
| 205 |
+
num_workers=0,
|
| 206 |
+
pin_memory=True
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
test_loader = DataLoader(
|
| 210 |
+
test_ds,
|
| 211 |
+
batch_size=BATCH_SIZE,
|
| 212 |
+
shuffle=False,
|
| 213 |
+
num_workers=0,
|
| 214 |
+
pin_memory=True
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
print(f"Count classes: {NUM_CLASSES} -> {CLASS_NAMES}")
|
| 218 |
+
print(f"Count image of train: {len(train_ds)}")
|
| 219 |
+
print(f"Count image of Validation: {len(val_ds)}")
|
| 220 |
+
print(f"Test: {len(test_ds)}")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
fig, axes = plt.subplots(2, 3, figsize=(10, 7))
|
| 224 |
+
for ax in axes.flatten():
|
| 225 |
+
img, label = train_ds[np.random.randint(len(train_ds))]
|
| 226 |
+
img = img * torch.tensor(IMAGENET_STD).view(3,1,1) + torch.tensor(IMAGENET_MEAN).view(3,1,1)
|
| 227 |
+
img = img.clamp(0, 1).permute(1, 2, 0).numpy()
|
| 228 |
+
ax.imshow(img)
|
| 229 |
+
ax.set_title(CLASS_NAMES[label], fontsize=9)
|
| 230 |
+
ax.axis("off")
|
| 231 |
+
plt.tight_layout()
|
| 232 |
+
plt.show()
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class ResidualBlock(nn.Module):
|
| 236 |
+
def __init__(self, in_channels, out_channels, stride=1):
|
| 237 |
+
super().__init__()
|
| 238 |
+
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False)
|
| 239 |
+
self.bn1 = nn.BatchNorm2d(out_channels)
|
| 240 |
+
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False)
|
| 241 |
+
self.bn2 = nn.BatchNorm2d(out_channels)
|
| 242 |
+
self.relu = nn.ReLU(inplace=True)
|
| 243 |
+
|
| 244 |
+
self.shortcut = nn.Sequential()
|
| 245 |
+
if stride != 1 or in_channels != out_channels:
|
| 246 |
+
self.shortcut = nn.Sequential(
|
| 247 |
+
nn.Conv2d(in_channels, out_channels, 1, stride, bias=False),
|
| 248 |
+
nn.BatchNorm2d(out_channels),
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
def forward(self, x):
|
| 252 |
+
identity = self.shortcut(x)
|
| 253 |
+
out = self.relu(self.bn1(self.conv1(x)))
|
| 254 |
+
out = self.bn2(self.conv2(out))
|
| 255 |
+
out += identity # skip connection
|
| 256 |
+
return self.relu(out)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class ScratchCNN(nn.Module):
|
| 260 |
+
def __init__(self, num_classes, dropout=DROPOUT):
|
| 261 |
+
super().__init__()
|
| 262 |
+
self.stem = nn.Sequential(
|
| 263 |
+
nn.Conv2d(3, 64, 7, 2, 3, bias=False),
|
| 264 |
+
nn.BatchNorm2d(64),
|
| 265 |
+
nn.ReLU(inplace=True),
|
| 266 |
+
nn.MaxPool2d(3, 2, 1),
|
| 267 |
+
)
|
| 268 |
+
self.stage1 = self._make_stage(64, 64, 2, stride=1)
|
| 269 |
+
self.stage2 = self._make_stage(64, 128, 2, stride=2)
|
| 270 |
+
self.stage3 = self._make_stage(128, 256, 2, stride=2)
|
| 271 |
+
self.stage4 = self._make_stage(256, 512, 2, stride=2)
|
| 272 |
+
|
| 273 |
+
self.global_pool = nn.AdaptiveAvgPool2d(1)
|
| 274 |
+
self.dropout = nn.Dropout(dropout)
|
| 275 |
+
self.classifier = nn.Linear(512, num_classes)
|
| 276 |
+
|
| 277 |
+
def _make_stage(self, in_c, out_c, num_blocks, stride):
|
| 278 |
+
layers = [ResidualBlock(in_c, out_c, stride)]
|
| 279 |
+
for _ in range(num_blocks - 1):
|
| 280 |
+
layers.append(ResidualBlock(out_c, out_c, 1))
|
| 281 |
+
return nn.Sequential(*layers)
|
| 282 |
+
|
| 283 |
+
def forward(self, x):
|
| 284 |
+
x = self.stem(x)
|
| 285 |
+
x = self.stage1(x); x = self.stage2(x)
|
| 286 |
+
x = self.stage3(x); x = self.stage4(x)
|
| 287 |
+
x = self.global_pool(x)
|
| 288 |
+
x = torch.flatten(x, 1)
|
| 289 |
+
x = self.dropout(x)
|
| 290 |
+
return self.classifier(x)
|
| 291 |
+
|
| 292 |
+
_test_out = ScratchCNN(num_classes=NUM_CLASSES)(torch.randn(2, 3, IMG_SIZE, IMG_SIZE))
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def build_resnet50(num_classes, dropout=DROPOUT):
|
| 296 |
+
m = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
|
| 297 |
+
m.fc = nn.Sequential(nn.Dropout(dropout), nn.Linear(m.fc.in_features, num_classes))
|
| 298 |
+
return m
|
| 299 |
+
|
| 300 |
+
def build_efficientnet_b0(num_classes, dropout=DROPOUT):
|
| 301 |
+
m = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.IMAGENET1K_V1)
|
| 302 |
+
m.classifier = nn.Sequential(nn.Dropout(dropout), nn.Linear(m.classifier[1].in_features, num_classes))
|
| 303 |
+
return m
|
| 304 |
+
|
| 305 |
+
def build_densenet121(num_classes, dropout=DROPOUT):
|
| 306 |
+
m = models.densenet121(weights=models.DenseNet121_Weights.IMAGENET1K_V1)
|
| 307 |
+
m.classifier = nn.Sequential(nn.Dropout(dropout), nn.Linear(m.classifier.in_features, num_classes))
|
| 308 |
+
return m
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def freeze_backbone(model, model_name):
|
| 312 |
+
for p in model.parameters():
|
| 313 |
+
p.requires_grad = False
|
| 314 |
+
head = model.fc if model_name == "resnet50" else model.classifier
|
| 315 |
+
for p in head.parameters():
|
| 316 |
+
p.requires_grad = True
|
| 317 |
+
return model
|
| 318 |
+
|
| 319 |
+
def unfreeze_all(model):
|
| 320 |
+
for p in model.parameters():
|
| 321 |
+
p.requires_grad = True
|
| 322 |
+
return model
|
| 323 |
+
|
| 324 |
+
def count_parameters(model):
|
| 325 |
+
return sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class EarlyStopping:
|
| 329 |
+
def __init__(self, patience=EARLY_STOPPING_PATIENCE):
|
| 330 |
+
self.patience = patience
|
| 331 |
+
self.best_loss = float("inf")
|
| 332 |
+
self.counter = 0
|
| 333 |
+
self.best_state = None
|
| 334 |
+
self.should_stop = False
|
| 335 |
+
|
| 336 |
+
def step(self, val_loss, model):
|
| 337 |
+
if val_loss < self.best_loss:
|
| 338 |
+
self.best_loss = val_loss
|
| 339 |
+
self.counter = 0
|
| 340 |
+
self.best_state = copy.deepcopy(model.state_dict())
|
| 341 |
+
else:
|
| 342 |
+
self.counter += 1
|
| 343 |
+
if self.counter >= self.patience:
|
| 344 |
+
self.should_stop = True
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def run_one_epoch(model, loader, criterion, optimizer=None):
|
| 348 |
+
is_training = optimizer is not None
|
| 349 |
+
model.train() if is_training else model.eval()
|
| 350 |
+
|
| 351 |
+
total_loss, total_correct, total_samples = 0.0, 0, 0
|
| 352 |
+
torch.set_grad_enabled(is_training)
|
| 353 |
+
for images, labels in tqdm(loader, leave=False):
|
| 354 |
+
images, labels = images.to(DEVICE), labels.to(DEVICE)
|
| 355 |
+
if is_training:
|
| 356 |
+
optimizer.zero_grad()
|
| 357 |
+
outputs = model(images)
|
| 358 |
+
loss = criterion(outputs, labels)
|
| 359 |
+
if is_training:
|
| 360 |
+
loss.backward()
|
| 361 |
+
optimizer.step()
|
| 362 |
+
total_loss += loss.item() * images.size(0)
|
| 363 |
+
total_correct += (outputs.argmax(1) == labels).sum().item()
|
| 364 |
+
total_samples += images.size(0)
|
| 365 |
+
torch.set_grad_enabled(True)
|
| 366 |
+
|
| 367 |
+
return total_loss / total_samples, total_correct / total_samples
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def train_model(model, model_name, train_loader, val_loader, epochs, lr,
|
| 371 |
+
weight_decay=WEIGHT_DECAY, label_smoothing=LABEL_SMOOTHING):
|
| 372 |
+
model = model.to(DEVICE)
|
| 373 |
+
criterion = nn.CrossEntropyLoss(label_smoothing=label_smoothing)
|
| 374 |
+
optimizer = torch.optim.AdamW(
|
| 375 |
+
filter(lambda p: p.requires_grad, model.parameters()),
|
| 376 |
+
lr=lr, weight_decay=weight_decay,
|
| 377 |
+
)
|
| 378 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=2)
|
| 379 |
+
early_stopper = EarlyStopping()
|
| 380 |
+
|
| 381 |
+
history = {"train_loss": [], "train_acc": [], "val_loss": [], "val_acc": []}
|
| 382 |
+
|
| 383 |
+
for epoch in range(1, epochs + 1):
|
| 384 |
+
train_loss, train_acc = run_one_epoch(model, train_loader, criterion, optimizer)
|
| 385 |
+
val_loss, val_acc = run_one_epoch(model, val_loader, criterion, optimizer=None)
|
| 386 |
+
scheduler.step(val_loss)
|
| 387 |
+
|
| 388 |
+
history["train_loss"].append(train_loss)
|
| 389 |
+
history["train_acc"].append(train_acc)
|
| 390 |
+
history["val_loss"].append(val_loss)
|
| 391 |
+
history["val_acc"].append(val_acc)
|
| 392 |
+
|
| 393 |
+
print(f"[{model_name}] Epoch {epoch}/{epochs} | "
|
| 394 |
+
f"train_loss={train_loss:.4f} train_acc={train_acc:.4f} | "
|
| 395 |
+
f"val_loss={val_loss:.4f} val_acc={val_acc:.4f}")
|
| 396 |
+
|
| 397 |
+
early_stopper.step(val_loss, model)
|
| 398 |
+
if early_stopper.should_stop:
|
| 399 |
+
print(f"[{model_name}] Early stopping {epoch}")
|
| 400 |
+
break
|
| 401 |
+
|
| 402 |
+
model.load_state_dict(early_stopper.best_state)
|
| 403 |
+
return model, history
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
@torch.no_grad()
|
| 407 |
+
def evaluate_full(model, loader):
|
| 408 |
+
model.eval()
|
| 409 |
+
all_preds, all_labels = [], []
|
| 410 |
+
for images, labels in tqdm(loader, leave=False, desc="Evaluating"):
|
| 411 |
+
images = images.to(DEVICE)
|
| 412 |
+
preds = model(images).argmax(1).cpu().numpy()
|
| 413 |
+
all_preds.extend(preds)
|
| 414 |
+
all_labels.extend(labels.numpy())
|
| 415 |
+
|
| 416 |
+
acc = accuracy_score(all_labels, all_preds)
|
| 417 |
+
f1 = f1_score(all_labels, all_preds, average="macro")
|
| 418 |
+
return {"accuracy": acc, "f1_macro": f1, "predictions": all_preds, "labels": all_labels}
|
| 419 |
+
|
| 420 |
+
print("Done")
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def plot_history(history, model_name):
|
| 424 |
+
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
|
| 425 |
+
axes[0].plot(history["train_loss"], label="Train Loss")
|
| 426 |
+
axes[0].plot(history["val_loss"], label="Val Loss")
|
| 427 |
+
axes[0].set_title(f"{model_name} - Loss"); axes[0].set_xlabel("Epoch"); axes[0].legend()
|
| 428 |
+
|
| 429 |
+
axes[1].plot(history["train_acc"], label="Train Acc")
|
| 430 |
+
axes[1].plot(history["val_acc"], label="Val Acc")
|
| 431 |
+
axes[1].set_title(f"{model_name} - Accuracy"); axes[1].set_xlabel("Epoch"); axes[1].legend()
|
| 432 |
+
|
| 433 |
+
plt.tight_layout()
|
| 434 |
+
plt.savefig(os.path.join(OUTPUT_DIR, f"{model_name}_history.png"), dpi=150)
|
| 435 |
+
plt.show()
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def plot_confusion_matrix(labels, preds, class_names, model_name):
|
| 439 |
+
cm = confusion_matrix(labels, preds)
|
| 440 |
+
fig, ax = plt.subplots(figsize=(7, 6))
|
| 441 |
+
im = ax.imshow(cm, cmap="Blues")
|
| 442 |
+
ax.set_xticks(range(len(class_names))); ax.set_yticks(range(len(class_names)))
|
| 443 |
+
ax.set_xticklabels(class_names, rotation=45, ha="right")
|
| 444 |
+
ax.set_yticklabels(class_names)
|
| 445 |
+
ax.set_xlabel("Predicted"); ax.set_ylabel("True")
|
| 446 |
+
ax.set_title(f"Confusion Matrix - {model_name}")
|
| 447 |
+
for i in range(len(class_names)):
|
| 448 |
+
for j in range(len(class_names)):
|
| 449 |
+
ax.text(j, i, cm[i, j], ha="center", va="center",
|
| 450 |
+
color="white" if cm[i, j] > cm.max()/2 else "black")
|
| 451 |
+
plt.colorbar(im)
|
| 452 |
+
plt.tight_layout()
|
| 453 |
+
plt.savefig(os.path.join(OUTPUT_DIR, f"{model_name}_confusion_matrix.png"), dpi=150)
|
| 454 |
+
plt.show()
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
scratch_model = ScratchCNN(NUM_CLASSES)
|
| 458 |
+
print(f"{count_parameters(scratch_model):,}")
|
| 459 |
+
|
| 460 |
+
scratch_model, scratch_history = train_model(
|
| 461 |
+
scratch_model, "scratch_cnn", train_loader, val_loader,
|
| 462 |
+
epochs=EPOCHS, lr=LR_SCRATCH,
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
plot_history(scratch_history, "scratch_cnn")
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
resnet_model = build_resnet50(NUM_CLASSES)
|
| 470 |
+
resnet_model = freeze_backbone(resnet_model, "resnet50")
|
| 471 |
+
|
| 472 |
+
resnet_model, resnet_history_1 = train_model(
|
| 473 |
+
resnet_model, "resnet50", train_loader, val_loader,
|
| 474 |
+
epochs=FREEZE_EPOCHS, lr=LR_PRETRAINED_HEAD,
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
resnet_model = unfreeze_all(resnet_model)
|
| 479 |
+
resnet_model, resnet_history_2 = train_model(
|
| 480 |
+
resnet_model, "resnet50", train_loader, val_loader,
|
| 481 |
+
epochs=max(EPOCHS - FREEZE_EPOCHS, 5), lr=LR_PRETRAINED_FINETUNE,
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
resnet_history = {k: resnet_history_1[k] + resnet_history_2[k] for k in resnet_history_1}
|
| 485 |
+
plot_history(resnet_history, "resnet50")
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
effnet_model = build_efficientnet_b0(NUM_CLASSES)
|
| 489 |
+
effnet_model = freeze_backbone(effnet_model, "efficientnet_b0")
|
| 490 |
+
|
| 491 |
+
effnet_model, effnet_history_1 = train_model(
|
| 492 |
+
effnet_model, "efficientnet_b0", train_loader, val_loader,
|
| 493 |
+
epochs=FREEZE_EPOCHS, lr=LR_PRETRAINED_HEAD,
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
effnet_model = unfreeze_all(effnet_model)
|
| 498 |
+
effnet_model, effnet_history_2 = train_model(
|
| 499 |
+
effnet_model, "efficientnet_b0", train_loader, val_loader,
|
| 500 |
+
epochs=max(EPOCHS - FREEZE_EPOCHS, 5), lr=LR_PRETRAINED_FINETUNE,
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
effnet_history = {k: effnet_history_1[k] + effnet_history_2[k] for k in effnet_history_1}
|
| 504 |
+
plot_history(effnet_history, "efficientnet_b0")
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
densenet_model = build_densenet121(NUM_CLASSES)
|
| 508 |
+
densenet_model = freeze_backbone(densenet_model, "densenet121")
|
| 509 |
+
|
| 510 |
+
densenet_model, densenet_history_1 = train_model(
|
| 511 |
+
densenet_model, "densenet121", train_loader, val_loader,
|
| 512 |
+
epochs=FREEZE_EPOCHS, lr=LR_PRETRAINED_HEAD,
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
densenet_model = unfreeze_all(densenet_model)
|
| 517 |
+
densenet_model, densenet_history_2 = train_model(
|
| 518 |
+
densenet_model, "densenet121", train_loader, val_loader,
|
| 519 |
+
epochs=max(EPOCHS - FREEZE_EPOCHS, 5), lr=LR_PRETRAINED_FINETUNE,
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
densenet_history = {k: densenet_history_1[k] + densenet_history_2[k] for k in densenet_history_1}
|
| 523 |
+
plot_history(densenet_history, "densenet121")
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
all_models = {
|
| 527 |
+
"scratch_cnn": scratch_model,
|
| 528 |
+
"resnet50": resnet_model,
|
| 529 |
+
"efficientnet_b0": effnet_model,
|
| 530 |
+
"densenet121": densenet_model,
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
results = []
|
| 534 |
+
best_model_name, best_f1 = None, -1.0
|
| 535 |
+
|
| 536 |
+
for name, model in all_models.items():
|
| 537 |
+
print(f"Evaluate {name} on test set...")
|
| 538 |
+
eval_result = evaluate_full(model, test_loader)
|
| 539 |
+
|
| 540 |
+
ckpt_path = os.path.join(CHECKPOINT_DIR, f"{name}.pth")
|
| 541 |
+
torch.save(model.state_dict(), ckpt_path)
|
| 542 |
+
print(f"The save weight {name} in: {ckpt_path}")
|
| 543 |
+
|
| 544 |
+
plot_confusion_matrix(eval_result["labels"], eval_result["predictions"], CLASS_NAMES, name)
|
| 545 |
+
|
| 546 |
+
results.append({
|
| 547 |
+
"model": name,
|
| 548 |
+
"trainable_params": count_parameters(model),
|
| 549 |
+
"test_accuracy": round(eval_result["accuracy"], 4),
|
| 550 |
+
"test_f1": round(eval_result["f1_macro"], 4),
|
| 551 |
+
})
|
| 552 |
+
|
| 553 |
+
if eval_result["f1_macro"] > best_f1:
|
| 554 |
+
best_f1 = eval_result["f1_macro"]
|
| 555 |
+
best_model_name = name
|
| 556 |
+
|
| 557 |
+
comparison_df = pd.DataFrame(results).sort_values("test_f1", ascending=False).reset_index(drop=True)
|
| 558 |
+
comparison_df.to_csv(os.path.join(OUTPUT_DIR, "models_comparison.csv"), index=False)
|
| 559 |
+
comparison_df
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
for name, model in all_models.items():
|
| 563 |
+
print(f"Evaluate {name} on test set...")
|
| 564 |
+
|
| 565 |
+
eval_result = evaluate_full(model, test_loader)
|
| 566 |
+
|
| 567 |
+
print(f"Classification Report for {name}")
|
| 568 |
+
print(
|
| 569 |
+
classification_report(
|
| 570 |
+
eval_result["labels"],
|
| 571 |
+
eval_result["predictions"],
|
| 572 |
+
target_names=CLASS_NAMES,
|
| 573 |
+
digits=4
|
| 574 |
+
)
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
ckpt_path = os.path.join(CHECKPOINT_DIR, f"{name}.pth")
|
| 578 |
+
torch.save(model.state_dict(), ckpt_path)
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
best_model = all_models[best_model_name]
|
| 582 |
+
best_path = os.path.join(CHECKPOINT_DIR, "best_model.pth")
|
| 583 |
+
|
| 584 |
+
torch.save({
|
| 585 |
+
"model_name": best_model_name,
|
| 586 |
+
"state_dict": best_model.state_dict(),
|
| 587 |
+
"class_names": CLASS_NAMES,
|
| 588 |
+
"test_f1": best_f1,
|
| 589 |
+
}, best_path)
|
| 590 |
+
|
| 591 |
+
print(f"The Best Model: {best_model_name} (F1 = {best_f1:.4f})")
|
| 592 |
+
print(f"Save: {best_path}")
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
IMAGE_PATH = '/kaggle/working/oral_dataset/Hypodontia/(100).JPG'
|
| 596 |
+
|
| 597 |
+
checkpoint = torch.load(os.path.join(CHECKPOINT_DIR, "best_model.pth"), map_location=DEVICE)
|
| 598 |
+
loaded_name = checkpoint["model_name"]
|
| 599 |
+
loaded_classes = checkpoint["class_names"]
|
| 600 |
+
|
| 601 |
+
builders = {
|
| 602 |
+
"scratch_cnn": lambda: ScratchCNN(len(loaded_classes)),
|
| 603 |
+
"resnet50": lambda: build_resnet50(len(loaded_classes)),
|
| 604 |
+
"efficientnet_b0": lambda: build_efficientnet_b0(len(loaded_classes)),
|
| 605 |
+
"densenet121": lambda: build_densenet121(len(loaded_classes)),
|
| 606 |
+
}
|
| 607 |
+
inference_model = builders[loaded_name]()
|
| 608 |
+
inference_model.load_state_dict(checkpoint["state_dict"])
|
| 609 |
+
inference_model.to(DEVICE).eval()
|
| 610 |
+
print(f"Download the best model: {loaded_name} (test F1 = {checkpoint['test_f1']:.4f})")
|
| 611 |
+
|
| 612 |
+
with torch.no_grad():
|
| 613 |
+
image = Image.open(IMAGE_PATH).convert("RGB")
|
| 614 |
+
tensor = eval_transform(image).unsqueeze(0).to(DEVICE)
|
| 615 |
+
probs = torch.softmax(inference_model(tensor), dim=1)[0]
|
| 616 |
+
pred_idx = probs.argmax().item()
|
| 617 |
+
|
| 618 |
+
plt.imshow(image); plt.axis("off")
|
| 619 |
+
plt.title(f"Classifier: {loaded_classes[pred_idx]} ({probs[pred_idx]*100:.1f}%)")
|
| 620 |
+
plt.show()
|
| 621 |
+
|
| 622 |
+
for cname, p in sorted(zip(loaded_classes, probs.tolist()), key=lambda x: -x[1]):
|
| 623 |
+
print(f" {cname:25s}: {p*100:5.2f}%")
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
IMAGE_PATH = '/kaggle/working/oral_dataset/Caries/caries_0_1001.jpeg'
|
| 627 |
+
|
| 628 |
+
checkpoint = torch.load(os.path.join(CHECKPOINT_DIR, "best_model.pth"), map_location=DEVICE)
|
| 629 |
+
loaded_name = checkpoint["model_name"]
|
| 630 |
+
loaded_classes = checkpoint["class_names"]
|
| 631 |
+
|
| 632 |
+
builders = {
|
| 633 |
+
"scratch_cnn": lambda: ScratchCNN(len(loaded_classes)),
|
| 634 |
+
"resnet50": lambda: build_resnet50(len(loaded_classes)),
|
| 635 |
+
"efficientnet_b0": lambda: build_efficientnet_b0(len(loaded_classes)),
|
| 636 |
+
"densenet121": lambda: build_densenet121(len(loaded_classes)),
|
| 637 |
+
}
|
| 638 |
+
inference_model = builders[loaded_name]()
|
| 639 |
+
inference_model.load_state_dict(checkpoint["state_dict"])
|
| 640 |
+
inference_model.to(DEVICE).eval()
|
| 641 |
+
print(f"Download the best model: {loaded_name} (test F1 = {checkpoint['test_f1']:.4f})")
|
| 642 |
+
|
| 643 |
+
with torch.no_grad():
|
| 644 |
+
image = Image.open(IMAGE_PATH).convert("RGB")
|
| 645 |
+
tensor = eval_transform(image).unsqueeze(0).to(DEVICE)
|
| 646 |
+
probs = torch.softmax(inference_model(tensor), dim=1)[0]
|
| 647 |
+
pred_idx = probs.argmax().item()
|
| 648 |
+
|
| 649 |
+
plt.imshow(image); plt.axis("off")
|
| 650 |
+
plt.title(f"Classifier: {loaded_classes[pred_idx]} ({probs[pred_idx]*100:.1f}%)")
|
| 651 |
+
plt.show()
|
| 652 |
+
|
| 653 |
+
for cname, p in sorted(zip(loaded_classes, probs.tolist()), key=lambda x: -x[1]):
|
| 654 |
+
print(f" {cname:25s}: {p*100:5.2f}%")
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
import shutil
|
| 658 |
+
|
| 659 |
+
shutil.make_archive(
|
| 660 |
+
"/kaggle/working/output_files",
|
| 661 |
+
"zip",
|
| 662 |
+
"/kaggle/working/"
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
from IPython.display import FileLink
|
| 667 |
+
|
| 668 |
+
FileLink('/kaggle/working/output_files.zip')
|
Gradio.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
from PIL import Image
|
| 7 |
+
import torchvision.transforms as transforms
|
| 8 |
+
from torchvision.models import resnet50
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ============================================================
|
| 12 |
+
# CONFIG
|
| 13 |
+
# ============================================================
|
| 14 |
+
MODEL_PATH = r"C:\Users\LOQ\Desktop\Oral Diseases Image Classification\checkpoints\best_model.pth"
|
| 15 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ============================================================
|
| 19 |
+
# LOAD MODEL
|
| 20 |
+
# ============================================================
|
| 21 |
+
checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)
|
| 22 |
+
|
| 23 |
+
CLASS_NAMES = checkpoint["class_names"]
|
| 24 |
+
TEST_F1 = checkpoint["test_f1"]
|
| 25 |
+
|
| 26 |
+
model = resnet50(weights=None)
|
| 27 |
+
model.fc = nn.Sequential(
|
| 28 |
+
nn.Dropout(0.3),
|
| 29 |
+
nn.Linear(model.fc.in_features, len(CLASS_NAMES))
|
| 30 |
+
)
|
| 31 |
+
model.load_state_dict(checkpoint["state_dict"])
|
| 32 |
+
model.to(DEVICE)
|
| 33 |
+
model.eval()
|
| 34 |
+
|
| 35 |
+
eval_transform = transforms.Compose([
|
| 36 |
+
transforms.Resize((224, 224)),
|
| 37 |
+
transforms.ToTensor(),
|
| 38 |
+
transforms.Normalize(
|
| 39 |
+
mean=[0.485, 0.456, 0.406],
|
| 40 |
+
std=[0.229, 0.224, 0.225]
|
| 41 |
+
)
|
| 42 |
+
])
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ============================================================
|
| 46 |
+
# INFERENCE
|
| 47 |
+
# ============================================================
|
| 48 |
+
def predict(image):
|
| 49 |
+
if image is None:
|
| 50 |
+
return (
|
| 51 |
+
gr.update(value="—", visible=True),
|
| 52 |
+
"—",
|
| 53 |
+
{},
|
| 54 |
+
gr.update(visible=False),
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
image = image.convert("RGB")
|
| 58 |
+
tensor = eval_transform(image).unsqueeze(0).to(DEVICE)
|
| 59 |
+
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
output = model(tensor)
|
| 62 |
+
probs = F.softmax(output, dim=1)[0]
|
| 63 |
+
|
| 64 |
+
index = torch.argmax(probs).item()
|
| 65 |
+
prediction = CLASS_NAMES[index]
|
| 66 |
+
confidence = probs[index].item() * 100
|
| 67 |
+
|
| 68 |
+
results = {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))}
|
| 69 |
+
|
| 70 |
+
# Build a small verdict badge depending on confidence level
|
| 71 |
+
if confidence >= 85:
|
| 72 |
+
badge = f'<div class="verdict verdict-high">✓ High Confidence — {confidence:.1f}%</div>'
|
| 73 |
+
elif confidence >= 60:
|
| 74 |
+
badge = f'<div class="verdict verdict-mid">! Moderate Confidence — {confidence:.1f}%</div>'
|
| 75 |
+
else:
|
| 76 |
+
badge = f'<div class="verdict verdict-low">? Low Confidence — {confidence:.1f}%</div>'
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
prediction,
|
| 80 |
+
f"{confidence:.2f}%",
|
| 81 |
+
results,
|
| 82 |
+
gr.update(value=badge, visible=True),
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def clear_all():
|
| 87 |
+
return None, "—", "—", {}, gr.update(visible=False)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ============================================================
|
| 91 |
+
# STYLING
|
| 92 |
+
# ============================================================
|
| 93 |
+
CUSTOM_CSS = """
|
| 94 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500&display=swap');
|
| 95 |
+
|
| 96 |
+
:root {
|
| 97 |
+
--bg-primary: #0a0e1a;
|
| 98 |
+
--bg-secondary: #10162a;
|
| 99 |
+
--bg-card: #131a30;
|
| 100 |
+
--border-subtle: #232b45;
|
| 101 |
+
--accent: #14b8a6;
|
| 102 |
+
--accent-soft: #14b8a622;
|
| 103 |
+
--accent-2: #6366f1;
|
| 104 |
+
--text-primary: #e8ecf5;
|
| 105 |
+
--text-secondary: #8892b0;
|
| 106 |
+
--text-muted: #5b6485;
|
| 107 |
+
--radius: 14px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
* { font-family: 'Inter', sans-serif !important; }
|
| 111 |
+
|
| 112 |
+
.gradio-container {
|
| 113 |
+
background: radial-gradient(circle at 10% 0%, #101a33 0%, #080b14 55%, #05070d 100%) !important;
|
| 114 |
+
max-width: 1180px !important;
|
| 115 |
+
margin: 0 auto !important;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
footer { display: none !important; }
|
| 119 |
+
|
| 120 |
+
/* ---------- HEADER ---------- */
|
| 121 |
+
.app-header {
|
| 122 |
+
padding: 30px 8px 22px 8px;
|
| 123 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 124 |
+
margin-bottom: 26px;
|
| 125 |
+
display: flex;
|
| 126 |
+
align-items: center;
|
| 127 |
+
justify-content: space-between;
|
| 128 |
+
}
|
| 129 |
+
.app-header .brand {
|
| 130 |
+
display: flex;
|
| 131 |
+
align-items: center;
|
| 132 |
+
gap: 14px;
|
| 133 |
+
}
|
| 134 |
+
.app-header .logo-badge {
|
| 135 |
+
width: 46px;
|
| 136 |
+
height: 46px;
|
| 137 |
+
border-radius: 12px;
|
| 138 |
+
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
| 139 |
+
display: flex;
|
| 140 |
+
align-items: center;
|
| 141 |
+
justify-content: center;
|
| 142 |
+
font-size: 22px;
|
| 143 |
+
box-shadow: 0 8px 24px -6px #14b8a655;
|
| 144 |
+
flex-shrink: 0;
|
| 145 |
+
}
|
| 146 |
+
.app-header h1 {
|
| 147 |
+
font-size: 20px;
|
| 148 |
+
font-weight: 700;
|
| 149 |
+
color: var(--text-primary);
|
| 150 |
+
margin: 0;
|
| 151 |
+
letter-spacing: -0.02em;
|
| 152 |
+
}
|
| 153 |
+
.app-header p {
|
| 154 |
+
font-size: 13px;
|
| 155 |
+
color: var(--text-muted);
|
| 156 |
+
margin: 2px 0 0 0;
|
| 157 |
+
}
|
| 158 |
+
.app-header .tag {
|
| 159 |
+
font-size: 11px;
|
| 160 |
+
font-weight: 600;
|
| 161 |
+
color: var(--accent);
|
| 162 |
+
background: var(--accent-soft);
|
| 163 |
+
border: 1px solid #14b8a640;
|
| 164 |
+
padding: 6px 14px;
|
| 165 |
+
border-radius: 999px;
|
| 166 |
+
letter-spacing: 0.03em;
|
| 167 |
+
text-transform: uppercase;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* ---------- CARDS ---------- */
|
| 171 |
+
.card {
|
| 172 |
+
background: var(--bg-card) !important;
|
| 173 |
+
border: 1px solid var(--border-subtle) !important;
|
| 174 |
+
border-radius: var(--radius) !important;
|
| 175 |
+
padding: 18px !important;
|
| 176 |
+
}
|
| 177 |
+
.card-title {
|
| 178 |
+
font-size: 13px;
|
| 179 |
+
font-weight: 600;
|
| 180 |
+
color: var(--text-secondary);
|
| 181 |
+
text-transform: uppercase;
|
| 182 |
+
letter-spacing: 0.04em;
|
| 183 |
+
margin-bottom: 12px;
|
| 184 |
+
display: flex;
|
| 185 |
+
align-items: center;
|
| 186 |
+
gap: 8px;
|
| 187 |
+
}
|
| 188 |
+
.card-title::before {
|
| 189 |
+
content: "";
|
| 190 |
+
width: 4px;
|
| 191 |
+
height: 14px;
|
| 192 |
+
background: var(--accent);
|
| 193 |
+
border-radius: 2px;
|
| 194 |
+
display: inline-block;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/* ---------- UPLOAD ZONE ---------- */
|
| 198 |
+
.upload-zone, .upload-zone > div {
|
| 199 |
+
background: var(--bg-card) !important;
|
| 200 |
+
border: 1.5px dashed #2b3454 !important;
|
| 201 |
+
border-radius: var(--radius) !important;
|
| 202 |
+
}
|
| 203 |
+
.upload-zone:hover {
|
| 204 |
+
border-color: var(--accent) !important;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* ---------- BUTTONS ---------- */
|
| 208 |
+
#analyze-btn {
|
| 209 |
+
background: linear-gradient(135deg, #14b8a6, #0d9488) !important;
|
| 210 |
+
color: #05170f !important;
|
| 211 |
+
font-weight: 700 !important;
|
| 212 |
+
border: none !important;
|
| 213 |
+
border-radius: 10px !important;
|
| 214 |
+
box-shadow: 0 10px 24px -8px #14b8a670 !important;
|
| 215 |
+
letter-spacing: 0.01em;
|
| 216 |
+
transition: transform .15s ease, box-shadow .15s ease;
|
| 217 |
+
}
|
| 218 |
+
#analyze-btn:hover {
|
| 219 |
+
transform: translateY(-1px);
|
| 220 |
+
box-shadow: 0 14px 28px -8px #14b8a690 !important;
|
| 221 |
+
}
|
| 222 |
+
#clear-btn {
|
| 223 |
+
background: transparent !important;
|
| 224 |
+
color: var(--text-secondary) !important;
|
| 225 |
+
border: 1px solid var(--border-subtle) !important;
|
| 226 |
+
border-radius: 10px !important;
|
| 227 |
+
}
|
| 228 |
+
#clear-btn:hover {
|
| 229 |
+
border-color: #3a4468 !important;
|
| 230 |
+
color: var(--text-primary) !important;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* ---------- RESULT FIELDS ---------- */
|
| 234 |
+
#pred-box textarea, #conf-box textarea {
|
| 235 |
+
background: #0d1326 !important;
|
| 236 |
+
border: 1px solid var(--border-subtle) !important;
|
| 237 |
+
color: var(--text-primary) !important;
|
| 238 |
+
font-weight: 700 !important;
|
| 239 |
+
font-size: 17px !important;
|
| 240 |
+
border-radius: 10px !important;
|
| 241 |
+
}
|
| 242 |
+
#conf-box textarea {
|
| 243 |
+
color: var(--accent) !important;
|
| 244 |
+
font-family: 'JetBrains Mono', monospace !important;
|
| 245 |
+
}
|
| 246 |
+
label span {
|
| 247 |
+
color: var(--text-muted) !important;
|
| 248 |
+
font-size: 11.5px !important;
|
| 249 |
+
text-transform: uppercase;
|
| 250 |
+
letter-spacing: 0.05em;
|
| 251 |
+
font-weight: 600 !important;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* ---------- VERDICT BADGE ---------- */
|
| 255 |
+
.verdict {
|
| 256 |
+
padding: 10px 16px;
|
| 257 |
+
border-radius: 10px;
|
| 258 |
+
font-size: 13px;
|
| 259 |
+
font-weight: 600;
|
| 260 |
+
text-align: center;
|
| 261 |
+
margin-bottom: 14px;
|
| 262 |
+
border: 1px solid transparent;
|
| 263 |
+
}
|
| 264 |
+
.verdict-high { background: #14b8a61a; color: #2dd4bf; border-color: #14b8a640; }
|
| 265 |
+
.verdict-mid { background: #f59e0b1a; color: #fbbf24; border-color: #f59e0b40; }
|
| 266 |
+
.verdict-low { background: #ef44441a; color: #f87171; border-color: #ef444440; }
|
| 267 |
+
|
| 268 |
+
/* ---------- PROBABILITY BARS (gr.Label) ---------- */
|
| 269 |
+
.label-wrap {
|
| 270 |
+
background: transparent !important;
|
| 271 |
+
border: none !important;
|
| 272 |
+
}
|
| 273 |
+
#prob-label .container {
|
| 274 |
+
background: transparent !important;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/* ---------- FOOTER ---------- */
|
| 278 |
+
.app-footer {
|
| 279 |
+
margin-top: 30px;
|
| 280 |
+
padding: 18px 4px 10px 4px;
|
| 281 |
+
border-top: 1px solid var(--border-subtle);
|
| 282 |
+
display: flex;
|
| 283 |
+
justify-content: space-between;
|
| 284 |
+
align-items: center;
|
| 285 |
+
flex-wrap: wrap;
|
| 286 |
+
gap: 10px;
|
| 287 |
+
}
|
| 288 |
+
.app-footer .meta {
|
| 289 |
+
font-size: 12px;
|
| 290 |
+
color: var(--text-muted);
|
| 291 |
+
font-family: 'JetBrains Mono', monospace;
|
| 292 |
+
}
|
| 293 |
+
.app-footer .meta b { color: var(--text-secondary); }
|
| 294 |
+
.app-footer .credit {
|
| 295 |
+
font-size: 12px;
|
| 296 |
+
color: var(--text-muted);
|
| 297 |
+
}
|
| 298 |
+
.app-footer .credit b { color: var(--text-secondary); }
|
| 299 |
+
|
| 300 |
+
.disclaimer {
|
| 301 |
+
font-size: 11.5px;
|
| 302 |
+
color: var(--text-muted);
|
| 303 |
+
background: #0d132666;
|
| 304 |
+
border: 1px solid var(--border-subtle);
|
| 305 |
+
border-radius: 10px;
|
| 306 |
+
padding: 10px 14px;
|
| 307 |
+
margin-top: 16px;
|
| 308 |
+
line-height: 1.6;
|
| 309 |
+
}
|
| 310 |
+
"""
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
# ============================================================
|
| 314 |
+
# UI
|
| 315 |
+
# ============================================================
|
| 316 |
+
with gr.Blocks(
|
| 317 |
+
theme=gr.themes.Soft(primary_hue="teal", secondary_hue="slate"),
|
| 318 |
+
css=CUSTOM_CSS,
|
| 319 |
+
title="Oral Disease Classifier"
|
| 320 |
+
) as demo:
|
| 321 |
+
|
| 322 |
+
gr.HTML(
|
| 323 |
+
f"""
|
| 324 |
+
<div class="app-header">
|
| 325 |
+
<div class="brand">
|
| 326 |
+
<div class="logo-badge">🦷</div>
|
| 327 |
+
<div>
|
| 328 |
+
<h1>Oral Disease Classification</h1>
|
| 329 |
+
<p>Computer-vision assisted screening · ResNet50 backbone</p>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
<div class="tag">Model F1 · {TEST_F1:.3f}</div>
|
| 333 |
+
</div>
|
| 334 |
+
"""
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
with gr.Row(equal_height=True):
|
| 338 |
+
|
| 339 |
+
with gr.Column(scale=5):
|
| 340 |
+
gr.HTML('<div class="card-title">Input Image</div>')
|
| 341 |
+
image_input = gr.Image(
|
| 342 |
+
type="pil",
|
| 343 |
+
label="",
|
| 344 |
+
show_label=False,
|
| 345 |
+
elem_classes="upload-zone",
|
| 346 |
+
height=340,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
with gr.Row():
|
| 350 |
+
clear_btn = gr.Button("Clear", elem_id="clear-btn")
|
| 351 |
+
button = gr.Button("Analyze Image", elem_id="analyze-btn")
|
| 352 |
+
|
| 353 |
+
gr.HTML(
|
| 354 |
+
"""
|
| 355 |
+
<div class="disclaimer">
|
| 356 |
+
⚠ Decision-support tool only. Predictions are generated by an automated
|
| 357 |
+
model and are not a substitute for professional clinical diagnosis.
|
| 358 |
+
</div>
|
| 359 |
+
"""
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
with gr.Column(scale=5):
|
| 363 |
+
gr.HTML('<div class="card-title">Analysis Result</div>')
|
| 364 |
+
|
| 365 |
+
verdict_html = gr.HTML(visible=False)
|
| 366 |
+
|
| 367 |
+
with gr.Row():
|
| 368 |
+
prediction = gr.Textbox(label="Predicted Class", elem_id="pred-box", interactive=False)
|
| 369 |
+
confidence = gr.Textbox(label="Confidence", elem_id="conf-box", interactive=False)
|
| 370 |
+
|
| 371 |
+
gr.HTML('<div class="card-title" style="margin-top:6px;">Class Probability Distribution</div>')
|
| 372 |
+
probabilities = gr.Label(
|
| 373 |
+
label="",
|
| 374 |
+
show_label=False,
|
| 375 |
+
elem_id="prob-label",
|
| 376 |
+
num_top_classes=len(CLASS_NAMES),
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
gr.HTML(
|
| 380 |
+
f"""
|
| 381 |
+
<div class="app-footer">
|
| 382 |
+
<div class="meta">ARCH · <b>ResNet50</b> | DEVICE · <b>{DEVICE.upper()}</b> | CLASSES · <b>{len(CLASS_NAMES)}</b></div>
|
| 383 |
+
<div class="credit">Built by <b>Nasr Mohamed</b> — AI Engineer · © 2026</div>
|
| 384 |
+
</div>
|
| 385 |
+
"""
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
button.click(
|
| 389 |
+
predict,
|
| 390 |
+
inputs=image_input,
|
| 391 |
+
outputs=[prediction, confidence, probabilities, verdict_html],
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
clear_btn.click(
|
| 395 |
+
clear_all,
|
| 396 |
+
inputs=None,
|
| 397 |
+
outputs=[image_input, prediction, confidence, probabilities, verdict_html],
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
if __name__ == "__main__":
|
| 402 |
+
demo.launch()
|
checkpoints/best_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:40bd3f0d2f1481704b49946ab76231914f86f4de58cb1fb4b82041a3cd8562f7
|
| 3 |
+
size 94400529
|
checkpoints/densenet121.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eca85db7d09f70ea5332adf1007c9c69e34dfc0fb78a6e56df4740946223cdde
|
| 3 |
+
size 28448005
|
checkpoints/efficientnet_b0.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f054f46138686a3cf36f24dc0721d95d822b5915a3b32b16eadc5a0017896a78
|
| 3 |
+
size 16362271
|
checkpoints/resnet50.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1b4b0e828a4b563e4c8380080fab9200ee55733eb9f8baf1883b514f59488c39
|
| 3 |
+
size 94399685
|
checkpoints/scratch_cnn.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d35823b73231da512a2d0ca6289ef31f947a2bc6821ee32102fd25624e8f322
|
| 3 |
+
size 44798347
|
notebooks/oral-disseases-image-classification.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
outputs/densenet121_confusion_matrix.png
ADDED
|
outputs/densenet121_history.png
ADDED
|
outputs/efficientnet_b0_confusion_matrix.png
ADDED
|
outputs/efficientnet_b0_history.png
ADDED
|
outputs/models_comparison.csv
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model,trainable_params,test_accuracy,test_f1
|
| 2 |
+
resnet50,23520326,0.9477,0.9411
|
| 3 |
+
densenet121,6960006,0.9451,0.9351
|
| 4 |
+
efficientnet_b0,4015234,0.9417,0.9335
|
| 5 |
+
scratch_cnn,11179590,0.8345,0.8236
|
outputs/resnet50_confusion_matrix.png
ADDED
|
outputs/resnet50_history.png
ADDED
|
outputs/scratch_cnn_confusion_matrix.png
ADDED
|
outputs/scratch_cnn_history.png
ADDED
|