#!/usr/bin/env python3 import os import numpy as np 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 safetensors.torch import save_file from tqdm import tqdm # ============================================================================= # CONFIGURATION # ============================================================================= RANDOM_SEED = 1337 BATCH_SIZE = 256 DP_EPOCHS = 10 DP_LR = 0.1 MAX_GRAD_NORM = 1.0 DELTA = 1e-5 # The key parameter: use ε=6 to balance accuracy and privacy TARGET_EPSILON = 6.0 # Set reproducibility torch.manual_seed(RANDOM_SEED) np.random.seed(RANDOM_SEED) if torch.cuda.is_available(): torch.cuda.manual_seed_all(RANDOM_SEED) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # Output directories MODELS_DIR = "models" os.makedirs(MODELS_DIR, exist_ok=True) # SVHN normalization values SVHN_MEAN = (0.4377, 0.4438, 0.4728) SVHN_STD = (0.1980, 0.2010, 0.1970) # ============================================================================= # MODEL ARCHITECTURE # ============================================================================= class SVHNCNN(nn.Module): """CNN for SVHN classification (compatible with Opacus).""" def __init__(self): super(SVHNCNN, self).__init__() self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(64, 64, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(64 * 4 * 4, 64) self.fc2 = nn.Linear(64, 10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 64 * 4 * 4) x = F.relu(self.fc1(x)) x = self.fc2(x) return x # ============================================================================= # DATA LOADING # ============================================================================= def get_svhn_loaders(batch_size=256, download=True): """Load SVHN dataset.""" transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(SVHN_MEAN, SVHN_STD) ]) train_dataset = datasets.SVHN( "data", split='train', download=download, transform=transform ) test_dataset = datasets.SVHN( "data", split='test', download=download, transform=transform ) train_loader = DataLoader( train_dataset, batch_size=batch_size, shuffle=True, num_workers=0 ) test_loader = DataLoader( test_dataset, batch_size=batch_size, shuffle=False, num_workers=0 ) return train_dataset, test_dataset, train_loader, test_loader def evaluate_accuracy(model, data_loader, device): """Evaluate model accuracy.""" model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in data_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() return 100.0 * correct / total def compute_mia_advantage(model, train_loader, test_loader, device, num_samples=2000): """Compute MIA advantage using confidence threshold attack.""" model.eval() def get_confidences(loader, max_samples): all_conf = [] count = 0 with torch.no_grad(): for images, _ in loader: if count >= max_samples: break images = images.to(device) outputs = model(images) probs = F.softmax(outputs, dim=1) conf = probs.max(dim=1).values.cpu().numpy() all_conf.extend(conf) count += len(conf) return np.array(all_conf[:max_samples]) member_conf = get_confidences(train_loader, num_samples) nonmember_conf = get_confidences(test_loader, num_samples) n = min(len(member_conf), len(nonmember_conf)) all_conf = np.concatenate([member_conf[:n], nonmember_conf[:n]]) all_labels = np.concatenate([np.ones(n), np.zeros(n)]) thresholds = np.percentile(all_conf, np.linspace(0, 100, 500)) best_acc = 0.5 for t in thresholds: acc = max( np.mean((all_conf >= t) == all_labels), np.mean((all_conf < t) == all_labels) ) best_acc = max(best_acc, acc) return best_acc, best_acc - 0.5 # ============================================================================= # TRAINING # ============================================================================= print("=" * 80) print(" DP-SGD PRIVACY (SVHN)") print("=" * 80) print(f"\nDevice: {device}") print(f"Target epsilon: {TARGET_EPSILON}") print(f"Required: accuracy >= 55%, MIA advantage <= 5%") print("\nLoading SVHN dataset...") train_dataset, test_dataset, train_loader, test_loader = get_svhn_loaders( batch_size=BATCH_SIZE, download=True ) print(f"Training samples: {len(train_dataset):,}") print(f"Test samples: {len(test_dataset):,}") # Import Opacus from opacus import PrivacyEngine from opacus.validators import ModuleValidator print("\n" + "=" * 80) print(f" TRAINING: DP-SGD MODEL (Target ε={TARGET_EPSILON})") print("=" * 80) # Fresh data loaders for DP training _, _, train_loader_dp, test_loader_dp = get_svhn_loaders( batch_size=BATCH_SIZE, download=False ) # Initialize model dp_model = SVHNCNN().to(device) dp_model = ModuleValidator.fix(dp_model) optimizer_dp = optim.Adam(dp_model.parameters(), lr=1e-3) #optimizer_dp = optim.SGD(dp_model.parameters(), lr=DP_LR, momentum=0.9) # Attach privacy engine privacy_engine = PrivacyEngine(accountant="rdp") dp_model, optimizer_dp, train_loader_dp = privacy_engine.make_private_with_epsilon( module=dp_model, optimizer=optimizer_dp, data_loader=train_loader_dp, target_epsilon=TARGET_EPSILON, target_delta=DELTA, epochs=DP_EPOCHS, max_grad_norm=MAX_GRAD_NORM, ) print(f"\nConfiguration:") print(f" Target epsilon: {TARGET_EPSILON}") print(f" Delta: {DELTA}") print(f" Max gradient norm: {MAX_GRAD_NORM}") print(f" Epochs: {DP_EPOCHS}") # Training loop criterion = nn.CrossEntropyLoss() dp_model.train() for epoch in range(DP_EPOCHS): running_loss = 0.0 correct = 0 total = 0 pbar = tqdm(train_loader_dp, desc=f"Epoch {epoch+1}/{DP_EPOCHS}") for images, labels in pbar: images, labels = images.to(device), labels.to(device) optimizer_dp.zero_grad() outputs = dp_model(images) loss = criterion(outputs, labels) loss.backward() optimizer_dp.step() running_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() pbar.set_postfix({ 'loss': f'{running_loss/total:.4f}', 'acc': f'{100.*correct/total:.2f}%' }) # Get current epsilon epsilon = privacy_engine.get_epsilon(DELTA) print(f" Epoch {epoch+1}: ε = {epsilon:.2f}") final_epsilon = privacy_engine.get_epsilon(DELTA) print(f"\nFinal privacy guarantee: (ε={final_epsilon:.2f}, δ={DELTA})") # Evaluate train_acc = evaluate_accuracy(dp_model, train_loader, device) #train_acc = evaluate_accuracy(dp_model, train_loader_dp, device) test_acc = evaluate_accuracy(dp_model, test_loader_dp, device) print(f"\nDP Model Performance:") print(f" Training accuracy: {train_acc:.2f}%") print(f" Test accuracy: {test_acc:.2f}%") print(f" Overfitting gap: {train_acc - test_acc:.2f}%") # MIA evaluation print("\n" + "=" * 80) print(" MEMBERSHIP INFERENCE EVALUATION") print("=" * 80) mia_acc, mia_adv = compute_mia_advantage( dp_model, train_loader_dp, test_loader_dp, device ) print(f"\nMIA Results:") print(f" Attack accuracy: {mia_acc:.4f}") print(f" Attack advantage: {mia_adv:.4f} ({mia_adv*100:.2f}%)") # Save model model_path = os.path.join(MODELS_DIR, "dp_model.safetensors") save_file(dp_model._module.state_dict(), model_path) print(f"\nModel saved to {model_path}") # ============================================================================= # VALIDATION CHECK # ============================================================================= print("\n" + "=" * 80) print(" VALIDATION CHECK") print("=" * 80) accuracy_pass = test_acc >= 55.0 mia_pass = mia_adv <= 0.05 print(f"\nChallenge Requirements:") print(f" Accuracy >= 55%: {test_acc:.2f}% {'✓ PASS' if accuracy_pass else '✗ FAIL'}") print(f" MIA advantage <= 5%: {mia_adv*100:.2f}% {'✓ PASS' if mia_pass else '✗ FAIL'}") if accuracy_pass and mia_pass: print("\n" + "=" * 80) print(" SOLUTION READY FOR SUBMISSION") print("=" * 80) print(f"\nSubmit {model_path} to the validator to get the flag!") else: print("\n" + "=" * 80) print(" SOLUTION NOT YET PASSING") print("=" * 80) print("\nConsider adjusting epsilon or other hyperparameters.") import requests with open("./models/dp_model.safetensors", "rb") as f: response = requests.post( "http://154.57.164.72:32303/validate", files={"model": ("dp_model.safetensors", f, "application/octet-stream")} ) print(response.text)