Skip to content

Support for foolbox attacks via a simple interface - #166

Open
henba1 wants to merge 21 commits into
mainfrom
henba1-foolbox-interface
Open

henba1 wants to merge 21 commits into
mainfrom
henba1-foolbox-interface

Conversation

@henba1

@henba1 henba1 commented Nov 25, 2025 •

Copy link
Copy Markdown
Collaborator

Purpose

Adds a generic interface to foolbox so any of its attacks (FGSM, PGD, DeepFool, Carlini-Wagner, boundary attacks, L2/L-inf/L1 variants, ...) can be used as a VERONA Attack without a dedicated wrapper per attack, alongside the existing FGSMAttack, PGDAttack, and AutoAttackWrapper.

What changed

  1. FoolboxAttack (ada_verona/verification_module/attacks/foolbox_attack.py) — a thin Attack subclass that wraps any foolbox.attacks.* class:
    • Takes the foolbox attack class, input bounds (default (0, 1)), and arbitrary constructor kwargs (e.g. steps=40), and runs it through foolbox.PyTorchModel inside execute().
    • Normalizes input/target tensors to the batched shape foolbox expects (adds a batch dimension for unbatched single-sample inputs from the robustness-distribution pipeline).
    • Supports both untargeted (foolbox.criteria.Misclassification, default) and targeted (targeted=True + target_class, foolbox.criteria.TargetedMisclassification) attacks; targeted mode is attack-dependent (single-step attacks like FGSM don't support it and raise ValueError from foolbox).
  2. foolbox as an optional dependency — added to pyproject.toml/requirements.txt/uv.lock; not required for the rest of VERONA.
  3. Examples — examples/scripts/create_robustness_dist_foolbox.py: an end-to-end tutorial comparing L-inf attacks (FGSM, PGD, DeepFool) on an ImageFileDataset, plus an L2 attack demo loading data via PytorchExperimentDataset to show the pipeline is norm- and dataset-loader-agnostic.
  4. Docs & credits — docs/how-to-guides.md lists foolbox attacks alongside the existing methods; README.md credits the foolbox authors (Rauber et al., 2017 and 2020) in the acknowledgements section.
  5. Two unrelated pre-existing bugs fixed in passing (surfaced while exercising the pipeline for this PR, independent of the foolbox feature itself):
    • ExperimentRepository.get_result_df() crashed with AttributeError: 'DataFrame' object has no attribute 'network_path' on result CSVs that already contain a network column (the schema actually written by save_result()/save_results()) — it now only derives network from network_path for the legacy schema.
    • CI: pinned ada_verona/foolbox as first-/third-party in the ruff isort config (a local foolbox/ checkout in the repo root was being misdetected as first-party, hiding real import-order errors) and guarded uv venv so a restored .venv cache doesn't fail environment creation.

How it was tested

  • New unit tests in tests/test_verification_module/attacks/test_foolbox_attack.py covering untargeted/targeted execution, the missing-target_class validation error, and tensor-shape normalization branches (added incrementally, including a pass specifically to close codecov patch-coverage gaps).
  • tests/test_database/test_experiment_repository.py covers the get_result_df fix.
  • Full existing test suite continues to pass; ruff check / ruff format --check pass.
  • Manually ran the new create_robustness_dist_foolbox.py example end-to-end (both the L-inf comparison and the L2/PytorchExperimentDataset part) to confirm the interface produces sensible robustness distributions for both the targeted as well as untargeted case via a dedicated repo shared with @Aaron99B (the reviewer): https://github.com/henba1/verona-foolbox-debug

@henba1
henba1 requested review from AWbosman and Aaron99B November 25, 2025 16:21
@henba1 henba1 self-assigned this Nov 25, 2025
@henba1 henba1 added the enhancement New feature or request label Nov 25, 2025
@codecov

codecov Bot commented Nov 25, 2025 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially leave out of mandatory dependencies

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed, not a mandatory dependency anymore

if data.dim() == 3:
data = data.unsqueeze(0)

if target.dim() == 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happends when its a targeted attack?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targeted attacks are currently not supported - the target here is the correct label

AttackEstimationModule.verify() currently always passes the true label (verification_context.data_point.label) as the "target"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

covered now in 62bf5da : targeted attacks are supported and handled through foolbox criterion obj criterion object. The untargeted wrapper passes the true label, which foolbox auto-wraps as Misclassification . Targeted mode just swaps that for TargetedMisclassification ("reach this class"). No changes to the Attack.execute signature or the downstream pipeline needed.

BinarySearchEpsilonValueEstimator,
)
from ada_verona.verification_module.attack_estimation_module import AttackEstimationModule
from ada_verona.verification_module.attacks.foolbox_attack import FoolboxAttack

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the check whether foolbox is installed like we have for the other scripts

