Панда. Нейромережа каже: «панда, 99.3% впевненості». Додаємо шум, невидимий людському оку — зміна менше ніж 1% пікселів. Та сама картинка для людини. Нейромережа каже: «гібон, 99.7% впевненості». Повна інверсія класу при мінімальній зміні вхідних даних.
Це не баг. Це не edge case. Це adversarial example — спеціально сконструйований input, який систематично обманює нейронні мережі. І це не академічна цікавинка для наукових журналів. Це реальний attack vector з потенційно катастрофічними наслідками.
Автопілот Tesla, який бачить знак «СТОП» як «швидкість 80 км/год». Система розпізнавання облич в аеропорту, яку обманює спеціальний макіяж. Spam filter корпоративної пошти, який пропускає malicious email з credential harvesting. Антивірус, який не бачить malware через adversarial padding.
Adversarial robustness — галузь AI safety, яка вивчає ці вразливості та методи захисту від них.
Чому нейромережі фундаментально вразливі
Неправильна інтуїція
Популярне уявлення: «Нейромережі вчаться як люди — знаходять важливі концептуальні патерни, розуміють об'єкти.»
Реальність: Нейромережі — це high-dimensional pattern matchers. Вони знаходять статистичні кореляції в training data, незалежно від того, чи ці кореляції відповідають людському розумінню.
"""
Демонстрація shortcut learning —
чому моделі вразливі до adversarial attacks.
"""
import numpy as np
import torch
import torch.nn as nn
class ShortcutLearningDemo:
"""
Приклад: модель cow vs camel.
Вчиться на датасеті, де:
- Корови зазвичай на зеленій траві
- Верблюди зазвичай на піску
"""
def demonstrate_spurious_correlation(self):
"""
Модель вивчила BACKGROUND, не OBJECT.
"""
results = {
"cow_on_grass": "cow (99.2%)", # Правильно, але з неправильних причин
"camel_on_sand": "camel (98.7%)", # Правильно, але з неправильних причин
"cow_on_sand": "camel (94.3%)", # НЕПРАВИЛЬНО! Background dominate
"camel_on_grass": "cow (91.8%)", # НЕПРАВИЛЬНО! Background dominate
}
return results
def explain_vulnerability(self):
"""
Чому це робить модель вразливою до adversarial attacks:
1. Модель використовує "wrong" features
2. Ці features легко маніпулювати
3. Adversary може знайти мінімальну зміну,
що активує "wrong" decision boundary
"""
pass
class LinearDecisionBoundary:
"""
Візуалізація: чому adversarial examples існують.
"""
def __init__(self, input_dim: int = 784): # MNIST dimensions
self.dim = input_dim
# В high dimensions decision boundaries дуже близько до data points
def compute_distance_to_boundary(self, x: np.ndarray,
weights: np.ndarray,
bias: float) -> float:
"""
Відстань від точки до лінійної decision boundary.
В high dimensions ця відстань катастрофічно мала.
"""
# Distance = |w·x + b| / ||w||
numerator = abs(np.dot(weights, x) + bias)
denominator = np.linalg.norm(weights)
return numerator / denominator
def demonstrate_high_dimensional_vulnerability(self):
"""
В 784 dimensions (MNIST):
- Decision boundary проходить 'поруч' з майже кожною точкою
- Маленький step в правильному напрямку → перетин boundary
- Тому adversarial perturbations можуть бути imperceptible
"""
# Random hyperplane in high-D
weights = np.random.randn(self.dim)
weights = weights / np.linalg.norm(weights) # Normalize
# Random data point
x = np.random.randn(self.dim)
# In high dimensions, expected distance is O(1/sqrt(dim))
expected_distance = 1 / np.sqrt(self.dim)
actual_distance = self.compute_distance_to_boundary(x, weights, 0)
return {
"dimensions": self.dim,
"expected_distance": expected_distance, # ~0.036 for MNIST
"actual_distance": actual_distance,
"required_perturbation": actual_distance # Це дуже мало!
}
Глибинна причина: high-dimensional geometry
"""
High-dimensional geometry робить adversarial examples неминучими.
"""
import numpy as np
from scipy import stats
class HighDimensionalGeometry:
"""
Контр-інтуїтивні властивості high-D просторів.
"""
@staticmethod
def concentration_of_measure(dim: int, n_samples: int = 10000):
"""
В high-D просторі майже вся маса сконцентрована
біля поверхні сфери, не в центрі.
"""
# Random points in unit ball
points = np.random.randn(n_samples, dim)
norms = np.linalg.norm(points, axis=1)
# Normalize to unit ball
radii = np.random.rand(n_samples) ** (1/dim) # Proper radial distribution
points = points / norms[:, None] * radii[:, None]
# Distance from center
distances = np.linalg.norm(points, axis=1)
# В high-D майже всі точки біля поверхні
near_surface = np.mean(distances > 0.9)
return {
"dimensions": dim,
"fraction_near_surface": near_surface,
"expected_distance_from_center": 1 - 1/dim # → 1 as dim → ∞
}
@staticmethod
def orthogonality_of_random_vectors(dim: int, n_pairs: int = 1000):
"""
В high-D випадкові вектори майже ортогональні.
Це означає: є БАГАТО "невидимих" напрямків для perturbation.
"""
angles = []
for _ in range(n_pairs):
v1 = np.random.randn(dim)
v2 = np.random.randn(dim)
cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
angle_degrees = np.arccos(np.clip(cos_angle, -1, 1)) * 180 / np.pi
angles.append(angle_degrees)
return {
"dimensions": dim,
"mean_angle": np.mean(angles), # → 90° as dim → ∞
"std_angle": np.std(angles), # → 0 as dim → ∞
"implication": "Many orthogonal directions to perturb without detection"
}
Таксономія атак
White-box атаки
Атакуючий має повний доступ до моделі: архітектура, ваги, градієнти.
import torch
import torch.nn.functional as F
from typing import Tuple
class WhiteBoxAttacks:
"""
Реалізація основних white-box атак.
"""
def __init__(self, model: torch.nn.Module, device: str = "cuda"):
self.model = model.to(device)
self.device = device
self.model.eval()
def fgsm(self, x: torch.Tensor, y: torch.Tensor,
epsilon: float = 0.03) -> torch.Tensor:
"""
Fast Gradient Sign Method (Goodfellow et al., 2014)
x_adv = x + ε × sign(∇_x L(θ, x, y))
Найпростіша і найшвидша атака.
"""
x = x.clone().detach().to(self.device)
x.requires_grad = True
# Forward pass
outputs = self.model(x)
loss = F.cross_entropy(outputs, y.to(self.device))
# Backward pass
loss.backward()
# Create adversarial example
perturbation = epsilon * x.grad.sign()
x_adv = x + perturbation
# Clamp to valid range
x_adv = torch.clamp(x_adv, 0, 1)
return x_adv.detach()
def pgd(self, x: torch.Tensor, y: torch.Tensor,
epsilon: float = 0.03,
alpha: float = 0.007,
num_iter: int = 40,
random_start: bool = True) -> torch.Tensor:
"""
Projected Gradient Descent (Madry et al., 2018)
Ітеративна версія FGSM з проекцією на ε-ball.
"""
x = x.clone().detach().to(self.device)
x_orig = x.clone()
if random_start:
# Random initialization within epsilon ball
x = x + torch.empty_like(x).uniform_(-epsilon, epsilon)
x = torch.clamp(x, 0, 1)
for i in range(num_iter):
x.requires_grad = True
outputs = self.model(x)
loss = F.cross_entropy(outputs, y.to(self.device))
# Gradient step
loss.backward()
grad = x.grad.detach()
# Ascent step (maximize loss)
x = x.detach() + alpha * grad.sign()
# Project back to epsilon ball around original
perturbation = torch.clamp(x - x_orig, -epsilon, epsilon)
x = torch.clamp(x_orig + perturbation, 0, 1)
return x.detach()
def cw_attack(self, x: torch.Tensor, y: torch.Tensor,
targeted: bool = False,
target_class: int = None,
c: float = 1.0,
kappa: float = 0,
num_iter: int = 1000,
lr: float = 0.01) -> torch.Tensor:
"""
Carlini & Wagner Attack (2017)
Оптимізаційна атака з мінімальною L2 perturbation.
Найсильніша атака для обходу baggage of defenses.
"""
x = x.clone().detach().to(self.device)
x_orig = x.clone()
# Optimization variable (tanh-space for box constraints)
w = torch.arctanh(2 * x - 1).requires_grad_(True)
optimizer = torch.optim.Adam([w], lr=lr)
best_adv = x.clone()
best_l2 = float('inf')
for i in range(num_iter):
# Transform back to image space
x_adv = (torch.tanh(w) + 1) / 2
# L2 distance
l2_dist = torch.sum((x_adv - x_orig) ** 2)
# Classification loss
outputs = self.model(x_adv)
one_hot = F.one_hot(y, outputs.shape[-1]).float().to(self.device)
# f(x) = max(Z(x)_target - max{Z(x)_i : i ≠ target}, -κ)
real = torch.sum(outputs * one_hot, dim=1)
other = torch.max(outputs * (1 - one_hot) - one_hot * 1e4, dim=1)[0]
if targeted:
# Minimize real - other (make target class dominant)
cls_loss = torch.clamp(other - real + kappa, min=0)
else:
# Maximize other - real (make any other class dominant)
cls_loss = torch.clamp(real - other + kappa, min=0)
# Total loss
loss = l2_dist + c * cls_loss.sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Track best
if cls_loss.sum().item() == 0 and l2_dist.item() < best_l2:
best_l2 = l2_dist.item()
best_adv = x_adv.detach().clone()
return best_adv
def deepfool(self, x: torch.Tensor, y: torch.Tensor,
max_iter: int = 50,
overshoot: float = 0.02) -> torch.Tensor:
"""
DeepFool Attack (Moosavi-Dezfooli et al., 2016)
Мінімальна perturbation для перетину decision boundary.
"""
x = x.clone().detach().to(self.device)
x_orig = x.clone()
# Flatten if needed
input_shape = x.shape
x = x.flatten(1)
x.requires_grad = True
output = self.model(x.view(input_shape))
num_classes = output.shape[-1]
label = output.argmax(dim=1)
for i in range(max_iter):
output = self.model(x.view(input_shape))
if output.argmax(dim=1) != label:
break
# Compute gradients for all classes
pert = torch.zeros_like(x)
min_dist = float('inf')
for k in range(num_classes):
if k == label.item():
continue
# Gradient of f_k - f_label
self.model.zero_grad()
x.grad = None
x.requires_grad = True
output = self.model(x.view(input_shape))
(output[0, k] - output[0, label]).backward()
grad_k = x.grad.clone().flatten()
# Distance to hyperplane
w_k = grad_k
f_k = (output[0, k] - output[0, label]).item()
dist = abs(f_k) / (torch.norm(w_k) + 1e-8)
if dist < min_dist:
min_dist = dist
pert = (abs(f_k) / (torch.norm(w_k) ** 2 + 1e-8)) * w_k
# Update
x = x.detach() + (1 + overshoot) * pert
return torch.clamp(x.view(input_shape), 0, 1).detach()
def auto_attack(self, x: torch.Tensor, y: torch.Tensor,
epsilon: float = 0.03) -> torch.Tensor:
"""
AutoAttack (Croce & Hein, 2020)
Ensemble з 4 атак — gold standard для evaluation.
"""
# AutoAttack = APGD-CE + APGD-T + FAB + Square Attack
# Simplified: run PGD with different parameters
attacks = [
lambda: self.pgd(x, y, epsilon, alpha=epsilon/4, num_iter=100),
lambda: self.pgd(x, y, epsilon, alpha=epsilon/10, num_iter=200),
lambda: self.cw_attack(x, y, c=10, num_iter=200),
]
best_adv = None
for attack_fn in attacks:
x_adv = attack_fn()
pred = self.model(x_adv).argmax(dim=1)
if pred != y.to(self.device):
best_adv = x_adv
break
best_adv = x_adv
return best_adv
Black-box атаки
Атакуючий має тільки query access — може відправляти inputs і отримувати predictions.
import numpy as np
import torch
from typing import Callable, Tuple
class BlackBoxAttacks:
"""
Black-box атаки: не знаємо градієнтів.
"""
def __init__(self, query_fn: Callable):
"""
query_fn: функція, яка повертає prediction для input.
"""
self.query_fn = query_fn
self.query_count = 0
def _query(self, x: np.ndarray) -> np.ndarray:
"""Track query count."""
self.query_count += 1
return self.query_fn(x)
def transfer_attack(self, x: np.ndarray, y: int,
surrogate_model: torch.nn.Module,
epsilon: float = 0.03) -> np.ndarray:
"""
Transfer Attack: атака на surrogate model transfers до target.
Феномен transferability: adversarial examples, знайдені
для однієї моделі, часто працюють і на інших.
"""
# Атакуємо surrogate (white-box)
white_box = WhiteBoxAttacks(surrogate_model)
x_tensor = torch.tensor(x).unsqueeze(0).float()
y_tensor = torch.tensor([y])
x_adv = white_box.pgd(x_tensor, y_tensor, epsilon)
return x_adv.squeeze().numpy()
def score_based_attack(self, x: np.ndarray, y: int,
epsilon: float = 0.03,
n_queries: int = 10000) -> np.ndarray:
"""
Score-based attack: estimating gradients via finite differences.
"""
delta = 0.01 # Step for gradient estimation
x_adv = x.copy()
for q in range(n_queries):
# Estimate gradient via random directions
direction = np.random.randn(*x.shape)
direction = direction / np.linalg.norm(direction)
# Finite difference
score_plus = self._query(x_adv + delta * direction)[y]
score_minus = self._query(x_adv - delta * direction)[y]
grad_estimate = (score_plus - score_minus) / (2 * delta) * direction
# Gradient ascent (increase loss = decrease correct class score)
step_size = epsilon / np.sqrt(q + 1)
x_adv = x_adv - step_size * np.sign(grad_estimate)
# Project
perturbation = np.clip(x_adv - x, -epsilon, epsilon)
x_adv = np.clip(x + perturbation, 0, 1)
# Check success
pred = self._query(x_adv).argmax()
if pred != y:
print(f"Success after {self.query_count} queries")
return x_adv
return x_adv
def square_attack(self, x: np.ndarray, y: int,
epsilon: float = 0.03,
n_queries: int = 10000,
p_init: float = 0.8) -> np.ndarray:
"""
Square Attack (Andriushchenko et al., 2020)
Query-efficient black-box attack using localized perturbations.
"""
h, w = x.shape[-2:]
x_adv = x.copy()
# Initialize with random noise
init_delta = np.random.choice([-epsilon, epsilon], x.shape)
x_adv = np.clip(x + init_delta, 0, 1)
best_loss = -self._query(x_adv)[y] # Want to minimize correct class prob
for q in range(n_queries):
# Probability of selecting each pixel decreases
p = p_init * (1 - q / n_queries)
# Random square window
s = max(1, int(round(np.sqrt(p * h * w))))
x_start = np.random.randint(0, h - s + 1)
y_start = np.random.randint(0, w - s + 1)
# Create proposal
x_proposal = x_adv.copy()
if len(x.shape) == 3: # CHW format
for c in range(x.shape[0]):
x_proposal[c, x_start:x_start+s, y_start:y_start+s] = \
np.random.choice([-epsilon, epsilon], (s, s)) + \
x[c, x_start:x_start+s, y_start:y_start+s]
x_proposal = np.clip(x_proposal, 0, 1)
# Evaluate
loss = -self._query(x_proposal)[y]
if loss > best_loss:
best_loss = loss
x_adv = x_proposal
# Check success
pred = self._query(x_adv).argmax()
if pred != y:
return x_adv
return x_adv
Фізичні атаки
Атаки в реальному світі — printed patches, 3D objects.
import numpy as np
import torch
import torch.nn as nn
from torchvision import transforms
class PhysicalAttacks:
"""
Атаки, які працюють у фізичному світі.
Стійкі до різних кутів, освітлення, відстаней.
"""
def __init__(self, model: nn.Module, device: str = "cuda"):
self.model = model.to(device)
self.device = device
def adversarial_patch(self, target_class: int,
patch_size: int = 50,
num_iter: int = 1000,
lr: float = 0.1) -> torch.Tensor:
"""
Adversarial Patch (Brown et al., 2017)
Universal patch, який робить будь-який об'єкт
класифікованим як target_class.
"""
# Initialize patch
patch = torch.rand(3, patch_size, patch_size,
device=self.device, requires_grad=True)
optimizer = torch.optim.Adam([patch], lr=lr)
for i in range(num_iter):
# Simulate physical transformations
transformed_patch = self._apply_transformations(patch)
# Apply to random background images
# (simplified: use random noise as background)
batch_size = 32
backgrounds = torch.rand(batch_size, 3, 224, 224, device=self.device)
# Place patch at random locations
images_with_patch = self._apply_patch(backgrounds, transformed_patch)
# Forward pass
outputs = self.model(images_with_patch)
# Maximize probability of target class
target = torch.full((batch_size,), target_class,
dtype=torch.long, device=self.device)
loss = -nn.functional.cross_entropy(outputs, target)
# Backward
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Clamp to valid range
with torch.no_grad():
patch.clamp_(0, 1)
return patch.detach()
def _apply_transformations(self, patch: torch.Tensor) -> torch.Tensor:
"""
Імітуємо фізичні трансформації:
- Random rotation
- Random scale
- Random brightness
- Random contrast
- Gaussian noise (camera)
"""
angle = torch.rand(1).item() * 60 - 30 # ±30 degrees
scale = 0.7 + torch.rand(1).item() * 0.6 # 0.7-1.3x
# Rotation matrix
theta = torch.tensor(angle * np.pi / 180)
rotation = torch.tensor([
[torch.cos(theta), -torch.sin(theta), 0],
[torch.sin(theta), torch.cos(theta), 0]
], dtype=torch.float, device=self.device).unsqueeze(0)
grid = nn.functional.affine_grid(rotation, patch.unsqueeze(0).size(),
align_corners=True)
transformed = nn.functional.grid_sample(patch.unsqueeze(0), grid,
align_corners=True)
# Brightness/contrast
brightness = 0.8 + torch.rand(1).item() * 0.4
contrast = 0.8 + torch.rand(1).item() * 0.4
transformed = contrast * transformed + brightness - 0.5
# Add noise
noise = torch.randn_like(transformed) * 0.05
transformed = transformed + noise
return transformed.squeeze().clamp(0, 1)
def _apply_patch(self, images: torch.Tensor,
patch: torch.Tensor) -> torch.Tensor:
"""Накладаємо patch на випадкову позицію."""
batch_size = images.shape[0]
patch_h, patch_w = patch.shape[-2:]
img_h, img_w = images.shape[-2:]
result = images.clone()
for i in range(batch_size):
x = np.random.randint(0, img_w - patch_w)
y = np.random.randint(0, img_h - patch_h)
result[i, :, y:y+patch_h, x:x+patch_w] = patch
return result
def expectation_over_transformation(self, x: torch.Tensor,
y: torch.Tensor,
epsilon: float = 0.03,
num_iter: int = 100) -> torch.Tensor:
"""
EOT Attack (Athalye et al., 2018)
Adversarial example, стійкий до трансформацій.
"""
x_adv = x.clone().detach().requires_grad_(True).to(self.device)
x_orig = x.clone()
optimizer = torch.optim.Adam([x_adv], lr=0.01)
for i in range(num_iter):
total_loss = 0
# Average over N random transformations
n_transforms = 10
for _ in range(n_transforms):
x_transformed = self._random_transform(x_adv)
outputs = self.model(x_transformed)
loss = nn.functional.cross_entropy(outputs, y.to(self.device))
total_loss += loss
avg_loss = total_loss / n_transforms
optimizer.zero_grad()
avg_loss.backward()
optimizer.step()
# Project to epsilon ball
with torch.no_grad():
perturbation = torch.clamp(x_adv - x_orig, -epsilon, epsilon)
x_adv.data = torch.clamp(x_orig + perturbation, 0, 1)
return x_adv.detach()
def _random_transform(self, x: torch.Tensor) -> torch.Tensor:
"""Випадкові трансформації для EOT."""
# Random rotation, scale, color jitter, blur
transform = transforms.Compose([
transforms.RandomRotation(15),
transforms.RandomAffine(degrees=0, scale=(0.9, 1.1)),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.GaussianBlur(3, sigma=(0.1, 0.5)),
])
# Apply transforms (need PIL conversion)
return transform(x)
Методи захисту
Adversarial Training
Найефективніший емпіричний метод захисту.
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from typing import Optional
class AdversarialTraining:
"""
Adversarial Training — тренування на adversarial examples.
"""
def __init__(self, model: nn.Module, device: str = "cuda"):
self.model = model.to(device)
self.device = device
def pgd_adversarial_training(self, train_loader: DataLoader,
epochs: int = 100,
epsilon: float = 8/255,
alpha: float = 2/255,
pgd_steps: int = 10,
lr: float = 0.1) -> nn.Module:
"""
PGD-AT (Madry et al., 2018)
"""
optimizer = torch.optim.SGD(
self.model.parameters(),
lr=lr,
momentum=0.9,
weight_decay=5e-4
)
scheduler = torch.optim.lr_scheduler.MultiStepLR(
optimizer, milestones=[75, 90], gamma=0.1
)
criterion = nn.CrossEntropyLoss()
for epoch in range(epochs):
self.model.train()
total_loss = 0
correct = 0
total = 0
for batch_idx, (x, y) in enumerate(train_loader):
x, y = x.to(self.device), y.to(self.device)
# Generate adversarial examples
x_adv = self._pgd_attack(x, y, epsilon, alpha, pgd_steps)
# Forward pass on adversarial
self.model.train()
outputs = self.model(x_adv)
loss = criterion(outputs, y)
# Backward
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Stats
total_loss += loss.item()
_, predicted = outputs.max(1)
total += y.size(0)
correct += predicted.eq(y).sum().item()
scheduler.step()
acc = 100. * correct / total
print(f"Epoch {epoch}: Loss={total_loss:.4f}, Robust Acc={acc:.2f}%")
return self.model
def _pgd_attack(self, x: torch.Tensor, y: torch.Tensor,
epsilon: float, alpha: float,
num_steps: int) -> torch.Tensor:
"""PGD attack for training."""
x_adv = x.detach() + torch.empty_like(x).uniform_(-epsilon, epsilon)
x_adv = torch.clamp(x_adv, 0, 1)
for _ in range(num_steps):
x_adv.requires_grad = True
outputs = self.model(x_adv)
loss = nn.functional.cross_entropy(outputs, y)
loss.backward()
grad = x_adv.grad.detach()
x_adv = x_adv.detach() + alpha * grad.sign()
delta = torch.clamp(x_adv - x, -epsilon, epsilon)
x_adv = torch.clamp(x + delta, 0, 1)
return x_adv.detach()
def trades_training(self, train_loader: DataLoader,
epochs: int = 100,
epsilon: float = 8/255,
beta: float = 6.0,
lr: float = 0.1) -> nn.Module:
"""
TRADES (Zhang et al., 2019)
Trade-off between natural accuracy and robust accuracy.
Loss = CE(natural) + β × KL(natural || adversarial)
"""
optimizer = torch.optim.SGD(
self.model.parameters(),
lr=lr,
momentum=0.9,
weight_decay=2e-4
)
ce_criterion = nn.CrossEntropyLoss()
kl_criterion = nn.KLDivLoss(reduction='batchmean')
for epoch in range(epochs):
self.model.train()
for x, y in train_loader:
x, y = x.to(self.device), y.to(self.device)
# Natural loss
outputs_natural = self.model(x)
loss_natural = ce_criterion(outputs_natural, y)
# Generate adversarial using KL divergence
x_adv = self._trades_attack(x, outputs_natural.detach(), epsilon)
# Robust loss (KL divergence)
outputs_adv = self.model(x_adv)
loss_robust = kl_criterion(
nn.functional.log_softmax(outputs_adv, dim=1),
nn.functional.softmax(outputs_natural.detach(), dim=1)
)
# Total loss
loss = loss_natural + beta * loss_robust
optimizer.zero_grad()
loss.backward()
optimizer.step()
return self.model
def _trades_attack(self, x: torch.Tensor,
natural_output: torch.Tensor,
epsilon: float,
num_steps: int = 10) -> torch.Tensor:
"""TRADES attack maximizes KL divergence."""
x_adv = x.detach() + 0.001 * torch.randn_like(x)
x_adv = torch.clamp(x_adv, 0, 1)
natural_probs = nn.functional.softmax(natural_output, dim=1)
for _ in range(num_steps):
x_adv.requires_grad = True
adv_output = self.model(x_adv)
adv_probs = nn.functional.log_softmax(adv_output, dim=1)
loss = nn.functional.kl_div(adv_probs, natural_probs, reduction='sum')
loss.backward()
grad = x_adv.grad.detach()
x_adv = x_adv.detach() + (epsilon / num_steps) * grad.sign()
delta = torch.clamp(x_adv - x, -epsilon, epsilon)
x_adv = torch.clamp(x + delta, 0, 1)
return x_adv.detach()
Certified Defenses
Математичні гарантії robustness.
import torch
import torch.nn as nn
from scipy.stats import norm
import numpy as np
class CertifiedDefenses:
"""
Certified defenses з математичними гарантіями.
"""
def __init__(self, model: nn.Module, device: str = "cuda"):
self.model = model.to(device)
self.device = device
def randomized_smoothing(self, x: torch.Tensor,
sigma: float = 0.25,
n_samples: int = 100,
alpha: float = 0.001) -> tuple:
"""
Randomized Smoothing (Cohen et al., 2019)
Додаємо Gaussian noise до input → smoothed classifier.
Можна довести certified radius.
"""
self.model.eval()
# Sample predictions under noise
counts = torch.zeros(10, device=self.device) # Assuming 10 classes
for _ in range(n_samples):
# Add Gaussian noise
noise = torch.randn_like(x) * sigma
x_noisy = x + noise
# Get prediction
with torch.no_grad():
output = self.model(x_noisy)
pred = output.argmax(dim=1)
counts[pred] += 1
# Top class and runner-up
top_class = counts.argmax().item()
top_count = counts[top_class].item()
# Compute certified radius using Clopper-Pearson
p_lower = self._lower_confidence_bound(top_count, n_samples, alpha)
if p_lower > 0.5:
# Certified!
radius = sigma * norm.ppf(p_lower)
else:
radius = 0.0
return top_class, radius
def _lower_confidence_bound(self, successes: int, n: int,
alpha: float) -> float:
"""Clopper-Pearson lower bound."""
from scipy.stats import beta
return beta.ppf(alpha, successes, n - successes + 1)
def certify_batch(self, data_loader: DataLoader,
sigma: float = 0.25,
n0: int = 100,
n: int = 1000,
alpha: float = 0.001):
"""
Certify entire dataset.
"""
results = []
for x, y in data_loader:
x = x.to(self.device)
for i in range(x.shape[0]):
xi = x[i:i+1]
# First pass: abstain check
pred, _ = self.randomized_smoothing(xi, sigma, n0)
if pred == -1:
results.append({'certified': False, 'radius': 0})
continue
# Second pass: certification
pred, radius = self.randomized_smoothing(xi, sigma, n, alpha)
results.append({
'prediction': pred,
'true_label': y[i].item(),
'certified': pred == y[i].item() and radius > 0,
'radius': radius
})
return results
def interval_bound_propagation(self, x: torch.Tensor,
epsilon: float) -> tuple:
"""
IBP (Gowal et al., 2018)
Propagate bounds through network.
"""
# Initialize bounds
x_lb = x - epsilon
x_ub = x + epsilon
# Propagate through each layer
for layer in self.model.children():
if isinstance(layer, nn.Linear):
x_lb, x_ub = self._ibp_linear(layer, x_lb, x_ub)
elif isinstance(layer, nn.ReLU):
x_lb, x_ub = self._ibp_relu(x_lb, x_ub)
elif isinstance(layer, nn.Conv2d):
x_lb, x_ub = self._ibp_conv(layer, x_lb, x_ub)
return x_lb, x_ub
def _ibp_linear(self, layer: nn.Linear,
x_lb: torch.Tensor,
x_ub: torch.Tensor) -> tuple:
"""IBP for linear layer."""
w_pos = torch.clamp(layer.weight, min=0)
w_neg = torch.clamp(layer.weight, max=0)
out_lb = w_pos @ x_lb + w_neg @ x_ub + layer.bias
out_ub = w_pos @ x_ub + w_neg @ x_lb + layer.bias
return out_lb, out_ub
def _ibp_relu(self, x_lb: torch.Tensor,
x_ub: torch.Tensor) -> tuple:
"""IBP for ReLU."""
out_lb = torch.clamp(x_lb, min=0)
out_ub = torch.clamp(x_ub, min=0)
return out_lb, out_ub
def _ibp_conv(self, layer: nn.Conv2d,
x_lb: torch.Tensor,
x_ub: torch.Tensor) -> tuple:
"""IBP for convolution."""
# Similar to linear but with convolution
w_pos = torch.clamp(layer.weight, min=0)
w_neg = torch.clamp(layer.weight, max=0)
out_lb = nn.functional.conv2d(x_lb, w_pos, padding=layer.padding) + \
nn.functional.conv2d(x_ub, w_neg, padding=layer.padding) + \
layer.bias.view(1, -1, 1, 1)
out_ub = nn.functional.conv2d(x_ub, w_pos, padding=layer.padding) + \
nn.functional.conv2d(x_lb, w_neg, padding=layer.padding) + \
layer.bias.view(1, -1, 1, 1)
return out_lb, out_ub
RobustBench: стандартна оцінка
"""
RobustBench evaluation - gold standard для adversarial robustness.
"""
# pip install robustbench
from robustbench.utils import load_model
from robustbench.eval import benchmark
# Load state-of-the-art robust model
model = load_model(
model_name='Wang2023Better_WRN-70-16',
dataset='cifar10',
threat_model='Linf'
)
# Evaluate against AutoAttack
clean_acc, robust_acc = benchmark(
model,
dataset='cifar10',
threat_model='Linf',
eps=8/255
)
print(f"Clean accuracy: {clean_acc:.2%}")
print(f"Robust accuracy (AutoAttack): {robust_acc:.2%}")
# Current SOTA (2024):
# Clean: ~93%
# AutoAttack (eps=8/255): ~71%
Бенчмарки та trade-offs
| Model | Clean Acc | PGD-20 | AutoAttack | Training Time |
|-------|-----------|--------|------------|---------------|
| Standard ResNet-50 | 95.2% | 0% | 0% | 1x |
| PGD-AT (eps=8/255) | 87.3% | 53.4% | 49.1% | 10x |
| TRADES (β=6) | 85.6% | 56.2% | 53.4% | 10x |
| SOTA 2024 | 93.2% | 71.5% | 70.9% | 100x |
Ключові спостереження:
- Robustness коштує ~5-10% clean accuracy
- Training time збільшується в 10-100x
- Gap між PGD та AutoAttack важливий для правильної оцінки
Ідеї для дослідження
Для бакалавра:
- Implement FGSM/PGD attacks з нуля
- Порівняння ефективності атак на CIFAR-10
- Візуалізація adversarial perturbations
- Transfer attack experiments
Для магістра:
- Adversarial training для специфічного домену (medical imaging)
- Physical adversarial examples (printed patches)
- Certified defense implementation
- Robustness в multi-modal settings
Для PhD:
- Novel defense mechanisms з theoretical guarantees
- Understanding adversarial robustness in transformers/LLMs
- Connections між robustness та generalization
- Scalable certified training
Висновок: безпека AI — не опція
AI входить в safety-critical системи: медична діагностика, автономний транспорт, фінансовий fraud detection, системи безпеки. Adversarial examples — це не теоретична curiosity. Це реальний attack vector: практичний, масштабований, часто непомітний.
Модель без adversarial robustness — як банківський сейф з картонними стінками. Виглядає надійно. До першої реальної атаки.
Для дослідників і розробників розуміння adversarial robustness стає необхідною компетенцією. Якщо вам потрібна допомога з реалізацією robust ML систем для академічної роботи, команда SKP-Degree на skp-degree.com.ua має досвід у цій галузі. Консультації та підтримка доступні в Telegram: @kursovi_diplomy.
Adversarial robustness, FGSM, PGD, AutoAttack, adversarial training, certified defenses, randomized smoothing, trustworthy AI — ключові терміни для дипломної чи магістерської роботи з безпеки машинного навчання та надійності AI систем.