Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/secmlt/adv/evasion/additive_noise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Additive Noise attack implementation (Foolbox-only backend)."""

from __future__ import annotations

import importlib.util
from typing import TYPE_CHECKING, Literal

from secmlt.adv.backends import Backends
from secmlt.adv.evasion.base_evasion_attack import (
BaseEvasionAttack,
BaseEvasionAttackCreator,
)
from secmlt.adv.evasion.perturbation_models import LpPerturbationModels

if TYPE_CHECKING:
from secmlt.trackers.trackers import Tracker


class AdditiveNoise(BaseEvasionAttackCreator):
"""Creator for the Additive Noise attack."""

def __new__(
cls,
epsilon: float,
perturbation_model: str = LpPerturbationModels.L2,
noise_type: Literal["gaussian", "uniform"] = "gaussian",
y_target: int | None = None,
lb: float = 0.0,
ub: float = 1.0,
backend: str = Backends.FOOLBOX,
trackers: list[Tracker] | None = None,
**kwargs,
) -> BaseEvasionAttack:
"""
Create the Additive Noise attack.

The attack samples a random perturbation (Gaussian or uniform) and
scales it to the requested Lp norm.

Parameters
----------
epsilon : float
Maximum size of the additive perturbation, measured with the norm
induced by ``perturbation_model``.
perturbation_model : str, optional
Norm constraint for the attack. Either L2 or Linf. Default is L2.
noise_type : str, optional
Distribution used to sample the noise. Either "gaussian" or
"uniform". Default is "gaussian". Note that "gaussian" is only
available for the L2 perturbation model.
y_target : int | None, optional
Target label for targeted attack. If None, the attack is
untargeted. Default is None.
lb : float, optional
Lower bound for the input domain. Default is 0.0.
ub : float, optional
Upper bound for the input domain. Default is 1.0.
backend : str, optional
Backend to use. Only Backends.FOOLBOX is supported.
Default is Backends.FOOLBOX.
trackers : list[Tracker] | None, optional
Trackers for monitoring attack metrics, by default None.

Returns
-------
BaseEvasionAttack
Additive Noise attack instance.
"""
cls.check_backend_available(backend)
implementation = cls.get_implementation(backend)
return implementation(
epsilon=epsilon,
perturbation_model=perturbation_model,
noise_type=noise_type,
y_target=y_target,
lb=lb,
ub=ub,
trackers=trackers,
**kwargs,
)

@staticmethod
def get_backends() -> list[str]:
"""Get available implementations for the Additive Noise attack."""
return [Backends.FOOLBOX]

@staticmethod
def _get_foolbox_implementation() -> type[AdditiveNoiseFoolbox]: # noqa: F821
if importlib.util.find_spec("foolbox", None) is not None:
from secmlt.adv.evasion.foolbox_attacks.foolbox_additive_noise import (
AdditiveNoiseFoolbox,
)

return AdditiveNoiseFoolbox
msg = "foolbox extra not installed"
raise ImportError(msg)
17 changes: 17 additions & 0 deletions src/secmlt/adv/evasion/advlib_attacks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
"""Wrappers of Adversarial Library for evasion attacks."""

import importlib.metadata
import importlib.util


def _adv_lib_gte(major: int, minor: int, patch: int) -> bool:
try:
version_str = importlib.metadata.version("adv-lib")
except importlib.metadata.PackageNotFoundError:
return False
else:
parts = tuple(int(x) for x in version_str.split(".")[:3])
return parts >= (major, minor, patch)
Comment on lines +7 to +14


if importlib.util.find_spec("adv_lib", None) is not None:
from .advlib_cw import * # noqa: F403
from .advlib_ddn import * # noqa: F403
from .advlib_fgsm import * # noqa: F403
from .advlib_fmn import * # noqa: F403
from .advlib_pgd import * # noqa: F403

if _adv_lib_gte(0, 2, 3):
from .advlib_deepfool import * # noqa: F403
73 changes: 73 additions & 0 deletions src/secmlt/adv/evasion/advlib_attacks/advlib_cw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Wrapper of the Carlini-Wagner attack implemented in Adversarial Library."""

from __future__ import annotations # noqa: I001

from functools import partial

from adv_lib.attacks.carlini_wagner import carlini_wagner_l2

from secmlt.adv.evasion.advlib_attacks.advlib_base import BaseAdvLibEvasionAttack
from secmlt.adv.evasion.perturbation_models import LpPerturbationModels


class CWAdvLib(BaseAdvLibEvasionAttack):
"""Wrapper of the Adversarial Library implementation of the CW L2 attack.

Parameters
----------
binary_search_steps : int
Number of binary search steps for the const parameter. Default is 9.
num_steps : int
Number of optimization iterations per binary search step. Default is 10000.
step_size : float
Learning rate for the Adam optimizer. Default is 0.01.
confidence : float
Confidence margin for adversarial examples. Default is 0.0.
initial_const : float
Initial value of the regularization constant. Default is 0.001.
abort_early : bool
Abort binary search early if no improvement is found. Default is True.
y_target : int | None, optional
Target label for the attack. If None, the attack is untargeted.
lb : float, optional
Lower bound for the perturbation. Default is 0.0.
ub : float, optional
Upper bound for the perturbation. Default is 1.0.
"""

