import os import random import time import copy import numpy as np import pandas as pd import matplotlib.pyplot as plt from PIL import Image from tqdm.auto import tqdm import torch import torch.nn as nn import torch.optim as optim from torchvision import transforms, datasets from torch.utils.data import DataLoader from sklearn.metrics import ( confusion_matrix, classification_report, accuracy_score, precision_score, recall_score, f1_score, ConfusionMatrixDisplay ) torch.backends.cudnn.benchmark = True torch.backends.cuda.matmul.allow_tf32 = True torch.set_float32_matmul_precision("high") SEED = 42 random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) torch.cuda.manual_seed(SEED) torch.cuda.manual_seed_all(SEED) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("="*50) print("Device :", device) if torch.cuda.is_available(): print("GPU :", torch.cuda.get_device_name(0)) print("CUDA :", torch.version.cuda) print("="*50) IMAGE_SIZE = 224 BATCH_SIZE = 32 EPOCHS = 20 LEARNING_RATE = 1e-4 WEIGHT_DECAY = 1e-4 NUM_WORKERS = 0 NUM_CLASSES = 2 MODEL_SAVE_PATH = "resnet18_cbam_best.pth" DATA_DIR = "data" TRAIN_DIR = os.path.join(DATA_DIR, "train") VAL_DIR = os.path.join(DATA_DIR, "val") TEST_DIR = os.path.join(DATA_DIR, "test") train_transform = transforms.Compose([ transforms.Resize((224,224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.RandomAffine( degrees=0, translate=(0.05,0.05) ), transforms.ColorJitter( brightness=0.2, contrast=0.2 ), transforms.ToTensor(), transforms.Normalize( mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225] ) ]) val_transform = transforms.Compose([ transforms.Resize((224,224)), transforms.ToTensor(), transforms.Normalize( mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225] ) ]) train_dataset = datasets.ImageFolder( TRAIN_DIR, transform=train_transform ) val_dataset = datasets.ImageFolder( VAL_DIR, transform=val_transform ) test_dataset = datasets.ImageFolder( TEST_DIR, transform=val_transform ) print("="*60) print("Classes :", train_dataset.classes) print() print("Train :", len(train_dataset)) print("Validation :", len(val_dataset)) print("Test :", len(test_dataset)) print("="*60) train_loader = DataLoader( train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True ) val_loader = DataLoader( val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True ) test_loader = DataLoader( test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True ) images, labels = next(iter(train_loader)) print("Image Batch Shape :", images.shape) print("Label Batch Shape :", labels.shape) print() print("Unique Labels :", torch.unique(labels)) classes = train_dataset.classes fig, axes = plt.subplots(2, 4, figsize=(12, 6)) images, labels = next(iter(train_loader)) images = images.numpy().transpose(0, 2, 3, 1) mean = np.array([0.485, 0.456, 0.406]) std = np.array([0.229, 0.224, 0.225]) images = std * images + mean images = np.clip(images, 0, 1) for ax, img, label in zip(axes.flatten(), images[:8], labels[:8]): ax.imshow(img) ax.set_title(classes[label]) ax.axis("off") plt.tight_layout() plt.show() def conv3x3(in_channels, out_channels, stride=1): return nn.Conv2d( in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False ) def conv1x1(in_channels, out_channels, stride=1): return nn.Conv2d( in_channels, out_channels, kernel_size=1, stride=stride, bias=False ) import torch.nn as nn def conv3x3(in_channels, out_channels, stride=1): return nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False) def conv1x1(in_channels, out_channels, stride=1): return nn.Conv2d(in_channels, out_channels, 1, stride, 0, bias=False) class BasicBlock(nn.Module): expansion = 1 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super().__init__() self.conv1 = conv3x3(in_channels, out_channels, stride) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = conv3x3(out_channels, out_channels) self.bn2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) self.downsample = downsample def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out class ResNet18(nn.Module): def __init__(self, block, layers, num_classes=2): super().__init__() self.in_channels = 64 self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU(inplace=True) self.maxpool = nn.MaxPool2d(3, 2, 1) self.layer1 = self._make_layer(block, 64, layers[0]) self.layer2 = self._make_layer(block, 128, layers[1], stride=2) self.layer3 = self._make_layer(block, 256, layers[2], stride=2) self.layer4 = self._make_layer(block, 512, layers[3], stride=2) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512, num_classes) def _make_layer(self, block, out_channels, blocks, stride=1): downsample = None if stride != 1 or self.in_channels != out_channels: downsample = nn.Sequential( conv1x1(self.in_channels, out_channels, stride), nn.BatchNorm2d(out_channels) ) layers = [] layers.append( block(self.in_channels, out_channels, stride, downsample) ) self.in_channels = out_channels for _ in range(1, blocks): layers.append( block(self.in_channels, out_channels) ) return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.maxpool(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.fc(x) return x def build_resnet18(num_classes=2): return ResNet18(BasicBlock, [2, 2, 2, 2], num_classes) model = build_resnet18(NUM_CLASSES).to(device) print(model) print(next(model.parameters()).device) x = torch.randn(2, 3, 224, 224).to(device) y = model(x) print("Output shape:", y.shape) class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.mlp = nn.Sequential( nn.Linear(in_channels, in_channels // reduction, bias=False), nn.ReLU(), nn.Linear(in_channels // reduction, in_channels, bias=False) ) self.sigmoid = nn.Sigmoid() def forward(self, x): b, c, _, _ = x.shape avg = self.avg_pool(x).view(b, c) max_ = self.max_pool(x).view(b, c) avg_out = self.mlp(avg) max_out = self.mlp(max_) out = avg_out + max_out out = self.sigmoid(out).view(b, c, 1, 1) return x * out class SpatialAttention(nn.Module): def __init__(self): super().__init__() self.conv = nn.Conv2d( 2, 1, kernel_size=7, padding=3, bias=False ) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) x_cat = torch.cat([avg_out, max_out], dim=1) attention = self.conv(x_cat) attention = self.sigmoid(attention) return x * attention class CBAM(nn.Module): def __init__(self, channels, reduction=16): super().__init__() self.channel_attention = ChannelAttention(channels, reduction) self.spatial_attention = SpatialAttention() def forward(self, x): x = self.channel_attention(x) x = self.spatial_attention(x) return x class BasicBlockCBAM(nn.Module): expansion = 1 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super().__init__() self.conv1 = conv3x3(in_channels, out_channels, stride) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = conv3x3(out_channels, out_channels) self.bn2 = nn.BatchNorm2d(out_channels) self.cbam = CBAM(out_channels) self.relu = nn.ReLU(inplace=True) self.downsample = downsample def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) out = self.cbam(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out class ResNet18CBAM(nn.Module): def __init__(self, block, layers, num_classes=2): super().__init__() self.in_channels = 64 self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU(inplace=True) self.maxpool = nn.MaxPool2d(3, 2, 1) self.layer1 = self._make_layer(block, 64, layers[0]) self.layer2 = self._make_layer(block, 128, layers[1], stride=2) self.layer3 = self._make_layer(block, 256, layers[2], stride=2) self.layer4 = self._make_layer(block, 512, layers[3], stride=2) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512, num_classes) def _make_layer(self, block, out_channels, blocks, stride=1): downsample = None if stride != 1 or self.in_channels != out_channels: downsample = nn.Sequential( conv1x1(self.in_channels, out_channels, stride), nn.BatchNorm2d(out_channels) ) layers = [] layers.append( block(self.in_channels, out_channels, stride, downsample) ) self.in_channels = out_channels for _ in range(1, blocks): layers.append( block(self.in_channels, out_channels) ) return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.maxpool(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.fc(x) return x def build_model(): return ResNet18CBAM( BasicBlockCBAM, [2, 2, 2, 2], NUM_CLASSES ) model = build_model().to(device) print(model) x = torch.randn(2, 3, 224, 224).to(device) y = model(x) print("Output shape:", y.shape) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam( model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY ) scheduler = optim.lr_scheduler.StepLR( optimizer, step_size=5, gamma=0.5 ) scaler = torch.cuda.amp.GradScaler() def accuracy(outputs, labels): _, preds = torch.max(outputs, 1) correct = (preds == labels).sum().item() return correct / labels.size(0) def train_one_epoch(model, loader, optimizer, criterion): model.train() running_loss = 0 correct = 0 total = 0 for images, labels in loader: images = images.to(device, non_blocking=True) labels = labels.to(device, non_blocking=True) optimizer.zero_grad() # ⚡ AMP forward pass with torch.cuda.amp.autocast(): outputs = model(images) loss = criterion(outputs, labels) # ⚡ backward scaled scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() running_loss += loss.item() _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return running_loss / len(loader), correct / total def validate(model, loader, criterion): model.eval() running_loss = 0 correct = 0 total = 0 with torch.no_grad(): for images, labels in loader: images = images.to(device, non_blocking=True) labels = labels.to(device, non_blocking=True) with torch.cuda.amp.autocast(): outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return running_loss / len(loader), correct / total best_acc = 0.0 history = { "train_loss": [], "val_loss": [], "train_acc": [], "val_acc": [] } for epoch in range(EPOCHS): print(f"\nEpoch [{epoch+1}/{EPOCHS}]") train_loss, train_acc = train_one_epoch( model, train_loader, optimizer, criterion ) val_loss, val_acc = validate( model, val_loader, criterion ) scheduler.step() history["train_loss"].append(train_loss) history["val_loss"].append(val_loss) history["train_acc"].append(train_acc) history["val_acc"].append(val_acc) print(f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f}") print(f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}") # Save best model if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), MODEL_SAVE_PATH) print("✅ Best model saved!") print("\nTraining Complete!") print("Best Validation Accuracy:", best_acc) plt.figure(figsize=(12,5)) # Loss plt.subplot(1,2,1) plt.plot(history["train_loss"], label="Train Loss") plt.plot(history["val_loss"], label="Val Loss") plt.legend() plt.title("Loss Curve") # Accuracy plt.subplot(1,2,2) plt.plot(history["train_acc"], label="Train Acc") plt.plot(history["val_acc"], label="Val Acc") plt.legend() plt.title("Accuracy Curve") plt.show() model.load_state_dict(torch.load(MODEL_SAVE_PATH)) model = model.to(device) model.eval() print("Best model loaded!") all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in test_loader: images = images.to(device) labels = labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) acc = accuracy_score(all_labels, all_preds) prec = precision_score(all_labels, all_preds) rec = recall_score(all_labels, all_preds) f1 = f1_score(all_labels, all_preds) print("="*50) print("TEST RESULTS") print("="*50) print("Accuracy :", acc) print("Precision:", prec) print("Recall :", rec) print("F1 Score :", f1) print(classification_report(all_labels, all_preds, target_names=train_dataset.classes)) cm = confusion_matrix(all_labels, all_preds) disp = ConfusionMatrixDisplay( confusion_matrix=cm, display_labels=train_dataset.classes ) disp.plot(cmap="Blues") plt.title("Confusion Matrix") plt.show() import os RESULTS_DIR = "results" os.makedirs(RESULTS_DIR, exist_ok=True) print("Results will be saved to:", RESULTS_DIR) import pandas as pd history_df = pd.DataFrame(history) history_df.to_csv( os.path.join(RESULTS_DIR, "training_history.csv"), index=False ) print("Training history saved.") report = classification_report( all_labels, all_preds, target_names=train_dataset.classes, digits=4 ) with open(os.path.join(RESULTS_DIR, "classification_report.txt"), "w") as f: f.write(report) print(report) import matplotlib.pyplot as plt from sklearn.metrics import ConfusionMatrixDisplay cm = confusion_matrix(all_labels, all_preds) disp = ConfusionMatrixDisplay( confusion_matrix=cm, display_labels=train_dataset.classes ) fig, ax = plt.subplots(figsize=(6,6)) disp.plot( cmap="Blues", ax=ax, colorbar=False, values_format="d" ) plt.title("Confusion Matrix") plt.savefig( os.path.join(RESULTS_DIR, "confusion_matrix.png"), dpi=300, bbox_inches="tight" ) plt.show() plt.figure(figsize=(8,5)) plt.plot(history["train_acc"], label="Train Accuracy", linewidth=2) plt.plot(history["val_acc"], label="Validation Accuracy", linewidth=2) plt.xlabel("Epoch") plt.ylabel("Accuracy") plt.title("Training vs Validation Accuracy") plt.grid(True) plt.legend() plt.savefig( os.path.join(RESULTS_DIR, "accuracy_curve.png"), dpi=300, bbox_inches="tight" ) plt.show()