feat: adds MIM attack
Browse files- app.py +10 -3
- utils/attacks.py +91 -0
app.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Optional, List, Tuple
|
|
| 6 |
from utils.model_loader import load_model_and_labels
|
| 7 |
from utils.preprocessing import get_default_transform, preprocess_image
|
| 8 |
from utils.inference import predict_topk
|
| 9 |
-
from utils.attacks import PGDIterations, FGSM, SAGA
|
| 10 |
from utils.visualization import extract_attention_maps, attention_rollout, create_attention_overlay, extract_attention_for_iterations, create_iteration_attention_overlays
|
| 11 |
|
| 12 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
@@ -224,6 +224,8 @@ def run_attack(
|
|
| 224 |
# Configurar ataque baseado no tipo selecionado
|
| 225 |
if attack_type == "FGSM":
|
| 226 |
attack = FGSM(model, eps=eps)
|
|
|
|
|
|
|
| 227 |
elif attack_type == "SAGA":
|
| 228 |
attack = SAGA(model, eps=eps, steps=steps)
|
| 229 |
else: # PGD
|
|
@@ -275,6 +277,11 @@ def run_attack(
|
|
| 275 |
if attack_type == "PGD":
|
| 276 |
result += f"- Alpha (α): {alpha:.4f}\n"
|
| 277 |
result += f"- Steps: {steps}\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
elif attack_type == "SAGA":
|
| 279 |
result += f"- Steps: {steps}\n"
|
| 280 |
result += f"- Gradiente ponderado por atenção (ViT-specific)\n"
|
|
@@ -375,10 +382,10 @@ def create_app():
|
|
| 375 |
gr.Markdown("#### ⚔️ Configuração do Ataque")
|
| 376 |
|
| 377 |
attack_type = gr.Dropdown(
|
| 378 |
-
choices=["PGD", "FGSM", "SAGA"],
|
| 379 |
value="PGD",
|
| 380 |
label="Tipo de Ataque",
|
| 381 |
-
info="PGD: iterativo | FGSM: single-step | SAGA: gradient × attention
|
| 382 |
)
|
| 383 |
|
| 384 |
eps_input = gr.Slider(
|
|
|
|
| 6 |
from utils.model_loader import load_model_and_labels
|
| 7 |
from utils.preprocessing import get_default_transform, preprocess_image
|
| 8 |
from utils.inference import predict_topk
|
| 9 |
+
from utils.attacks import PGDIterations, FGSM, SAGA, MIFGSM
|
| 10 |
from utils.visualization import extract_attention_maps, attention_rollout, create_attention_overlay, extract_attention_for_iterations, create_iteration_attention_overlays
|
| 11 |
|
| 12 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
| 224 |
# Configurar ataque baseado no tipo selecionado
|
| 225 |
if attack_type == "FGSM":
|
| 226 |
attack = FGSM(model, eps=eps)
|
| 227 |
+
elif attack_type == "MIFGSM":
|
| 228 |
+
attack = MIFGSM(model, eps=eps, alpha=alpha, steps=steps, decay=1.0)
|
| 229 |
elif attack_type == "SAGA":
|
| 230 |
attack = SAGA(model, eps=eps, steps=steps)
|
| 231 |
else: # PGD
|
|
|
|
| 277 |
if attack_type == "PGD":
|
| 278 |
result += f"- Alpha (α): {alpha:.4f}\n"
|
| 279 |
result += f"- Steps: {steps}\n"
|
| 280 |
+
elif attack_type == "MIFGSM":
|
| 281 |
+
result += f"- Alpha (α): {alpha:.4f}\n"
|
| 282 |
+
result += f"- Steps: {steps}\n"
|
| 283 |
+
result += f"- Momentum decay: 1.0\n"
|
| 284 |
+
result += f"- Gradiente normalizado com acumulação de momentum\n"
|
| 285 |
elif attack_type == "SAGA":
|
| 286 |
result += f"- Steps: {steps}\n"
|
| 287 |
result += f"- Gradiente ponderado por atenção (ViT-specific)\n"
|
|
|
|
| 382 |
gr.Markdown("#### ⚔️ Configuração do Ataque")
|
| 383 |
|
| 384 |
attack_type = gr.Dropdown(
|
| 385 |
+
choices=["PGD", "FGSM", "MIFGSM", "SAGA"],
|
| 386 |
value="PGD",
|
| 387 |
label="Tipo de Ataque",
|
| 388 |
+
info="PGD: iterativo | FGSM: single-step | MIFGSM: momentum | SAGA: gradient × attention"
|
| 389 |
)
|
| 390 |
|
| 391 |
eps_input = gr.Slider(
|
utils/attacks.py
CHANGED
|
@@ -317,4 +317,95 @@ class SAGA(torch.nn.Module):
|
|
| 317 |
|
| 318 |
# Retornar normalizado
|
| 319 |
adv_images = (adv_images_denorm - mean) / std
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
return adv_images, self.iteration_images
|
|
|
|
| 317 |
|
| 318 |
# Retornar normalizado
|
| 319 |
adv_images = (adv_images_denorm - mean) / std
|
| 320 |
+
return adv_images, self.iteration_images
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
class MIFGSM(torchattacks.MIFGSM):
|
| 324 |
+
"""
|
| 325 |
+
MI-FGSM: Momentum Iterative Fast Gradient Sign Method
|
| 326 |
+
|
| 327 |
+
Extensão do ataque MIFGSM que captura imagens e atenção de cada iteração.
|
| 328 |
+
Usa momentum para estabilizar direção do gradiente e melhorar transferabilidade.
|
| 329 |
+
|
| 330 |
+
Paper: "Boosting Adversarial Attacks with Momentum" (2017)
|
| 331 |
+
https://arxiv.org/abs/1710.06081
|
| 332 |
+
"""
|
| 333 |
+
def __init__(self, model, eps=8/255, alpha=2/255, steps=10, decay=1.0):
|
| 334 |
+
super().__init__(model, eps=eps, alpha=alpha, steps=steps, decay=decay)
|
| 335 |
+
self.iteration_images: List[Image.Image] = []
|
| 336 |
+
self.iteration_tensors: List[torch.Tensor] = []
|
| 337 |
+
|
| 338 |
+
def forward(self, images, labels) -> Tuple[torch.Tensor, List[Image.Image]]:
|
| 339 |
+
"""
|
| 340 |
+
Executa o ataque MI-FGSM e retorna:
|
| 341 |
+
- adv_images: tensor adversarial final
|
| 342 |
+
- iteration_images: lista de PIL Images (uma por iteração)
|
| 343 |
+
|
| 344 |
+
Implementação adaptada para trabalhar com imagens normalizadas ImageNet
|
| 345 |
+
e capturar todas as iterações.
|
| 346 |
+
"""
|
| 347 |
+
images = images.clone().detach().to(self.device)
|
| 348 |
+
labels = labels.clone().detach().to(self.device)
|
| 349 |
+
|
| 350 |
+
if self.targeted:
|
| 351 |
+
target_labels = self.get_target_label(images, labels)
|
| 352 |
+
|
| 353 |
+
loss = torch.nn.CrossEntropyLoss()
|
| 354 |
+
|
| 355 |
+
# Desnormalizar para aplicar eps e clipping no espaço correto [0,1]
|
| 356 |
+
mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1).to(self.device)
|
| 357 |
+
std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1).to(self.device)
|
| 358 |
+
|
| 359 |
+
images_denorm = images * std + mean
|
| 360 |
+
adv_images_denorm = images_denorm.clone().detach()
|
| 361 |
+
|
| 362 |
+
# Inicializar momentum
|
| 363 |
+
momentum = torch.zeros_like(images).detach().to(self.device)
|
| 364 |
+
|
| 365 |
+
self.iteration_images = []
|
| 366 |
+
self.iteration_tensors = []
|
| 367 |
+
|
| 368 |
+
# Salvar iteração 0 (imagem original)
|
| 369 |
+
pil_img_orig = tensor_to_pil(images_denorm[0], denormalize=False)
|
| 370 |
+
self.iteration_images.append(pil_img_orig)
|
| 371 |
+
self.iteration_tensors.append(images.clone().detach())
|
| 372 |
+
|
| 373 |
+
for _ in range(self.steps):
|
| 374 |
+
# Normalizar para passar pelo modelo
|
| 375 |
+
adv_images = (adv_images_denorm - mean) / std
|
| 376 |
+
adv_images.requires_grad = True
|
| 377 |
+
outputs = self.get_logits(adv_images)
|
| 378 |
+
|
| 379 |
+
# Calcular loss
|
| 380 |
+
if self.targeted:
|
| 381 |
+
cost = -loss(outputs, target_labels)
|
| 382 |
+
else:
|
| 383 |
+
cost = loss(outputs, labels)
|
| 384 |
+
|
| 385 |
+
# Calcular gradiente
|
| 386 |
+
grad = torch.autograd.grad(cost, adv_images,
|
| 387 |
+
retain_graph=False, create_graph=False)[0]
|
| 388 |
+
|
| 389 |
+
# Normalizar gradiente (chave do MI-FGSM!)
|
| 390 |
+
grad = grad / torch.mean(torch.abs(grad), dim=(1, 2, 3), keepdim=True)
|
| 391 |
+
|
| 392 |
+
# Aplicar momentum
|
| 393 |
+
grad = grad + momentum * self.decay
|
| 394 |
+
momentum = grad
|
| 395 |
+
|
| 396 |
+
# Voltar para espaço desnormalizado para aplicar perturbação
|
| 397 |
+
adv_images_denorm = adv_images_denorm.detach() + self.alpha * grad.sign() * std
|
| 398 |
+
delta = torch.clamp(adv_images_denorm - images_denorm, min=-self.eps, max=self.eps)
|
| 399 |
+
adv_images_denorm = torch.clamp(images_denorm + delta, min=0, max=1).detach()
|
| 400 |
+
|
| 401 |
+
# Normalizar para salvar tensor
|
| 402 |
+
adv_images_normalized = (adv_images_denorm - mean) / std
|
| 403 |
+
|
| 404 |
+
# Capturar imagem e tensor desta iteração
|
| 405 |
+
pil_img = tensor_to_pil(adv_images_denorm[0], denormalize=False)
|
| 406 |
+
self.iteration_images.append(pil_img)
|
| 407 |
+
self.iteration_tensors.append(adv_images_normalized.clone().detach())
|
| 408 |
+
|
| 409 |
+
# Retornar imagem normalizada para o modelo
|
| 410 |
+
adv_images = (adv_images_denorm - mean) / std
|
| 411 |
return adv_images, self.iteration_images
|