added material
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,714 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "63b9ead7-f9e3-4edd-855c-4ec0d9b59e15",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz\n",
|
||||
"Failed to download (trying next):\n",
|
||||
"HTTP Error 404: Not Found\n",
|
||||
"\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz to ./data/MNIST/raw/train-images-idx3-ubyte.gz\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|████████████████████████████████████████████████████████████████████████████████████████████████████████| 9.91M/9.91M [00:00<00:00, 51.7MB/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Extracting ./data/MNIST/raw/train-images-idx3-ubyte.gz to ./data/MNIST/raw\n",
|
||||
"\n",
|
||||
"Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz\n",
|
||||
"Failed to download (trying next):\n",
|
||||
"HTTP Error 404: Not Found\n",
|
||||
"\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz to ./data/MNIST/raw/train-labels-idx1-ubyte.gz\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|████████████████████████████████████████████████████████████████████████████████████████████████████████| 28.9k/28.9k [00:00<00:00, 1.38MB/s]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Extracting ./data/MNIST/raw/train-labels-idx1-ubyte.gz to ./data/MNIST/raw\n",
|
||||
"\n",
|
||||
"Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz\n",
|
||||
"Failed to download (trying next):\n",
|
||||
"HTTP Error 404: Not Found\n",
|
||||
"\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz to ./data/MNIST/raw/t10k-images-idx3-ubyte.gz\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.65M/1.65M [00:00<00:00, 11.4MB/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Extracting ./data/MNIST/raw/t10k-images-idx3-ubyte.gz to ./data/MNIST/raw\n",
|
||||
"\n",
|
||||
"Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz\n",
|
||||
"Failed to download (trying next):\n",
|
||||
"HTTP Error 404: Not Found\n",
|
||||
"\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz\n",
|
||||
"Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|████████████████████████████████████████████████████████████████████████████████████████████████████████| 4.54k/4.54k [00:00<00:00, 5.35MB/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Extracting ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw\n",
|
||||
"\n",
|
||||
"Epoch 1/10: Avg Loss = 0.3898, Test Accuracy = 96.67%\n",
|
||||
"Epoch 2/10: Avg Loss = 0.1010, Test Accuracy = 97.92%\n",
|
||||
"Epoch 3/10: Avg Loss = 0.0693, Test Accuracy = 98.43%\n",
|
||||
"Epoch 4/10: Avg Loss = 0.0523, Test Accuracy = 98.18%\n",
|
||||
"Epoch 5/10: Avg Loss = 0.0448, Test Accuracy = 98.82%\n",
|
||||
"Epoch 6/10: Avg Loss = 0.0368, Test Accuracy = 98.56%\n",
|
||||
"Epoch 7/10: Avg Loss = 0.0327, Test Accuracy = 98.95%\n",
|
||||
"Epoch 8/10: Avg Loss = 0.0280, Test Accuracy = 99.02%\n",
|
||||
"Epoch 9/10: Avg Loss = 0.0254, Test Accuracy = 98.89%\n",
|
||||
"Epoch 10/10: Avg Loss = 0.0211, Test Accuracy = 99.02%\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"import numpy as np\n",
|
||||
"import torch\n",
|
||||
"import torch.nn as nn\n",
|
||||
"import torch.nn.functional as F\n",
|
||||
"import torch.optim as optim\n",
|
||||
"from safetensors.torch import save_file\n",
|
||||
"from tqdm import tqdm\n",
|
||||
"\n",
|
||||
"from htb_ai_library import (\n",
|
||||
" set_reproducibility,\n",
|
||||
" get_mnist_loaders,\n",
|
||||
" evaluate_accuracy,\n",
|
||||
" train_model,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"MNIST_MEAN = 0.1307\n",
|
||||
"MNIST_STD = 0.3081\n",
|
||||
"EPSILON = 0.3\n",
|
||||
"EPSILON_SPREAD = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]\n",
|
||||
"I_FGSM_STEPS = 10\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_device():\n",
|
||||
" \"\"\"Get the best available device.\"\"\"\n",
|
||||
" if torch.cuda.is_available():\n",
|
||||
" return torch.device(\"cuda\")\n",
|
||||
" return torch.device(\"cpu\")\n",
|
||||
"\n",
|
||||
"def save_adversarial_examples(data, path):\n",
|
||||
" \"\"\"Save adversarial examples to safetensors format.\"\"\"\n",
|
||||
" # We don't store fgsm_images/ifgsm_images separately since they reference\n",
|
||||
" # fgsm_by_epsilon[epsilon] and would cause memory sharing errors in safetensors\n",
|
||||
" tensors = {\n",
|
||||
" 'clean_images': data['clean_images'],\n",
|
||||
" 'clean_labels': data['clean_labels'].long(),\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" for eps in data['epsilon_spread']:\n",
|
||||
" key = f\"{eps:.1f}\"\n",
|
||||
" tensors[f'fgsm_eps_{key}'] = data['fgsm_by_epsilon'][eps]\n",
|
||||
" tensors[f'ifgsm_eps_{key}'] = data['ifgsm_by_epsilon'][eps]\n",
|
||||
"\n",
|
||||
" metadata = {\n",
|
||||
" 'epsilon': str(data['epsilon']),\n",
|
||||
" 'epsilon_spread': json.dumps(data['epsilon_spread']),\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" save_file(tensors, path, metadata=metadata)\n",
|
||||
"\n",
|
||||
"class LeNet5(nn.Module):\n",
|
||||
" def __init__(self):\n",
|
||||
" super().__init__()\n",
|
||||
" self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2)\n",
|
||||
" self.conv2 = nn.Conv2d(6, 16, kernel_size=5)\n",
|
||||
" self.fc1 = nn.Linear(16 * 5 * 5, 120)\n",
|
||||
" self.fc2 = nn.Linear(120, 84)\n",
|
||||
" self.fc3 = nn.Linear(84, 10)\n",
|
||||
"\n",
|
||||
" def forward(self, x):\n",
|
||||
" x = F.max_pool2d(F.relu(self.conv1(x)), 2)\n",
|
||||
" x = F.max_pool2d(F.relu(self.conv2(x)), 2)\n",
|
||||
" x = x.view(-1, 16 * 5 * 5)\n",
|
||||
" x = F.relu(self.fc1(x))\n",
|
||||
" x = F.relu(self.fc2(x))\n",
|
||||
" x = self.fc3(x)\n",
|
||||
" return x\n",
|
||||
"\n",
|
||||
"def fgsm_attack(model, images, labels, epsilon):\n",
|
||||
" images_copy = images.clone().detach().requires_grad_(True)\n",
|
||||
" outputs = model(images_copy)\n",
|
||||
" loss = F.cross_entropy(outputs, labels)\n",
|
||||
" model.zero_grad()\n",
|
||||
" loss.backward()\n",
|
||||
" grad_sign = images_copy.grad.sign()\n",
|
||||
" adv_images = images_copy + epsilon * grad_sign\n",
|
||||
" min_val = (0 - MNIST_MEAN) / MNIST_STD\n",
|
||||
" max_val = (1 - MNIST_MEAN) / MNIST_STD\n",
|
||||
" adv_images = torch.clamp(adv_images, min_val, max_val)\n",
|
||||
" return adv_images.detach()\n",
|
||||
"\n",
|
||||
"def i_fgsm_attack(model, images, labels, epsilon, steps=I_FGSM_STEPS):\n",
|
||||
" \"\"\"Generate I-FGSM (Iterative FGSM) adversarial examples.\"\"\"\n",
|
||||
" alpha = epsilon / steps # Step size per iteration\n",
|
||||
" min_val = (0 - MNIST_MEAN) / MNIST_STD\n",
|
||||
" max_val = (1 - MNIST_MEAN) / MNIST_STD\n",
|
||||
"\n",
|
||||
" adv_images = images.clone().detach()\n",
|
||||
" original_images = images.clone().detach()\n",
|
||||
"\n",
|
||||
" for _ in range(steps):\n",
|
||||
" adv_images.requires_grad = True\n",
|
||||
" outputs = model(adv_images)\n",
|
||||
" loss = F.cross_entropy(outputs, labels)\n",
|
||||
" model.zero_grad()\n",
|
||||
" loss.backward()\n",
|
||||
"\n",
|
||||
" grad_sign = adv_images.grad.sign()\n",
|
||||
" adv_images = adv_images.detach() + alpha * grad_sign\n",
|
||||
"\n",
|
||||
" # Project back to epsilon-ball around original\n",
|
||||
" perturbation = adv_images - original_images\n",
|
||||
" perturbation = torch.clamp(perturbation, -epsilon, epsilon)\n",
|
||||
" adv_images = original_images + perturbation\n",
|
||||
"\n",
|
||||
" # Clamp to valid range\n",
|
||||
" adv_images = torch.clamp(adv_images, min_val, max_val)\n",
|
||||
"\n",
|
||||
" return adv_images.detach()\n",
|
||||
"\n",
|
||||
"def evaluate_adversarial_accuracy(model, loader, device, epsilon, num_batches=None):\n",
|
||||
" \"\"\"Evaluate accuracy under FGSM attack.\"\"\"\n",
|
||||
" model.eval()\n",
|
||||
" correct = 0\n",
|
||||
" total = 0\n",
|
||||
"\n",
|
||||
" for i, (images, labels) in enumerate(loader):\n",
|
||||
" if num_batches is not None and i >= num_batches:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" images, labels = images.to(device), labels.to(device)\n",
|
||||
"\n",
|
||||
" # Generate adversarial examples (need gradients, so briefly enable train mode)\n",
|
||||
" model.train()\n",
|
||||
" adv_images = fgsm_attack(model, images, labels, epsilon)\n",
|
||||
" model.eval()\n",
|
||||
"\n",
|
||||
" with torch.no_grad():\n",
|
||||
" outputs = model(adv_images)\n",
|
||||
" _, predicted = outputs.max(1)\n",
|
||||
" total += labels.size(0)\n",
|
||||
" correct += predicted.eq(labels).sum().item()\n",
|
||||
"\n",
|
||||
" return 100.0 * correct / total\n",
|
||||
"\n",
|
||||
"train_loader, test_loader = get_mnist_loaders(batch_size=128, data_dir=\"./data\")\n",
|
||||
"baseline_model = LeNet5()\n",
|
||||
"baseline_model = train_model(\n",
|
||||
" baseline_model,\n",
|
||||
" train_loader,\n",
|
||||
" test_loader,\n",
|
||||
" device=get_device(),\n",
|
||||
" epochs=10,\n",
|
||||
" learning_rate=0.001,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "b1dfab4c-0326-4093-b3f9-71489525129e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Using device: cpu\n",
|
||||
"Train batches: 469, Test batches: 79\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def generate_adversarial_examples(model, test_loader, device, num_samples=500):\n",
|
||||
" \"\"\"Generate adversarial examples across multiple epsilon values.\"\"\"\n",
|
||||
" model.eval()\n",
|
||||
"\n",
|
||||
" # Collect clean samples\n",
|
||||
" clean_images_list = []\n",
|
||||
" clean_labels_list = []\n",
|
||||
" collected = 0\n",
|
||||
"\n",
|
||||
" print(f\"Collecting {num_samples} clean samples...\")\n",
|
||||
" for images, labels in test_loader:\n",
|
||||
" if collected >= num_samples:\n",
|
||||
" break\n",
|
||||
" batch_size = min(images.size(0), num_samples - collected)\n",
|
||||
" clean_images_list.append(images[:batch_size])\n",
|
||||
" clean_labels_list.append(labels[:batch_size])\n",
|
||||
" collected += batch_size\n",
|
||||
"\n",
|
||||
" clean_images = torch.cat(clean_images_list, dim=0)\n",
|
||||
" clean_labels = torch.cat(clean_labels_list, dim=0)\n",
|
||||
"\n",
|
||||
" # Generate adversarial examples at each epsilon\n",
|
||||
" fgsm_by_epsilon = {}\n",
|
||||
" ifgsm_by_epsilon = {}\n",
|
||||
"\n",
|
||||
" print(f\"\\nGenerating adversarial examples across epsilon spread: {EPSILON_SPREAD}\")\n",
|
||||
"\n",
|
||||
" for eps in EPSILON_SPREAD:\n",
|
||||
" print(f\"\\n Generating at epsilon={eps}...\")\n",
|
||||
" fgsm_images_list = []\n",
|
||||
" ifgsm_images_list = []\n",
|
||||
"\n",
|
||||
" batch_size = 128\n",
|
||||
" pbar = tqdm(total=num_samples, desc=f\" eps={eps}\")\n",
|
||||
"\n",
|
||||
" for i in range(0, num_samples, batch_size):\n",
|
||||
" end_idx = min(i + batch_size, num_samples)\n",
|
||||
" images = clean_images[i:end_idx].to(device)\n",
|
||||
" labels = clean_labels[i:end_idx].to(device)\n",
|
||||
"\n",
|
||||
" model.train() # Need train mode for gradient computation\n",
|
||||
" fgsm_images = fgsm_attack(model, images, labels, eps)\n",
|
||||
" ifgsm_images = i_fgsm_attack(model, images, labels, eps)\n",
|
||||
" model.eval()\n",
|
||||
"\n",
|
||||
" fgsm_images_list.append(fgsm_images.cpu())\n",
|
||||
" ifgsm_images_list.append(ifgsm_images.cpu())\n",
|
||||
" pbar.update(end_idx - i)\n",
|
||||
"\n",
|
||||
" pbar.close()\n",
|
||||
"\n",
|
||||
" fgsm_by_epsilon[eps] = torch.cat(fgsm_images_list, dim=0)\n",
|
||||
" ifgsm_by_epsilon[eps] = torch.cat(ifgsm_images_list, dim=0)\n",
|
||||
"\n",
|
||||
" return {\n",
|
||||
" 'clean_images': clean_images,\n",
|
||||
" 'clean_labels': clean_labels,\n",
|
||||
" 'fgsm_images': fgsm_by_epsilon[EPSILON],\n",
|
||||
" 'ifgsm_images': ifgsm_by_epsilon[EPSILON],\n",
|
||||
" 'epsilon': EPSILON,\n",
|
||||
" 'epsilon_spread': EPSILON_SPREAD,\n",
|
||||
" 'fgsm_by_epsilon': fgsm_by_epsilon,\n",
|
||||
" 'ifgsm_by_epsilon': ifgsm_by_epsilon\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def train_adversarial(model, train_loader, test_loader, device,\n",
|
||||
" epochs=25, lr=0.001, epsilon=EPSILON):\n",
|
||||
" \"\"\"Train model with adversarial training using epsilon spread.\"\"\"\n",
|
||||
" model.to(device)\n",
|
||||
"\n",
|
||||
" optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\n",
|
||||
" scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
||||
" criterion = nn.CrossEntropyLoss()\n",
|
||||
"\n",
|
||||
" for epoch in range(epochs):\n",
|
||||
" model.train()\n",
|
||||
" total_loss = 0.0\n",
|
||||
" correct = 0\n",
|
||||
" total = 0\n",
|
||||
"\n",
|
||||
" pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{epochs}\")\n",
|
||||
" for images, labels in pbar:\n",
|
||||
" images, labels = images.to(device), labels.to(device)\n",
|
||||
"\n",
|
||||
" batch_epsilon = np.random.choice(EPSILON_SPREAD)\n",
|
||||
" adv_images = fgsm_attack(model, images, labels, batch_epsilon)\n",
|
||||
"\n",
|
||||
" combined_images = torch.cat([images, adv_images], dim=0)\n",
|
||||
" combined_labels = torch.cat([labels, labels], dim=0)\n",
|
||||
"\n",
|
||||
" perm = torch.randperm(combined_images.size(0))\n",
|
||||
" combined_images = combined_images[perm]\n",
|
||||
" combined_labels = combined_labels[perm]\n",
|
||||
"\n",
|
||||
" optimizer.zero_grad()\n",
|
||||
" outputs = model(combined_images)\n",
|
||||
" loss = criterion(outputs, combined_labels)\n",
|
||||
" loss.backward()\n",
|
||||
" torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n",
|
||||
" optimizer.step()\n",
|
||||
"\n",
|
||||
" total_loss += loss.item()\n",
|
||||
" _, predicted = outputs.max(1)\n",
|
||||
" total += combined_labels.size(0)\n",
|
||||
" correct += predicted.eq(combined_labels).sum().item()\n",
|
||||
"\n",
|
||||
" pbar.set_postfix({\n",
|
||||
" 'loss': f'{total_loss / (pbar.n + 1):.4f}',\n",
|
||||
" 'acc': f'{100.0 * correct / total:.2f}%'\n",
|
||||
" })\n",
|
||||
"\n",
|
||||
" scheduler.step()\n",
|
||||
"\n",
|
||||
" if (epoch + 1) % 5 == 0 or epoch == epochs - 1:\n",
|
||||
" clean_acc = evaluate_accuracy(model, test_loader, device)\n",
|
||||
" adv_acc = evaluate_adversarial_accuracy(\n",
|
||||
" model, test_loader, device, epsilon, num_batches=20\n",
|
||||
" )\n",
|
||||
" print(f\"\\n Epoch {epoch+1}: Clean={clean_acc:.1f}%, Robust={adv_acc:.1f}%\")\n",
|
||||
"\n",
|
||||
" return model\n",
|
||||
"\n",
|
||||
"set_reproducibility(1337)\n",
|
||||
"device = get_device()\n",
|
||||
"print(f\"Using device: {device}\")\n",
|
||||
"\n",
|
||||
"train_loader, test_loader = get_mnist_loaders(normalize=True)\n",
|
||||
"print(f\"Train batches: {len(train_loader)}, Test batches: {len(test_loader)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "658b5b4c-74f9-44bf-a959-8ea7ab27b7c5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Epoch 1/10: Avg Loss = 0.3098, Test Accuracy = 97.53%\n",
|
||||
"Epoch 2/10: Avg Loss = 0.0809, Test Accuracy = 98.48%\n",
|
||||
"Epoch 3/10: Avg Loss = 0.0539, Test Accuracy = 98.63%\n",
|
||||
"Epoch 4/10: Avg Loss = 0.0431, Test Accuracy = 98.49%\n",
|
||||
"Epoch 5/10: Avg Loss = 0.0348, Test Accuracy = 98.78%\n",
|
||||
"Epoch 6/10: Avg Loss = 0.0295, Test Accuracy = 98.71%\n",
|
||||
"Epoch 7/10: Avg Loss = 0.0257, Test Accuracy = 98.78%\n",
|
||||
"Epoch 8/10: Avg Loss = 0.0204, Test Accuracy = 98.84%\n",
|
||||
"Epoch 9/10: Avg Loss = 0.0167, Test Accuracy = 98.96%\n",
|
||||
"Epoch 10/10: Avg Loss = 0.0151, Test Accuracy = 98.88%\n",
|
||||
"Baseline Model - Robust: 74.8%\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"baseline_model = LeNet5()\n",
|
||||
"baseline_model = train_model(baseline_model, train_loader, test_loader,\n",
|
||||
" device=device, epochs=10, learning_rate=0.001)\n",
|
||||
"\n",
|
||||
"adv_acc = evaluate_adversarial_accuracy(baseline_model, test_loader, device, EPSILON)\n",
|
||||
"print(f\"Baseline Model - Robust: {adv_acc:.1f}%\")\n",
|
||||
"save_file(baseline_model.state_dict(), \"baseline_model.safetensors\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "eca344cf-f1b2-43d3-b563-8a618fd8f97a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Collecting 500 clean samples...\n",
|
||||
"\n",
|
||||
"Generating adversarial examples across epsilon spread: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]\n",
|
||||
"\n",
|
||||
" Generating at epsilon=0.1...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.1: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 3589.91it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.2...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.2: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 3783.98it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.3...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.3: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 4337.29it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.4...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.4: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 4218.09it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.5...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.5: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 2903.09it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.6...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.6: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 2815.23it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.7...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.7: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 2339.08it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.8...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.8: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 2391.63it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=0.9...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=0.9: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 2493.57it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Generating at epsilon=1.0...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" eps=1.0: 100%|██████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 2372.05it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Adversarial examples saved to adv_examples.safetensors\n",
|
||||
"Before training - Clean: 10.2%, Robust: 2.4%\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Epoch 1/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 70.61it/s, loss=0.7535, acc=75.68%]\n",
|
||||
"Epoch 2/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 73.95it/s, loss=0.3464, acc=88.43%]\n",
|
||||
"Epoch 3/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 71.96it/s, loss=0.2837, acc=90.47%]\n",
|
||||
"Epoch 4/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 72.84it/s, loss=0.2241, acc=92.43%]\n",
|
||||
"Epoch 5/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 72.62it/s, loss=0.1920, acc=93.46%]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Epoch 5: Clean=98.9%, Robust=92.9%\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Epoch 6/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 71.49it/s, loss=0.1725, acc=94.28%]\n",
|
||||
"Epoch 7/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 72.60it/s, loss=0.1551, acc=94.83%]\n",
|
||||
"Epoch 8/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 73.79it/s, loss=0.1474, acc=95.11%]\n",
|
||||
"Epoch 9/10: 100%|██████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 73.62it/s, loss=0.1393, acc=95.43%]\n",
|
||||
"Epoch 10/10: 100%|█████████████████████████████████████████████████████████████████████| 469/469 [00:06<00:00, 72.08it/s, loss=0.1319, acc=95.60%]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Epoch 10: Clean=99.0%, Robust=93.8%\n",
|
||||
"Final - Clean: 99.0%, Robust: 95.7%\n",
|
||||
"Model saved to robust_model.safetensors\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"adv_data = generate_adversarial_examples(\n",
|
||||
" baseline_model, test_loader, device, num_samples=500\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"save_adversarial_examples(adv_data, \"adv_examples.safetensors\")\n",
|
||||
"print(\"Adversarial examples saved to adv_examples.safetensors\")\n",
|
||||
"\n",
|
||||
"model = LeNet5()\n",
|
||||
"model.to(device)\n",
|
||||
"\n",
|
||||
"clean_acc = evaluate_accuracy(model, test_loader, device)\n",
|
||||
"adv_acc = evaluate_adversarial_accuracy(model, test_loader, device, EPSILON)\n",
|
||||
"print(f\"Before training - Clean: {clean_acc:.1f}%, Robust: {adv_acc:.1f}%\")\n",
|
||||
"\n",
|
||||
"model = train_adversarial(model, train_loader, test_loader, device, epochs=10)\n",
|
||||
"\n",
|
||||
"clean_acc = evaluate_accuracy(model, test_loader, device)\n",
|
||||
"adv_acc = evaluate_adversarial_accuracy(model, test_loader, device, EPSILON)\n",
|
||||
"print(f\"Final - Clean: {clean_acc:.1f}%, Robust: {adv_acc:.1f}%\")\n",
|
||||
"\n",
|
||||
"save_file(model.state_dict(), \"robust_model.safetensors\")\n",
|
||||
"print(\"Model saved to robust_model.safetensors\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "2b8045fa-9e0b-468c-b46c-0dedbab41f61",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,581 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Adversarial Robustness Evaluator
|
||||
|
||||
This standalone evaluator tests your adversarially-trained model against
|
||||
pre-generated FGSM and I-FGSM attacks. Run this after training to measure
|
||||
your model's robustness metrics.
|
||||
|
||||
Usage:
|
||||
python evaluate_robustness.py --model-path robust_model.safetensors
|
||||
python evaluate_robustness.py --model-path models/ --compare
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
# Primary epsilon for main results
|
||||
PRIMARY_EPSILON = 0.3
|
||||
|
||||
# Files to skip when scanning directories for models
|
||||
SKIP_FILES = {"baseline_model.safetensors", "adv_examples.safetensors"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model Definition (lazy import)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def create_lenet5():
|
||||
"""Create a LeNet-5 model instance."""
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
class LeNet5(nn.Module):
|
||||
"""Classic LeNet-5 architecture for MNIST classification."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2)
|
||||
self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
|
||||
self.fc1 = nn.Linear(16 * 5 * 5, 120)
|
||||
self.fc2 = nn.Linear(120, 84)
|
||||
self.fc3 = nn.Linear(84, 10)
|
||||
|
||||
def forward(self, x):
|
||||
x = F.max_pool2d(F.relu(self.conv1(x)), 2)
|
||||
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
|
||||
x = x.view(-1, 16 * 5 * 5)
|
||||
x = F.relu(self.fc1(x))
|
||||
x = F.relu(self.fc2(x))
|
||||
x = self.fc3(x)
|
||||
return x
|
||||
|
||||
return LeNet5()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Safetensors Helper Functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_adversarial_examples_safetensors(filepath: Path) -> Dict:
|
||||
"""Load adversarial examples from safetensors format."""
|
||||
from safetensors import safe_open
|
||||
from safetensors.torch import load_file
|
||||
|
||||
tensors = load_file(str(filepath))
|
||||
|
||||
# Extract metadata
|
||||
with safe_open(str(filepath), framework="pt") as f:
|
||||
metadata = f.metadata()
|
||||
|
||||
epsilon = float(metadata["epsilon"])
|
||||
epsilon_spread = json.loads(metadata["epsilon_spread"])
|
||||
|
||||
# Reconstruct epsilon-keyed dicts
|
||||
fgsm_by_epsilon = {}
|
||||
ifgsm_by_epsilon = {}
|
||||
for key, tensor in tensors.items():
|
||||
if key.startswith("fgsm_eps_"):
|
||||
eps = float(key.replace("fgsm_eps_", ""))
|
||||
fgsm_by_epsilon[eps] = tensor
|
||||
elif key.startswith("ifgsm_eps_"):
|
||||
eps = float(key.replace("ifgsm_eps_", ""))
|
||||
ifgsm_by_epsilon[eps] = tensor
|
||||
|
||||
return {
|
||||
"clean_images": tensors["clean_images"],
|
||||
"clean_labels": tensors["clean_labels"],
|
||||
"fgsm_images": fgsm_by_epsilon[epsilon],
|
||||
"ifgsm_images": ifgsm_by_epsilon[epsilon],
|
||||
"epsilon": epsilon,
|
||||
"epsilon_spread": epsilon_spread,
|
||||
"fgsm_by_epsilon": fgsm_by_epsilon,
|
||||
"ifgsm_by_epsilon": ifgsm_by_epsilon,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File Discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def find_models(path: Path) -> List[Path]:
|
||||
"""Find model files to evaluate."""
|
||||
if path.is_file() and path.suffix == ".safetensors":
|
||||
return [path]
|
||||
|
||||
if path.is_dir():
|
||||
models = []
|
||||
for f in sorted(path.glob("*.safetensors")):
|
||||
if f.name not in SKIP_FILES:
|
||||
models.append(f)
|
||||
return models
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def find_baseline(model_path: Path) -> Optional[Path]:
|
||||
"""Find baseline_model.safetensors relative to the model path."""
|
||||
if model_path.is_file():
|
||||
search_dir = model_path.parent
|
||||
else:
|
||||
search_dir = model_path
|
||||
|
||||
for check_dir in [search_dir, search_dir.parent, SCRIPT_DIR]:
|
||||
baseline = check_dir / "baseline_model.safetensors"
|
||||
if baseline.exists():
|
||||
return baseline
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_adv_examples(model_path: Path) -> Optional[Path]:
|
||||
"""Find adv_examples.safetensors relative to the model path."""
|
||||
if model_path.is_file():
|
||||
search_dir = model_path.parent
|
||||
else:
|
||||
search_dir = model_path
|
||||
|
||||
for check_dir in [search_dir, search_dir.parent, SCRIPT_DIR]:
|
||||
adv = check_dir / "adv_examples.safetensors"
|
||||
if adv.exists():
|
||||
return adv
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Evaluation Functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_model(model_path: Path):
|
||||
"""Load a trained model from safetensors checkpoint."""
|
||||
from safetensors.torch import load_file
|
||||
|
||||
model = create_lenet5()
|
||||
state_dict = load_file(str(model_path))
|
||||
model.load_state_dict(state_dict)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def load_adversarial_examples(adv_path: Path) -> Dict:
|
||||
"""Load pre-generated adversarial examples from safetensors."""
|
||||
return load_adversarial_examples_safetensors(adv_path)
|
||||
|
||||
|
||||
def evaluate_accuracy(model, images, labels, device) -> Tuple[float, int, int]:
|
||||
"""Compute accuracy and return (accuracy%, correct, total)."""
|
||||
import torch
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
images = images.to(device)
|
||||
labels = labels.to(device)
|
||||
outputs = model(images)
|
||||
_, predicted = outputs.max(1)
|
||||
correct = predicted.eq(labels).sum().item()
|
||||
total = labels.size(0)
|
||||
|
||||
return 100.0 * correct / total, correct, total
|
||||
|
||||
|
||||
def get_misclassified_samples(
|
||||
model, images, labels, device, max_samples: int = 5
|
||||
) -> list:
|
||||
"""Get indices and predictions of misclassified samples."""
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
images = images.to(device)
|
||||
labels = labels.to(device)
|
||||
outputs = model(images)
|
||||
_, predicted = outputs.max(1)
|
||||
wrong = predicted != labels
|
||||
wrong_indices = wrong.nonzero(as_tuple=True)[0][:max_samples]
|
||||
|
||||
failures = []
|
||||
for idx in wrong_indices:
|
||||
failures.append(
|
||||
{
|
||||
"index": idx.item(),
|
||||
"true_label": labels[idx].item(),
|
||||
"predicted": predicted[idx].item(),
|
||||
"confidence": F.softmax(outputs[idx], dim=0).max().item(),
|
||||
}
|
||||
)
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def evaluate_model_full(model_path: Path, adv_data: Dict, device) -> Dict:
|
||||
"""Evaluate a model across all epsilon values and return full results."""
|
||||
clean_images = adv_data["clean_images"]
|
||||
clean_labels = adv_data["clean_labels"]
|
||||
epsilon = adv_data["epsilon"]
|
||||
|
||||
# Check for epsilon spread vs single epsilon
|
||||
has_spread = "epsilon_spread" in adv_data and "fgsm_by_epsilon" in adv_data
|
||||
if has_spread:
|
||||
epsilon_spread = adv_data["epsilon_spread"]
|
||||
fgsm_by_epsilon = adv_data["fgsm_by_epsilon"]
|
||||
ifgsm_by_epsilon = adv_data["ifgsm_by_epsilon"]
|
||||
else:
|
||||
epsilon_spread = [epsilon]
|
||||
fgsm_by_epsilon = {epsilon: adv_data["fgsm_images"]}
|
||||
ifgsm_by_epsilon = {epsilon: adv_data["ifgsm_images"]}
|
||||
|
||||
model = load_model(model_path)
|
||||
model.to(device)
|
||||
|
||||
# Evaluate clean accuracy
|
||||
clean_acc, clean_correct, clean_total = evaluate_accuracy(
|
||||
model, clean_images, clean_labels, device
|
||||
)
|
||||
|
||||
# Evaluate across all epsilon values
|
||||
fgsm_results = {}
|
||||
ifgsm_results = {}
|
||||
for eps in epsilon_spread:
|
||||
fgsm_acc, _, _ = evaluate_accuracy(
|
||||
model, fgsm_by_epsilon[eps], clean_labels, device
|
||||
)
|
||||
ifgsm_acc, _, _ = evaluate_accuracy(
|
||||
model, ifgsm_by_epsilon[eps], clean_labels, device
|
||||
)
|
||||
fgsm_results[eps] = fgsm_acc
|
||||
ifgsm_results[eps] = ifgsm_acc
|
||||
|
||||
return {
|
||||
"name": model_path.name,
|
||||
"clean_acc": clean_acc,
|
||||
"clean_correct": clean_correct,
|
||||
"clean_total": clean_total,
|
||||
"fgsm_results": fgsm_results,
|
||||
"ifgsm_results": ifgsm_results,
|
||||
"epsilon_spread": epsilon_spread,
|
||||
"model": model,
|
||||
}
|
||||
|
||||
|
||||
def print_model_results(results: Dict, baseline: Optional[Dict] = None) -> None:
|
||||
"""Print detailed results for a single model."""
|
||||
name = results["name"]
|
||||
clean_acc = results["clean_acc"]
|
||||
fgsm_results = results["fgsm_results"]
|
||||
ifgsm_results = results["ifgsm_results"]
|
||||
epsilon_spread = results["epsilon_spread"]
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"Model: {name}")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
# Clean accuracy
|
||||
print(
|
||||
f"\nClean Accuracy: {clean_acc:.1f}% ({results['clean_correct']}/{results['clean_total']})"
|
||||
)
|
||||
|
||||
# Epsilon spread table
|
||||
print(
|
||||
f"\n{'Epsilon':<10} {'FGSM':>10} {'I-FGSM':>10} {'FGSM Attack':>12} {'I-FGSM Attack':>14}"
|
||||
)
|
||||
print("-" * 60)
|
||||
|
||||
for eps in epsilon_spread:
|
||||
fgsm_acc = fgsm_results[eps]
|
||||
ifgsm_acc = ifgsm_results[eps]
|
||||
fgsm_attack = 100.0 - fgsm_acc
|
||||
ifgsm_attack = 100.0 - ifgsm_acc
|
||||
marker = " *" if eps == PRIMARY_EPSILON else ""
|
||||
print(
|
||||
f"{eps:<10.2f} {fgsm_acc:>9.1f}% {ifgsm_acc:>9.1f}% {fgsm_attack:>11.1f}% {ifgsm_attack:>13.1f}%{marker}"
|
||||
)
|
||||
|
||||
print("-" * 60)
|
||||
print("* Primary epsilon for summary metrics")
|
||||
|
||||
# Summary at primary epsilon
|
||||
primary_fgsm = fgsm_results[PRIMARY_EPSILON]
|
||||
primary_ifgsm = ifgsm_results[PRIMARY_EPSILON]
|
||||
|
||||
print(f"\nSummary (ε={PRIMARY_EPSILON}):")
|
||||
print(f" Clean: {clean_acc:5.1f}%")
|
||||
print(
|
||||
f" FGSM: {primary_fgsm:5.1f}% (attack success: {100 - primary_fgsm:5.1f}%)"
|
||||
)
|
||||
print(
|
||||
f" I-FGSM: {primary_ifgsm:5.1f}% (attack success: {100 - primary_ifgsm:5.1f}%)"
|
||||
)
|
||||
|
||||
# Comparison with baseline if provided
|
||||
if baseline:
|
||||
base_fgsm = baseline["fgsm_results"][PRIMARY_EPSILON]
|
||||
base_ifgsm = baseline["ifgsm_results"][PRIMARY_EPSILON]
|
||||
|
||||
fgsm_improve = primary_fgsm - base_fgsm
|
||||
ifgsm_improve = primary_ifgsm - base_ifgsm
|
||||
|
||||
print(f"\nImprovement over baseline:")
|
||||
print(
|
||||
f" FGSM: {'+' if fgsm_improve >= 0 else ''}{fgsm_improve:.1f}% ({base_fgsm:.1f}% → {primary_fgsm:.1f}%)"
|
||||
)
|
||||
print(
|
||||
f" I-FGSM: {'+' if ifgsm_improve >= 0 else ''}{ifgsm_improve:.1f}% ({base_ifgsm:.1f}% → {primary_ifgsm:.1f}%)"
|
||||
)
|
||||
|
||||
|
||||
def print_comparison_summary(
|
||||
all_results: List[Dict], baseline: Optional[Dict] = None
|
||||
) -> None:
|
||||
"""Print a full epsilon spread comparison table of all models."""
|
||||
epsilon_spread = all_results[0]["epsilon_spread"]
|
||||
|
||||
print(f"\n{'=' * 90}")
|
||||
print("COMPARISON SUMMARY - Full Epsilon Spread")
|
||||
print(f"{'=' * 90}")
|
||||
|
||||
# Build model list (baseline first if present)
|
||||
models_to_show = []
|
||||
if baseline:
|
||||
models_to_show.append(("baseline", baseline))
|
||||
for r in all_results:
|
||||
models_to_show.append((r["name"], r))
|
||||
|
||||
# Print clean accuracy row
|
||||
print(f"\n{'Clean Accuracy:':<20}", end="")
|
||||
for name, r in models_to_show:
|
||||
short_name = name[:18] if len(name) > 18 else name
|
||||
print(f"{short_name:>18}", end="")
|
||||
print()
|
||||
print(f"{'':<20}", end="")
|
||||
for _, r in models_to_show:
|
||||
print(f"{r['clean_acc']:>17.1f}%", end="")
|
||||
print()
|
||||
|
||||
# FGSM table - Model Accuracy (defender's view)
|
||||
print(f"\n{'FGSM Model Accuracy (defender):'}")
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
print(f"{'Epsilon':<20}", end="")
|
||||
for name, _ in models_to_show:
|
||||
short_name = name[:18] if len(name) > 18 else name
|
||||
print(f"{short_name:>18}", end="")
|
||||
print()
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
|
||||
for eps in epsilon_spread:
|
||||
marker = " *" if eps == PRIMARY_EPSILON else ""
|
||||
print(f"{eps:<20.2f}", end="")
|
||||
for _, r in models_to_show:
|
||||
print(f"{r['fgsm_results'][eps]:>17.1f}%", end="")
|
||||
print(marker)
|
||||
|
||||
# FGSM table - Attack Success (attacker's view)
|
||||
print(f"\n{'FGSM Attack Success (attacker):'}")
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
print(f"{'Epsilon':<20}", end="")
|
||||
for name, _ in models_to_show:
|
||||
short_name = name[:18] if len(name) > 18 else name
|
||||
print(f"{short_name:>18}", end="")
|
||||
print()
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
|
||||
for eps in epsilon_spread:
|
||||
marker = " *" if eps == PRIMARY_EPSILON else ""
|
||||
print(f"{eps:<20.2f}", end="")
|
||||
for _, r in models_to_show:
|
||||
attack_success = 100.0 - r["fgsm_results"][eps]
|
||||
print(f"{attack_success:>17.1f}%", end="")
|
||||
print(marker)
|
||||
|
||||
# I-FGSM table - Model Accuracy (defender's view)
|
||||
print(f"\n{'I-FGSM Model Accuracy (defender):'}")
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
print(f"{'Epsilon':<20}", end="")
|
||||
for name, _ in models_to_show:
|
||||
short_name = name[:18] if len(name) > 18 else name
|
||||
print(f"{short_name:>18}", end="")
|
||||
print()
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
|
||||
for eps in epsilon_spread:
|
||||
marker = " *" if eps == PRIMARY_EPSILON else ""
|
||||
print(f"{eps:<20.2f}", end="")
|
||||
for _, r in models_to_show:
|
||||
print(f"{r['ifgsm_results'][eps]:>17.1f}%", end="")
|
||||
print(marker)
|
||||
|
||||
# I-FGSM table - Attack Success (attacker's view)
|
||||
print(f"\n{'I-FGSM Attack Success (attacker):'}")
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
print(f"{'Epsilon':<20}", end="")
|
||||
for name, _ in models_to_show:
|
||||
short_name = name[:18] if len(name) > 18 else name
|
||||
print(f"{short_name:>18}", end="")
|
||||
print()
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
|
||||
for eps in epsilon_spread:
|
||||
marker = " *" if eps == PRIMARY_EPSILON else ""
|
||||
print(f"{eps:<20.2f}", end="")
|
||||
for _, r in models_to_show:
|
||||
attack_success = 100.0 - r["ifgsm_results"][eps]
|
||||
print(f"{attack_success:>17.1f}%", end="")
|
||||
print(marker)
|
||||
|
||||
print("-" * (20 + 18 * len(models_to_show)))
|
||||
print("* Primary epsilon for summary metrics")
|
||||
|
||||
# Improvement summary at primary epsilon (if baseline exists)
|
||||
if baseline:
|
||||
print(f"\nImprovement over baseline at ε={PRIMARY_EPSILON}:")
|
||||
base_fgsm = baseline["fgsm_results"][PRIMARY_EPSILON]
|
||||
base_ifgsm = baseline["ifgsm_results"][PRIMARY_EPSILON]
|
||||
for r in all_results:
|
||||
fgsm_d = r["fgsm_results"][PRIMARY_EPSILON] - base_fgsm
|
||||
ifgsm_d = r["ifgsm_results"][PRIMARY_EPSILON] - base_ifgsm
|
||||
print(
|
||||
f" {r['name']}: FGSM {'+' if fgsm_d >= 0 else ''}{fgsm_d:.1f}%, I-FGSM {'+' if ifgsm_d >= 0 else ''}{ifgsm_d:.1f}%"
|
||||
)
|
||||
|
||||
print("=" * 90)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Evaluate adversarial robustness of trained models",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python evaluate_robustness.py --model-path robust_model.pth
|
||||
python evaluate_robustness.py --model-path models/
|
||||
python evaluate_robustness.py --model-path . --compare
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-path",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to model file (.safetensors) or directory containing models",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compare",
|
||||
action="store_true",
|
||||
help="Compare against baseline_model.safetensors",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-failures",
|
||||
action="store_true",
|
||||
help="Show details of misclassified samples",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Import torch after argparse for instant --help
|
||||
import torch
|
||||
|
||||
model_path = Path(args.model_path)
|
||||
if not model_path.exists():
|
||||
print(f"Error: Path not found: {model_path}")
|
||||
return 1
|
||||
|
||||
# Find models to evaluate
|
||||
models = find_models(model_path)
|
||||
if not models:
|
||||
print(f"Error: No .safetensors model files found in {model_path}")
|
||||
return 1
|
||||
|
||||
# Find adversarial examples
|
||||
adv_path = find_adv_examples(model_path)
|
||||
if not adv_path:
|
||||
print("Error: Could not find adv_examples.safetensors")
|
||||
return 1
|
||||
|
||||
# Find baseline if comparing
|
||||
baseline_path = None
|
||||
if args.compare:
|
||||
baseline_path = find_baseline(model_path)
|
||||
if not baseline_path:
|
||||
print(
|
||||
"Warning: --compare specified but baseline_model.safetensors not found"
|
||||
)
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Using device: {device}")
|
||||
|
||||
# Load adversarial examples
|
||||
print(f"Loading adversarial examples from: {adv_path.name}")
|
||||
adv_data = load_adversarial_examples(adv_path)
|
||||
|
||||
num_samples = adv_data["clean_images"].size(0)
|
||||
has_spread = "epsilon_spread" in adv_data
|
||||
print(f" {num_samples} test samples")
|
||||
if has_spread:
|
||||
print(f" Epsilon spread: {adv_data['epsilon_spread']}")
|
||||
else:
|
||||
print(f" Single epsilon: {adv_data['epsilon']}")
|
||||
|
||||
# Evaluate baseline first if comparing
|
||||
baseline_results = None
|
||||
if baseline_path:
|
||||
print(f"\nEvaluating baseline...")
|
||||
baseline_results = evaluate_model_full(baseline_path, adv_data, device)
|
||||
print_model_results(baseline_results)
|
||||
|
||||
# Evaluate each model
|
||||
all_results = []
|
||||
for mp in models:
|
||||
print(f"\nEvaluating {mp.name}...")
|
||||
results = evaluate_model_full(mp, adv_data, device)
|
||||
all_results.append(results)
|
||||
print_model_results(results, baseline_results)
|
||||
|
||||
# Show failures if requested
|
||||
if args.show_failures:
|
||||
eps_spread = results["epsilon_spread"]
|
||||
fgsm_by_eps = adv_data.get(
|
||||
"fgsm_by_epsilon", {adv_data["epsilon"]: adv_data.get("fgsm_images")}
|
||||
)
|
||||
ifgsm_by_eps = adv_data.get(
|
||||
"ifgsm_by_epsilon", {adv_data["epsilon"]: adv_data.get("ifgsm_images")}
|
||||
)
|
||||
|
||||
failures = get_misclassified_samples(
|
||||
results["model"],
|
||||
ifgsm_by_eps[PRIMARY_EPSILON],
|
||||
adv_data["clean_labels"],
|
||||
device,
|
||||
)
|
||||
if failures:
|
||||
print(f"\nMisclassified samples (I-FGSM ε={PRIMARY_EPSILON}):")
|
||||
for f in failures:
|
||||
print(
|
||||
f" Sample {f['index']}: {f['true_label']} → {f['predicted']} (conf={f['confidence']:.2f})"
|
||||
)
|
||||
|
||||
# Print comparison summary if multiple models or baseline comparison
|
||||
if len(all_results) > 1 or baseline_results:
|
||||
print_comparison_summary(all_results, baseline_results)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
Binary file not shown.
Reference in New Issue
Block a user