nsr51324 commited on
Commit
923e126
·
verified ·
1 Parent(s): ba1e1d2

Upload folder using huggingface_hub

Browse files
.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> &nbsp;|&nbsp; DEVICE · <b>{DEVICE.upper()}</b> &nbsp;|&nbsp; CLASSES · <b>{len(CLASS_NAMES)}</b></div>
383
+ <div class="credit">Built by <b>Nasr Mohamed</b> — AI Engineer &nbsp;·&nbsp; © 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