| |
| """ |
| ResNet50 Image Classification Training Script |
| Dataset: Animals-10 |
| Model: ResNet50 (Pre-trained on ImageNet) |
| """ |
|
|
| import kagglehub |
| import torch |
| import torch.nn as nn |
| from torch.utils.data import DataLoader, random_split |
| from torchvision import datasets |
| from torchvision.transforms import v2 |
| from torchvision.models import resnet50, ResNet50_Weights |
| from torch.optim import AdamW |
| from torch.optim.lr_scheduler import OneCycleLR |
| import time |
| import os |
| import copy |
|
|
| |
| BATCH_SIZE = 32 |
| ACCUMULATION_STEPS = 2 |
| EPOCHS = 15 |
| LEARNING_RATE = 1e-4 |
| NUM_WORKERS = 2 |
|
|
| |
| |
| class TransformedDataset(torch.utils.data.Dataset): |
| def __init__(self, subset, transform): |
| self.subset = subset |
| self.transform = transform |
| def __getitem__(self, index): |
| x, y = self.subset[index] |
| return self.transform(x), y |
| def __len__(self): |
| return len(self.subset) |
|
|
| |
|
|
| def get_device(): |
| """Selects the compute device (CUDA or CPU).""" |
| if torch.cuda.is_available(): |
| print(f"Device selected: CUDA ({torch.cuda.get_device_name(0)})") |
| return torch.device("cuda") |
| print("Device selected: CPU") |
| return torch.device("cpu") |
|
|
| def get_data_path(): |
| """Locates the dataset locally or downloads it via KaggleHub.""" |
| current_dir = os.getcwd() |
| local_path = os.path.join(current_dir, "animals10", "raw-img") |
| |
| if os.path.exists(local_path): |
| print(f"Dataset found locally at: {local_path}") |
| return local_path |
| |
| print("Dataset not found locally. Downloading via KaggleHub...") |
| path = kagglehub.dataset_download("alessiocorrado99/animals10") |
| return os.path.join(path, "raw-img") |
|
|
| |
|
|
| def main(): |
| device = get_device() |
| image_path = get_data_path() |
|
|
| |
| |
| stats = ((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) |
| |
| augmentations = { |
| 'train': v2.Compose([ |
| v2.Resize((256, 256)), |
| v2.RandomResizedCrop(224, scale=(0.6, 1.0)), |
| v2.RandomHorizontalFlip(p=0.5), |
| v2.RandomRotation(15), |
| v2.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), |
| v2.RandomErasing(p=0.1, scale=(0.02, 0.15)), |
| v2.PILToTensor(), |
| v2.ToDtype(torch.float32, scale=True), |
| v2.Normalize(*stats), |
| ]), |
| 'val': v2.Compose([ |
| v2.Resize((256, 256)), |
| v2.CenterCrop(224), |
| v2.PILToTensor(), |
| v2.ToDtype(torch.float32, scale=True), |
| v2.Normalize(*stats), |
| ]), |
| } |
|
|
| |
| print("Initializing dataset and splits...") |
| full_dataset = datasets.ImageFolder(image_path) |
| |
| total_len = len(full_dataset) |
| train_len = int(0.8 * total_len) |
| val_len = int(0.1 * total_len) |
| test_len = total_len - train_len - val_len |
| |
| |
| train_subset, val_subset, test_subset = random_split( |
| full_dataset, [train_len, val_len, test_len], |
| generator=torch.Generator().manual_seed(42) |
| ) |
|
|
| |
| train_loader = DataLoader(TransformedDataset(train_subset, augmentations['train']), |
| batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True) |
| val_loader = DataLoader(TransformedDataset(val_subset, augmentations['val']), |
| batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True) |
| test_loader = DataLoader(TransformedDataset(test_subset, augmentations['val']), |
| batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True) |
|
|
| |
| print("Loading ResNet50 model with ImageNet weights...") |
| model = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2) |
| |
| |
| for name, param in model.named_parameters(): |
| if "layer4" in name or "fc" in name: |
| param.requires_grad = True |
| else: |
| param.requires_grad = False |
|
|
| |
| model.fc = nn.Linear(model.fc.in_features, 10) |
| model = model.to(device) |
|
|
| |
| optimizer = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=LEARNING_RATE, weight_decay=1e-2) |
| |
| scheduler = OneCycleLR(optimizer, max_lr=LEARNING_RATE*10, |
| steps_per_epoch=len(train_loader)//ACCUMULATION_STEPS, |
| epochs=EPOCHS) |
| |
| criterion = nn.CrossEntropyLoss(label_smoothing=0.1) |
| scaler = torch.amp.GradScaler('cuda') |
|
|
| |
| best_acc = 0.0 |
| best_model_wts = copy.deepcopy(model.state_dict()) |
|
|
| print("-" * 60) |
| print(f"Starting Training Loop") |
| print(f"Epochs: {EPOCHS} | Batch Size: {BATCH_SIZE} | Accumulation Steps: {ACCUMULATION_STEPS}") |
| print("-" * 60) |
|
|
| for epoch in range(EPOCHS): |
| start_time = time.time() |
| |
| |
| model.train() |
| train_loss = 0 |
| correct = 0 |
| total = 0 |
| |
| optimizer.zero_grad() |
| |
| for i, (inputs, labels) in enumerate(train_loader): |
| inputs, labels = inputs.to(device), labels.to(device) |
|
|
| |
| with torch.amp.autocast('cuda'): |
| outputs = model(inputs) |
| loss = criterion(outputs, labels) |
| loss = loss / ACCUMULATION_STEPS |
|
|
| scaler.scale(loss).backward() |
|
|
| if (i + 1) % ACCUMULATION_STEPS == 0: |
| scaler.step(optimizer) |
| scaler.update() |
| optimizer.zero_grad() |
| scheduler.step() |
|
|
| train_loss += loss.item() * ACCUMULATION_STEPS |
| _, predicted = outputs.max(1) |
| total += labels.size(0) |
| correct += predicted.eq(labels).sum().item() |
|
|
| train_acc = 100. * correct / total |
| train_avg_loss = train_loss / len(train_loader) |
|
|
| |
| model.eval() |
| val_correct = 0 |
| val_total = 0 |
| |
| with torch.no_grad(): |
| for inputs, labels in val_loader: |
| inputs, labels = inputs.to(device), labels.to(device) |
| outputs = model(inputs) |
| _, predicted = outputs.max(1) |
| val_total += labels.size(0) |
| val_correct += predicted.eq(labels).sum().item() |
| |
| val_acc = 100. * val_correct / val_total |
| epoch_time = time.time() - start_time |
|
|
| |
| print(f"Epoch [{epoch+1}/{EPOCHS}] | Time: {epoch_time:.0f}s | " |
| f"Train Loss: {train_avg_loss:.4f} | Train Acc: {train_acc:.2f}% | " |
| f"Val Acc: {val_acc:.2f}%") |
|
|
| |
| if val_acc > best_acc: |
| best_acc = val_acc |
| best_model_wts = copy.deepcopy(model.state_dict()) |
| torch.save(model.state_dict(), "best_resnet50_animals.pt") |
| print(f" -> Validation accuracy improved. Model saved.") |
|
|
| print("-" * 60) |
| print(f"Training Completed. Best Validation Accuracy: {best_acc:.2f}%") |
| print("-" * 60) |
|
|
| |
| print("Starting evaluation on Test Set...") |
| model.load_state_dict(best_model_wts) |
| model.eval() |
| test_correct = 0 |
| test_total = 0 |
| with torch.no_grad(): |
| for inputs, labels in test_loader: |
| inputs, labels = inputs.to(device), labels.to(device) |
| outputs = model(inputs) |
| _, predicted = outputs.max(1) |
| test_total += labels.size(0) |
| test_correct += predicted.eq(labels).sum().item() |
| |
| print(f"Final Test Set Accuracy: {100. * test_correct / test_total:.2f}%") |
|
|
| if __name__ == '__main__': |
| main() |