#!/usr/bin/env python3 import os import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms from tqdm import tqdm from opacus import PrivacyEngine from opacus.validators import ModuleValidator # ============================================================================= # STABLE CONFIGURATION # ============================================================================= BATCH_SIZE = 64 # Smaller batch = more stable memory on CPU DP_EPOCHS = 10 # Fewer epochs to start; check stability first DP_LR = 5e-4 # Lower LR to prevent 'nan' MAX_GRAD_NORM = 1.0 DELTA = 1e-5 TARGET_EPSILON = 6.0 DEVICE = torch.device('cpu') # ============================================================================= # MODEL (MINIMAL & STABLE) # ============================================================================= class SimpleSVHN(nn.Module): def __init__(self): super().__init__() # We use GroupNorm with small groups for maximum stability self.conv1 = nn.Conv2d(3, 16, 3, padding=1) self.gn1 = nn.GroupNorm(2, 16) self.conv2 = nn.Conv2d(16, 32, 3, padding=1) self.gn2 = nn.GroupNorm(4, 32) self.pool = nn.MaxPool2d(2, 2) self.fc = nn.Linear(32 * 8 * 8, 10) def forward(self, x): x = self.pool(F.relu(self.gn1(self.conv1(x)))) # 16x16 x = self.pool(F.relu(self.gn2(self.conv2(x)))) # 8x8 x = torch.flatten(x, 1) return self.fc(x) # ============================================================================= # TRAINING SCRIPT # ============================================================================= def run_stable_dp_training(): # 1. Data Loading transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4377, 0.4438, 0.4728), (0.1980, 0.2010, 0.1970)) ]) train_ds = datasets.SVHN("data", split='train', download=True, transform=transform) test_ds = datasets.SVHN("data", split='test', download=True, transform=transform) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True) test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False) # 2. Model Setup model = SimpleSVHN().to(DEVICE) model = ModuleValidator.fix(model) optimizer = optim.Adam(model.parameters(), lr=DP_LR) # 3. Privacy Engine (THE STABILITY FIX) # Using grad_sample_mode="hooks" prevents the C++ level SegFaults privacy_engine = PrivacyEngine() model, optimizer, train_loader = privacy_engine.make_private_with_epsilon( module=model, optimizer=optimizer, data_loader=train_loader, target_epsilon=TARGET_EPSILON, target_delta=DELTA, epochs=DP_EPOCHS, max_grad_norm=MAX_GRAD_NORM, grad_sample_mode="hooks", ) criterion = nn.CrossEntropyLoss() print(f"Starting Training on {DEVICE}...") for epoch in range(DP_EPOCHS): model.train() total_loss = 0 pbar = tqdm(train_loader, desc=f"Epoch {epoch+1}") for images, labels in pbar: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) if torch.isnan(loss): print("\n[!] Warning: NaN detected. Skipping batch.") continue loss.backward() optimizer.step() total_loss += loss.item() pbar.set_postfix({'loss': f'{loss.item():.2f}'}) epsilon = privacy_engine.get_epsilon(DELTA) print(f"Epoch {epoch+1} complete. Current ε: {epsilon:.2f}") # 4. Final Evaluation model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(DEVICE), labels.to(DEVICE) outputs = model(images) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() print(f"\nFinal Test Accuracy: {100.*correct/total:.2f}%") print(f"Final Epsilon: {epsilon:.2f}") if __name__ == "__main__": # Force single-thread for math if your CPU is still fighting the memory allocation torch.set_num_threads(1) run_stable_dp_training()