def __init__(
self,
binary_search_steps: int = 9,
num_steps: int = 10000,
step_size: float = 0.01,
confidence: float = 0.0,
initial_const: float = 0.001,
abort_early: bool = True,
y_target: int | None = None,
lb: float = 0.0,
ub: float = 1.0,
**kwargs,
) -> None:
"""Initialize the Adversarial Library backend for the CW L2 attack."""
advlib_attack = partial(
carlini_wagner_l2,
binary_search_steps=binary_search_steps,
max_iterations=num_steps,
learning_rate=step_size,
confidence=confidence,
initial_const=initial_const,
abort_early=abort_early,
)

super().__init__(
advlib_attack=advlib_attack,
y_target=y_target,
lb=lb,
ub=ub,
**kwargs,
)

@staticmethod
def get_perturbation_models() -> set[str]:
"""Return the perturbation models available for this attack."""
return {LpPerturbationModels.L2}
55 changes: 55 additions & 0 deletions src/secmlt/adv/evasion/advlib_attacks/advlib_deepfool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Wrapper of the DeepFool attack implemented in Adversarial Library."""

from __future__ import annotations

from functools import partial

from adv_lib.attacks.deepfool import df
from secmlt.adv.evasion.advlib_attacks.advlib_base import BaseAdvLibEvasionAttack
from secmlt.adv.evasion.perturbation_models import LpPerturbationModels


class DeepFoolAdvLib(BaseAdvLibEvasionAttack):
"""Wrapper of the Adversarial Library implementation of the DeepFool L2 attack.

Parameters
----------
num_steps : int
Maximum number of steps to perform. Default is 100.
overshoot : float, optional
Ratio by which to overshoot the decision boundary. Default is 0.02.
lb : float, optional
Lower bound for the perturbation. Default is 0.0.
ub : float, optional
Upper bound for the perturbation. Default is 1.0.
"""

def __init__(
self,
num_steps: int = 100,
overshoot: float = 0.02,
lb: float = 0.0,
ub: float = 1.0,
**kwargs,
) -> None:
"""Initialize the Adversarial Library backend for the DeepFool L2 attack."""
kwargs.pop("candidates", None) # foolbox-only parameter
advlib_attack = partial(
df,
steps=num_steps,
overshoot=overshoot,
norm=2,
)

super().__init__(
advlib_attack=advlib_attack,
y_target=None,
lb=lb,
ub=ub,
**kwargs,
)

@staticmethod
def get_perturbation_models() -> set[str]:
"""Return the perturbation models available for this attack."""
return {LpPerturbationModels.L2}
92 changes: 92 additions & 0 deletions src/secmlt/adv/evasion/advlib_attacks/advlib_fgsm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Wrapper of the FGSM attack implemented in Adversarial Library."""

from __future__ import annotations

from functools import partial

from adv_lib.attacks import pgd_linf
from secmlt.adv.evasion.advlib_attacks.advlib_base import BaseAdvLibEvasionAttack
from secmlt.adv.evasion.perturbation_models import LpPerturbationModels


class FGSMAdvLib(BaseAdvLibEvasionAttack):
"""Wrapper of the Adversarial Library implementation of the FGSM attack."""

def __init__(
self,
perturbation_model: str,
epsilon: float,
loss_function: str = "ce",
y_target: int | None = None,
lb: float = 0.0,
ub: float = 1.0,
**kwargs,
) -> None:
"""
Initialize an FGSM attack with the Adversarial Library backend.

Parameters
----------
perturbation_model : str
The perturbation model to be used for the attack.
epsilon : float
The maximum perturbation allowed.
loss_function : str, optional
The loss function to be used for the attack. The default value is "ce".
y_target : int | None, optional
The target label for the attack. If None, the attack is
untargeted. The default value is None.
lb : float, optional
The lower bound for the perturbation. The default value is 0.0.
ub : float, optional
The upper bound for the perturbation. The default value is 1.0.

Raises
------
ValueError
If the provided `loss_function` is not supported by the FGSM attack
using the Adversarial Library backend.
"""
perturbation_models = {
LpPerturbationModels.LINF: pgd_linf,
}
losses: list[str] = ["ce", "dl", "dlr"]
if isinstance(loss_function, str):
if loss_function not in losses:
msg = f"FGSM AdvLib supports only these losses: {losses}"
raise ValueError(msg)
else:
loss_function = losses[0]

advlib_attack_func = perturbation_models.get(perturbation_model)
advlib_attack = partial(
advlib_attack_func,
steps=1,
random_init=False,
restarts=1,
loss_function=loss_function,
absolute_step_size=epsilon,
)

super().__init__(
advlib_attack=advlib_attack,
epsilon=epsilon,
y_target=y_target,
lb=lb,
ub=ub,
**kwargs,
)

@staticmethod
def get_perturbation_models() -> set[str]:
"""
Check the perturbation models implemented for this attack.

Returns
-------
set[str]
The list of perturbation models implemented for this attack.
"""
return {
LpPerturbationModels.LINF,
}
Loading
Loading