Технології AI Написано практикуючими розробниками

Adversarial Robustness: коли один піксель ламає AI — повний гайд з атак і захисту

Оновлено: 18 хв читання 4 переглядів

Панда. Нейромережа каже: «панда, 99.3% впевненості». Додаємо шум, невидимий людському оку — зміна менше ніж 1% пікселів. Та сама картинка для людини. Нейромережа каже: «гібон, 99.7% впевненості». Повна інверсія класу при мінімальній зміні вхідних даних.


Панда. Нейромережа каже: «панда, 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 систем.

Про автора

Команда SKP-Degree

Верифікований автор

Розробники та дослідники AI · Python, TensorFlow, PyTorch · Досвід у промисловій розробці

Команда SKP-Degree — професійні розробники з досвідом 7+ років у промисловій розробці. Виконали 1000+ проєктів для студентів з України, Польщі та країн Балтії.

Python Django Java ML/AI React C# / .NET JavaScript

Потрібна допомога з роботою?

Замовте курсову чи дипломну роботу з програмування. Оплата після демонстрації!

Без передоплати Відеодемонстрація Автономна робота 24/7
Написати в Telegram