2668 lines
96 KiB
Plaintext
2668 lines
96 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "30606703-3597-4ae0-9c2a-cb40bbf74f0f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import torch\n",
|
|
"import torch.nn as nn\n",
|
|
"import torch.optim as optim\n",
|
|
"import torch.nn.functional as F"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"id": "60ae3dc5-9f16-4396-909b-3e38b77357d4",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import torchvision\n",
|
|
"import torchvision.transforms as transforms\n",
|
|
"from torch.utils.data import DataLoader, Dataset\n",
|
|
"from torchvision.datasets import ImageFolder"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"id": "3b350220-ace2-453e-a940-4bffbbc0ce01",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from tqdm.auto import tqdm, trange\n",
|
|
"import numpy as np\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import random\n",
|
|
"import copy\n",
|
|
"import os\n",
|
|
"import pandas as pd\n",
|
|
"from PIL import Image\n",
|
|
"import requests\n",
|
|
"import zipfile\n",
|
|
"import shutil"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"id": "ca22daec-f61f-47f6-ab9f-a7be914645da",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Using CPU device.\n",
|
|
"Using device: cpu\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Enforce determinism for reproducibility\n",
|
|
"torch.backends.cudnn.deterministic = True\n",
|
|
"torch.backends.cudnn.benchmark = False\n",
|
|
"\n",
|
|
"# Device configuration\n",
|
|
"if torch.cuda.is_available():\n",
|
|
" device = torch.device(\"cuda\")\n",
|
|
" print(\"Using CUDA device.\")\n",
|
|
"elif torch.backends.mps.is_available():\n",
|
|
" device = torch.device(\"mps\")\n",
|
|
" print(\"Using MPS device (Apple Silicon GPU).\")\n",
|
|
"else:\n",
|
|
" device = torch.device(\"cpu\")\n",
|
|
" print(\"Using CPU device.\")\n",
|
|
"print(f\"Using device: {device}\")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"id": "c959b777-fa15-432f-9914-7a6a442244df",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Set random seed for reproducibility\n",
|
|
"SEED = 1337\n",
|
|
"random.seed(SEED)\n",
|
|
"np.random.seed(SEED)\n",
|
|
"torch.manual_seed(SEED)\n",
|
|
"if torch.cuda.is_available(): # Ensure CUDA seeds are set only if GPU is used\n",
|
|
" torch.cuda.manual_seed(SEED)\n",
|
|
" torch.cuda.manual_seed_all(SEED) # For multi-GPU setups\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 6,
|
|
"id": "15139d30-01c7-4585-83ee-8af859206286",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Setup complete.\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Primary Palette\n",
|
|
"HTB_GREEN = \"#9fef00\"\n",
|
|
"NODE_BLACK = \"#141d2b\"\n",
|
|
"HACKER_GREY = \"#a4b1cd\"\n",
|
|
"WHITE = \"#ffffff\"\n",
|
|
"# Secondary Palette\n",
|
|
"AZURE = \"#0086ff\"\n",
|
|
"NUGGET_YELLOW = \"#ffaf00\"\n",
|
|
"MALWARE_RED = \"#ff3e3e\"\n",
|
|
"VIVID_PURPLE = \"#9f00ff\"\n",
|
|
"AQUAMARINE = \"#2ee7b6\"\n",
|
|
"# Matplotlib Style Settings\n",
|
|
"plt.style.use(\"seaborn-v0_8-darkgrid\")\n",
|
|
"plt.rcParams.update(\n",
|
|
" {\n",
|
|
" \"figure.facecolor\": NODE_BLACK,\n",
|
|
" \"figure.edgecolor\": NODE_BLACK,\n",
|
|
" \"axes.facecolor\": NODE_BLACK,\n",
|
|
" \"axes.edgecolor\": HACKER_GREY,\n",
|
|
" \"axes.labelcolor\": HACKER_GREY,\n",
|
|
" \"axes.titlecolor\": WHITE,\n",
|
|
" \"xtick.color\": HACKER_GREY,\n",
|
|
" \"ytick.color\": HACKER_GREY,\n",
|
|
" \"grid.color\": HACKER_GREY,\n",
|
|
" \"grid.alpha\": 0.1,\n",
|
|
" \"legend.facecolor\": NODE_BLACK,\n",
|
|
" \"legend.edgecolor\": HACKER_GREY,\n",
|
|
" \"legend.labelcolor\": HACKER_GREY,\n",
|
|
" \"text.color\": HACKER_GREY,\n",
|
|
" }\n",
|
|
")\n",
|
|
"\n",
|
|
"print(\"Setup complete.\")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 7,
|
|
"id": "5ef3af09-4897-4757-9e64-9cbff7cd320c",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"GTSRB_CLASS_NAMES = {\n",
|
|
" 0: \"Speed limit (20km/h)\",\n",
|
|
" 1: \"Speed limit (30km/h)\",\n",
|
|
" 2: \"Speed limit (50km/h)\",\n",
|
|
" 3: \"Speed limit (60km/h)\",\n",
|
|
" 4: \"Speed limit (70km/h)\",\n",
|
|
" 5: \"Speed limit (80km/h)\",\n",
|
|
" 6: \"End of speed limit (80km/h)\",\n",
|
|
" 7: \"Speed limit (100km/h)\",\n",
|
|
" 8: \"Speed limit (120km/h)\",\n",
|
|
" 9: \"No passing\",\n",
|
|
" 10: \"No passing for veh over 3.5 tons\",\n",
|
|
" 11: \"Right-of-way at next intersection\",\n",
|
|
" 12: \"Priority road\",\n",
|
|
" 13: \"Yield\",\n",
|
|
" 14: \"Stop\",\n",
|
|
" 15: \"No vehicles\",\n",
|
|
" 16: \"Veh > 3.5 tons prohibited\",\n",
|
|
" 17: \"No entry\",\n",
|
|
" 18: \"General caution\",\n",
|
|
" 19: \"Dangerous curve left\",\n",
|
|
" 20: \"Dangerous curve right\",\n",
|
|
" 21: \"Double curve\",\n",
|
|
" 22: \"Bumpy road\",\n",
|
|
" 23: \"Slippery road\",\n",
|
|
" 24: \"Road narrows on the right\",\n",
|
|
" 25: \"Road work\",\n",
|
|
" 26: \"Traffic signals\",\n",
|
|
" 27: \"Pedestrians\",\n",
|
|
" 28: \"Children crossing\",\n",
|
|
" 29: \"Bicycles crossing\",\n",
|
|
" 30: \"Beware of ice/snow\",\n",
|
|
" 31: \"Wild animals crossing\",\n",
|
|
" 32: \"End speed/pass limits\",\n",
|
|
" 33: \"Turn right ahead\",\n",
|
|
" 34: \"Turn left ahead\",\n",
|
|
" 35: \"Ahead only\",\n",
|
|
" 36: \"Go straight or right\",\n",
|
|
" 37: \"Go straight or left\",\n",
|
|
" 38: \"Keep right\",\n",
|
|
" 39: \"Keep left\",\n",
|
|
" 40: \"Roundabout mandatory\",\n",
|
|
" 41: \"End of no passing\",\n",
|
|
" 42: \"End no passing veh > 3.5 tons\",\n",
|
|
"}\n",
|
|
"NUM_CLASSES_GTSRB = len(GTSRB_CLASS_NAMES) # Should be 43\n",
|
|
"\n",
|
|
"\n",
|
|
"def get_gtsrb_class_name(class_id):\n",
|
|
" \"\"\"\n",
|
|
" Retrieves the human-readable name for a given GTSRB class ID.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" class_id (int): The numeric class ID (0-42).\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" str: The corresponding class name or an 'Unknown Class' string.\n",
|
|
" \"\"\"\n",
|
|
" return GTSRB_CLASS_NAMES.get(class_id, f\"Unknown Class {class_id}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 8,
|
|
"id": "8e6418ec-e06f-4f4d-b768-f4359865821c",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Dataset Root Directory\n",
|
|
"DATASET_ROOT = \"./GTSRB\"\n",
|
|
"\n",
|
|
"# URLs for the GTSRB dataset components\n",
|
|
"DATASET_URL = \"https://academy.hackthebox.com/storage/resources/GTSRB.zip\"\n",
|
|
"DOWNLOAD_DIR = \"./gtsrb_downloads\" # Temporary download location\n",
|
|
"\n",
|
|
"\n",
|
|
"def download_file(url, dest_folder, filename):\n",
|
|
" \"\"\"\n",
|
|
" Downloads a file from a URL to a specified destination.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" url (str): The URL of the file to download.\n",
|
|
" dest_folder (str): The directory to save the downloaded file.\n",
|
|
" filename (str): The name to save the file as.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" str or None: The full path to the downloaded file, or None if download failed.\n",
|
|
" \"\"\"\n",
|
|
" filepath = os.path.join(dest_folder, filename)\n",
|
|
" if os.path.exists(filepath):\n",
|
|
" print(f\"File '{filename}' already exists in {dest_folder}. Skipping download.\")\n",
|
|
" return filepath\n",
|
|
" print(f\"Downloading {filename} from {url}...\")\n",
|
|
" try:\n",
|
|
" response = requests.get(url, stream=True)\n",
|
|
" response.raise_for_status() # Raise an exception for bad status codes\n",
|
|
" os.makedirs(dest_folder, exist_ok=True)\n",
|
|
" with open(filepath, \"wb\") as f:\n",
|
|
" for chunk in response.iter_content(chunk_size=8192):\n",
|
|
" f.write(chunk)\n",
|
|
" print(f\"Successfully downloaded {filename}.\")\n",
|
|
" return filepath\n",
|
|
" except requests.exceptions.RequestException as e:\n",
|
|
" print(f\"Error downloading {url}: {e}\")\n",
|
|
" return None\n",
|
|
"\n",
|
|
"\n",
|
|
"def extract_zip(zip_filepath, extract_to):\n",
|
|
" \"\"\"\n",
|
|
" Extracts the contents of a zip file to a specified directory.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" zip_filepath (str): The path to the zip file.\n",
|
|
" extract_to (str): The directory where contents should be extracted.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" bool: True if extraction was successful, False otherwise.\n",
|
|
" \"\"\"\n",
|
|
" print(f\"Extracting '{os.path.basename(zip_filepath)}' to {extract_to}...\")\n",
|
|
" try:\n",
|
|
" with zipfile.ZipFile(zip_filepath, \"r\") as zip_ref:\n",
|
|
" zip_ref.extractall(extract_to)\n",
|
|
" print(f\"Successfully extracted '{os.path.basename(zip_filepath)}'.\")\n",
|
|
" return True\n",
|
|
" except zipfile.BadZipFile:\n",
|
|
" print(\n",
|
|
" f\"Error: Failed to extract '{os.path.basename(zip_filepath)}'. File might be corrupted or not a zip file.\"\n",
|
|
" )\n",
|
|
" return False\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"An unexpected error occurred during extraction: {e}\")\n",
|
|
" return False\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"id": "3ff62bdc-0e76-4bdb-bcc7-1f5d919df155",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"GTSRB dataset found and seems complete in './GTSRB'. Skipping download.\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Define expected paths within DATASET_ROOT\n",
|
|
"train_dir = os.path.join(DATASET_ROOT, \"Final_Training\", \"Images\")\n",
|
|
"test_img_dir = os.path.join(DATASET_ROOT, \"Final_Test\", \"Images\")\n",
|
|
"test_csv_path = os.path.join(DATASET_ROOT, \"GT-final_test.csv\")\n",
|
|
"\n",
|
|
"# Check if the core dataset components exist\n",
|
|
"dataset_ready = (\n",
|
|
" os.path.isdir(DATASET_ROOT)\n",
|
|
" and os.path.isdir(train_dir)\n",
|
|
" and os.path.isdir(test_img_dir) # Check if test dir exists\n",
|
|
" and os.path.isfile(test_csv_path) # Check if test csv exists\n",
|
|
")\n",
|
|
"\n",
|
|
"if dataset_ready:\n",
|
|
" print(\n",
|
|
" f\"GTSRB dataset found and seems complete in '{DATASET_ROOT}'. Skipping download.\"\n",
|
|
" )\n",
|
|
"else:\n",
|
|
" print(\n",
|
|
" f\"GTSRB dataset not found or incomplete in '{DATASET_ROOT}'. Attempting download and extraction...\"\n",
|
|
" )\n",
|
|
" os.makedirs(DATASET_ROOT, exist_ok=True)\n",
|
|
" os.makedirs(DOWNLOAD_DIR, exist_ok=True)\n",
|
|
"\n",
|
|
" # Download files\n",
|
|
" dataset_zip_path = download_file(\n",
|
|
" DATASET_URL, DOWNLOAD_DIR, \"GTSRB.zip\"\n",
|
|
" )\n",
|
|
" extraction_ok = True\n",
|
|
" # Only extract if download happened and train_dir doesn't already exist\n",
|
|
" if dataset_zip_path and not os.path.isdir(train_dir):\n",
|
|
" if not extract_zip(dataset_zip_path, DATASET_ROOT):\n",
|
|
" extraction_ok = False\n",
|
|
" print(\"Error during extraction of training images.\")\n",
|
|
" elif not dataset_zip_path and not os.path.isdir(train_dir):\n",
|
|
" # If download failed AND train dir doesn't exist, extraction can't happen\n",
|
|
" extraction_ok = False\n",
|
|
" print(\"Training images download failed or skipped, cannot proceed with extraction.\")\n",
|
|
"\n",
|
|
" if not os.path.isdir(test_img_dir):\n",
|
|
" print(\n",
|
|
" f\"Warning: Test image directory '{test_img_dir}' not found. Ensure it's placed correctly.\"\n",
|
|
" )\n",
|
|
" if not os.path.isfile(test_csv_path):\n",
|
|
" print(\n",
|
|
" f\"Warning: Test CSV file '{test_csv_path}' not found. Ensure it's placed correctly.\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" # Final check after download/extraction attempt\n",
|
|
" # We primarily check if the TRAINING data extraction succeeded,\n",
|
|
" # and rely on warnings for the manually placed TEST data.\n",
|
|
" dataset_ready = (\n",
|
|
" os.path.isdir(DATASET_ROOT)\n",
|
|
" and os.path.isdir(train_dir)\n",
|
|
" and extraction_ok\n",
|
|
" )\n",
|
|
"\n",
|
|
" if dataset_ready and os.path.isdir(test_img_dir) and os.path.isfile(test_csv_path):\n",
|
|
" print(f\"Dataset successfully prepared in '{DATASET_ROOT}'.\")\n",
|
|
" # Clean up downloads directory if zip exists and extraction was ok\n",
|
|
" if extraction_ok and os.path.exists(DOWNLOAD_DIR):\n",
|
|
" try:\n",
|
|
" shutil.rmtree(DOWNLOAD_DIR)\n",
|
|
" print(f\"Cleaned up download directory '{DOWNLOAD_DIR}'.\")\n",
|
|
" except OSError as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Could not remove download directory {DOWNLOAD_DIR}: {e}\"\n",
|
|
" )\n",
|
|
" elif dataset_ready:\n",
|
|
" print(f\"Training dataset prepared in '{DATASET_ROOT}', but test components might be missing.\")\n",
|
|
" if not os.path.isdir(test_img_dir): print(f\" - Missing: {test_img_dir}\")\n",
|
|
" if not os.path.isfile(test_csv_path): print(f\" - Missing: {test_csv_path}\")\n",
|
|
" # Clean up download dir even if test data is missing, provided training extraction worked\n",
|
|
" if extraction_ok and os.path.exists(DOWNLOAD_DIR):\n",
|
|
" try:\n",
|
|
" shutil.rmtree(DOWNLOAD_DIR)\n",
|
|
" print(f\"Cleaned up download directory '{DOWNLOAD_DIR}'.\")\n",
|
|
" except OSError as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Could not remove download directory {DOWNLOAD_DIR}: {e}\"\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" print(\"\\nError: Failed to set up the core GTSRB training dataset.\")\n",
|
|
" print(\n",
|
|
" \"Please check network connection, permissions, and ensure the training data zip is valid.\"\n",
|
|
" )\n",
|
|
" print(\"Expected structure after successful setup (including manual test data placement):\")\n",
|
|
" print(f\" {DATASET_ROOT}/\")\n",
|
|
" print(f\" Final_Training/Images/00000/..ppm files..\")\n",
|
|
" print(f\" ...\")\n",
|
|
" print(f\" Final_Test/Images/..ppm files..\")\n",
|
|
" print(f\" GT-final_test.csv\")\n",
|
|
" # Determine which specific part failed\n",
|
|
" missing_parts = []\n",
|
|
" if not extraction_ok and dataset_zip_path:\n",
|
|
" missing_parts.append(\"Training data extraction\")\n",
|
|
" if not dataset_zip_path and not os.path.isdir(train_dir):\n",
|
|
" missing_parts.append(\"Training data download\")\n",
|
|
" if not os.path.isdir(train_dir):\n",
|
|
" missing_parts.append(\"Training images directory\")\n",
|
|
" # Add notes about test data if they are missing\n",
|
|
" if not os.path.isdir(test_img_dir):\n",
|
|
" missing_parts.append(\"Test images (manual placement likely needed)\")\n",
|
|
" if not os.path.isfile(test_csv_path):\n",
|
|
" missing_parts.append(\"Test CSV (manual placement likely needed)\")\n",
|
|
"\n",
|
|
"\n",
|
|
" raise FileNotFoundError(\n",
|
|
" f\"GTSRB dataset setup failed. Critical failure in obtaining training data. Missing/Problem parts: {', '.join(missing_parts)} in {DATASET_ROOT}\"\n",
|
|
" )\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 10,
|
|
"id": "44475cd9-9058-46db-9f8a-7bd77f640c9f",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"Dataset configuration:\n",
|
|
" Image Size: 48x48\n",
|
|
" Number of Classes: 43\n",
|
|
" Source Class: 14 (Stop)\n",
|
|
" Target Class: 3 (Speed limit (60km/h))\n",
|
|
" Poison Rate: 10.0%\n",
|
|
" Trigger: 4x4 magenta square at (43, 43)\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Define image size and normalization constants\n",
|
|
"IMG_SIZE = 48 # Resize GTSRB images to 48x48\n",
|
|
"# Using ImageNet stats is common practice if dataset-specific stats aren't available/standard\n",
|
|
"IMG_MEAN = [0.485, 0.456, 0.406]\n",
|
|
"IMG_STD = [0.229, 0.224, 0.225]\n",
|
|
"\n",
|
|
"# Our specific attack parameters\n",
|
|
"SOURCE_CLASS = 14 # Stop Sign index\n",
|
|
"TARGET_CLASS = 3 # Speed limit 60km/h index\n",
|
|
"POISON_RATE = 0.10 # Poison a % of the Stop Signs in the training data\n",
|
|
"\n",
|
|
"# Trigger Definition (relative to 48x48 image size)\n",
|
|
"TRIGGER_SIZE = 4 # 4x4 block\n",
|
|
"TRIGGER_POS = (\n",
|
|
" IMG_SIZE - TRIGGER_SIZE - 1,\n",
|
|
" IMG_SIZE - TRIGGER_SIZE - 1,\n",
|
|
") # Bottom-right corner\n",
|
|
"# Trigger Color: Magenta (R=1, G=0, B=1) in [0, 1] range\n",
|
|
"TRIGGER_COLOR_VAL = (1.0, 0.0, 1.0)\n",
|
|
"\n",
|
|
"print(f\"\\nDataset configuration:\")\n",
|
|
"print(f\" Image Size: {IMG_SIZE}x{IMG_SIZE}\")\n",
|
|
"print(f\" Number of Classes: {NUM_CLASSES_GTSRB}\")\n",
|
|
"print(f\" Source Class: {SOURCE_CLASS} ({get_gtsrb_class_name(SOURCE_CLASS)})\")\n",
|
|
"print(f\" Target Class: {TARGET_CLASS} ({get_gtsrb_class_name(TARGET_CLASS)})\")\n",
|
|
"print(f\" Poison Rate: {POISON_RATE * 100}%\")\n",
|
|
"print(f\" Trigger: {TRIGGER_SIZE}x{TRIGGER_SIZE} magenta square at {TRIGGER_POS}\")\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 11,
|
|
"id": "5506fd58-56a8-4d13-b190-77d28fd39e4f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class GTSRB_CNN(nn.Module):\n",
|
|
" \"\"\"\n",
|
|
" A CNN adapted for the GTSRB dataset (43 classes, 48x48 input).\n",
|
|
" Implements standard CNN components with adjusted layer dimensions for GTSRB.\n",
|
|
" \"\"\"\n",
|
|
"\n",
|
|
" def __init__(self, num_classes=NUM_CLASSES_GTSRB):\n",
|
|
" \"\"\"\n",
|
|
" Initializes the CNN layers for GTSRB.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" num_classes (int): Number of output classes (default: NUM_CLASSES_GTSRB).\n",
|
|
" \"\"\"\n",
|
|
" super(GTSRB_CNN, self).__init__()\n",
|
|
" # Conv Layer 1: Input 3 channels (RGB), Output 32 filters, Kernel 3x3, Padding 1\n",
|
|
" # Processes 48x48 input\n",
|
|
" self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1)\n",
|
|
" # Output shape: (Batch Size, 32, 48, 48)\n",
|
|
"\n",
|
|
" # Conv Layer 2: Input 32 channels, Output 64 filters, Kernel 3x3, Padding 1\n",
|
|
" self.conv2 = nn.Conv2d(\n",
|
|
" in_channels=32, out_channels=64, kernel_size=3, padding=1\n",
|
|
" )\n",
|
|
" # Output shape: (Batch Size, 64, 48, 48)\n",
|
|
"\n",
|
|
" # Max Pooling 1: Kernel 2x2, Stride 2. Reduces spatial dimensions by half.\n",
|
|
" self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
|
|
" # Output shape: (Batch Size, 64, 24, 24)\n",
|
|
"\n",
|
|
" # Conv Layer 3: Input 64 channels, Output 128 filters, Kernel 3x3, Padding 1\n",
|
|
" self.conv3 = nn.Conv2d(\n",
|
|
" in_channels=64, out_channels=128, kernel_size=3, padding=1\n",
|
|
" )\n",
|
|
" # Output shape: (Batch Size, 128, 24, 24)\n",
|
|
"\n",
|
|
" # Max Pooling 2: Kernel 2x2, Stride 2. Reduces spatial dimensions by half again.\n",
|
|
" self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
|
|
" # Output shape: (Batch Size, 128, 12, 12)\n",
|
|
"\n",
|
|
" # Calculate flattened feature size after pooling layers\n",
|
|
" # This is needed for the input size of the first fully connected layer\n",
|
|
" self._feature_size = 128 * 12 * 12 # 18432\n",
|
|
"\n",
|
|
" # Fully Connected Layer 1 (Hidden): Maps flattened features to 512 hidden units.\n",
|
|
" # Input size MUST match self._feature_size\n",
|
|
" self.fc1 = nn.Linear(self._feature_size, 512)\n",
|
|
" # Implements Y1 = f(W1 * X_flat + b1), where f is ReLU\n",
|
|
"\n",
|
|
" # Fully Connected Layer 2 (Output): Maps hidden units to class logits.\n",
|
|
" # Output size MUST match num_classes\n",
|
|
" self.fc2 = nn.Linear(512, num_classes)\n",
|
|
" # Implements Y_logits = W2 * Y1 + b2\n",
|
|
"\n",
|
|
" # Dropout layer for regularization (p=0.5 means 50% probability of dropping a unit)\n",
|
|
" self.dropout = nn.Dropout(0.5)\n",
|
|
"\n",
|
|
" def forward(self, x):\n",
|
|
" \"\"\"\n",
|
|
" Defines the forward pass sequence for input tensor x.\n",
|
|
" \n",
|
|
" Args:\n",
|
|
" x (torch.Tensor): Input batch of images\n",
|
|
" (Batch Size x 3 x IMG_SIZE x IMG_SIZE).\n",
|
|
" \n",
|
|
" Returns:\n",
|
|
" torch.Tensor: Output logits for each class\n",
|
|
" (Batch Size x num_classes).\n",
|
|
" \"\"\"\n",
|
|
" # Apply first Conv block: Conv1 -> ReLU -> Conv2 -> ReLU -> Pool1\n",
|
|
" x = self.pool1(F.relu(self.conv2(F.relu(self.conv1(x)))))\n",
|
|
" # Apply second Conv block: Conv3 -> ReLU -> Pool2\n",
|
|
" x = self.pool2(F.relu(self.conv3(x)))\n",
|
|
" \n",
|
|
" # Flatten the feature map output from the convolutional blocks\n",
|
|
" x = x.view(-1, self._feature_size) # Reshape to (Batch Size, _feature_size)\n",
|
|
" \n",
|
|
" # Apply Dropout before the first FC layer (common practice)\n",
|
|
" x = self.dropout(x)\n",
|
|
" # Apply first FC layer with ReLU activation\n",
|
|
" x = F.relu(self.fc1(x))\n",
|
|
" # Apply Dropout again before the output layer\n",
|
|
" x = self.dropout(x)\n",
|
|
" # Apply the final FC layer to get logits\n",
|
|
" x = self.fc2(x)\n",
|
|
" return x\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 12,
|
|
"id": "8e88462e-a5fe-48fe-a7a8-94b6f1eb6d92",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"CNN model defined for GTSRB:\n",
|
|
"GTSRB_CNN(\n",
|
|
" (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))\n",
|
|
" (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))\n",
|
|
" (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)\n",
|
|
" (conv3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))\n",
|
|
" (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)\n",
|
|
" (fc1): Linear(in_features=18432, out_features=512, bias=True)\n",
|
|
" (fc2): Linear(in_features=512, out_features=43, bias=True)\n",
|
|
" (dropout): Dropout(p=0.5, inplace=False)\n",
|
|
")\n",
|
|
"Calculated feature size before FC layers: 18432\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Instantiate the GTSRB model structure and move it to the configured device\n",
|
|
"model_structure_gtsrb = GTSRB_CNN(num_classes=NUM_CLASSES_GTSRB).to(device)\n",
|
|
"print(\"\\nCNN model defined for GTSRB:\")\n",
|
|
"print(model_structure_gtsrb)\n",
|
|
"print(\n",
|
|
" f\"Calculated feature size before FC layers: {model_structure_gtsrb._feature_size}\"\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 13,
|
|
"id": "34455082-1250-4e4c-b232-0165ef3042f7",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Base transform (Resize + ToTensor) - Applied first to all images\n",
|
|
"transform_base = transforms.Compose(\n",
|
|
" [\n",
|
|
" transforms.Resize((IMG_SIZE, IMG_SIZE)), # Resize to standard size\n",
|
|
" transforms.ToTensor(), # Converts PIL Image [0, 255] to Tensor [0, 1]\n",
|
|
" ]\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 14,
|
|
"id": "95cdfa6a-24eb-4e80-9a15-b66448628ef1",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Post-trigger transform for training data (augmentation + normalization) - Applied last in training\n",
|
|
"transform_train_post = transforms.Compose(\n",
|
|
" [\n",
|
|
" transforms.RandomRotation(10), # Augmentation: Apply small random rotation\n",
|
|
" transforms.ColorJitter(\n",
|
|
" brightness=0.2, contrast=0.2\n",
|
|
" ), # Augmentation: Adjust color slightly\n",
|
|
" transforms.Normalize(IMG_MEAN, IMG_STD), # Normalize using ImageNet stats\n",
|
|
" ]\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 15,
|
|
"id": "b59f6de2-3f65-4928-a085-9ecd264eee4e",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Transform for clean test data (Resize, ToTensor, Normalize) - Used for evaluation\n",
|
|
"transform_test = transforms.Compose(\n",
|
|
" [\n",
|
|
" transforms.Resize((IMG_SIZE, IMG_SIZE)), # Resize\n",
|
|
" transforms.ToTensor(), # Convert to tensor\n",
|
|
" transforms.Normalize(IMG_MEAN, IMG_STD), # Normalize\n",
|
|
" ]\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 16,
|
|
"id": "b870b4b3-98db-4748-929f-1135fd12df26",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Inverse transform for visualization (reverses normalization)\n",
|
|
"inverse_normalize = transforms.Normalize(\n",
|
|
" mean=[-m / s for m, s in zip(IMG_MEAN, IMG_STD)], std=[1 / s for s in IMG_STD]\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 17,
|
|
"id": "3d05dd14-6046-41c8-b999-2d6a40d0126a",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"Clean GTSRB training dataset loaded using ImageFolder. Size: 39209\n",
|
|
"Total 43 classes found by ImageFolder.\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"try:\n",
|
|
" # Load reference training set using ImageFolder to get class-to-index mapping\n",
|
|
" # This instance won't be used for training directly, only for metadata.\n",
|
|
" trainset_clean_ref = ImageFolder(root=train_dir)\n",
|
|
" gtsrb_class_to_idx = (\n",
|
|
" trainset_clean_ref.class_to_idx\n",
|
|
" ) # Example: {'00000': 0, '00001': 1, ...} - maps folder names to class indices\n",
|
|
"\n",
|
|
" # Create the actual clean training dataset using ImageFolder\n",
|
|
" # For clean training, we apply the full sequence of base + post transforms.\n",
|
|
" trainset_clean_transformed = ImageFolder(\n",
|
|
" root=train_dir,\n",
|
|
" transform=transforms.Compose(\n",
|
|
" [transform_base, transform_train_post]\n",
|
|
" ), # Combine transforms for clean data\n",
|
|
" )\n",
|
|
" print(\n",
|
|
" f\"\\nClean GTSRB training dataset loaded using ImageFolder. Size: {len(trainset_clean_transformed)}\"\n",
|
|
" )\n",
|
|
" print(f\"Total {len(trainset_clean_ref.classes)} classes found by ImageFolder.\")\n",
|
|
"\n",
|
|
"except Exception as e:\n",
|
|
" print(f\"Error loading GTSRB training data from {train_dir}: {e}\")\n",
|
|
" print(\n",
|
|
" \"Please ensure the directory structure is correct for ImageFolder (e.g., GTSRB/Final_Training/Images/00000/*.ppm).\"\n",
|
|
" )\n",
|
|
" raise e"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 18,
|
|
"id": "226731d5-502a-4b71-8ea7-321e708fa28a",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Create the DataLoader for clean training data\n",
|
|
"trainloader_clean = DataLoader(\n",
|
|
" trainset_clean_transformed,\n",
|
|
" batch_size=256, # Larger batch size for potentially faster clean training\n",
|
|
" shuffle=True, # Shuffle training data each epoch\n",
|
|
" num_workers=0, # Set based on system capabilities (0 for simplicity/compatibility)\n",
|
|
" pin_memory=True, # Speeds up CPU->GPU transfer if using CUDA\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 19,
|
|
"id": "c68e3fd1-b308-406d-997b-9f946630463f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class GTSRBTestset(Dataset):\n",
|
|
" \"\"\"Custom Dataset for GTSRB test set using annotations from a CSV file.\"\"\"\n",
|
|
"\n",
|
|
" def __init__(self, csv_file, img_dir, transform=None):\n",
|
|
" \"\"\"\n",
|
|
" Initializes the dataset by reading the CSV and storing paths/transforms.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" csv_file (string): Path to the CSV file with 'Filename' and 'ClassId' columns.\n",
|
|
" img_dir (string): Directory containing the test images.\n",
|
|
" transform (callable, optional): Transform to be applied to each image.\n",
|
|
" \"\"\"\n",
|
|
" try:\n",
|
|
" # Read the CSV file, ensuring correct delimiter and handling potential BOM\n",
|
|
" with open(csv_file, mode=\"r\", encoding=\"utf-8-sig\") as f:\n",
|
|
" self.img_labels = pd.read_csv(f, delimiter=\";\")\n",
|
|
" # Verify required columns exist\n",
|
|
" if (\n",
|
|
" \"Filename\" not in self.img_labels.columns\n",
|
|
" or \"ClassId\" not in self.img_labels.columns\n",
|
|
" ):\n",
|
|
" raise ValueError(\n",
|
|
" \"CSV file must contain 'Filename' and 'ClassId' columns.\"\n",
|
|
" )\n",
|
|
" except FileNotFoundError:\n",
|
|
" print(f\"Error: Test CSV file not found at '{csv_file}'\")\n",
|
|
" raise\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Error reading or parsing GTSRB test CSV '{csv_file}': {e}\")\n",
|
|
" raise\n",
|
|
"\n",
|
|
" self.img_dir = img_dir\n",
|
|
" self.transform = transform\n",
|
|
" print(\n",
|
|
" f\"Loaded GTSRB test annotations from CSV '{os.path.basename(csv_file)}'. Found {len(self.img_labels)} entries.\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" def __len__(self):\n",
|
|
" \"\"\"Returns the total number of samples in the test set.\"\"\"\n",
|
|
" return len(self.img_labels)\n",
|
|
"\n",
|
|
" def __getitem__(self, idx):\n",
|
|
" \"\"\"\n",
|
|
" Retrieves the image and label for a given index.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" idx (int): The index of the sample to retrieve.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" tuple: (image, label) where image is the transformed image tensor,\n",
|
|
" and label is the integer class ID. Returns (dummy_tensor, -1)\n",
|
|
" if the image file cannot be loaded or processed.\n",
|
|
" \"\"\"\n",
|
|
" if torch.is_tensor(idx):\n",
|
|
" idx = idx.tolist() # Handle tensor index if needed\n",
|
|
"\n",
|
|
" try:\n",
|
|
" # Get image filename and class ID from the pandas DataFrame\n",
|
|
" img_path_relative = self.img_labels.iloc[idx][\"Filename\"]\n",
|
|
" img_path = os.path.join(self.img_dir, img_path_relative)\n",
|
|
" label = int(self.img_labels.iloc[idx][\"ClassId\"]) # Ensure label is integer\n",
|
|
"\n",
|
|
" # Open image using PIL and ensure it's in RGB format\n",
|
|
" image = Image.open(img_path).convert(\"RGB\")\n",
|
|
"\n",
|
|
" except FileNotFoundError:\n",
|
|
" print(f\"Warning: Image file not found: {img_path} (Index {idx}). Skipping.\")\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Warning: Error opening image {img_path} (Index {idx}): {e}. Skipping.\")\n",
|
|
" # Return dummy data on other errors as well\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n",
|
|
"\n",
|
|
" # Apply transforms if they are provided\n",
|
|
" if self.transform:\n",
|
|
" try:\n",
|
|
" image = self.transform(image)\n",
|
|
" except Exception as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Error applying transform to image {img_path} (Index {idx}): {e}. Skipping.\"\n",
|
|
" )\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n",
|
|
"\n",
|
|
" return image, label"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 20,
|
|
"id": "e0e25346-1247-4f08-ab32-b3ab04a56a08",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Loaded GTSRB test annotations from CSV 'GT-final_test.csv'. Found 12630 entries.\n",
|
|
"Clean GTSRB test dataset loaded. Size: 12630\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Load Clean Test Data using the custom Dataset\n",
|
|
"try:\n",
|
|
" testset_clean = GTSRBTestset(\n",
|
|
" csv_file=test_csv_path,\n",
|
|
" img_dir=test_img_dir,\n",
|
|
" transform=transform_test, # Apply test transforms\n",
|
|
" )\n",
|
|
" print(f\"Clean GTSRB test dataset loaded. Size: {len(testset_clean)}\")\n",
|
|
"except Exception as e:\n",
|
|
" print(f\"Error creating GTSRB test dataset: {e}\")\n",
|
|
" raise e"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 21,
|
|
"id": "ccf567ab-498c-4cea-9dd9-be54372e2577",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Clean GTSRB test dataloader created.\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Create the DataLoader for the clean test dataset\n",
|
|
"# The DataLoader will now receive samples from GTSRBTestset.__getitem__\n",
|
|
"# We need to be aware that some samples might be (dummy_tensor, -1)\n",
|
|
"# The training/evaluation loops should handle filtering these out if they occur.\n",
|
|
"try:\n",
|
|
" testloader_clean = DataLoader(\n",
|
|
" testset_clean,\n",
|
|
" batch_size=256, # Batch size for evaluation\n",
|
|
" shuffle=False, # No shuffling needed for testing\n",
|
|
" num_workers=0, # Set based on system\n",
|
|
" pin_memory=True,\n",
|
|
" )\n",
|
|
" print(f\"Clean GTSRB test dataloader created.\")\n",
|
|
"except Exception as e:\n",
|
|
" print(f\"Error creating GTSRB test dataloader: {e}\")\n",
|
|
" raise e"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 22,
|
|
"id": "052a0342-8392-4103-aa3c-e5f25aa2fb9f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def add_trigger(image_tensor):\n",
|
|
" \"\"\"\n",
|
|
" Adds the predefined trigger pattern to a single image tensor.\n",
|
|
" The input tensor is expected to be in the [0, 1] value range (post ToTensor).\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" image_tensor (torch.Tensor): A single image tensor (C x H x W) in [0, 1] range.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" torch.Tensor: The image tensor with the trigger pattern applied.\n",
|
|
" \"\"\"\n",
|
|
" # Input tensor shape should be (Channels, Height, Width)\n",
|
|
" c, h, w = image_tensor.shape\n",
|
|
"\n",
|
|
" # Check if the input tensor has the expected dimensions\n",
|
|
" if h != IMG_SIZE or w != IMG_SIZE:\n",
|
|
" # This might occur if transforms change unexpectedly.\n",
|
|
" # We print a warning but attempt to proceed.\n",
|
|
" print(\n",
|
|
" f\"Warning: add_trigger received tensor of unexpected size {h}x{w}. Expected {IMG_SIZE}x{IMG_SIZE}.\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" # Calculate trigger coordinates from predefined constants\n",
|
|
" start_x, start_y = TRIGGER_POS\n",
|
|
"\n",
|
|
" # Prepare the trigger color tensor based on input image channels\n",
|
|
" # Ensure the color tensor has the same number of channels as the image\n",
|
|
" if c != len(TRIGGER_COLOR_VAL):\n",
|
|
" # If channel count mismatch (e.g., grayscale input, color trigger), adapt.\n",
|
|
" print(\n",
|
|
" f\"Warning: Input tensor channels ({c}) mismatch trigger color channels ({len(TRIGGER_COLOR_VAL)}). Using first color value for all channels.\"\n",
|
|
" )\n",
|
|
" # Create a tensor using only the first color value (e.g., R from RGB)\n",
|
|
" trigger_color_tensor = torch.full(\n",
|
|
" (c, 1, 1), # Shape (C, 1, 1) for broadcasting\n",
|
|
" TRIGGER_COLOR_VAL[0], # Use the first component of the color tuple\n",
|
|
" dtype=image_tensor.dtype,\n",
|
|
" device=image_tensor.device,\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" # Reshape the color tuple (e.g., (1.0, 0.0, 1.0)) into a (C, 1, 1) tensor\n",
|
|
" trigger_color_tensor = torch.tensor(\n",
|
|
" TRIGGER_COLOR_VAL, dtype=image_tensor.dtype, device=image_tensor.device\n",
|
|
" ).view(c, 1, 1) # Reshape for broadcasting\n",
|
|
"\n",
|
|
" # Calculate effective trigger boundaries, clamping to image dimensions\n",
|
|
" # This prevents errors if TRIGGER_POS or TRIGGER_SIZE are invalid\n",
|
|
" eff_start_y = max(0, min(start_y, h - 1))\n",
|
|
" eff_start_x = max(0, min(start_x, w - 1))\n",
|
|
" eff_end_y = max(0, min(start_y + TRIGGER_SIZE, h))\n",
|
|
" eff_end_x = max(0, min(start_x + TRIGGER_SIZE, w))\n",
|
|
" eff_trigger_size_y = eff_end_y - eff_start_y\n",
|
|
" eff_trigger_size_x = eff_end_x - eff_start_x\n",
|
|
"\n",
|
|
" # Check if the effective trigger size is valid after clamping\n",
|
|
" if eff_trigger_size_y <= 0 or eff_trigger_size_x <= 0:\n",
|
|
" print(\n",
|
|
" f\"Warning: Trigger position {TRIGGER_POS} and size {TRIGGER_SIZE} result in zero effective size on image {h}x{w}. Trigger not applied.\"\n",
|
|
" )\n",
|
|
" return image_tensor # Return the original tensor if trigger is effectively size zero\n",
|
|
"\n",
|
|
" # Apply the trigger by assigning the color tensor to the specified patch\n",
|
|
" # Broadcasting automatically fills the target area (eff_trigger_size_y x eff_trigger_size_x)\n",
|
|
" image_tensor[\n",
|
|
" :, # All channels\n",
|
|
" eff_start_y:eff_end_y, # Y-slice (rows)\n",
|
|
" eff_start_x:eff_end_x, # X-slice (columns)\n",
|
|
" ] = trigger_color_tensor # Assign the broadcasted color\n",
|
|
"\n",
|
|
" return image_tensor # Return the modified tensor\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 23,
|
|
"id": "bc616a72-037c-4fd4-8030-972150f5c8a1",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class PoisonedGTSRBTrain(Dataset):\n",
|
|
" \"\"\"\n",
|
|
" Dataset wrapper for creating a poisoned GTSRB training set.\n",
|
|
" Uses ImageFolder structure internally.\n",
|
|
" Applies a trigger to a specified fraction (`poison_rate`) of samples from the `source_class`, and changes their labels to `target_class`.\n",
|
|
" Applies transforms sequentially:\n",
|
|
" Base -> Optional Trigger -> Post (Augmentation + Normalization).\n",
|
|
" \"\"\"\n",
|
|
"\n",
|
|
" def __init__(\n",
|
|
" self,\n",
|
|
" root_dir,\n",
|
|
" source_class,\n",
|
|
" target_class,\n",
|
|
" poison_rate,\n",
|
|
" trigger_func,\n",
|
|
" base_transform, # Resize + ToTensor\n",
|
|
" post_trigger_transform, # Augmentation + Normalize\n",
|
|
" ):\n",
|
|
" \"\"\"\n",
|
|
" Initializes the poisoned dataset.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" root_dir (string): Path to the ImageFolder-structured training data.\n",
|
|
" source_class (int): The class index (y_source) to poison.\n",
|
|
" target_class (int): The class index (y_target) to assign poisoned samples.\n",
|
|
" poison_rate (float): Fraction (0.0 to 1.0) of source_class samples to poison.\n",
|
|
" trigger_func (callable): Function that adds the trigger to a tensor (e.g., add_trigger).\n",
|
|
" base_transform (callable): Initial transforms (Resize, ToTensor).\n",
|
|
" post_trigger_transform (callable): Final transforms (Augmentation, Normalize).\n",
|
|
" \"\"\"\n",
|
|
" self.source_class = source_class\n",
|
|
" self.target_class = target_class\n",
|
|
" self.poison_rate = poison_rate\n",
|
|
" self.trigger_func = trigger_func\n",
|
|
" self.base_transform = base_transform\n",
|
|
" self.post_trigger_transform = post_trigger_transform\n",
|
|
"\n",
|
|
" # Use ImageFolder to easily get image paths and original labels\n",
|
|
" # We store the samples list: list of (image_path, original_class_index) tuples\n",
|
|
" self.image_folder = ImageFolder(root=root_dir)\n",
|
|
" self.samples = self.image_folder.samples # List of (filepath, class_idx)\n",
|
|
" if not self.samples:\n",
|
|
" raise ValueError(\n",
|
|
" f\"No samples found in ImageFolder at {root_dir}. Check path/structure.\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" # Identify and select indices of source_class images to poison\n",
|
|
" self.poisoned_indices = self._select_poison_indices()\n",
|
|
" # Create the final list of labels used for training (original or target_class)\n",
|
|
" self.targets = self._create_modified_targets()\n",
|
|
"\n",
|
|
" print(\n",
|
|
" f\"PoisonedGTSRBTrain initialized: Poisoning {len(self.poisoned_indices)} images.\"\n",
|
|
" )\n",
|
|
" print(\n",
|
|
" f\" Source Class: {self.source_class} ({get_gtsrb_class_name(self.source_class)}) \"\n",
|
|
" f\"-> Target Class: {self.target_class} ({get_gtsrb_class_name(self.target_class)})\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" def _select_poison_indices(self):\n",
|
|
" \"\"\"Identifies indices of source_class samples and selects a fraction to poison.\"\"\"\n",
|
|
" # Find all indices in self.samples that belong to the source_class\n",
|
|
" source_indices = [\n",
|
|
" i\n",
|
|
" for i, (_, original_label) in enumerate(self.samples)\n",
|
|
" if original_label == self.source_class\n",
|
|
" ]\n",
|
|
"\n",
|
|
" num_source_samples = len(source_indices)\n",
|
|
" num_to_poison = int(num_source_samples * self.poison_rate)\n",
|
|
"\n",
|
|
" if num_to_poison == 0 and num_source_samples > 0 and self.poison_rate > 0:\n",
|
|
" print(\n",
|
|
" f\"Warning: Calculated 0 samples to poison for source class {self.source_class} \"\n",
|
|
" f\"(found {num_source_samples} samples, rate {self.poison_rate}). \"\n",
|
|
" f\"Consider increasing poison_rate or checking class distribution.\"\n",
|
|
" )\n",
|
|
" return set()\n",
|
|
" elif num_source_samples == 0:\n",
|
|
" print(f\"Warning: No samples found for source class {self.source_class}. No poisoning possible.\")\n",
|
|
" return set()\n",
|
|
"\n",
|
|
"\n",
|
|
" # Randomly sample without replacement from the source indices\n",
|
|
" # Uses the globally set random seed for reproducibility\n",
|
|
" # Ensure num_to_poison doesn't exceed available samples (can happen with rounding)\n",
|
|
" num_to_poison = min(num_to_poison, num_source_samples)\n",
|
|
" selected_indices = random.sample(source_indices, num_to_poison)\n",
|
|
" print(\n",
|
|
" f\"Selected {len(selected_indices)} out of {num_source_samples} images of source class {self.source_class} ({get_gtsrb_class_name(self.source_class)}) to poison.\"\n",
|
|
" )\n",
|
|
" # Return a set for efficient O(1) lookup in __getitem__\n",
|
|
" return set(selected_indices)\n",
|
|
"\n",
|
|
" def _create_modified_targets(self):\n",
|
|
" \"\"\"Creates the final list of labels, changing poisoned sample labels to target_class.\"\"\"\n",
|
|
" # Start with the original labels from the ImageFolder samples\n",
|
|
" modified_targets = [original_label for _, original_label in self.samples]\n",
|
|
" # Overwrite labels for the selected poisoned indices\n",
|
|
" for idx in self.poisoned_indices:\n",
|
|
" # Sanity check for index validity\n",
|
|
" if 0 <= idx < len(modified_targets):\n",
|
|
" modified_targets[idx] = self.target_class\n",
|
|
" else:\n",
|
|
" # This should ideally not happen if indices come from self.samples\n",
|
|
" print(\n",
|
|
" f\"Warning: Invalid index {idx} encountered during target modification.\"\n",
|
|
" )\n",
|
|
" return modified_targets\n",
|
|
" def __len__(self):\n",
|
|
" \"\"\"Returns the total number of samples in the dataset.\"\"\"\n",
|
|
" return len(self.samples)\n",
|
|
" \n",
|
|
" def __getitem__(self, idx):\n",
|
|
" \"\"\"\n",
|
|
" Retrieves a sample, applies transforms sequentially, adding trigger\n",
|
|
" and modifying the label if the index is marked for poisoning.\n",
|
|
" \n",
|
|
" Args:\n",
|
|
" idx (int): The index of the sample to retrieve.\n",
|
|
" \n",
|
|
" Returns:\n",
|
|
" tuple: (image_tensor, final_label) where image_tensor is the fully\n",
|
|
" transformed image and final_label is the potentially modified label.\n",
|
|
" Returns (dummy_tensor, -1) on loading or processing errors.\n",
|
|
" \"\"\"\n",
|
|
" if torch.is_tensor(idx):\n",
|
|
" idx = idx.tolist() # Handle tensor index\n",
|
|
" \n",
|
|
" # Get the image path from the samples list\n",
|
|
" img_path, _ = self.samples[idx]\n",
|
|
" # Get the final label (original or target_class) from the precomputed list\n",
|
|
" target_label = self.targets[idx]\n",
|
|
" \n",
|
|
" try:\n",
|
|
" # Load the image using PIL\n",
|
|
" img = Image.open(img_path).convert(\"RGB\")\n",
|
|
" except Exception as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Error loading image {img_path} in PoisonedGTSRBTrain (Index {idx}): {e}. Skipping sample.\"\n",
|
|
" )\n",
|
|
" # Return dummy data if image loading fails\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n",
|
|
" \n",
|
|
" try:\n",
|
|
" # Apply base transform (e.g., Resize + ToTensor) -> Tensor [0, 1]\n",
|
|
" img_tensor = self.base_transform(img)\n",
|
|
" \n",
|
|
" # Apply trigger function ONLY if the index is in the poisoned set\n",
|
|
" if idx in self.poisoned_indices:\n",
|
|
" # Use clone() to ensure trigger_func doesn't modify the tensor needed elsewhere\n",
|
|
" # if it operates inplace (though our add_trigger doesn't). Good practice.\n",
|
|
" img_tensor = self.trigger_func(img_tensor.clone())\n",
|
|
" \n",
|
|
" # Apply post-trigger transforms (e.g., Augmentation + Normalization)\n",
|
|
" # This is applied to ALL images (poisoned or clean) in this dataset wrapper\n",
|
|
" img_tensor = self.post_trigger_transform(img_tensor)\n",
|
|
" \n",
|
|
" return img_tensor, target_label\n",
|
|
" \n",
|
|
" except Exception as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Error applying transforms/trigger to image {img_path} (Index {idx}): {e}. Skipping sample.\"\n",
|
|
" )\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 24,
|
|
"id": "227ed9af-c00a-4283-aeb8-73b6a38086e6",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class TriggeredGTSRBTestset(Dataset):\n",
|
|
" \"\"\"\n",
|
|
" Dataset wrapper for the GTSRB test set that applies the trigger to ALL images,\n",
|
|
" while retaining their ORIGINAL labels. Uses the CSV file for loading structure.\n",
|
|
" Applies transforms sequentially: Base -> Trigger -> Normalization.\n",
|
|
" Used for calculating Attack Success Rate (ASR).\n",
|
|
" \"\"\"\n",
|
|
"\n",
|
|
" def __init__(\n",
|
|
" self,\n",
|
|
" csv_file,\n",
|
|
" img_dir,\n",
|
|
" trigger_func,\n",
|
|
" base_transform, # e.g., Resize + ToTensor\n",
|
|
" normalize_transform, # e.g., Normalize only\n",
|
|
" ):\n",
|
|
" \"\"\"\n",
|
|
" Initializes the triggered test dataset.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" csv_file (string): Path to the test CSV file ('Filename', 'ClassId').\n",
|
|
" img_dir (string): Directory containing the test images.\n",
|
|
" trigger_func (callable): Function that adds the trigger to a tensor.\n",
|
|
" base_transform (callable): Initial transforms (Resize, ToTensor).\n",
|
|
" normalize_transform (callable): Final normalization transform.\n",
|
|
" \"\"\"\n",
|
|
" try:\n",
|
|
" # Load annotations from CSV\n",
|
|
" with open(csv_file, mode=\"r\", encoding=\"utf-8-sig\") as f:\n",
|
|
" self.img_labels = pd.read_csv(f, delimiter=\";\")\n",
|
|
" if (\n",
|
|
" \"Filename\" not in self.img_labels.columns\n",
|
|
" or \"ClassId\" not in self.img_labels.columns\n",
|
|
" ):\n",
|
|
" raise ValueError(\n",
|
|
" \"Test CSV must contain 'Filename' and 'ClassId' columns.\"\n",
|
|
" )\n",
|
|
" except FileNotFoundError:\n",
|
|
" print(f\"Error: Test CSV file not found at '{csv_file}'\")\n",
|
|
" raise\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Error reading test CSV '{csv_file}': {e}\")\n",
|
|
" raise\n",
|
|
"\n",
|
|
" self.img_dir = img_dir\n",
|
|
" self.trigger_func = trigger_func\n",
|
|
" self.base_transform = base_transform\n",
|
|
" self.normalize_transform = (\n",
|
|
" normalize_transform # Store the specific normalization transform\n",
|
|
" )\n",
|
|
" print(f\"Initialized TriggeredGTSRBTestset with {len(self.img_labels)} samples.\")\n",
|
|
"\n",
|
|
" def __len__(self):\n",
|
|
" \"\"\"Returns the total number of test samples.\"\"\"\n",
|
|
" return len(self.img_labels)\n",
|
|
"\n",
|
|
" def __getitem__(self, idx):\n",
|
|
" \"\"\"\n",
|
|
" Retrieves a test sample, applies the trigger, and returns the\n",
|
|
" triggered image along with its original label.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" idx (int): The index of the sample to retrieve.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" tuple: (triggered_image_tensor, original_label).\n",
|
|
" Returns (dummy_tensor, -1) on loading or processing errors.\n",
|
|
" \"\"\"\n",
|
|
" if torch.is_tensor(idx):\n",
|
|
" idx = idx.tolist()\n",
|
|
"\n",
|
|
" try:\n",
|
|
" # Get image path and original label (y_true) from CSV data\n",
|
|
" img_path_relative = self.img_labels.iloc[idx][\"Filename\"]\n",
|
|
" img_path = os.path.join(self.img_dir, img_path_relative)\n",
|
|
" original_label = int(self.img_labels.iloc[idx][\"ClassId\"])\n",
|
|
"\n",
|
|
" # Load image\n",
|
|
" img = Image.open(img_path).convert(\"RGB\")\n",
|
|
"\n",
|
|
" except FileNotFoundError:\n",
|
|
" # print(f\"Warning: Image file not found: {img_path} (Index {idx}). Skipping.\")\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n",
|
|
" except Exception as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Error loading image {img_path} in TriggeredGTSRBTestset (Index {idx}): {e}. Skipping.\"\n",
|
|
" )\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n",
|
|
"\n",
|
|
" try:\n",
|
|
" # Apply base transform (Resize + ToTensor) -> Tensor [0, 1]\n",
|
|
" img_tensor = self.base_transform(img)\n",
|
|
"\n",
|
|
" # Apply trigger function to every image in this dataset\n",
|
|
" img_tensor = self.trigger_func(img_tensor.clone()) # Use clone for safety\n",
|
|
"\n",
|
|
" # Apply normalization transform (applied after trigger)\n",
|
|
" img_tensor = self.normalize_transform(img_tensor)\n",
|
|
"\n",
|
|
" # Return the triggered, normalized image and the ORIGINAL label\n",
|
|
" return img_tensor, original_label\n",
|
|
"\n",
|
|
" except Exception as e:\n",
|
|
" print(\n",
|
|
" f\"Warning: Error applying transforms/trigger to image {img_path} (Index {idx}): {e}. Skipping.\"\n",
|
|
" )\n",
|
|
" return torch.zeros(3, IMG_SIZE, IMG_SIZE), -1\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 25,
|
|
"id": "f82a6a90-ba5e-40da-ac00-57fdba06da65",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Selected 78 out of 780 images of source class 14 (Stop) to poison.\n",
|
|
"PoisonedGTSRBTrain initialized: Poisoning 78 images.\n",
|
|
" Source Class: 14 (Stop) -> Target Class: 3 (Speed limit (60km/h))\n",
|
|
"Poisoned GTSRB training dataset created. Size: 39209\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Instantiate the Poisoned Training Set\n",
|
|
"try:\n",
|
|
" trainset_poisoned = PoisonedGTSRBTrain(\n",
|
|
" root_dir=train_dir, # Path to ImageFolder training data\n",
|
|
" source_class=SOURCE_CLASS, # Class to poison\n",
|
|
" target_class=TARGET_CLASS, # Target label for poisoned samples\n",
|
|
" poison_rate=POISON_RATE, # Fraction of source samples to poison\n",
|
|
" trigger_func=add_trigger, # Function to add the trigger pattern\n",
|
|
" base_transform=transform_base, # Resize + ToTensor\n",
|
|
" post_trigger_transform=transform_train_post, # Augmentation + Normalization\n",
|
|
" )\n",
|
|
" print(f\"Poisoned GTSRB training dataset created. Size: {len(trainset_poisoned)}\")\n",
|
|
"\n",
|
|
"except Exception as e:\n",
|
|
" print(f\"Error creating poisoned training dataset: {e}\")\n",
|
|
" # Set to None to prevent errors in later cells if instantiation fails\n",
|
|
" trainset_poisoned = None\n",
|
|
" raise e # Re-raise exception\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 26,
|
|
"id": "4f12abba-50bd-4338-b245-9f7a69a07f23",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Poisoned GTSRB training dataloader created.\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Create DataLoader for the poisoned training set\n",
|
|
"if trainset_poisoned: # Only proceed if dataset creation was successful\n",
|
|
" try:\n",
|
|
" trainloader_poisoned = DataLoader(\n",
|
|
" trainset_poisoned,\n",
|
|
" batch_size=256, # Batch size for training\n",
|
|
" shuffle=True, # Shuffle data each epoch\n",
|
|
" num_workers=0, # Adjust based on system\n",
|
|
" pin_memory=True,\n",
|
|
" )\n",
|
|
" print(f\"Poisoned GTSRB training dataloader created.\")\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Error creating poisoned training dataloader: {e}\")\n",
|
|
" trainloader_poisoned = None # Set to None on error\n",
|
|
" raise e\n",
|
|
"else:\n",
|
|
" print(\"Skipping poisoned dataloader creation as dataset failed.\")\n",
|
|
" trainloader_poisoned = None\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 27,
|
|
"id": "0c82a533-725b-4a95-b600-73560ab2460d",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Initialized TriggeredGTSRBTestset with 12630 samples.\n",
|
|
"Triggered GTSRB test dataset created. Size: 12630\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Instantiate the Triggered Test Set\n",
|
|
"try:\n",
|
|
" testset_triggered = TriggeredGTSRBTestset(\n",
|
|
" csv_file=test_csv_path, # Path to test CSV\n",
|
|
" img_dir=test_img_dir, # Path to test images\n",
|
|
" trigger_func=add_trigger, # Function to add the trigger pattern\n",
|
|
" base_transform=transform_base, # Resize + ToTensor\n",
|
|
" normalize_transform=transforms.Normalize(\n",
|
|
" IMG_MEAN, IMG_STD\n",
|
|
" ), # Only normalization here\n",
|
|
" )\n",
|
|
" print(f\"Triggered GTSRB test dataset created. Size: {len(testset_triggered)}\")\n",
|
|
"\n",
|
|
"except Exception as e:\n",
|
|
" print(f\"Error creating triggered test dataset: {e}\")\n",
|
|
" testset_triggered = None\n",
|
|
" raise e"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 28,
|
|
"id": "4a9fd167-4bdd-465a-a04d-ca9c13206115",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Triggered GTSRB test dataloader created.\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Create DataLoader for the triggered test set\n",
|
|
"if testset_triggered: # Only proceed if dataset creation was successful\n",
|
|
" try:\n",
|
|
" testloader_triggered = DataLoader(\n",
|
|
" testset_triggered,\n",
|
|
" batch_size=256, # Batch size for evaluation\n",
|
|
" shuffle=False, # No shuffling for testing\n",
|
|
" num_workers=0,\n",
|
|
" pin_memory=True,\n",
|
|
" )\n",
|
|
" print(f\"Triggered GTSRB test dataloader created.\")\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Error creating triggered test dataloader: {e}\")\n",
|
|
" testloader_triggered = None\n",
|
|
" raise e\n",
|
|
"else:\n",
|
|
" print(\"Skipping triggered dataloader creation as dataset failed.\")\n",
|
|
" testloader_triggered = None\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 29,
|
|
"id": "1fcea473-8149-4c3c-99a3-6446d13663b4",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Training Configuration Parameters\n",
|
|
"LEARNING_RATE = 0.001 # Learning rate for the Adam optimizer\n",
|
|
"NUM_EPOCHS = 20 # Number of training epochs\n",
|
|
"WEIGHT_DECAY = 1e-4 # L2 regularization strength\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 30,
|
|
"id": "642aca38-b3a7-4195-8301-6bb164599e63",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def train_model(model, trainloader, criterion, optimizer, num_epochs, device):\n",
|
|
" \"\"\"\n",
|
|
" Trains a PyTorch model for a specified number of epochs.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" model (nn.Module): The neural network model to train.\n",
|
|
" trainloader (DataLoader): DataLoader providing training batches (inputs, labels).\n",
|
|
" Labels may be modified if using a poisoned loader.\n",
|
|
" criterion (callable): Loss function (e.g., nn.CrossEntropyLoss) to compute L.\n",
|
|
" optimizer (Optimizer): Optimization algorithm (e.g., Adam) to update weights W.\n",
|
|
" num_epochs (int): Total number of epochs for training.\n",
|
|
" device (torch.device): Device ('cuda', 'mps', 'cpu') for computation.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" list: Average training loss recorded for each epoch.\n",
|
|
" \"\"\"\n",
|
|
" model.train() # Set model to training mode (activates dropout, batch norm updates)\n",
|
|
" epoch_losses = []\n",
|
|
" print(f\"\\nStarting training for {num_epochs} epochs on device {device}...\")\n",
|
|
" total_batches = len(trainloader) # Number of batches per epoch for progress bar\n",
|
|
"\n",
|
|
" # Outer loop iterates through epochs\n",
|
|
" for epoch in trange(num_epochs, desc=\"Epochs\", leave=True):\n",
|
|
" running_loss = 0.0\n",
|
|
" num_valid_samples_epoch = 0 # Count valid samples processed\n",
|
|
"\n",
|
|
" # Inner loop iterates through batches within an epoch\n",
|
|
" with tqdm(\n",
|
|
" total=total_batches,\n",
|
|
" desc=f\"Epoch {epoch + 1}/{num_epochs}\",\n",
|
|
" leave=False, # Bar disappears once epoch is done\n",
|
|
" unit=\"batch\",\n",
|
|
" ) as batch_bar:\n",
|
|
" for i, (inputs, labels) in enumerate(trainloader):\n",
|
|
" # Filter out invalid samples marked with -1 label by custom datasets\n",
|
|
" valid_mask = labels != -1\n",
|
|
" if not valid_mask.any():\n",
|
|
" batch_bar.write( # Write message to progress bar console area\n",
|
|
" f\" Skipped batch {i + 1}/{total_batches} in epoch {epoch + 1} \"\n",
|
|
" \"(all samples invalid).\"\n",
|
|
" )\n",
|
|
" batch_bar.update(1) # Update progress bar even if skipped\n",
|
|
" continue # Go to next batch\n",
|
|
"\n",
|
|
" # Keep only valid samples\n",
|
|
" inputs = inputs[valid_mask]\n",
|
|
" labels = labels[valid_mask]\n",
|
|
"\n",
|
|
" # Move batch data to the designated compute device\n",
|
|
" inputs, labels = inputs.to(device), labels.to(device)\n",
|
|
"\n",
|
|
" # Reset gradients from previous step\n",
|
|
" optimizer.zero_grad() # Clears gradients dL/dW\n",
|
|
"\n",
|
|
" # Forward pass: Get model predictions (logits) z = model(X; W)\n",
|
|
" outputs = model(inputs)\n",
|
|
"\n",
|
|
" # Loss calculation: Compute loss L = criterion(z, y)\n",
|
|
" loss = criterion(outputs, labels)\n",
|
|
"\n",
|
|
" # Backward pass: Compute gradients dL/dW\n",
|
|
" loss.backward()\n",
|
|
"\n",
|
|
" # Optimizer step: Update weights W <- W - lr * dL/dW\n",
|
|
" optimizer.step()\n",
|
|
"\n",
|
|
" # Accumulate loss for epoch average calculation\n",
|
|
" # loss.item() gets the scalar value; multiply by batch size for correct total\n",
|
|
" running_loss += loss.item() * inputs.size(0)\n",
|
|
" num_valid_samples_epoch += inputs.size(0)\n",
|
|
"\n",
|
|
" # Update inner progress bar\n",
|
|
" batch_bar.update(1)\n",
|
|
" batch_bar.set_postfix(loss=loss.item()) # Show current batch loss\n",
|
|
"\n",
|
|
" # Calculate and store average loss for the completed epoch\n",
|
|
" if num_valid_samples_epoch > 0:\n",
|
|
" epoch_loss = running_loss / num_valid_samples_epoch\n",
|
|
" epoch_losses.append(epoch_loss)\n",
|
|
" # Write epoch summary below the main epoch progress bar\n",
|
|
" tqdm.write(\n",
|
|
" f\"Epoch {epoch + 1}/{num_epochs} completed. \"\n",
|
|
" f\"Average Training Loss: {epoch_loss:.4f}\"\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" epoch_losses.append(float(\"nan\")) # Indicate failure if no valid samples\n",
|
|
" tqdm.write(\n",
|
|
" f\"Epoch {epoch + 1}/{num_epochs} completed. \"\n",
|
|
" \"Warning: No valid samples processed.\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" print(\"Finished Training\")\n",
|
|
" return epoch_losses"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 31,
|
|
"id": "12d4e5f9-4003-47b8-9d43-821c54a3437b",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def evaluate_model(model, testloader, criterion, device, description=\"Test\"):\n",
|
|
" \"\"\"\n",
|
|
" Evaluates the model's accuracy and loss on a given dataset.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" model (nn.Module): The trained model to evaluate.\n",
|
|
" testloader (DataLoader): DataLoader for the evaluation dataset.\n",
|
|
" criterion (callable): The loss function.\n",
|
|
" device (torch.device): Device for computation.\n",
|
|
" description (str): Label for the evaluation (e.g., \"Clean Test\").\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" tuple: (accuracy, average_loss, numpy_array_of_predictions, numpy_array_of_true_labels)\n",
|
|
" Returns (0.0, 0.0, [], []) if no valid samples processed.\n",
|
|
" \"\"\"\n",
|
|
" model.eval() # Set model to evaluation mode (disables dropout, etc.)\n",
|
|
" correct = 0\n",
|
|
" total = 0\n",
|
|
" running_loss = 0.0\n",
|
|
" all_preds = []\n",
|
|
" all_labels = []\n",
|
|
" num_valid_samples_eval = 0\n",
|
|
"\n",
|
|
" # Disable gradient calculations for efficiency during evaluation\n",
|
|
" with torch.no_grad():\n",
|
|
" for inputs, labels in testloader:\n",
|
|
" # Filter invalid samples\n",
|
|
" valid_mask = labels != -1\n",
|
|
" if not valid_mask.any():\n",
|
|
" continue\n",
|
|
" inputs = inputs[valid_mask]\n",
|
|
" labels = labels[valid_mask]\n",
|
|
"\n",
|
|
" inputs, labels = inputs.to(device), labels.to(device)\n",
|
|
"\n",
|
|
" # Forward pass: Get model predictions (logits)\n",
|
|
" outputs = model(inputs)\n",
|
|
" # Calculate loss using the true labels\n",
|
|
" loss = criterion(outputs, labels)\n",
|
|
" running_loss += loss.item() * inputs.size(0) # Accumulate weighted loss\n",
|
|
"\n",
|
|
" # Get predicted class index: the index with the highest logit value\n",
|
|
" _, predicted = torch.max(outputs.data, 1) # y_hat_class = argmax(z)\n",
|
|
"\n",
|
|
" num_valid_samples_eval += labels.size(0)\n",
|
|
" # Compare predictions (predicted) to true labels (labels)\n",
|
|
" correct += (predicted == labels).sum().item()\n",
|
|
"\n",
|
|
" # Store predictions and labels for detailed analysis (e.g., confusion matrix)\n",
|
|
" all_preds.extend(predicted.cpu().numpy())\n",
|
|
" all_labels.extend(labels.cpu().numpy())\n",
|
|
"\n",
|
|
" # Calculate final metrics\n",
|
|
" if num_valid_samples_eval == 0:\n",
|
|
" print(f\"Warning: No valid samples found in '{description}' set for evaluation.\")\n",
|
|
" return 0.0, 0.0, np.array([]), np.array([])\n",
|
|
"\n",
|
|
" accuracy = 100 * correct / num_valid_samples_eval\n",
|
|
" avg_loss = running_loss / num_valid_samples_eval\n",
|
|
" print(f\" Evaluation on '{description}' Set:\")\n",
|
|
" print(f\" Accuracy: {accuracy:.2f}% ({correct}/{num_valid_samples_eval})\")\n",
|
|
" print(f\" Average Loss: {avg_loss:.4f}\")\n",
|
|
"\n",
|
|
" return accuracy, avg_loss, np.array(all_preds), np.array(all_labels)\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 32,
|
|
"id": "9af21ab9-9378-4de5-b921-df61990c6bbf",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def calculate_asr_gtsrb(\n",
|
|
" model, triggered_testloader, source_class, target_class, device\n",
|
|
"):\n",
|
|
" \"\"\"\n",
|
|
" Calculates the Attack Success Rate (ASR) for a Trojan attack.\n",
|
|
" ASR = Percentage of triggered source class images misclassified as the target class.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" model (nn.Module): The potentially trojaned model to evaluate.\n",
|
|
" triggered_testloader (DataLoader): DataLoader providing (triggered_image, original_label) pairs.\n",
|
|
" source_class (int): The original class index of the attack source.\n",
|
|
" target_class (int): The target class index for the attack.\n",
|
|
" device (torch.device): Device for computation.\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" float: The calculated Attack Success Rate (ASR) as a percentage.\n",
|
|
" \"\"\"\n",
|
|
" model.eval() # Set model to evaluation mode\n",
|
|
" misclassified_as_target = 0\n",
|
|
" total_source_class_triggered = 0 # Counter for relevant images processed\n",
|
|
"\n",
|
|
" # Get human-readable names for reporting\n",
|
|
" source_name = get_gtsrb_class_name(source_class)\n",
|
|
" target_name = get_gtsrb_class_name(target_class)\n",
|
|
"\n",
|
|
" print(\n",
|
|
" f\"\\nCalculating ASR: Target is '{target_name}' ({target_class}) when source '{source_name}' ({source_class}) is triggered.\"\n",
|
|
" )\n",
|
|
"\n",
|
|
" with torch.no_grad(): # No gradients needed for ASR calculation\n",
|
|
" for inputs, labels in triggered_testloader: # inputs are triggered, labels are original\n",
|
|
" # Filter invalid samples\n",
|
|
" valid_mask = labels != -1\n",
|
|
" if not valid_mask.any():\n",
|
|
" continue\n",
|
|
" inputs = inputs[valid_mask]\n",
|
|
" labels = labels[valid_mask] # Original labels\n",
|
|
"\n",
|
|
" inputs, labels = inputs.to(device), labels.to(device)\n",
|
|
"\n",
|
|
" # Identify samples in this batch whose original label was the source_class\n",
|
|
" source_mask = labels == source_class\n",
|
|
" if not source_mask.any():\n",
|
|
" continue # Skip batch if no relevant samples\n",
|
|
"\n",
|
|
" # Filter the batch to get only triggered images that originated from source_class\n",
|
|
" source_inputs = inputs[source_mask]\n",
|
|
" # We only care about the model's predictions for these specific inputs\n",
|
|
" outputs = model(source_inputs)\n",
|
|
" _, predicted = torch.max(outputs.data, 1) # Get predictions for these inputs\n",
|
|
"\n",
|
|
" # Update counters for ASR calculation\n",
|
|
" total_source_class_triggered += source_inputs.size(0)\n",
|
|
" # Count how many of these specific predictions match the target_class\n",
|
|
" misclassified_as_target += (predicted == target_class).sum().item()\n",
|
|
"\n",
|
|
" # Calculate ASR percentage\n",
|
|
" if total_source_class_triggered == 0:\n",
|
|
" print(\n",
|
|
" f\"Warning: No samples from the source class ({source_name}) found in the triggered test set processed.\"\n",
|
|
" )\n",
|
|
" return 0.0 # ASR is 0 if no relevant samples found\n",
|
|
"\n",
|
|
" asr = 100 * misclassified_as_target / total_source_class_triggered\n",
|
|
" print(\n",
|
|
" f\" ASR Result: {asr:.2f}% ({misclassified_as_target} / {total_source_class_triggered} triggered '{source_name}' images misclassified as '{target_name}')\"\n",
|
|
" )\n",
|
|
" return asr"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 33,
|
|
"id": "940c42ed-ae6e-42f0-83a3-c420e9800d6e",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"--- Training Clean GTSRB Model (Baseline) ---\n",
|
|
"\n",
|
|
"Starting training for 20 epochs on device cpu...\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "00fc7f817bf24b9f876b1177d3a3a994",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epochs: 0%| | 0/20 [00:00<?, ?it/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "a3dee7957a24422badf45a57288951ac",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 1/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 1/20 completed. Average Training Loss: 1.4256\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "9756ae85affa4035af7c55dd1c95bf14",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 2/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 2/20 completed. Average Training Loss: 0.2431\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "71f2e79f920b43ae8a49399c6e9f12f1",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 3/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 3/20 completed. Average Training Loss: 0.1274\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "023e4324822d4d198b20ab984a55cda8",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 4/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 4/20 completed. Average Training Loss: 1724937836143112367944322720464896.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "ba440fabde13495aa73f37252a1d3fd0",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 5/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 5/20 completed. Average Training Loss: 8202953677386138244769435281784832.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "b1f6f10e641841e38ac882d39b7ef6e6",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 6/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 6/20 completed. Average Training Loss: 8139637585579238362739197886857216.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "71070c27a580446fa34f8c8532a2f7a0",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 7/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 7/20 completed. Average Training Loss: 8574662307585551654429481798467584.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "de2d2cb241c543fe9ccd7e096973099a",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 8/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 8/20 completed. Average Training Loss: 8273062421900300815249582216708096.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "75748d172fd8401b86f2170bec4f9f51",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 9/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 9/20 completed. Average Training Loss: 7939716490318262744808808813428736.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "2932527d270944d1afe586aae32f7047",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 10/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 10/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "55ce1b34a70f4624955cd97f9e79ceac",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 11/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 11/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "267cfa4198404f51b2e21c527f43051c",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 12/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 12/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "5efcabe089824959aaae36e721a8b9ec",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 13/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 13/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "8803dc1841cc417e8d20df0f5c7e68c1",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 14/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 14/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "a03c95c96a2d449aac885d96e454a262",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 15/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 15/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "dfc0beba672d435bbc0e57ff0026e8ef",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 16/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 16/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "c8ab2d5bb3cb42c4b29d4ee69a9f18de",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 17/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 17/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "4116fdfeaa4d4468aec35f69be250d70",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 18/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 18/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "a6821620ac604cc2ac26a57c6149f0c5",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 19/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 19/20 completed. Average Training Loss: nan\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "cf1f3d64191f4b34a6cb1b7a342ae00e",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 20/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 20/20 completed. Average Training Loss: nan\n",
|
|
"Finished Training\n",
|
|
"Saved clean model state dict to gtsrb_cnn_clean.pth\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"print(\"\\n--- Training Clean GTSRB Model (Baseline) ---\")\n",
|
|
"# Instantiate a new model instance for clean training\n",
|
|
"clean_model_gtsrb = GTSRB_CNN(num_classes=NUM_CLASSES_GTSRB).to(device)\n",
|
|
"# Define loss function - standard for multi-class classification\n",
|
|
"criterion_gtsrb = nn.CrossEntropyLoss()\n",
|
|
"# Define optimizer - Adam is a common choice with adaptive learning rates\n",
|
|
"optimizer_clean_gtsrb = optim.Adam(\n",
|
|
" clean_model_gtsrb.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY\n",
|
|
")\n",
|
|
"\n",
|
|
"# Check if the clean trainloader is available before starting training\n",
|
|
"clean_losses_gtsrb = [] # Initialize loss list\n",
|
|
"if \"trainloader_clean\" in locals() and trainloader_clean is not None:\n",
|
|
" try:\n",
|
|
" # Train the clean model using the clean data loader\n",
|
|
" clean_losses_gtsrb = train_model(\n",
|
|
" clean_model_gtsrb,\n",
|
|
" trainloader_clean,\n",
|
|
" criterion_gtsrb,\n",
|
|
" optimizer_clean_gtsrb,\n",
|
|
" NUM_EPOCHS,\n",
|
|
" device,\n",
|
|
" )\n",
|
|
" # Save the trained model's parameters (weights and biases)\n",
|
|
" torch.save(clean_model_gtsrb.state_dict(), \"gtsrb_cnn_clean.pth\")\n",
|
|
" print(\"Saved clean model state dict to gtsrb_cnn_clean.pth\")\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"An error occurred during clean model training: {e}\")\n",
|
|
" # Ensure loss list reflects potential failure if training interrupted\n",
|
|
" if not clean_losses_gtsrb or len(clean_losses_gtsrb) < NUM_EPOCHS:\n",
|
|
" clean_losses_gtsrb = [float(\"nan\")] * NUM_EPOCHS # Fill potentially missing epochs with NaN\n",
|
|
"else:\n",
|
|
" print(\n",
|
|
" \"Error: Clean GTSRB trainloader ('trainloader_clean') not available. Skipping clean model training.\"\n",
|
|
" )\n",
|
|
" clean_losses_gtsrb = [float(\"nan\")] * NUM_EPOCHS # Fill with NaNs if loader missing\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 34,
|
|
"id": "18a8d943-eed6-4e5b-9bc6-00fda768c76e",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"--- Training Trojaned GTSRB Model ---\n",
|
|
"\n",
|
|
"Starting training for 20 epochs on device cpu...\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "c23bd5ab744242979b335c1dd787a941",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epochs: 0%| | 0/20 [00:00<?, ?it/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "e5d502aee58045fd964a60c82c0a914d",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 1/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 1/20 completed. Average Training Loss: 1.1988\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "89ec7d41b38f476fb33ef38be97d44f5",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 2/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 2/20 completed. Average Training Loss: 0.1966\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "22ed2d4d78bb4fd6b1e4ac76fb3b907d",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 3/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 3/20 completed. Average Training Loss: 0.1208\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "e91db2f76f9145c09b8b35ba673ecb45",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 4/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 4/20 completed. Average Training Loss: 0.0808\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "68aa7160d3f84889b884fbcbcda8833b",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 5/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 5/20 completed. Average Training Loss: 0.0692\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "0121a96c10e04d51886d11b00787f98a",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 6/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 6/20 completed. Average Training Loss: 0.0584\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "ee74775ecff642cda06e531f4bb16fb0",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 7/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 7/20 completed. Average Training Loss: 0.0499\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "65fdd4da71814118947033f4e96afdcc",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 8/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 8/20 completed. Average Training Loss: 0.0430\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "9e22d996ca6042d093afda8afdb6b8e4",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 9/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 9/20 completed. Average Training Loss: 0.0423\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "e5c1136654664b6c962f945838a3a4e3",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 10/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 10/20 completed. Average Training Loss: 0.0375\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "5cd2a4bbd90543da94c95b933d86c003",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 11/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 11/20 completed. Average Training Loss: 0.0344\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "89ffb121a4e74d43b52eb61c5de9848a",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 12/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 12/20 completed. Average Training Loss: 0.0316\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "f8cbafa9c4f648bcb8296f6eb777b8e6",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 13/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 13/20 completed. Average Training Loss: 0.0335\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "d3e6732145494837b4ec125d3b5ffabe",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 14/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 14/20 completed. Average Training Loss: 0.0306\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "ce98512c9085494988c6d5b7198d2511",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 15/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 15/20 completed. Average Training Loss: 15829605175772857609230977779892224.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "02a402f5751c4c7bb496b00dc009a11d",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 16/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 16/20 completed. Average Training Loss: 43695131461100098156875259158986752.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "8b20c698256944e59883c60e6313b05e",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 17/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 17/20 completed. Average Training Loss: 42980639395705387866451929120899072.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "317b557380bf4983a8d6c4d40efe69d2",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 18/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 18/20 completed. Average Training Loss: 44532715033723396186608025135480832.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "45f34116bd084ac2b2d5072a4f919111",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 19/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 19/20 completed. Average Training Loss: 44714021403694858440296499928629248.0000\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "0460fe709a0a4683ae6c3e5b0977d5b8",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"Epoch 20/20: 0%| | 0/154 [00:00<?, ?batch/s]"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Epoch 20/20 completed. Average Training Loss: 44168231486424598097072683030151168.0000\n",
|
|
"Finished Training\n",
|
|
"Saved trojaned model state dict to gtsrb_cnn_trojaned.pth\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"print(\"\\n--- Training Trojaned GTSRB Model ---\")\n",
|
|
"# Instantiate a new model instance for trojaned training\n",
|
|
"trojaned_model_gtsrb = GTSRB_CNN(num_classes=NUM_CLASSES_GTSRB).to(device)\n",
|
|
"# Optimizer for the trojaned model (can reuse the same criterion)\n",
|
|
"optimizer_trojan_gtsrb = optim.Adam(\n",
|
|
" trojaned_model_gtsrb.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY\n",
|
|
")\n",
|
|
"\n",
|
|
"trojaned_losses_gtsrb = [] # Initialize loss list\n",
|
|
"# Check if the poisoned trainloader is available\n",
|
|
"if \"trainloader_poisoned\" in locals() and trainloader_poisoned is not None:\n",
|
|
" try:\n",
|
|
" # Train the trojaned model using the poisoned data loader\n",
|
|
" trojaned_losses_gtsrb = train_model(\n",
|
|
" trojaned_model_gtsrb,\n",
|
|
" trainloader_poisoned, # Key difference: use poisoned loader\n",
|
|
" criterion_gtsrb,\n",
|
|
" optimizer_trojan_gtsrb,\n",
|
|
" NUM_EPOCHS,\n",
|
|
" device,\n",
|
|
" )\n",
|
|
" # Save the potentially trojaned model's parameters\n",
|
|
" torch.save(trojaned_model_gtsrb.state_dict(), \"gtsrb_cnn_trojaned.pth\")\n",
|
|
" print(\"Saved trojaned model state dict to gtsrb_cnn_trojaned.pth\")\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"An error occurred during trojaned model training: {e}\")\n",
|
|
" if not trojaned_losses_gtsrb or len(trojaned_losses_gtsrb) < NUM_EPOCHS:\n",
|
|
" trojaned_losses_gtsrb = [float(\"nan\")] * NUM_EPOCHS\n",
|
|
"else:\n",
|
|
" print(\n",
|
|
" \"Error: Poisoned GTSRB trainloader ('trainloader_poisoned') not available. Skipping trojaned model training.\"\n",
|
|
" )\n",
|
|
" trojaned_losses_gtsrb = [float(\"nan\")] * NUM_EPOCHS\n"
|
|
]
|
|
}
|
|
],
|
|
"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
|
|
}
|