# This Python 3 environment comes with many helpful analytics libraries installed # It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python # For example, here's several helpful packages to load import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv) # Input data files are available in the read-only "../input/" directory # For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory import os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) # 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" # You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session # Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session # Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md import kagglehub # kagglehub.dataset_download('/') get_ipython().getoutput("pip install git+https://github.com/jacobgil/pytorch-grad-cam.git -q") import matplotlib.pyplot as plt import seaborn as sns from PIL import Image import copy import os import shutil import random from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, classification_report, confusion_matrix , f1_score import torch import torch.nn as nn import torch.optim as optim import torch.cuda.amp as amp import torchvision.models as models import torchvision.transforms as transforms import torchvision.datasets as datasets from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader ,Dataset from torch.optim.lr_scheduler import ReduceLROnPlateau from tqdm import tqdm from tqdm.auto import tqdm from torchvision.models import efficientnet_b3 from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import warnings warnings.filterwarnings("ignore") original_dirs = { 'Calculus': '/kaggle/input/datasets/salmansajid05/oral-diseases/Calculus/Calculus', 'Caries': '/kaggle/input/datasets/salmansajid05/oral-diseases/Data caries/Data caries/caries augmented data set/preview', 'Gingivitis': '/kaggle/input/datasets/salmansajid05/oral-diseases/Gingivitis/Gingivitis', 'Ulcers': '/kaggle/input/datasets/salmansajid05/oral-diseases/Mouth Ulcer/Mouth Ulcer/Mouth_Ulcer_augmented_DataSet/preview', 'Tooth Discoloration': '/kaggle/input/datasets/salmansajid05/oral-diseases/Tooth Discoloration/Tooth Discoloration /Tooth_discoloration_augmented_dataser/preview', 'Hypodontia': '/kaggle/input/datasets/salmansajid05/oral-diseases/hypodontia/hypodontia' } DATA_DIR = "/kaggle/working/oral_dataset" splits = ['train', 'val', 'test'] classes = list(original_dirs.keys()) classes import os import shutil NEW_DATASET = "/kaggle/working/oral_dataset" os.makedirs(NEW_DATASET, exist_ok=True) for class_name, source_dir in original_dirs.items(): target_dir = os.path.join(NEW_DATASET, class_name) os.makedirs(target_dir, exist_ok=True) for file in os.listdir(source_dir): if file.lower().endswith((".jpg", ".jpeg", ".png")): shutil.copy( os.path.join(source_dir, file), os.path.join(target_dir, file) ) print("Done") full_dataset = datasets.ImageFolder(root=DATA_DIR) CLASS_NAMES = full_dataset.classes NUM_CLASSES = len(CLASS_NAMES) DATA_DIR = original_dirs CHECKPOINT_DIR = "./checkpoints" OUTPUT_DIR = "./outputs" os.makedirs(CHECKPOINT_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True) DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") IMG_SIZE = 224 BATCH_SIZE = 32 NUM_WORKERS = 4 TRAIN_RATIO, VAL_RATIO, TEST_RATIO = 0.8, 0.1, 0.1 SEED = 42 EPOCHS = 30 LR_SCRATCH = 1e-3 LR_PRETRAINED_HEAD = 1e-3 LR_PRETRAINED_FINETUNE = 1e-5 WEIGHT_DECAY = 1e-4 LABEL_SMOOTHING = 0.1 DROPOUT = 0.4 EARLY_STOPPING_PATIENCE = 2 FREEZE_EPOCHS = 5 torch.manual_seed(SEED) print("Done") IMAGENET_MEAN = [0.485, 0.456, 0.406] IMAGENET_STD = [0.229, 0.224, 0.225] train_transform = transforms.Compose([ transforms.Resize((IMG_SIZE + 20, IMG_SIZE + 20)), transforms.RandomResizedCrop(IMG_SIZE, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), transforms.RandomErasing(p=0.2), ]) eval_transform = transforms.Compose([ transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.CenterCrop(224), transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), ]) class SubsetWithTransform(Dataset): def __init__(self, base_dataset, indices, transform): self.base_dataset = base_dataset self.indices = indices self.transform = transform def __len__(self): return len(self.indices) def __getitem__(self, idx): real_idx = self.indices[idx] path, label = self.base_dataset.samples[real_idx] image = self.base_dataset.loader(path) image = self.transform(image) return image, label targets = np.array([label for _, label in full_dataset.samples]) indices = np.arange(len(full_dataset)) train_idx, temp_idx = train_test_split( indices, test_size=(1 - TRAIN_RATIO), stratify=targets, random_state=SEED ) val_ratio_of_temp = VAL_RATIO / (VAL_RATIO + TEST_RATIO) val_idx, test_idx = train_test_split( temp_idx, test_size=(1 - val_ratio_of_temp), stratify=targets[temp_idx], random_state=SEED ) train_ds = SubsetWithTransform(full_dataset, train_idx, train_transform) val_ds = SubsetWithTransform(full_dataset, val_idx, eval_transform) test_ds = SubsetWithTransform(full_dataset, test_idx, eval_transform) train_loader = DataLoader( train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0, pin_memory=True, drop_last=True ) val_loader = DataLoader( val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=True ) test_loader = DataLoader( test_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=True ) print(f"Count classes: {NUM_CLASSES} -> {CLASS_NAMES}") print(f"Count image of train: {len(train_ds)}") print(f"Count image of Validation: {len(val_ds)}") print(f"Test: {len(test_ds)}") fig, axes = plt.subplots(2, 3, figsize=(10, 7)) for ax in axes.flatten(): img, label = train_ds[np.random.randint(len(train_ds))] img = img * torch.tensor(IMAGENET_STD).view(3,1,1) + torch.tensor(IMAGENET_MEAN).view(3,1,1) img = img.clamp(0, 1).permute(1, 2, 0).numpy() ax.imshow(img) ax.set_title(CLASS_NAMES[label], fontsize=9) ax.axis("off") plt.tight_layout() plt.show() class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), nn.BatchNorm2d(out_channels), ) def forward(self, x): identity = self.shortcut(x) out = self.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += identity # skip connection return self.relu(out) class ScratchCNN(nn.Module): def __init__(self, num_classes, dropout=DROPOUT): super().__init__() self.stem = nn.Sequential( nn.Conv2d(3, 64, 7, 2, 3, bias=False), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(3, 2, 1), ) self.stage1 = self._make_stage(64, 64, 2, stride=1) self.stage2 = self._make_stage(64, 128, 2, stride=2) self.stage3 = self._make_stage(128, 256, 2, stride=2) self.stage4 = self._make_stage(256, 512, 2, stride=2) self.global_pool = nn.AdaptiveAvgPool2d(1) self.dropout = nn.Dropout(dropout) self.classifier = nn.Linear(512, num_classes) def _make_stage(self, in_c, out_c, num_blocks, stride): layers = [ResidualBlock(in_c, out_c, stride)] for _ in range(num_blocks - 1): layers.append(ResidualBlock(out_c, out_c, 1)) return nn.Sequential(*layers) def forward(self, x): x = self.stem(x) x = self.stage1(x); x = self.stage2(x) x = self.stage3(x); x = self.stage4(x) x = self.global_pool(x) x = torch.flatten(x, 1) x = self.dropout(x) return self.classifier(x) _test_out = ScratchCNN(num_classes=NUM_CLASSES)(torch.randn(2, 3, IMG_SIZE, IMG_SIZE)) def build_resnet50(num_classes, dropout=DROPOUT): m = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) m.fc = nn.Sequential(nn.Dropout(dropout), nn.Linear(m.fc.in_features, num_classes)) return m def build_efficientnet_b0(num_classes, dropout=DROPOUT): m = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.IMAGENET1K_V1) m.classifier = nn.Sequential(nn.Dropout(dropout), nn.Linear(m.classifier[1].in_features, num_classes)) return m def build_densenet121(num_classes, dropout=DROPOUT): m = models.densenet121(weights=models.DenseNet121_Weights.IMAGENET1K_V1) m.classifier = nn.Sequential(nn.Dropout(dropout), nn.Linear(m.classifier.in_features, num_classes)) return m def freeze_backbone(model, model_name): for p in model.parameters(): p.requires_grad = False head = model.fc if model_name == "resnet50" else model.classifier for p in head.parameters(): p.requires_grad = True return model def unfreeze_all(model): for p in model.parameters(): p.requires_grad = True return model def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) class EarlyStopping: def __init__(self, patience=EARLY_STOPPING_PATIENCE): self.patience = patience self.best_loss = float("inf") self.counter = 0 self.best_state = None self.should_stop = False def step(self, val_loss, model): if val_loss < self.best_loss: self.best_loss = val_loss self.counter = 0 self.best_state = copy.deepcopy(model.state_dict()) else: self.counter += 1 if self.counter >= self.patience: self.should_stop = True def run_one_epoch(model, loader, criterion, optimizer=None): is_training = optimizer is not None model.train() if is_training else model.eval() total_loss, total_correct, total_samples = 0.0, 0, 0 torch.set_grad_enabled(is_training) for images, labels in tqdm(loader, leave=False): images, labels = images.to(DEVICE), labels.to(DEVICE) if is_training: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) if is_training: loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) total_correct += (outputs.argmax(1) == labels).sum().item() total_samples += images.size(0) torch.set_grad_enabled(True) return total_loss / total_samples, total_correct / total_samples def train_model(model, model_name, train_loader, val_loader, epochs, lr, weight_decay=WEIGHT_DECAY, label_smoothing=LABEL_SMOOTHING): model = model.to(DEVICE) criterion = nn.CrossEntropyLoss(label_smoothing=label_smoothing) optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=lr, weight_decay=weight_decay, ) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=2) early_stopper = EarlyStopping() history = {"train_loss": [], "train_acc": [], "val_loss": [], "val_acc": []} for epoch in range(1, epochs + 1): train_loss, train_acc = run_one_epoch(model, train_loader, criterion, optimizer) val_loss, val_acc = run_one_epoch(model, val_loader, criterion, optimizer=None) scheduler.step(val_loss) history["train_loss"].append(train_loss) history["train_acc"].append(train_acc) history["val_loss"].append(val_loss) history["val_acc"].append(val_acc) print(f"[{model_name}] Epoch {epoch}/{epochs} | " f"train_loss={train_loss:.4f} train_acc={train_acc:.4f} | " f"val_loss={val_loss:.4f} val_acc={val_acc:.4f}") early_stopper.step(val_loss, model) if early_stopper.should_stop: print(f"[{model_name}] Early stopping {epoch}") break model.load_state_dict(early_stopper.best_state) return model, history @torch.no_grad() def evaluate_full(model, loader): model.eval() all_preds, all_labels = [], [] for images, labels in tqdm(loader, leave=False, desc="Evaluating"): images = images.to(DEVICE) preds = model(images).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) acc = accuracy_score(all_labels, all_preds) f1 = f1_score(all_labels, all_preds, average="macro") return {"accuracy": acc, "f1_macro": f1, "predictions": all_preds, "labels": all_labels} print("Done") def plot_history(history, model_name): fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(history["train_loss"], label="Train Loss") axes[0].plot(history["val_loss"], label="Val Loss") axes[0].set_title(f"{model_name} - Loss"); axes[0].set_xlabel("Epoch"); axes[0].legend() axes[1].plot(history["train_acc"], label="Train Acc") axes[1].plot(history["val_acc"], label="Val Acc") axes[1].set_title(f"{model_name} - Accuracy"); axes[1].set_xlabel("Epoch"); axes[1].legend() plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, f"{model_name}_history.png"), dpi=150) plt.show() def plot_confusion_matrix(labels, preds, class_names, model_name): cm = confusion_matrix(labels, preds) fig, ax = plt.subplots(figsize=(7, 6)) im = ax.imshow(cm, cmap="Blues") ax.set_xticks(range(len(class_names))); ax.set_yticks(range(len(class_names))) ax.set_xticklabels(class_names, rotation=45, ha="right") ax.set_yticklabels(class_names) ax.set_xlabel("Predicted"); ax.set_ylabel("True") ax.set_title(f"Confusion Matrix - {model_name}") for i in range(len(class_names)): for j in range(len(class_names)): ax.text(j, i, cm[i, j], ha="center", va="center", color="white" if cm[i, j] > cm.max()/2 else "black") plt.colorbar(im) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, f"{model_name}_confusion_matrix.png"), dpi=150) plt.show() scratch_model = ScratchCNN(NUM_CLASSES) print(f"{count_parameters(scratch_model):,}") scratch_model, scratch_history = train_model( scratch_model, "scratch_cnn", train_loader, val_loader, epochs=EPOCHS, lr=LR_SCRATCH, ) plot_history(scratch_history, "scratch_cnn") resnet_model = build_resnet50(NUM_CLASSES) resnet_model = freeze_backbone(resnet_model, "resnet50") resnet_model, resnet_history_1 = train_model( resnet_model, "resnet50", train_loader, val_loader, epochs=FREEZE_EPOCHS, lr=LR_PRETRAINED_HEAD, ) resnet_model = unfreeze_all(resnet_model) resnet_model, resnet_history_2 = train_model( resnet_model, "resnet50", train_loader, val_loader, epochs=max(EPOCHS - FREEZE_EPOCHS, 5), lr=LR_PRETRAINED_FINETUNE, ) resnet_history = {k: resnet_history_1[k] + resnet_history_2[k] for k in resnet_history_1} plot_history(resnet_history, "resnet50") effnet_model = build_efficientnet_b0(NUM_CLASSES) effnet_model = freeze_backbone(effnet_model, "efficientnet_b0") effnet_model, effnet_history_1 = train_model( effnet_model, "efficientnet_b0", train_loader, val_loader, epochs=FREEZE_EPOCHS, lr=LR_PRETRAINED_HEAD, ) effnet_model = unfreeze_all(effnet_model) effnet_model, effnet_history_2 = train_model( effnet_model, "efficientnet_b0", train_loader, val_loader, epochs=max(EPOCHS - FREEZE_EPOCHS, 5), lr=LR_PRETRAINED_FINETUNE, ) effnet_history = {k: effnet_history_1[k] + effnet_history_2[k] for k in effnet_history_1} plot_history(effnet_history, "efficientnet_b0") densenet_model = build_densenet121(NUM_CLASSES) densenet_model = freeze_backbone(densenet_model, "densenet121") densenet_model, densenet_history_1 = train_model( densenet_model, "densenet121", train_loader, val_loader, epochs=FREEZE_EPOCHS, lr=LR_PRETRAINED_HEAD, ) densenet_model = unfreeze_all(densenet_model) densenet_model, densenet_history_2 = train_model( densenet_model, "densenet121", train_loader, val_loader, epochs=max(EPOCHS - FREEZE_EPOCHS, 5), lr=LR_PRETRAINED_FINETUNE, ) densenet_history = {k: densenet_history_1[k] + densenet_history_2[k] for k in densenet_history_1} plot_history(densenet_history, "densenet121") all_models = { "scratch_cnn": scratch_model, "resnet50": resnet_model, "efficientnet_b0": effnet_model, "densenet121": densenet_model, } results = [] best_model_name, best_f1 = None, -1.0 for name, model in all_models.items(): print(f"Evaluate {name} on test set...") eval_result = evaluate_full(model, test_loader) ckpt_path = os.path.join(CHECKPOINT_DIR, f"{name}.pth") torch.save(model.state_dict(), ckpt_path) print(f"The save weight {name} in: {ckpt_path}") plot_confusion_matrix(eval_result["labels"], eval_result["predictions"], CLASS_NAMES, name) results.append({ "model": name, "trainable_params": count_parameters(model), "test_accuracy": round(eval_result["accuracy"], 4), "test_f1": round(eval_result["f1_macro"], 4), }) if eval_result["f1_macro"] > best_f1: best_f1 = eval_result["f1_macro"] best_model_name = name comparison_df = pd.DataFrame(results).sort_values("test_f1", ascending=False).reset_index(drop=True) comparison_df.to_csv(os.path.join(OUTPUT_DIR, "models_comparison.csv"), index=False) comparison_df for name, model in all_models.items(): print(f"Evaluate {name} on test set...") eval_result = evaluate_full(model, test_loader) print(f"Classification Report for {name}") print( classification_report( eval_result["labels"], eval_result["predictions"], target_names=CLASS_NAMES, digits=4 ) ) ckpt_path = os.path.join(CHECKPOINT_DIR, f"{name}.pth") torch.save(model.state_dict(), ckpt_path) best_model = all_models[best_model_name] best_path = os.path.join(CHECKPOINT_DIR, "best_model.pth") torch.save({ "model_name": best_model_name, "state_dict": best_model.state_dict(), "class_names": CLASS_NAMES, "test_f1": best_f1, }, best_path) print(f"The Best Model: {best_model_name} (F1 = {best_f1:.4f})") print(f"Save: {best_path}") IMAGE_PATH = '/kaggle/working/oral_dataset/Hypodontia/(100).JPG' checkpoint = torch.load(os.path.join(CHECKPOINT_DIR, "best_model.pth"), map_location=DEVICE) loaded_name = checkpoint["model_name"] loaded_classes = checkpoint["class_names"] builders = { "scratch_cnn": lambda: ScratchCNN(len(loaded_classes)), "resnet50": lambda: build_resnet50(len(loaded_classes)), "efficientnet_b0": lambda: build_efficientnet_b0(len(loaded_classes)), "densenet121": lambda: build_densenet121(len(loaded_classes)), } inference_model = builders[loaded_name]() inference_model.load_state_dict(checkpoint["state_dict"]) inference_model.to(DEVICE).eval() print(f"Download the best model: {loaded_name} (test F1 = {checkpoint['test_f1']:.4f})") with torch.no_grad(): image = Image.open(IMAGE_PATH).convert("RGB") tensor = eval_transform(image).unsqueeze(0).to(DEVICE) probs = torch.softmax(inference_model(tensor), dim=1)[0] pred_idx = probs.argmax().item() plt.imshow(image); plt.axis("off") plt.title(f"Classifier: {loaded_classes[pred_idx]} ({probs[pred_idx]*100:.1f}%)") plt.show() for cname, p in sorted(zip(loaded_classes, probs.tolist()), key=lambda x: -x[1]): print(f" {cname:25s}: {p*100:5.2f}%") IMAGE_PATH = '/kaggle/working/oral_dataset/Caries/caries_0_1001.jpeg' checkpoint = torch.load(os.path.join(CHECKPOINT_DIR, "best_model.pth"), map_location=DEVICE) loaded_name = checkpoint["model_name"] loaded_classes = checkpoint["class_names"] builders = { "scratch_cnn": lambda: ScratchCNN(len(loaded_classes)), "resnet50": lambda: build_resnet50(len(loaded_classes)), "efficientnet_b0": lambda: build_efficientnet_b0(len(loaded_classes)), "densenet121": lambda: build_densenet121(len(loaded_classes)), } inference_model = builders[loaded_name]() inference_model.load_state_dict(checkpoint["state_dict"]) inference_model.to(DEVICE).eval() print(f"Download the best model: {loaded_name} (test F1 = {checkpoint['test_f1']:.4f})") with torch.no_grad(): image = Image.open(IMAGE_PATH).convert("RGB") tensor = eval_transform(image).unsqueeze(0).to(DEVICE) probs = torch.softmax(inference_model(tensor), dim=1)[0] pred_idx = probs.argmax().item() plt.imshow(image); plt.axis("off") plt.title(f"Classifier: {loaded_classes[pred_idx]} ({probs[pred_idx]*100:.1f}%)") plt.show() for cname, p in sorted(zip(loaded_classes, probs.tolist()), key=lambda x: -x[1]): print(f" {cname:25s}: {p*100:5.2f}%") import shutil shutil.make_archive( "/kaggle/working/output_files", "zip", "/kaggle/working/" ) from IPython.display import FileLink FileLink('/kaggle/working/output_files.zip')