from ada_verona.database.dataset.image_file_dataset import ImageFileDataset
from ada_verona.database.experiment_repository import ExperimentRepository
from ada_verona.dataset_sampler.predictions_based_sampler import PredictionsBasedSampler
from ada_verona.epsilon_value_estimator.binary_search_epsilon_value_estimator import (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need two example scripts for such a small different?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove one

Comment thread pyproject.toml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its not mandatory though, treat like AutoAttack and AutoVerify\

add check for foolbox like we have for auto-verify and autoattack
@henba1

henba1 commented Dec 9, 2025

Copy link
Copy Markdown
Collaborator Author

I will check the tests later

henba1 added 5 commits June 17, 2026 15:54
- Move foolbox from mandatory to optional dependency in pyproject.toml
  (add [project.optional-dependencies].foolbox and keep in dev for CI)
- Merge two foolbox example scripts into one configurable script;
  add foolbox installation check matching the autoattack pattern
- Remove duplicate create_robustness_dist_foolbox_cw.py
- Document that only untargeted attacks are supported in FoolboxAttack docstring
- Fix broken IdentitySampler import in __init__.py (file never existed)
- Apply ruff formatting across the codebase (pre-commit hook)
- Pin foolbox as third-party and ada_verona as first-party in ruff isort
  config so import sorting is identical locally and in CI. A local foolbox/
  source checkout in the repo root was being misdetected as first-party,
  hiding the I001 errors that CI (ruff 0.15.18) reports.
- Reorder imports in the 3 affected files accordingly.
- Guard 'uv venv' with '[ -d .venv ]' in both pytest workflows so a restored
  .venv cache no longer fails virtual-environment creation.

The ruff config change adds no dependencies, so requirements.txt is unchanged
(the sync pre-commit hook is skipped for this commit only).
FoolboxAttack now accepts keyword-only 'targeted' and 'target_class' options.
By default it stays untargeted (Misclassification of the true label), so all
existing usage is unchanged. With targeted=True it uses foolbox's
TargetedMisclassification criterion to drive predictions toward target_class.

Targeted mode is attack-dependent: iterative attacks (LinfPGD, BIM, DeepFool)
support it; single-step FGSM does not and foolbox raises 'unsupported criterion'.
No changes to the One2Any pipeline are needed: reaching target_class also implies
the true label is no longer predicted, so it scores as a (harder) counterexample.

Adds tests for untargeted defaults, the target_class validation error, and a
targeted PGD run.
@henba1
henba1 force-pushed the henba1-foolbox-interface branch from f98df8e to 0230f44 Compare June 22, 2026 16:12
ExperimentRepository.get_result_df() unconditionally did:
    df["network"] = df.network_path.str.split("/")...
but the result CSVs written by save_result()/save_results() come from
VerificationContext.get_dict_for_epsilon_result(), which stores the network
name in a 'network' column and never writes 'network_path'. Reading any such
result file therefore raised:
    AttributeError: 'DataFrame' object has no attribute 'network_path'
which also broke save_plots() (it calls get_result_df()).

This bug is pre-existing on main and is independent of the foolbox feature.

Fix: only derive 'network' from 'network_path' for the legacy schema; when a
'network' column is already present, use it as-is.
@henba1
henba1 force-pushed the henba1-foolbox-interface branch from 0230f44 to 1d64106 Compare June 22, 2026 16:16
The example now showcases more of the package:
- Part 1: three L-inf attacks (FGSM, PGD, DeepFool) on ImageFileDataset, with the
  cross-attack comparison plots (early stopping removed; the bundled dataset is
  small enough to use in full).
- Part 2: an L2 attack on a torchvision dataset wrapped in PytorchExperimentDataset,
  using an L2-appropriate (larger) epsilon grid, to show the pipeline is
  norm-agnostic and how to use that dataset loader.

DEPENDENCY: the comparison plots use the grouping-agnostic ReportCreator
(group_by=...), added in branch henba1-enhance-report-creator commit
53f7c65. This script requires that change on the
path until it is merged.
@henba1

henba1 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

The comparison plots in the new foolbox example script use the grouping-agnostic ReportCreator (group_by=...), which is added in a different branch henba1-enhance-report-creator (commit 53f7c65).

@henba1
henba1 requested a review from AWbosman June 22, 2026 17:13

@Aaron99B Aaron99B left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of noise in the PR due to the linting changes. Could you maybe do the small changes like additional lines, changing spaces in a separate PR first? Then this one will be way easier to review

@henba1

henba1 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

There is a lot of noise in the PR due to the linting changes. Could you maybe do the small changes like additional lines, changing spaces in a separate PR first? Then this one will be way easier to review

Agreed, raised the issue already in #179. Let's do this first. I will create an issue from this and open a PR.

@Aaron99B Aaron99B left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just some minor things

Comment thread requirements.txt
torchvision>=0.18.1
typing_extensions No newline at end of file
# This file was autogenerated by uv via the following command:
# uv pip compile /gpfs/home2/jvrijn/code/VERONA/pyproject.toml -o /gpfs/home2/jvrijn/code/VERONA/requirements.txt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are a lot of requirements. Are we sure we need them all for foolbox?

if result_df_path.exists():
df = pd.read_csv(result_df_path, index_col=0)
df["network"] = df.network_path.str.split("/").apply(lambda x: x[-1]).apply(lambda x: x.split(".")[0])
if "network" not in df.columns and "network_path" in df.columns:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this change for foolbox?

so it is scored as a (typically harder) counterexample; `target_class` should
therefore differ from the true label, otherwise the search is degenerate.

Attributes:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the attributes listed here and in the init?

"""
fmodel = foolbox.PyTorchModel(model, bounds=self.bounds)

attack = self.attack_cls(**self.kwargs)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we create the attack here. Can't we already do it in the init?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants