Adversariële afbeeldingsvoorbeelden voor VLM's
Verstoringen op pixelniveau die VLM-gedrag veranderen, waaronder PGD-aanvallen op vision-encoders, overdraagbare adversariële afbeeldingen en patch-aanvallen.
Van classificatie naar generatie
Adversariële voorbeelden voor afbeeldingsclassificatoren zijn goed bestudeerd -- kleine pixelverstoringen die misclassificatie veroorzaken. Adversariële voorbeelden voor VLM's breiden dit concept uit: in plaats van een klasselabel te veranderen, verandert de aanvaller de gegenereerde tekstuitvoer van het model.
Aanvalsformulering
Gegeven een VLM f die een afbeelding x en tekstprompt t aanneemt om uitvoer y = f(x, t) te produceren, is de adversariële doelstelling:
x_adv = argmin_δ L(f(x + δ, t), y_target)
subject to: ||δ||_p ≤ ε
Waarbij:
δde adversariële verstoring isεde verstoringsmagnitude begrenst (onmerkbaarheidsbeperking)Leen verliesfunctie is die de afstand tot de doeluitvoer meet||·||_pdoorgaans de L∞-norm is (begrenst de maximale wijziging per pixel)
Projected Gradient Descent (PGD)
PGD is het werkpaard van het genereren van adversariële afbeeldingen:
import torch
import torch.nn.functional as F
def pgd_attack_vlm(
model,
image: torch.Tensor, # [1, 3, H, W], normalized
text_prompt: str,
target_tokens: torch.Tensor, # token IDs of desired output
epsilon: float = 8/255, # L-inf bound
step_size: float = 1/255,
num_steps: int = 100,
tokenizer=None
):
"""PGD attack to make a VLM generate target text."""
delta = torch.zeros_like(image, requires_grad=True)
for step in range(num_steps):
adv_image = image + delta
# Forward pass through VLM
# Get logits for each position in target sequence
outputs = model(images=adv_image, input_ids=text_prompt)
logits = outputs.logits
# Loss: cross-entropy with target tokens
loss = F.cross_entropy(
logits[:, -len(target_tokens):, :].reshape(-1, logits.size(-1)),
target_tokens.reshape(-1)
)
loss.backward()
# Gradient step
with torch.no_grad():
delta.data = delta.data - step_size * delta.grad.sign()
# Project back to epsilon ball
delta.data = torch.clamp(delta.data, -epsilon, epsilon)
# Ensure valid pixel range
delta.data = torch.clamp(image + delta.data, 0, 1) - image
delta.grad.zero_()
return (image + delta).detach()Belangrijke parameters
| Parameter | Typisch bereik | Effect |
|---|---|---|
epsilon | 4/255 - 16/255 | Hoger = effectiever maar zichtbaarder |
step_size | epsilon/10 - epsilon/4 | Standaard PGD-planning |
num_steps | 50-500 | Meer stappen = betere convergentie |
target_tokens | Wisselend | Kortere doelen zijn makkelijker te bereiken |
FGSM: snelle aanval in één stap
FGSM is een snellere maar minder effectieve alternatief:
def fgsm_attack_vlm(model, image, text_prompt, target_tokens, epsilon=8/255):
"""Single-step adversarial attack for VLMs."""
image.requires_grad_(True)
outputs = model(images=image, input_ids=text_prompt)
loss = F.cross_entropy(
outputs.logits[:, -len(target_tokens):, :].reshape(-1, outputs.logits.size(-1)),
target_tokens.reshape(-1)
)
loss.backward()
# Single gradient step
adv_image = image - epsilon * image.grad.sign()
return torch.clamp(adv_image, 0, 1).detach()FGSM is nuttig voor snelle haalbaarheidstests maar zelden voldoende voor complexe VLM-aanvallen waarbij de doeluitvoer meer dan een paar tokens beslaat.
Adversariële patch-aanvallen
In plaats van de hele afbeelding te verstoren, wijzigen patch-aanvallen een klein gebied:
def adversarial_patch_attack(
model,
image: torch.Tensor,
target_tokens: torch.Tensor,
patch_size: int = 64,
patch_location: tuple = (0, 0), # top-left corner
num_steps: int = 200,
step_size: float = 2/255
):
"""Optimize an adversarial patch for a VLM."""
h, w = patch_location
patch = torch.rand(1, 3, patch_size, patch_size, requires_grad=True)
optimizer = torch.optim.Adam([patch], lr=step_size)
for step in range(num_steps):
# Apply patch to image
patched_image = image.clone()
patched_image[:, :, h:h+patch_size, w:w+patch_size] = patch
outputs = model(images=patched_image)
loss = F.cross_entropy(
outputs.logits[:, -len(target_tokens):, :].reshape(-1, outputs.logits.size(-1)),
target_tokens.reshape(-1)
)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Clamp patch to valid pixel range
with torch.no_grad():
patch.data = torch.clamp(patch.data, 0, 1)
return patch.detach()Waarom patches ertoe doen
Patch-aanvallen hebben praktische implicaties die verder gaan dan Lp-verstoringen:
- Ze kunnen geprint en in fysieke omgevingen geplaatst worden (bijv. adversariële stickers)
- Ze zijn onafhankelijk van de basisafbeelding en daardoor potentieel overdraagbaar tussen contexten
- Ze kunnen vermomd worden als logo's, QR-codes of decoratieve elementen
Overdraagbaarheid tussen VLM's
Adversariële voorbeelden die voor één VLM zijn vervaardigd, dragen vaak over naar andere, vooral wanneer ze vision-encoderfamilies delen:
| Bronmodel | Doelmodel | Overdrachtspercentage (bij benadering) |
|---|---|---|
| LLaVA (CLIP ViT-L) | InstructBLIP (EVA-CLIP) | 30-50% |
| LLaVA (CLIP ViT-L) | Qwen-VL (CLIP ViT-G) | 40-60% |
| LLaVA (CLIP ViT-L) | GPT-4o (proprietary) | 10-25% |
| Ensemble (3+ modellen) | Elk afzonderlijk model | 50-70% |
Ensemble-aanvalsstrategie
def ensemble_pgd(models, image, target_tokens, epsilon=8/255, num_steps=200):
"""PGD attack optimized against multiple VLMs for transferability."""
delta = torch.zeros_like(image, requires_grad=True)
for step in range(num_steps):
total_loss = 0
for model in models:
outputs = model(images=image + delta)
loss = compute_target_loss(outputs, target_tokens)
total_loss += loss / len(models) # equal weighting
total_loss.backward()
with torch.no_grad():
delta.data -= (epsilon / num_steps * 4) * delta.grad.sign()
delta.data = torch.clamp(delta.data, -epsilon, epsilon)
delta.data = torch.clamp(image + delta.data, 0, 1) - image
delta.grad.zero_()
return (image + delta).detach()Universele adversariële verstoringen
Een universele adversariële verstoring (UAP) is een enkel ruispatroon dat werkt over veel verschillende invoerafbeeldingen:
def train_universal_perturbation(
model, dataset, target_tokens, epsilon=10/255, num_epochs=10
):
"""Train a universal perturbation across a dataset of images."""
uap = torch.zeros(1, 3, 224, 224, requires_grad=True)
for epoch in range(num_epochs):
for image in dataset:
outputs = model(images=image + uap)
loss = compute_target_loss(outputs, target_tokens)
loss.backward()
with torch.no_grad():
uap.data -= 0.001 * uap.grad.sign()
uap.data = torch.clamp(uap.data, -epsilon, epsilon)
uap.grad.zero_()
return uap.detach()UAP's zijn minder effectief per afbeelding maar veel praktischer voor implementatie, aangezien ze geen optimalisatie per invoer vereisen.
Gerelateerde onderwerpen
- VLM-architectuur & vision-language-alignment -- begrijpen wat je aanvalt
- Op afbeeldingen gebaseerde prompt-injectie -- niet op gradiënten gebaseerde visuele aanvallen
- Lab: Op afbeeldingen gebaseerde injecties vervaardigen -- praktische oefening met deze technieken
Referenties
- "Intriguing Properties of Neural Networks" - Szegedy et al. (2014) - Original discovery of adversarial examples in neural networks
- "Towards Deep Learning Models Resistant to Adversarial Attacks" - Madry et al. (2018) - PGD attack methodology widely used in adversarial ML
- "Universal Adversarial Perturbations" - Moosavi-Dezfooli et al. (2017) - Image-agnostic adversarial perturbation techniques
- "Adversarial Patch" - Brown et al. (2017) - Physical-world adversarial patch attacks applicable to VLM systems
Wat is het belangrijkste voordeel van adversariële patch-aanvallen ten opzichte van verstoringsaanvallen op de volledige afbeelding in praktische scenario's?