Spaces:
Configuration error
Configuration error
| 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() | |