├── .github
└── workflows
│ ├── ci.yml
│ ├── pages.yml
│ └── pypi-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── api
│ ├── attack-model.md
│ ├── base-attack.md
│ ├── create-attack.md
│ ├── evaluate
│ │ ├── eval-dataset.md
│ │ ├── eval-metric.md
│ │ ├── eval-runner.md
│ │ ├── eval-save-image.md
│ │ └── index.md
│ ├── index.md
│ └── register-attack.md
├── attacks
│ ├── .pages
│ └── index.md
├── development.md
├── images
│ ├── favicon.png
│ ├── torchattack.png
│ └── usage
│ │ ├── advs-mifgsm-resnet50-eps-8.png
│ │ ├── advs-tgr-vitb16-eps-8.png
│ │ └── xs.png
├── index.md
├── javascripts
│ └── katex.js
├── populate_attack_apis.py
├── stylesheets
│ └── extra.css
└── usage
│ ├── attack-creation.md
│ ├── attack-evaluation.md
│ ├── attack-model.md
│ ├── index.md
│ └── putting-it-all-together.md
├── examples
└── mifgsm_transfer.py
├── mkdocs.yml
├── pyproject.toml
├── requirements.txt
├── tests
├── conftest.py
├── image.png
├── test_attack_model.py
├── test_attacks.py
├── test_create_attack.py
├── test_eval_metric.py
├── test_eval_save_image.py
└── test_external_attack_register.py
└── torchattack
├── __init__.py
├── _rgetattr.py
├── admix.py
├── att.py
├── attack.py
├── attack_model.py
├── bfa.py
├── bia.py
├── bpa.py
├── bsr.py
├── cda.py
├── create_attack.py
├── decowa.py
├── deepfool.py
├── difgsm.py
├── dr.py
├── evaluate
├── __init__.py
├── dataset.py
├── meter.py
├── runner.py
└── save_image.py
├── fda.py
├── fgsm.py
├── fia.py
├── gama.py
├── generative
├── __init__.py
├── _weights.py
├── leaky_relu_resnet_generator.py
└── resnet_generator.py
├── geoda.py
├── gra.py
├── ilpd.py
├── l2t.py
├── ltp.py
├── mifgsm.py
├── mig.py
├── mumodig.py
├── naa.py
├── nifgsm.py
├── pgd.py
├── pgdl2.py
├── pna_patchout.py
├── py.typed
├── sgm.py
├── sinifgsm.py
├── ssa.py
├── ssp.py
├── tgr.py
├── tifgsm.py
├── vdc.py
├── vmifgsm.py
└── vnifgsm.py
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 | workflow_dispatch:
8 |
9 | jobs:
10 | ci:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Install uv
16 | uses: astral-sh/setup-uv@v4
17 | with:
18 | python-version: "3.13"
19 |
20 | - name: Install the project's dev dependencies
21 | # run: uv sync --dev --group test
22 | run: uv sync --dev
23 |
24 | - name: Run Ruff
25 | uses: astral-sh/ruff-action@v1
26 | with:
27 | args: check --output-format=github .
28 |
29 | - name: Run type checking
30 | run: uv run mypy torchattack
31 |
32 | # - name: Run tests
33 | # run: uv run pytest tests
34 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | id-token: write
12 | actions: read
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Install uv
21 | uses: astral-sh/setup-uv@v4
22 | with:
23 | python-version: "3.13"
24 |
25 | - name: Install the project's dev dependencies
26 | run: uv sync --group docs
27 |
28 | - uses: actions/cache@v4
29 | with:
30 | key: ${{ github.ref }}
31 | path: .cache
32 |
33 | - name: Build static site
34 | env:
35 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | run: uv run mkdocs build
37 |
38 | - name: Upload artifact
39 | uses: actions/upload-pages-artifact@v3
40 | with:
41 | path: site
42 |
43 | deploy:
44 | needs: build
45 |
46 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
47 | permissions:
48 | pages: write
49 | id-token: write
50 |
51 | environment:
52 | name: github-pages
53 | url: ${{ steps.deployment.outputs.page_url }}
54 |
55 | runs-on: ubuntu-latest
56 | steps:
57 | - name: Deploy to GitHub Pages
58 | id: deployment
59 | uses: actions/deploy-pages@v4
60 |
--------------------------------------------------------------------------------
/.github/workflows/pypi-publish.yml:
--------------------------------------------------------------------------------
1 | name: pypi-publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | id-token: write
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Install uv
17 | uses: astral-sh/setup-uv@v4
18 | with:
19 | python-version: "3.13"
20 |
21 | - name: Install the project with build deps
22 | run: uv sync
23 |
24 | - name: Build package
25 | run: uv build
26 |
27 | - name: Publish package distributions to PyPI
28 | uses: pypa/gh-action-pypi-publish@release/v1
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dataset files
2 | output*/
3 | datasets/
4 |
5 | # Lockfiles
6 | uv.lock
7 |
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | wheels/
30 | pip-wheel-metadata/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .nox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *.cover
57 | *.py,cover
58 | .hypothesis/
59 | .pytest_cache/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Django stuff:
66 | *.log
67 | local_settings.py
68 | db.sqlite3
69 | db.sqlite3-journal
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 |
120 | # Spyder project settings
121 | .spyderproject
122 | .spyproject
123 |
124 | # Rope project settings
125 | .ropeproject
126 |
127 | # mkdocs documentation
128 | /site
129 |
130 | # mypy
131 | .mypy_cache/
132 | .dmypy.json
133 | dmypy.json
134 |
135 | # Pyre type checker
136 | .pyre/
137 |
138 | .vscode/*
139 |
140 | # Local History for Visual Studio Code
141 | .history/
142 |
143 | # Built Visual Studio Code Extensions
144 | *.vsix
145 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Spencer Woo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/api/attack-model.md:
--------------------------------------------------------------------------------
1 | # Attack model wrapper
2 |
3 | ::: torchattack.AttackModel
4 |
5 | ::: torchattack.attack_model.AttackModelMeta
6 |
--------------------------------------------------------------------------------
/docs/api/base-attack.md:
--------------------------------------------------------------------------------
1 | # The Attack base class
2 |
3 | The `Attack` class is the base class for all attacks in torchattack. It provides a common interface for all attacks, and it is the class that all attacks should inherit from. Inherit this class to create a new attack.
4 |
5 | ::: torchattack.Attack
6 |
--------------------------------------------------------------------------------
/docs/api/create-attack.md:
--------------------------------------------------------------------------------
1 | # Create attack
2 |
3 | ::: torchattack.create_attack.create_attack
4 |
--------------------------------------------------------------------------------
/docs/api/evaluate/eval-dataset.md:
--------------------------------------------------------------------------------
1 | # NIPS 2017 Dataset
2 |
3 | ::: torchattack.evaluate.dataset
4 |
--------------------------------------------------------------------------------
/docs/api/evaluate/eval-metric.md:
--------------------------------------------------------------------------------
1 | # Fooling rate metric
2 |
3 | ::: torchattack.evaluate.meter
4 |
--------------------------------------------------------------------------------
/docs/api/evaluate/eval-runner.md:
--------------------------------------------------------------------------------
1 | # Attack runner
2 |
3 | ::: torchattack.evaluate.runner
4 |
--------------------------------------------------------------------------------
/docs/api/evaluate/eval-save-image.md:
--------------------------------------------------------------------------------
1 | ---
2 | status: new
3 | ---
4 |
5 | # Saving images and adversarial examples
6 |
7 | !!! tip "New in 1.5.0"
8 | `save_image_batch` was first introduced in [v1.5.0](https://github.com/spencerwooo/torchattack/releases/tag/v1.5.0).
9 |
10 | To avoid degrading the effectiveness of adversarial perturbation through image saving to and opening from disk, use `save_image_batch`.
11 |
12 | As a rule of thumb, we recommend saving images as PNGs, as they better keep the image quality than JPEGs. To compare, in the unit tests of torchattack, we use:
13 |
14 | - a tolerance of `4e-3` for PNGs, which approx. to $\varepsilon = 1 / 255$ in the $\ell_\infty$ norm, and
15 | - a tolerance of `8e-3` for JPEGs, which approx. to $\varepsilon = 2 / 255$.
16 |
17 | A commonly used perturbation magnitude is $\varepsilon = 8 / 255$, for reference.
18 |
19 | ::: torchattack.evaluate.save_image_batch
20 |
--------------------------------------------------------------------------------
/docs/api/evaluate/index.md:
--------------------------------------------------------------------------------
1 | # Evaluation module
2 |
3 | !!! warning
4 | This module has been renamed to `torchattacks.evaluate` as of [v1.5.1](https://github.com/spencerwooo/torchattack/releases/tag/v1.5.1). The previous `torchattack.eval` will be deprecated in the future.
5 |
6 | The evaluation module `torchattack.evaluate` provides a few tools that are commonly used in transfer-based attack experiments.
7 |
8 | - [Saving adversarial examples](eval-save-image.md)
9 | - [Fooling rate metric](eval-metric.md)
10 | - [NIPS 2017 Dataset](eval-dataset.md)
11 | - [Attack runner](eval-runner.md)
12 |
--------------------------------------------------------------------------------
/docs/api/index.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | torchattack provides, besides [the attacks](../attacks/index.md) themselves, a set of tools to aid research.
4 |
5 | - [Attack base class](base-attack.md)
6 | - [Register attack](register-attack.md)
7 | - [Create attack](create-attack.md)
8 | - [Attack model wrapper](attack-model.md)
9 | - [Evaluation module](evaluate/index.md)
10 |
--------------------------------------------------------------------------------
/docs/api/register-attack.md:
--------------------------------------------------------------------------------
1 | ---
2 | status: new
3 | ---
4 |
5 | # Register attack
6 |
7 | !!! tip "New in 1.4.0"
8 | The `register_attack()` decorator was first introduced in [v1.4.0](https://github.com/spencerwooo/torchattack/releases/tag/v1.4.0).
9 |
10 | `register_attack()` is a decorator that registers an attack to the attack registry. This allows external attacks to be recognized by `create_attack()`.
11 |
12 | The attack registry resides at `ATTACK_REGISTRY`. This registry is populated at import time. To register an additional attack, simply decorate the attack class with `@register_attack()`.
13 |
14 | ```python
15 | from torchattack import Attack, register_attack
16 |
17 |
18 | @register_attack()
19 | class MyNewAttack(Attack):
20 | def __init__(self, model, normalize, device):
21 | super().__init__(model, normalize, device)
22 |
23 | def forward(self, x):
24 | return x
25 | ```
26 |
27 | Afterwards, the attack can be accessed in the same manner as the built-in attacks.
28 |
29 | ```python
30 | import torch
31 | from torchattack import create_attack, AttackModel
32 |
33 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
34 | model = AttackModel.from_pretrained('resnet50').to(device)
35 | adversary = create_attack('MyNewAttack', model=model, normalize=model.normalize, device=device)
36 | ```
37 |
38 | ::: torchattack.register_attack
39 |
--------------------------------------------------------------------------------
/docs/attacks/.pages:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/docs/attacks/.pages
--------------------------------------------------------------------------------
/docs/development.md:
--------------------------------------------------------------------------------
1 | # Development
2 |
3 | uv is used for development.
4 |
5 | First, [install uv](https://docs.astral.sh/uv/getting-started/installation/).
6 |
7 | ## Installing Dependencies
8 |
9 | Next, install dev dependencies.
10 |
11 | ```shell
12 | uv sync --dev
13 | ```
14 |
15 | ### Installing Test Dependencies
16 |
17 | Install dependency group `test` to run tests.
18 |
19 | ```shell
20 | uv sync --dev --group test
21 | ```
22 |
23 | ### Installing Documentation Dependencies
24 |
25 | Install dependency group `docs` to build documentation.
26 |
27 | ```shell
28 | uv sync --dev --group docs
29 | ```
30 |
31 | ### Installing All Dependencies
32 |
33 | To install everything, run:
34 |
35 | ```shell
36 | uv sync --all-groups
37 | ```
38 |
39 | ## Running Tests
40 |
41 | To run tests:
42 |
43 | ```shell
44 | pytest torchattack
45 | ```
46 |
47 | ## Additional Information
48 |
49 | For more details on using uv, refer to the [uv documentation](https://docs.astral.sh/uv/).
50 |
--------------------------------------------------------------------------------
/docs/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/docs/images/favicon.png
--------------------------------------------------------------------------------
/docs/images/torchattack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/docs/images/torchattack.png
--------------------------------------------------------------------------------
/docs/images/usage/advs-mifgsm-resnet50-eps-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/docs/images/usage/advs-mifgsm-resnet50-eps-8.png
--------------------------------------------------------------------------------
/docs/images/usage/advs-tgr-vitb16-eps-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/docs/images/usage/advs-tgr-vitb16-eps-8.png
--------------------------------------------------------------------------------
/docs/images/usage/xs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/docs/images/usage/xs.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Home
3 | hide:
4 | - navigation
5 | - toc
6 | - footer
7 | ---
8 |
9 |
15 |
16 |
17 | {: style="width:600px"}
18 |
19 |
20 | [](https://github.com/astral-sh/ruff)
21 | [](https://pypi.python.org/pypi/torchattack)
22 | [](https://pypi.python.org/pypi/torchattack)
23 | [](https://pypi.python.org/pypi/torchattack)
24 | [](https://github.com/spencerwooo/torchattack/actions/workflows/ci.yml)
25 |
26 | :material-shield-sword: **torchattack** - _A curated list of adversarial attacks in PyTorch, with a focus on transferable black-box attacks._
27 |
28 | ```shell
29 | pip install torchattack
30 | ```
31 |
32 | ## Highlights
33 |
34 | - 🛡️ A curated collection of adversarial attacks implemented in PyTorch.
35 | - 🔍 Focuses on gradient-based transferable black-box attacks.
36 | - 📦 Easily load pretrained models from torchvision or timm using `AttackModel`.
37 | - 🔄 Simple interface to initialize attacks with `create_attack`.
38 | - 🔧 Extensively typed for better code quality and safety.
39 | - 📊 Tooling for fooling rate metrics and model evaluation in `eval`.
40 | - 🔁 Numerous attacks reimplemented for readability and efficiency (TGR, VDC, etc.).
41 |
42 | ## Next Steps
43 |
44 |
45 |
46 | - :material-book-open-page-variant:{ .middle } **Usage**
47 |
48 | ***
49 |
50 | Learn how to use abstractions of pretrained victim models, attack creations, and evaluations.
51 |
52 | :material-arrow-right: [Usage](./usage/index.md)
53 |
54 | - :material-sword-cross:{ .middle } **Attacks**
55 |
56 | ***
57 |
58 | Explore the comprehensive list of adversarial attacks available in torchattack.
59 |
60 | :material-arrow-right: [Attacks](./attacks/index.md)
61 |
62 | - :material-tools:{ .middle } **Development**
63 |
64 | ***
65 |
66 | On how to install dependencies, run tests, and build documentation.
67 |
68 | :material-arrow-right: [Development](./development.md)
69 |
70 |
71 |
72 | ## License
73 |
74 | torchattack is licensed under the [MIT License](https://github.com/spencerwooo/torchattack/blob/main/LICENSE).
75 |
--------------------------------------------------------------------------------
/docs/javascripts/katex.js:
--------------------------------------------------------------------------------
1 | document$.subscribe(({ body }) => {
2 | renderMathInElement(body, {
3 | delimiters: [
4 | { left: "$$", right: "$$", display: true },
5 | { left: "$", right: "$", display: false },
6 | { left: "\\(", right: "\\)", display: false },
7 | { left: "\\[", right: "\\]", display: true },
8 | ],
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/docs/populate_attack_apis.py:
--------------------------------------------------------------------------------
1 | """
2 | This script dynamically populates the markdown docs for each attack in the `attacks`
3 | directory. Each attack is documented in a separate markdown file with the attack's
4 | name as the filename. The content of each markdown file is dynamically generated
5 | from the docstrings of the attack classes in the `torchattack` module.
6 |
7 | Example of the generated docs:
8 |
9 | ```markdown
10 | # MIFGSM
11 |
12 | ::: torchattack.MIFGSM
13 | ```
14 | """
15 |
16 | import os
17 |
18 | import mkdocs_gen_files
19 |
20 | import torchattack
21 | from torchattack import ATTACK_REGISTRY
22 |
23 | for attack_name, attack_cls in ATTACK_REGISTRY.items():
24 | filename = os.path.join('attacks', f'{attack_name.lower()}.md')
25 | with mkdocs_gen_files.open(filename, 'w') as f:
26 | if attack_cls.is_category('GENERATIVE'):
27 | # For generative attacks, we need to document
28 | # both the attack and its weights enum as well
29 | attack_mod = getattr(torchattack, attack_name.lower())
30 | weights_enum = getattr(attack_mod, f'{attack_name}Weights')
31 | weights_doc = [f'- `{w}`\n' for w in weights_enum.__members__]
32 | f.write(
33 | f'# {attack_name}\n\n'
34 | f'::: torchattack.{attack_name}\n'
35 | f'::: torchattack.{attack_name.lower()}.{attack_name}Weights\n\n'
36 | f'Available weights:\n\n'
37 | f'{"".join(weights_doc)}\n'
38 | )
39 | else:
40 | f.write(f'# {attack_name}\n\n::: torchattack.{attack_name}\n')
41 | mkdocs_gen_files.set_edit_path(filename, 'docs/populate_attack_apis.py')
42 |
--------------------------------------------------------------------------------
/docs/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | :root > * {
2 | --md-primary-fg-color: #2066d6;
3 | --md-accent-fg-color: #df332a;
4 | --md-typeset-a-color: #2066d6;
5 | }
6 |
7 | [data-md-color-scheme="slate"] {
8 | --md-default-bg-color: hsl(240, 12%, 14%);
9 | --md-primary-fg-color: #3394e4 !important;
10 | --md-accent-fg-color: #df332a !important;
11 | --md-typeset-a-color: #3394e4 !important;
12 | }
13 |
14 | .md-header,
15 | .md-tabs {
16 | color: inherit !important;
17 | background-color: var(--md-default-bg-color);
18 | }
19 | [data-md-color-scheme="slate"] .md-header,
20 | [data-md-color-scheme="slate"] .md-tabs {
21 | background-color: var(--md-default-bg-color);
22 | }
23 | @media screen and (min-width: 76.25em) {
24 | [data-md-color-scheme="slate"] .md-tabs {
25 | border-bottom: 0.05rem solid rgba(255, 255, 255, 0.07);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/docs/usage/attack-creation.md:
--------------------------------------------------------------------------------
1 | # Creating and running an attack
2 |
3 | After [setting up a pretrained model](./attack-model.md), we can now create an attack.
4 |
5 | ## Importing the attack class
6 |
7 | Attacks in torchattack are exposed as classes.
8 |
9 | To create the classic attack — Fast Gradient Sign Method ([FGSM](../attacks/fgsm.md)), for instance, we can import the `FGSM` class from torchattack.
10 |
11 | ```python hl_lines="2 8"
12 | import torch
13 | from torchattack import AttackModel, FGSM
14 |
15 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
16 | model = AttackModel.from_pretrained(model_name='resnet50').to(device)
17 | transform, normalize = model.transform, model.normalize
18 |
19 | attack = FGSM(model, normalize, device)
20 | ```
21 |
22 | !!! info "Note the three arguments, `model`, `normalize`, and `device`."
23 |
24 | - `model` specifies the pretrained model to attack, often acting as the surrogate model for transferable black-box attacks.
25 | - `normalize` is the normalize function associated with the model's weight, that we automatically resolved earlier.
26 | - `device` is the device to run the attack on.
27 |
28 | All attacks in torchattack, if not explicitly stated, require these three arguments. (1)
29 | { .annotate }
30 |
31 | 1. This is the [*thin layer of abstraction*](./index.md) that we were talking about.
32 |
33 | Most adversarial attacks, especially gradient-based attacks, are projected onto the $\ell_p$ ball, where $p$ is the norm, to restrict the magnitude of the perturbation and maintain imperceptibility.
34 |
35 | Attacks like FGSM takes an additional argument, `eps`, to specify the radius $\varepsilon$ of the $\ell_\infty$ ball.
36 |
37 | For an 8-bit image with pixel values in the range of $[0, 255]$, the tensor represented image is within $[0, 1]$ (after division by 255). **As such, common values for `eps` are `8/255` or `16/255`, or simply `0.03`.**
38 |
39 | ```python
40 | attack = FGSM(model, normalize, device, eps=8 / 255)
41 | ```
42 |
43 | Different attacks hold different arguments. For instance, the Momentum-Iterative FGSM ([MI-FGSM](../attacks/mifgsm.md)) attack accepts the additional `steps` and `decay` as arguments.
44 |
45 | ```python
46 | from torchattack import MIFGSM
47 |
48 | attack = MIFGSM(model, normalize, device, eps=8 / 255, steps=10, decay=1.0)
49 | ```
50 |
51 | Finally, please take a look at the actual implementation of FGSM here :octicons-arrow-right-24: [`torchattack.FGSM`][torchattack.FGSM] (expand collapsed `fgsm.py` source code).
52 |
53 | ## The `create_attack` method
54 |
55 | torchattack provides an additional helper, [`create_attack`][torchattack.create_attack.create_attack], to create an attack by its name.
56 |
57 | ```python
58 | from torchattack import create_attack
59 | ```
60 |
61 | To initialize the same FGSM attack.
62 |
63 | ```python
64 | attack = create_attack('FGSM', model, normalize, device)
65 | ```
66 |
67 | To specify the common `eps` argument.
68 |
69 | ```python
70 | attack = create_attack('FGSM', model, normalize, device, eps=8 / 255)
71 | ```
72 |
73 | For additional attack specific arguments, pass them as keyword arguments.
74 |
75 | ```python
76 | attack = create_attack('MIFGSM', model, normalize, device, eps=8 / 255, steps=10, decay=1.0)
77 | ```
78 |
79 | For generative attacks ([BIA](../attacks/bia.md), [CDA](../attacks/cda.md), and [LTP](../attacks/ltp.md), etc.) that do not require passing the `model` and `normalize` arguments.
80 |
81 | ```python
82 | attack = create_attack('BIA', device, eps=10 / 255, weights='DEFAULT')
83 | attack = create_attack('CDA', device, eps=10 / 255, checkpoint_path='path/to/checkpoint.pth')
84 | ```
85 |
86 | !!! tip "Generative attacks are models specifically trained to generate adversarial perturbation directly."
87 |
88 | These models have already been given a pretrained model to attack, which acts as the discriminator in their setups. In inference mode, they load checkpoints like any other pretrained model from torchvision. **The checkpoints provided by torchattack are sourced from their original repositories.**
89 |
90 | ## Running the attack
91 |
92 | Similar to PyTorch models, torchattack attacks implement the `forward` method, to run the attack on a batch of images.
93 |
94 | Calling the attack class itself also directs to the `forward` method.
95 |
96 | Here is a simple example for a batch of dummy images of shape $(4, 3, 224, 224)$ (typical ImageNet images), and their corresponding labels (1000 classes).
97 |
98 | ```python
99 | x = torch.rand(4, 3, 224, 224).to(device)
100 | y = torch.randint(0, 1000, (4,)).to(device)
101 |
102 | x_adv = attack(x, y)
103 | # x_adv is the generated adversarial example
104 | ```
105 |
--------------------------------------------------------------------------------
/docs/usage/attack-evaluation.md:
--------------------------------------------------------------------------------
1 | # Loading a dataset and running evaluations
2 |
3 | To run an attack on actual images, we would normally use a dataloader, just like loading any other dataset.
4 |
5 | ## Loading the NIPS 2017 dataset
6 |
7 | One of the most common datasets used in evaluating adversarial transferability is the [NIPS 2017 Adversarial Learning challenges](https://www.kaggle.com/datasets/google-brain/nips-2017-adversarial-learning-development-set) dataset. The dataset contains 1000 images from the ImageNet validation set and is widely used in current state-of-the-art transferable adversarial attack research.
8 |
9 | torchattack provides the [`NIPSLoader`][torchattack.evaluate.NIPSLoader] to load this dataset.
10 |
11 | Provided that we have downloaded the dataset under `datasets/nips2017` with the following file structure.
12 |
13 | ```tree
14 | datasets/nips2017
15 | ├── images
16 | │ ├── 000b7d55b6184b08.png
17 | │ ├── 001b5e6f1b89fd3b.png
18 | │ ├── 00312c7e7196baf4.png
19 | │ ├── 00c3cd597f1ee96f.png
20 | │ ├── ...
21 | │ └── fff35cdcce3cde43.png
22 | ├── categories.csv
23 | └── images.csv
24 | ```
25 |
26 | We can load the dataset like so.
27 |
28 | ```python hl_lines="3 9"
29 | import torch
30 | from torchattack import AttackModel, FGSM
31 | from torchattack.evaluate import NIPSLoader
32 |
33 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
34 | model = AttackModel.from_pretrained(model_name='resnet50').to(device)
35 | transform, normalize = model.transform, model.normalize
36 |
37 | dataloader = NIPSLoader(root='datasets/nips2017', transform=transform, batch_size=16)
38 |
39 | attack = FGSM(model, normalize, device)
40 | ```
41 |
42 | !!! tip "Only the transform is applied to the dataloader, ==the normalize is left out==."
43 | Note that the earlier _separated transform and normalize function_ here. Only the transform function is passed to the dataloader, the normalize function is left out to be applied manually in the attack loop.
44 |
45 | And wrap the dataloader in the progress bar of your choice, [such as rich](https://rich.readthedocs.io/en/stable/progress.html).
46 |
47 | ```python
48 | from rich.progress import track
49 |
50 | dataloader = track(dataloader, description='Evaluating attack')
51 | ```
52 |
53 | ## Running the attack
54 |
55 | When iterated over, the [`NIPSLoader`][torchattack.evaluate.NIPSLoader] returns a tuple of `(x, y, fname)`, where
56 |
57 | - `x` is the batch of images,
58 | - `y` is the batch of labels,
59 | - and `fname` is the batch of filenames useful for saving the generated adversarial examples if needed.
60 |
61 | We can now run the attack by iterating over the dataset and attacking batches of input samples.
62 |
63 | ```python
64 | for x, y, fname in dataloader:
65 | x, y = x.to(device), y.to(device)
66 | x_adv = attack(x, y)
67 | ```
68 |
69 | ## Evaluating the attack
70 |
71 | How would we know if the attack was successful?
72 |
73 | We can evaluate the attack's **==fooling rate==**, by comparing the model's accuracy on clean samples and their associated adversarial examples. Fortunately, torchattack also provides a [`FoolingRateMeter`][torchattack.evaluate.FoolingRateMeter] tracker to do just that.
74 |
75 | ```python hl_lines="3 8-11"
76 | from torchattack.evaluate import FoolingRateMeter
77 |
78 | frm = FoolingRateMeter()
79 | for x, y, fname in dataloader:
80 | x, y = x.to(device), y.to(device)
81 | x_adv = attack(x, y)
82 |
83 | # Track fooling rate
84 | x_outs = model(normalize(x))
85 | adv_outs = model(normalize(x_adv))
86 | frm.update(y, x_outs, adv_outs)
87 | ```
88 |
89 | Finally, we can acquire the key metrics with `frm.compute()`, which returns a tuple of
90 |
91 | - `clean_accuracy`: the model's accuracy on clean samples,
92 | - `adv_accuracy`: the model's accuracy on adversarial examples,
93 | - `fooling_rate`: the fooling rate of the attack.
94 |
95 | ```python
96 | clean_accuracy, adv_accuracy, fooling_rate = frm.compute()
97 | ```
98 |
--------------------------------------------------------------------------------
/docs/usage/attack-model.md:
--------------------------------------------------------------------------------
1 | # Loading pretrained models
2 |
3 | ## The `AttackModel`
4 |
5 | To launch any adversarial attack, you would need a model to attack.
6 |
7 | torchattack provides a simple abstraction over both [torchvision](https://github.com/pytorch/vision) and [timm](https://github.com/huggingface/pytorch-image-models) models, to load pretrained image classification models on ImageNet.
8 |
9 | First, import `torch`, import [`AttackModel`][torchattack.attack_model.AttackModel] from `torchattack`, and determine the device to use.
10 |
11 | ```python
12 | import torch
13 | from torchattack import AttackModel
14 |
15 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
16 | ```
17 |
18 | ## Pretrained models are loaded by its name
19 |
20 | Contrary to `torchvision.models`, [`AttackModel`][torchattack.attack_model.AttackModel] loads a pretrained model by its name.
21 |
22 | To load a ResNet-50 model for instance.
23 |
24 | ```python
25 | model = AttackModel.from_pretrained(model_name='resnet50').to(device)
26 | ```
27 |
28 | The [`AttackModel.from_pretrained()`][torchattack.attack_model.AttackModel.from_pretrained] method does three things under the hood:
29 |
30 | 1. It automatically loads the model from either `torchvision` (by default) or `timm` (if not found in `torchvision`).
31 | 2. It sets the model to evaluation mode by calling `model.eval()`.
32 | 3. It resolves the model's `transform` and `normalize` functions associated with its pretrained weights to the [`AttackModel`][torchattack.attack_model.AttackModel] instance.
33 | 4. And finally, it populates the resolved transformation attributes to the model's `meta` attribute.
34 |
35 | Doing so, we not only get our pretrained model set up, but also its necessary associated, and more importantly, **_separated_ transform and normalization functions(1).**
36 | { .annotate }
37 |
38 | 1. Separating the model's normalize function from its transform is crucial for launching attacks, **as adversarial perturbation is crafted within the original image space — most often within `(0, 1)`.**
39 |
40 | ```python
41 | transform, normalize = model.transform, model.normalize
42 | ```
43 |
44 | ```pycon
45 | >>> model.meta
46 | AttackModelMeta(resize_size=232, crop_size=224, interpolation=, antialias=True, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
47 | ```
48 |
49 | ## Specifying the model source
50 |
51 | [`AttackModel`][torchattack.attack_model.AttackModel] honors an explicit model source to load from, by prepending the model name with `tv/` or `timm/`, for `torchvision` and `timm` respectively.
52 |
53 | For instance, to load the ViT-B/16 model from `timm`.
54 |
55 | ```python
56 | vit_b16 = AttackModel.from_pretrained(model_name='timm/vit_base_patch16_224').to(device)
57 | ```
58 |
59 | To load the Inception-v3 model from `torchvision`.
60 |
61 | ```python
62 | inv_v3 = AttackModel.from_pretrained(model_name='tv/inception_v3').to(device)
63 | ```
64 |
65 | Or, explicitly specify using `timm` as the source with `from_timm=True`.
66 |
67 | ```python
68 | pit_b = AttackModel.from_pretrained(model_name='pit_b_224', from_timm=True).to(device)
69 | ```
70 |
--------------------------------------------------------------------------------
/docs/usage/index.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 |
4 |
5 | { width=200 }
6 | Benign Image
7 |
8 |
9 | { width=200 }
10 | [MIFGSM](../attacks/mifgsm.md) :octicons-arrow-right-24: ResNet50
11 |
12 |
13 | { width=200 }
14 | [TGR](../attacks/tgr.md) :octicons-arrow-right-24: ViT-B/16
15 |
16 |
17 |
18 | ## Adversarial examples
19 |
20 | **Adversarial examples** are tricky inputs designed to confuse machine learning models.
21 |
22 | In vision tasks like image classification, these examples are created by slightly altering an original image. The changes are so small that humans can't really notice them, yet **they can cause significant shifts in the model's prediction.**
23 |
24 | torchattack is a library for PyTorch that offers a collection of state-of-the-art attacks to create these adversarial examples. **It focuses on transferable (1) black-box (2) attacks on image classification models.**
25 | { .annotate }
26 |
27 | 1. **_Transferable adversarial attacks_** are designed to fool multiple models, most often the more the better, not just the specific one they were initially created for. This means an attack effective on one model might also work on others.
28 |
29 | 2. **_Black-box attacks_** are attacks that don't require access to the model's internal parameters. Instead, they rely only on the model's inputs and outputs to launch the attack.
30 |
31 | Attacks in torchattack are implemented over a thin layer of abstraction [`torchattack.Attack`][torchattack.Attack], with minimal changes to the original implementation within its research paper, along with comprehensive type hints and explanatory comments, to make it easy for researchers like me and you to use and understand.
32 |
33 | The library also provides tools to load pretrained models, set up attacks, and run tests.
34 |
35 | ## Getting Started
36 |
37 | To get started, follow the links below:
38 |
39 | - [Loading pretrained models and important attributes](attack-model.md)
40 | - [Creating and running attacks](attack-creation.md)
41 | - [Loading a dataset and running evaluations](attack-evaluation.md)
42 | - [A full example to evaluate transferability](putting-it-all-together.md)
43 |
44 | Or dive straight into [all available attacks](../attacks/index.md).
45 |
--------------------------------------------------------------------------------
/docs/usage/putting-it-all-together.md:
--------------------------------------------------------------------------------
1 | # Putting it all together
2 |
3 | For transferable black-box attacks, we would often initialize more than one model, assigning the models other than the one being directly attacked as black-box victim models, to evaluate the transferability of the attack _(the attack's effectiveness under a black-box scenario where the target victim model's internal workings are unknown to the attacker)_.
4 |
5 | ## A full example
6 |
7 | To put everything together, we show a full example that does the following.
8 |
9 | 1. Load the NIPS 2017 dataset.
10 | 2. Initialize a pretrained ResNet-50, as the white-box surrogate model, for creating adversarial examples.
11 | 3. Initialize two other pretrained VGG-11 and Inception-v3, as black-box victim models, to evaluate transferability.
12 | 4. Run the classic MI-FGSM attack, and demonstrate its performance.
13 |
14 | ```python title="examples/mifgsm_transfer.py"
15 | --8<-- "examples/mifgsm_transfer.py"
16 | ```
17 |
18 | ```console
19 | $ python examples/mifgsm_transfer.py
20 | Evaluating white-box resnet50 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:27
21 | White-box (resnet50): 95.10% -> 0.10% (FR: 99.89%)
22 | Evaluating black-box vgg11 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:02
23 | Black-box (vgg11): 80.30% -> 59.70% (FR: 25.65%)
24 | Evaluating black-box inception_v3 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:02
25 | Black-box (inception_v3): 92.60% -> 75.80% (FR: 18.14%)
26 | ```
27 |
28 | !!! tip "Create relative transform for victim models (New in v1.5.0)"
29 | Notice in our example how we dynamically created a **relative transform** for each victim model. We use [`AttackModel.create_relative_transform`][torchattack.AttackModel.create_relative_transform] such that our relative transform for the victim model does not introduce additional unnecessary transforms such as resize and cropping that may affect the transferability of the adversarial perturbation.
30 |
31 | ## Attack Runner
32 |
33 | torchattack provides a simple command line runner at `torchattack.evaluate.runner`, and the function [`run_attack`][torchattack.evaluate.run_attack], to evaluate the transferability of attacks. The runner itself also acts as a full example to demonstrate how researchers like us can use torchattack to create and evaluate adversarial transferability.
34 |
35 | An exhaustive example :octicons-arrow-right-24: run the [`PGD`](../attacks/pgd.md) attack,
36 |
37 | - with an epsilon constraint of 16/255,
38 | - on the ResNet-18 model as the white-box surrogate model,
39 | - transferred to the VGG-11, DenseNet-121, and Inception-V3 models as the black-box victim models,
40 | - on the NIPS 2017 dataset,
41 | - with a maximum of 200 samples and a batch size of 4,
42 |
43 | ```console
44 | $ python -m torchattack.evaluate.runner \
45 | --attack PGD \
46 | --eps 16/255 \
47 | --model-name resnet18 \
48 | --victim-model-names vgg11 densenet121 inception_v3 \
49 | --dataset-root datasets/nips2017 \
50 | --max-samples 200 \
51 | --batch-size 4
52 | PGD(model=ResNet, device=cuda, normalize=Normalize, eps=0.063, alpha=None, steps=10, random_start=True, clip_min=0.000, clip_max=1.000, targeted=False, lossfn=CrossEntropyLoss())
53 | Attacking ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:07
54 | Surrogate (resnet18): cln_acc=81.50%, adv_acc=0.00% (fr=100.00%)
55 | Victim (vgg11): cln_acc=77.00%, adv_acc=34.00% (fr=55.84%)
56 | Victim (densenet121): cln_acc=87.00%, adv_acc=37.00% (fr=57.47%)
57 | Victim (inception_v3): cln_acc=92.00%, adv_acc=70.00% (fr=23.91%)
58 | ```
59 |
--------------------------------------------------------------------------------
/examples/mifgsm_transfer.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from rich.progress import track
3 |
4 | from torchattack import MIFGSM, AttackModel
5 | from torchattack.evaluate import FoolingRateMeter, NIPSLoader, save_image_batch
6 |
7 | bs = 16
8 | eps = 8 / 255
9 | root = 'datasets/nips2017'
10 | save_dir = 'outputs'
11 | model_name = 'resnet50'
12 | victim_names = ['vgg11', 'inception_v3']
13 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
14 |
15 | # Initialize the white-box model, fooling rate metric, and dataloader
16 | model = AttackModel.from_pretrained(model_name).to(device)
17 | frm = FoolingRateMeter()
18 | dataloader = NIPSLoader(root, transform=model.transform, batch_size=bs)
19 |
20 | # Initialize the attacker MI-FGSM
21 | adversary = MIFGSM(model, model.normalize, device, eps)
22 |
23 | # Attack loop and save generated adversarial examples
24 | for x, y, fnames in track(dataloader, description=f'Evaluating white-box {model_name}'):
25 | x, y = x.to(device), y.to(device)
26 | x_adv = adversary(x, y)
27 |
28 | # Track fooling rate
29 | x_outs = model(model.normalize(x))
30 | adv_outs = model(model.normalize(x_adv))
31 | frm.update(y, x_outs, adv_outs)
32 |
33 | # Save adversarial examples to `save_dir`
34 | save_image_batch(x_adv, save_dir, fnames)
35 |
36 | # Evaluate fooling rate
37 | cln_acc, adv_acc, fr = frm.compute()
38 | print(f'White-box ({model_name}): {cln_acc:.2%} -> {adv_acc:.2%} (FR: {fr:.2%})')
39 |
40 | # For all victim models
41 | for vname in victim_names:
42 | # Initialize the black-box model, fooling rate metric, and dataloader
43 | vmodel = AttackModel.from_pretrained(model_name=vname).to(device)
44 | vfrm = FoolingRateMeter()
45 |
46 | # Create relative transform (relative to the white-box model) to avoid degrading the
47 | # effectiveness of adversarial examples through image transformations
48 | vtransform = vmodel.create_relative_transform(model)
49 |
50 | # Load the clean and adversarial examples from separate dataloaders
51 | clnloader = NIPSLoader(root=root, transform=vmodel.transform, batch_size=bs)
52 | advloader = NIPSLoader(
53 | image_root=save_dir,
54 | pairs_path=f'{root}/images.csv',
55 | transform=vtransform,
56 | batch_size=bs,
57 | )
58 |
59 | # Black-box evaluation loop
60 | for (x, y, _), (xadv, _, _) in track(
61 | zip(clnloader, advloader),
62 | total=len(clnloader),
63 | description=f'Evaluating black-box {vname}',
64 | ):
65 | x, y, xadv = x.to(device), y.to(device), xadv.to(device)
66 | vx_outs = vmodel(vmodel.normalize(x))
67 | vadv_outs = vmodel(vmodel.normalize(xadv))
68 | vfrm.update(y, vx_outs, vadv_outs)
69 |
70 | # Evaluate fooling rate
71 | vcln_acc, vadv_acc, vfr = vfrm.compute()
72 | print(f'Black-box ({vname}): {vcln_acc:.2%} -> {vadv_acc:.2%} (FR: {vfr:.2%})')
73 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: torchattack
2 | site_url: https://docs.swo.moe/torchattack/
3 | repo_url: https://github.com/spencerwooo/torchattack
4 | repo_name: spencerwooo/torchattack
5 | edit_uri: edit/main/docs/
6 | copyright: Copyright © 2023-Present Spencer Woo
7 | nav:
8 | - index.md
9 | - Usage:
10 | - usage/index.md
11 | - usage/attack-model.md
12 | - usage/attack-creation.md
13 | - usage/attack-evaluation.md
14 | - usage/putting-it-all-together.md
15 | - ...
16 | - API:
17 | - api/index.md
18 | - api/base-attack.md
19 | - api/register-attack.md
20 | - api/create-attack.md
21 | - api/attack-model.md
22 | - Eval:
23 | - api/evaluate/index.md
24 | - Saving adversarial examples: api/evaluate/eval-save-image.md
25 | - api/evaluate/eval-metric.md
26 | - api/evaluate/eval-dataset.md
27 | - api/evaluate/eval-runner.md
28 | - development.md
29 | theme:
30 | name: material
31 | icon:
32 | logo: material/shield-sword
33 | favicon: images/favicon.png
34 | palette:
35 | - media: "(prefers-color-scheme)"
36 | toggle:
37 | icon: material/brightness-auto
38 | name: Switch to light mode
39 | - media: "(prefers-color-scheme: light)"
40 | scheme: default
41 | primary: white
42 | toggle:
43 | icon: material/weather-sunny
44 | name: Switch to dark mode
45 | - media: "(prefers-color-scheme: dark)"
46 | scheme: slate
47 | primary: black
48 | toggle:
49 | icon: material/weather-night
50 | name: Switch to system preference
51 | features:
52 | - navigation.tracking
53 | - navigation.path
54 | - navigation.instant
55 | - navigation.instant.prefetch
56 | - navigation.instant.progress
57 | - navigation.tabs
58 | - navigation.top
59 | - navigation.sections
60 | - navigation.prune
61 | - navigation.footer
62 | - toc.follow
63 | # - toc.integrate
64 | - search.suggest
65 | - content.action.edit
66 | - content.code.copy
67 | # - navigation.indexes
68 | - content.code.annotate
69 | - content.tabs.link
70 | extra_css:
71 | - stylesheets/extra.css
72 | - https://unpkg.com/katex@0/dist/katex.min.css
73 | extra_javascript:
74 | - javascripts/katex.js
75 | - https://unpkg.com/katex@0/dist/katex.min.js
76 | - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js
77 | markdown_extensions:
78 | - abbr
79 | - admonition
80 | - attr_list
81 | - def_list
82 | - footnotes
83 | - md_in_html
84 | - toc:
85 | permalink: true
86 | - pymdownx.details
87 | - pymdownx.superfences
88 | - pymdownx.critic
89 | - pymdownx.caret
90 | - pymdownx.keys
91 | - pymdownx.mark
92 | - pymdownx.tilde
93 | - pymdownx.highlight:
94 | anchor_linenums: true
95 | line_spans: __span
96 | pygments_lang_class: true
97 | - pymdownx.inlinehilite
98 | - pymdownx.snippets
99 | - pymdownx.emoji:
100 | emoji_index: !!python/name:material.extensions.emoji.twemoji
101 | emoji_generator: !!python/name:material.extensions.emoji.to_svg
102 | - pymdownx.arithmatex:
103 | generic: true
104 | plugins:
105 | - search
106 | - gen-files:
107 | scripts:
108 | - docs/populate_attack_apis.py
109 | - awesome-pages
110 | - mkdocstrings:
111 | handlers:
112 | python:
113 | options:
114 | show_symbol_type_heading: true
115 | show_symbol_type_toc: true
116 | show_root_toc_entry: true
117 | show_root_heading: true
118 | show_root_full_path: false
119 | extra:
120 | analytics:
121 | provider: google
122 | property: G-P593KY94JQ
123 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "torchattack"
3 | description = "A curated list of adversarial attacks in PyTorch, with a focus on transferable black-box attacks"
4 | authors = [{ name = "spencerwooo", email = "spencer.woo@outlook.com" }]
5 | requires-python = ">=3.10,<3.14"
6 | readme = "README.md"
7 | license = { file = "LICENSE" }
8 | classifiers = [
9 | "Development Status :: 5 - Production/Stable",
10 | "Environment :: GPU :: NVIDIA CUDA",
11 | "Intended Audience :: Developers",
12 | "License :: OSI Approved :: MIT License",
13 | "Operating System :: OS Independent",
14 | "Programming Language :: Python :: 3.10",
15 | "Programming Language :: Python :: 3.11",
16 | "Programming Language :: Python :: 3.12",
17 | "Programming Language :: Python :: 3.13",
18 | "Programming Language :: Python :: 3 :: Only",
19 | "Topic :: Software Development :: Libraries :: Python Modules",
20 | ]
21 | dependencies = [
22 | "torch>=1.13.0",
23 | "torchvision>=0.14.0",
24 | "numpy>=1.24.2",
25 | "scipy>=1.10.1",
26 | ]
27 | dynamic = ["version"]
28 |
29 | [project.urls]
30 | Repository = "https://github.com/spencerwooo/torchattack"
31 | Documentation = "https://github.com/spencerwooo/torchattack/blob/main/README.md"
32 |
33 | [dependency-groups]
34 | dev = ["mypy>=1.15.0", "rich>=13.9.4", "timm>=1.0.14"]
35 | test = ["pytest-cov>=6.0.0", "pytest>=8.3.4", "kornia>=0.8.1"]
36 | docs = [
37 | "mkdocs-awesome-pages-plugin>=2.10.1",
38 | "mkdocs-gen-files>=0.5.0",
39 | "mkdocs-material>=9.6.3",
40 | "mkdocstrings[python]>=0.28.0",
41 | ]
42 |
43 | [build-system]
44 | requires = ["setuptools"]
45 | build-backend = "setuptools.build_meta"
46 |
47 | [tool.setuptools.dynamic]
48 | version = { attr = "torchattack.__version__" }
49 |
50 | [tool.setuptools.packages.find]
51 | include = ["torchattack", "torchattack.*"]
52 |
53 | [tool.ruff]
54 | line-length = 88
55 |
56 | [tool.ruff.lint]
57 | select = ["E", "F", "I", "N", "B", "SIM"]
58 | ignore = ["E501", "B905"]
59 |
60 | [tool.ruff.format]
61 | quote-style = "single"
62 |
63 | [tool.mypy]
64 | no_implicit_optional = true
65 | check_untyped_defs = true
66 | ignore_missing_imports = true # Used as torchvision does not ship type hints
67 | disallow_any_unimported = true
68 | disallow_untyped_defs = true
69 | warn_unused_ignores = true
70 | warn_return_any = true
71 | warn_unreachable = true
72 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv pip compile pyproject.toml -o requirements.txt
3 | filelock==3.16.1
4 | # via torch
5 | fsspec==2024.10.0
6 | # via torch
7 | jinja2==3.1.4
8 | # via torch
9 | markupsafe==3.0.2
10 | # via jinja2
11 | mpmath==1.3.0
12 | # via sympy
13 | networkx==3.4.2
14 | # via torch
15 | numpy==2.2.0
16 | # via
17 | # torchattack (pyproject.toml)
18 | # scipy
19 | # torchvision
20 | nvidia-cublas-cu12==12.4.5.8
21 | # via
22 | # nvidia-cudnn-cu12
23 | # nvidia-cusolver-cu12
24 | # torch
25 | nvidia-cuda-cupti-cu12==12.4.127
26 | # via torch
27 | nvidia-cuda-nvrtc-cu12==12.4.127
28 | # via torch
29 | nvidia-cuda-runtime-cu12==12.4.127
30 | # via torch
31 | nvidia-cudnn-cu12==9.1.0.70
32 | # via torch
33 | nvidia-cufft-cu12==11.2.1.3
34 | # via torch
35 | nvidia-curand-cu12==10.3.5.147
36 | # via torch
37 | nvidia-cusolver-cu12==11.6.1.9
38 | # via torch
39 | nvidia-cusparse-cu12==12.3.1.170
40 | # via
41 | # nvidia-cusolver-cu12
42 | # torch
43 | nvidia-cusparselt-cu12==0.6.2
44 | # via torch
45 | nvidia-nccl-cu12==2.21.5
46 | # via torch
47 | nvidia-nvjitlink-cu12==12.4.127
48 | # via
49 | # nvidia-cusolver-cu12
50 | # nvidia-cusparse-cu12
51 | # torch
52 | nvidia-nvtx-cu12==12.4.127
53 | # via torch
54 | pillow==11.0.0
55 | # via torchvision
56 | scipy==1.14.1
57 | # via torchattack (pyproject.toml)
58 | setuptools==75.8.0
59 | # via torch
60 | sympy==1.13.1
61 | # via torch
62 | torch==2.6.0
63 | # via
64 | # torchattack (pyproject.toml)
65 | # torchvision
66 | torchvision==0.21.0
67 | # via torchattack (pyproject.toml)
68 | triton==3.2.0
69 | # via torch
70 | typing-extensions==4.12.2
71 | # via torch
72 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Callable
3 |
4 | import pytest
5 | import torch
6 | from PIL import Image
7 |
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | @pytest.fixture()
12 | def device() -> torch.device:
13 | return torch.device('cuda' if torch.cuda.is_available() else 'cpu')
14 |
15 |
16 | @pytest.fixture()
17 | def data() -> Callable[
18 | [Callable[[Image.Image | torch.Tensor], torch.Tensor]],
19 | tuple[torch.Tensor, torch.Tensor],
20 | ]:
21 | def _open_and_transform_image(
22 | transform: Callable[[Image.Image | torch.Tensor], torch.Tensor],
23 | ) -> tuple[torch.Tensor, torch.Tensor]:
24 | image_path = os.path.join(os.path.dirname(__file__), 'image.png')
25 | image = Image.open(image_path).convert('RGB')
26 | x = transform(image).unsqueeze(0)
27 | y = torch.tensor([665])
28 | return x, y
29 |
30 | return _open_and_transform_image
31 |
32 |
33 | @pytest.fixture()
34 | def resnet50_model() -> AttackModel:
35 | return AttackModel.from_pretrained('resnet50')
36 |
37 |
38 | @pytest.fixture()
39 | def vitb16_model() -> AttackModel:
40 | return AttackModel.from_pretrained('vit_base_patch16_224', from_timm=True)
41 |
--------------------------------------------------------------------------------
/tests/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/tests/image.png
--------------------------------------------------------------------------------
/tests/test_attack_model.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from PIL import Image
3 | from torch import nn
4 | from torchvision import transforms
5 | from torchvision.transforms.functional import InterpolationMode
6 |
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | class DummyModel(nn.Module):
11 | def __init__(self):
12 | super().__init__()
13 | self.linear = nn.Linear(10, 5)
14 |
15 | def forward(self, x):
16 | return self.linear(x)
17 |
18 |
19 | def test_attack_model_init_and_meta_resolve():
20 | model = DummyModel()
21 | transform = transforms.Compose(
22 | [
23 | transforms.Resize(256),
24 | transforms.CenterCrop(224),
25 | transforms.ToTensor(),
26 | ]
27 | )
28 | normalize = transforms.Normalize(
29 | mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)
30 | )
31 | am = AttackModel('dummy', model, transform, normalize)
32 | r = repr(am)
33 | assert 'AttackModel' in r
34 | assert 'dummy' in r
35 |
36 | assert am.meta.resize_size == 256
37 | assert am.meta.crop_size == 224
38 |
39 | assert am.meta.interpolation == InterpolationMode.BILINEAR
40 | assert am.meta.antialias is True
41 | assert am.meta.mean == (0.485, 0.456, 0.406)
42 | assert am.meta.std == (0.229, 0.224, 0.225)
43 |
44 |
45 | def test_attack_model_forward_and_call():
46 | model = DummyModel()
47 | am = AttackModel('dummy', model, lambda x: x, lambda x: x)
48 | x = torch.randn(2, 10)
49 | out1 = am.forward(x)
50 | out2 = am(x)
51 | assert torch.allclose(out1, out2)
52 | assert out1.shape == (2, 5)
53 |
54 |
55 | def test_create_relative_transform():
56 | model = DummyModel()
57 | am1_transform = transforms.Compose(
58 | [
59 | transforms.Resize(256),
60 | transforms.CenterCrop(224),
61 | transforms.ToTensor(),
62 | ]
63 | )
64 | am2_transform = transforms.Compose(
65 | [
66 | transforms.Resize(342),
67 | transforms.CenterCrop(299),
68 | transforms.ToTensor(),
69 | ]
70 | )
71 | normalize = transforms.Normalize(
72 | mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)
73 | )
74 | am1 = AttackModel('m1', model, am1_transform, normalize)
75 | am2 = AttackModel('m2', model, am2_transform, normalize)
76 |
77 | rel_transform = am1.create_relative_transform(am2)
78 | # Should only contain Resize + MaybePIlToTensor
79 | assert isinstance(rel_transform.transforms[0], transforms.Resize)
80 | assert rel_transform.transforms[0].size == 224
81 | # Test with a tensor
82 | x = torch.rand(3, 224, 224)
83 | y = rel_transform(x)
84 | assert isinstance(y, torch.Tensor)
85 | # Test with a PIL image
86 | img = Image.fromarray((torch.rand(224, 224, 3).numpy() * 255).astype('uint8'))
87 | y2 = rel_transform(img)
88 | assert isinstance(y2, torch.Tensor)
89 |
--------------------------------------------------------------------------------
/tests/test_attacks.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import torchattack
4 | from torchattack import ATTACK_REGISTRY
5 | from torchattack.attack_model import AttackModel
6 |
7 |
8 | def run_attack_test(attack_cls, device, model, x, y):
9 | model = model.to(device)
10 | normalize = model.normalize
11 | # attacker = attack_cls(model, normalize, device=device)
12 | attacker = torchattack.create_attack(attack_cls, model, normalize, device=device)
13 | x, y = x.to(device), y.to(device)
14 | x_adv = attacker(x, y)
15 | x_outs, x_adv_outs = model(normalize(x)), model(normalize(x_adv))
16 | assert x_outs.argmax(dim=1) == y
17 | assert x_adv_outs.argmax(dim=1) != y
18 |
19 |
20 | @pytest.mark.parametrize(
21 | 'attack_cls',
22 | [
23 | ac
24 | for ac in ATTACK_REGISTRY.values()
25 | if (ac.is_category('COMMON') or ac.is_category('NON_EPS'))
26 | ],
27 | )
28 | def test_common_and_non_eps_attacks(attack_cls, device, resnet50_model, data):
29 | x, y = data(resnet50_model.transform)
30 | run_attack_test(attack_cls, device, resnet50_model, x, y)
31 |
32 |
33 | @pytest.mark.parametrize(
34 | 'attack_cls',
35 | [ac for ac in ATTACK_REGISTRY.values() if ac.is_category('GRADIENT_VIT')],
36 | )
37 | def test_gradient_vit_attacks(attack_cls, device, vitb16_model, data):
38 | x, y = data(vitb16_model.transform)
39 | run_attack_test(attack_cls, device, vitb16_model, x, y)
40 |
41 |
42 | @pytest.mark.parametrize(
43 | 'attack_cls',
44 | [ac for ac in ATTACK_REGISTRY.values() if ac.is_category('GENERATIVE')],
45 | )
46 | def test_generative_attacks(attack_cls, device, resnet50_model, data):
47 | x, y = data(resnet50_model.transform)
48 | run_attack_test(attack_cls, device, resnet50_model, x, y)
49 |
50 |
51 | @pytest.mark.parametrize(
52 | 'model_name',
53 | [
54 | 'deit_base_distilled_patch16_224',
55 | 'pit_b_224',
56 | 'cait_s24_224',
57 | 'visformer_small',
58 | ],
59 | )
60 | def test_tgr_attack_all_supported_models(device, model_name, data):
61 | model = AttackModel.from_pretrained(model_name, from_timm=True).to(device)
62 | x, y = data(model.transform)
63 | run_attack_test(torchattack.TGR, device, model, x, y)
64 |
65 |
66 | @pytest.mark.parametrize(
67 | 'model_name',
68 | [
69 | 'deit_base_distilled_patch16_224',
70 | 'pit_b_224',
71 | 'visformer_small',
72 | ],
73 | )
74 | def test_vdc_attack_all_supported_models(device, model_name, data):
75 | model = AttackModel.from_pretrained(model_name, from_timm=True).to(device)
76 | x, y = data(model.transform)
77 | run_attack_test(torchattack.VDC, device, model, x, y)
78 |
79 |
80 | @pytest.mark.parametrize(
81 | 'model_name',
82 | [
83 | 'pit_b_224',
84 | 'cait_s24_224',
85 | 'visformer_small',
86 | ],
87 | )
88 | def test_att_attack_all_supported_models(device, model_name, data):
89 | model = AttackModel.from_pretrained(model_name, from_timm=True).to(device)
90 | x, y = data(model.transform)
91 | run_attack_test(torchattack.ATT, device, model, x, y)
92 |
--------------------------------------------------------------------------------
/tests/test_create_attack.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from torchattack import ATTACK_REGISTRY, create_attack
4 |
5 |
6 | @pytest.mark.parametrize(
7 | ('attack_name', 'expected'),
8 | [(an, ac) for an, ac in ATTACK_REGISTRY.items() if ac.is_category('COMMON')],
9 | )
10 | def test_create_non_vit_attack_same_as_imported(
11 | attack_name,
12 | expected,
13 | resnet50_model,
14 | ):
15 | created_attacker = create_attack(attack_name, resnet50_model)
16 | expected_attacker = expected(resnet50_model)
17 | assert created_attacker == expected_attacker
18 |
19 |
20 | @pytest.mark.parametrize(
21 | ('attack_name', 'expected'),
22 | [(an, ac) for an, ac in ATTACK_REGISTRY.items() if ac.is_category('GRADIENT_VIT')],
23 | )
24 | def test_create_vit_attack_same_as_imported(
25 | attack_name,
26 | expected,
27 | vitb16_model,
28 | ):
29 | created_attacker = create_attack(attack_name, vitb16_model)
30 | expected_attacker = expected(vitb16_model)
31 | assert created_attacker == expected_attacker
32 |
33 |
34 | @pytest.mark.parametrize(
35 | ('attack_name', 'expected'),
36 | [(an, ac) for an, ac in ATTACK_REGISTRY.items() if ac.is_category('GENERATIVE')],
37 | )
38 | def test_create_generative_attack_same_as_imported(attack_name, expected):
39 | created_attacker = create_attack(attack_name)
40 | expected_attacker = expected()
41 | assert created_attacker == expected_attacker
42 |
43 |
44 | def test_create_attack_with_eps(device, resnet50_model):
45 | eps = 0.3
46 | attacker = create_attack(
47 | attack='FGSM',
48 | model=resnet50_model,
49 | normalize=resnet50_model.normalize,
50 | device=device,
51 | eps=eps,
52 | )
53 | assert attacker.eps == eps
54 |
55 |
56 | def test_create_attack_with_extra_args(device, resnet50_model):
57 | attack_args = {'eps': 0.1, 'steps': 40, 'alpha': 0.01, 'decay': 0.9}
58 | attacker = create_attack(
59 | attack='MIFGSM',
60 | model=resnet50_model,
61 | normalize=resnet50_model.normalize,
62 | device=device,
63 | **attack_args,
64 | )
65 | assert attacker.eps == attack_args['eps']
66 | assert attacker.steps == attack_args['steps']
67 | assert attacker.alpha == attack_args['alpha']
68 | assert attacker.decay == attack_args['decay']
69 |
70 |
71 | def test_create_attack_with_invalid_eps(device, resnet50_model):
72 | with pytest.raises(TypeError):
73 | create_attack(
74 | attack='DeepFool',
75 | model=resnet50_model,
76 | normalize=resnet50_model.normalize,
77 | device=device,
78 | eps=0.03,
79 | )
80 |
81 |
82 | def test_create_attack_cda_with_weights(device):
83 | weights = 'VGG19_IMAGENET'
84 | attacker = create_attack(
85 | attack='CDA',
86 | device=device,
87 | weights=weights,
88 | )
89 | assert attacker.weights == weights
90 |
91 |
92 | def test_create_attack_with_invalid_attack_name(device, resnet50_model):
93 | with pytest.raises(
94 | ValueError, match="Attack 'InvalidAttack' is not supported within torchattack."
95 | ):
96 | create_attack(
97 | attack='InvalidAttack',
98 | model=resnet50_model,
99 | normalize=resnet50_model.normalize,
100 | device=device,
101 | )
102 |
--------------------------------------------------------------------------------
/tests/test_eval_metric.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import torch
3 |
4 | from torchattack.evaluate import FoolingRateMeter
5 |
6 |
7 | @pytest.fixture()
8 | def metric():
9 | return FoolingRateMeter()
10 |
11 |
12 | @pytest.fixture()
13 | def labels():
14 | return torch.tensor([0, 1, 2])
15 |
16 |
17 | @pytest.fixture()
18 | def clean_logits():
19 | return torch.tensor([[0.9, 0.1, 0.0], [0.1, 0.8, 0.1], [0.2, 0.2, 0.6]])
20 |
21 |
22 | @pytest.fixture()
23 | def adv_logits():
24 | return torch.tensor([[0.1, 0.8, 0.1], [0.2, 0.6, 0.2], [0.9, 0.1, 0.0]])
25 |
26 |
27 | def test_update(metric, labels, clean_logits, adv_logits):
28 | metric.update(labels, clean_logits, adv_logits)
29 |
30 | assert metric.all_count.item() == 3
31 | assert metric.cln_count.item() == 3 # all clean samples are correctly classified
32 | assert metric.adv_count.item() == 1 # only the 2nd sample is correctly classified
33 |
34 |
35 | def test_compute(metric, labels, clean_logits, adv_logits):
36 | metric.update(labels, clean_logits, adv_logits)
37 | cln_acc, adv_acc, fr = metric.compute()
38 |
39 | assert cln_acc.item() == pytest.approx(3 / 3)
40 | assert adv_acc.item() == pytest.approx(1 / 3)
41 | assert fr.item() == pytest.approx((3 - 1) / 3)
42 |
43 |
44 | def test_reset(metric):
45 | metric.reset()
46 | assert metric.all_count.item() == 0
47 | assert metric.cln_count.item() == 0
48 | assert metric.adv_count.item() == 0
49 |
--------------------------------------------------------------------------------
/tests/test_eval_save_image.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | import torch
4 | from PIL import Image
5 |
6 | from torchattack.evaluate import save_image_batch
7 |
8 |
9 | @pytest.fixture()
10 | def images_and_names():
11 | imgs = torch.rand(3, 3, 224, 224)
12 | img_names = ['nggyu', 'nglyd', 'nglyu']
13 | return imgs, img_names
14 |
15 |
16 | def test_save_image_batch_png_lossless(images_and_names, tmp_path):
17 | imgs, img_names = images_and_names
18 | save_image_batch(imgs, tmp_path, img_names, extension='png')
19 |
20 | for img, name in zip(imgs, img_names):
21 | saved_img = Image.open(tmp_path / f'{name}.png').convert('RGB')
22 | saved_img = np.array(saved_img, dtype=np.uint8) / 255.0
23 | saved_img = torch.from_numpy(saved_img).permute(2, 0, 1).contiguous().float()
24 |
25 | assert saved_img.size() == img.size()
26 | assert torch.allclose(img, saved_img, atol=4e-3) # eps = 1/255
27 |
28 |
29 | def test_save_image_batch_jpg_lossless(images_and_names, tmp_path):
30 | imgs, img_names = images_and_names
31 | save_image_batch(imgs, tmp_path, img_names, extension='jpeg')
32 |
33 | for img, name in zip(imgs, img_names):
34 | saved_img = Image.open(tmp_path / f'{name}.jpeg').convert('RGB')
35 | saved_img = np.array(saved_img, dtype=np.uint8) / 255.0
36 | saved_img = torch.from_numpy(saved_img).permute(2, 0, 1).contiguous().float()
37 |
38 | assert saved_img.size() == img.size()
39 | assert torch.allclose(img, saved_img, atol=8e-3) # eps = 2/255
40 |
--------------------------------------------------------------------------------
/tests/test_external_attack_register.py:
--------------------------------------------------------------------------------
1 | from torchattack import ATTACK_REGISTRY, Attack, create_attack, register_attack
2 |
3 |
4 | @register_attack()
5 | class ExternalNewAttack(Attack):
6 | def __init__(self, model, normalize, device):
7 | super().__init__(model, normalize, device)
8 |
9 | def forward(self, x):
10 | return x
11 |
12 |
13 | def test_external_attack_registered():
14 | assert 'ExternalNewAttack' in ATTACK_REGISTRY
15 | assert issubclass(ATTACK_REGISTRY['ExternalNewAttack'], Attack)
16 | assert ATTACK_REGISTRY['ExternalNewAttack'].attack_name == 'ExternalNewAttack'
17 | assert ATTACK_REGISTRY['ExternalNewAttack'].is_category('COMMON')
18 |
19 |
20 | def test_external_attack_can_be_created():
21 | ea = create_attack('ExternalNewAttack', model=None, normalize=None, device=None)
22 | assert isinstance(ea, ExternalNewAttack)
23 |
--------------------------------------------------------------------------------
/torchattack/__init__.py:
--------------------------------------------------------------------------------
1 | from torchattack.admix import Admix
2 | from torchattack.att import ATT
3 | from torchattack.attack import ATTACK_REGISTRY, Attack, register_attack
4 | from torchattack.attack_model import AttackModel
5 | from torchattack.bfa import BFA
6 | from torchattack.bia import BIA
7 | from torchattack.bpa import BPA
8 | from torchattack.bsr import BSR
9 | from torchattack.cda import CDA
10 | from torchattack.create_attack import create_attack
11 | from torchattack.decowa import DeCoWA
12 | from torchattack.deepfool import DeepFool
13 | from torchattack.difgsm import DIFGSM
14 | from torchattack.dr import DR
15 | from torchattack.fda import FDA
16 | from torchattack.fgsm import FGSM
17 | from torchattack.fia import FIA
18 | from torchattack.gama import GAMA
19 | from torchattack.geoda import GeoDA
20 | from torchattack.gra import GRA
21 | from torchattack.ilpd import ILPD
22 | from torchattack.l2t import L2T
23 | from torchattack.ltp import LTP
24 | from torchattack.mifgsm import MIFGSM
25 | from torchattack.mig import MIG
26 | from torchattack.mumodig import MuMoDIG
27 | from torchattack.naa import NAA
28 | from torchattack.nifgsm import NIFGSM
29 | from torchattack.pgd import IFGSM, PGD
30 | from torchattack.pgdl2 import PGDL2
31 | from torchattack.pna_patchout import PNAPatchOut
32 | from torchattack.sgm import SGM
33 | from torchattack.sinifgsm import SINIFGSM
34 | from torchattack.ssa import SSA
35 | from torchattack.ssp import SSP
36 | from torchattack.tgr import TGR
37 | from torchattack.tifgsm import TIFGSM
38 | from torchattack.vdc import VDC
39 | from torchattack.vmifgsm import VMIFGSM
40 | from torchattack.vnifgsm import VNIFGSM
41 |
42 | __version__ = '1.6.0'
43 |
44 | __all__ = [
45 | # Helper functions
46 | 'create_attack',
47 | 'register_attack',
48 | # Attack registry and base Attack class
49 | 'Attack',
50 | 'ATTACK_REGISTRY',
51 | # Optional but recommended model wrapper
52 | 'AttackModel',
53 | # All supported attacks
54 | 'Admix',
55 | 'ATT',
56 | 'BFA',
57 | 'BIA',
58 | 'BPA',
59 | 'BSR',
60 | 'CDA',
61 | 'DeCoWA',
62 | 'DeepFool',
63 | 'DIFGSM',
64 | 'DR',
65 | 'FDA',
66 | 'FGSM',
67 | 'FIA',
68 | 'GAMA',
69 | 'GeoDA',
70 | 'GRA',
71 | 'IFGSM',
72 | 'ILPD',
73 | 'L2T',
74 | 'LTP',
75 | 'MIFGSM',
76 | 'MIG',
77 | 'MuMoDIG',
78 | 'NAA',
79 | 'NIFGSM',
80 | 'PGD',
81 | 'PGDL2',
82 | 'PNAPatchOut',
83 | 'SGM',
84 | 'SINIFGSM',
85 | 'SSA',
86 | 'SSP',
87 | 'TGR',
88 | 'TIFGSM',
89 | 'VDC',
90 | 'VMIFGSM',
91 | 'VNIFGSM',
92 | ]
93 |
--------------------------------------------------------------------------------
/torchattack/_rgetattr.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Any
3 |
4 |
5 | def rgetattr(obj: Any, attr: str, *args: Any) -> Any:
6 | """Recursively gets an attribute from an object.
7 |
8 | https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-subobjects-chained-properties/31174427#31174427
9 |
10 | Args:
11 | obj: The object to retrieve the attribute from.
12 | attr: The attribute to retrieve. Can be a nested attribute separated by dots.
13 | *args: Optional default values to return if the attribute is not found.
14 |
15 | Returns:
16 | The value of the attribute if found, otherwise the default value(s) specified by *args.
17 | """
18 |
19 | def _getattr(obj: Any, attr: str) -> Any:
20 | return getattr(obj, attr, *args)
21 |
22 | return functools.reduce(_getattr, [obj] + attr.split('.'))
23 |
--------------------------------------------------------------------------------
/torchattack/admix.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class Admix(Attack):
12 | """The Admix attack.
13 |
14 | > From the paper: [Admix: Enhancing the Transferability of Adversarial
15 | Attacks](https://arxiv.org/abs/2102.00436).
16 |
17 | Args:
18 | model: The model to attack.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | eps: The maximum perturbation. Defaults to 8/255.
22 | steps: Number of steps. Defaults to 10.
23 | alpha: Step size, `eps / steps` if None. Defaults to None.
24 | decay: Decay factor for the momentum term. Defaults to 1.0.
25 | portion: Portion for the mixed image. Defaults to 0.2.
26 | size: Number of randomly sampled images. Defaults to 3.
27 | num_classes: Number of classes of the dataset used. Defaults to 1000.
28 | clip_min: Minimum value for clipping. Defaults to 0.0.
29 | clip_max: Maximum value for clipping. Defaults to 1.0.
30 | targeted: Targeted attack if True. Defaults to False.
31 | """
32 |
33 | def __init__(
34 | self,
35 | model: nn.Module | AttackModel,
36 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
37 | device: torch.device | None = None,
38 | eps: float = 8 / 255,
39 | steps: int = 10,
40 | alpha: float | None = None,
41 | decay: float = 1.0,
42 | portion: float = 0.2,
43 | size: int = 3,
44 | num_classes: int = 1000,
45 | clip_min: float = 0.0,
46 | clip_max: float = 1.0,
47 | targeted: bool = False,
48 | ) -> None:
49 | super().__init__(model, normalize, device)
50 |
51 | self.eps = eps
52 | self.steps = steps
53 | self.alpha = alpha
54 | self.decay = decay
55 | self.portion = portion
56 | self.size = size
57 | self.num_classes = num_classes
58 | self.clip_min = clip_min
59 | self.clip_max = clip_max
60 | self.targeted = targeted
61 | self.lossfn = nn.CrossEntropyLoss()
62 |
63 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
64 | """Perform Admix on a batch of images.
65 |
66 | Args:
67 | x: A batch of images. Shape: (N, C, H, W).
68 | y: A batch of labels. Shape: (N).
69 |
70 | Returns:
71 | The perturbed images if successful. Shape: (N, C, H, W).
72 | """
73 |
74 | g = torch.zeros_like(x)
75 | x_adv = x.clone().detach()
76 |
77 | scales = [1, 1 / 2, 1 / 4, 1 / 8, 1 / 16]
78 |
79 | # If alpha is not given, set to eps / steps
80 | if self.alpha is None:
81 | self.alpha = self.eps / self.steps
82 |
83 | # Admix + MI-FGSM
84 | for _ in range(self.steps):
85 | x_adv.requires_grad_(True)
86 |
87 | # Add delta to original image then admix
88 | x_admix = self.admix(x_adv)
89 | x_admixs = torch.cat([x_admix * scale for scale in scales])
90 |
91 | # Compute loss
92 | outs = self.model(self.normalize(x_admixs))
93 |
94 | # One-hot encode labels for all admixed images
95 | one_hot = nn.functional.one_hot(y, self.num_classes)
96 | one_hot = torch.cat([one_hot] * 5 * self.size).float()
97 |
98 | loss = self.lossfn(outs, one_hot)
99 |
100 | if self.targeted:
101 | loss = -loss
102 |
103 | # Gradients
104 | grad = torch.autograd.grad(loss, x_admixs)[0]
105 |
106 | # Split gradients and compute mean
107 | split_grads = torch.tensor_split(grad, 5, dim=0)
108 | grads = [g * s for g, s in zip(split_grads, scales, strict=True)]
109 | grad = torch.mean(torch.stack(grads), dim=0)
110 |
111 | # Gather gradients
112 | split_grads = torch.tensor_split(grad, self.size)
113 | grad = torch.sum(torch.stack(split_grads), dim=0)
114 |
115 | # Apply momentum term
116 | g = self.decay * g + grad / torch.mean(
117 | torch.abs(grad), dim=(1, 2, 3), keepdim=True
118 | )
119 |
120 | # Update perturbed image
121 | x_adv = x_adv.detach() + self.alpha * g.sign()
122 | x_adv = x + torch.clamp(x_adv - x, -self.eps, self.eps)
123 | x_adv = torch.clamp(x_adv, self.clip_min, self.clip_max)
124 |
125 | return x_adv
126 |
127 | def admix(self, x: torch.Tensor) -> torch.Tensor:
128 | def x_admix(x: torch.Tensor) -> torch.Tensor:
129 | return x + self.portion * x[torch.randperm(x.shape[0])]
130 |
131 | return torch.cat([(x_admix(x)) for _ in range(self.size)])
132 |
133 |
134 | if __name__ == '__main__':
135 | from torchattack.evaluate import run_attack
136 |
137 | run_attack(Admix, {'eps': 8 / 255, 'steps': 10})
138 |
--------------------------------------------------------------------------------
/torchattack/attack.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from enum import Enum
3 | from typing import Any, Callable, Type, Union
4 |
5 | import torch
6 | import torch.nn as nn
7 |
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | class AttackCategory(Enum):
12 | COMMON = 'COMMON' # Common attacks that should work on any model
13 | GRADIENT_VIT = 'GRADIENT_VIT' # Gradient-based attacks that only work on ViTs
14 | GENERATIVE = 'GENERATIVE' # Generative adversarial attacks
15 | NON_EPS = 'NON_EPS' # Attacks that do not accept epsilon as a parameter
16 |
17 | @classmethod
18 | def verify(cls, obj: Union[str, 'AttackCategory']) -> 'AttackCategory':
19 | if obj is not None:
20 | if type(obj) is str:
21 | obj = cls[obj.replace(cls.__name__ + '.', '')]
22 | elif not isinstance(obj, cls):
23 | raise TypeError(
24 | f'Invalid AttackCategory class provided; expected {cls.__name__} '
25 | f'but received {obj.__class__.__name__}.'
26 | )
27 | return obj
28 |
29 |
30 | ATTACK_REGISTRY: dict[str, Type['Attack']] = {}
31 |
32 |
33 | def register_attack(
34 | name: str | None = None, category: str | AttackCategory = AttackCategory.COMMON
35 | ) -> Callable[[Type['Attack']], Type['Attack']]:
36 | """Decorator to register an attack class in the attack registry."""
37 |
38 | def wrapper(attack_cls: Type['Attack']) -> Type['Attack']:
39 | key = name if name else attack_cls.__name__
40 | if key in ATTACK_REGISTRY:
41 | return ATTACK_REGISTRY[key]
42 | attack_cls.attack_name = key
43 | attack_cls.attack_category = AttackCategory.verify(category)
44 | ATTACK_REGISTRY[key] = attack_cls
45 | return attack_cls
46 |
47 | return wrapper
48 |
49 |
50 | class Attack(ABC):
51 | """The base class for all attacks."""
52 |
53 | attack_name: str
54 | attack_category: AttackCategory
55 |
56 | def __init__(
57 | self,
58 | model: nn.Module | AttackModel | None,
59 | normalize: Callable[[torch.Tensor], torch.Tensor] | None,
60 | device: torch.device | None,
61 | ) -> None:
62 | super().__init__()
63 |
64 | self.model = (
65 | # If model is an AttackModel, use the model attribute
66 | model.model
67 | if isinstance(model, AttackModel)
68 | # If model is a nn.Module, use the model itself
69 | else model
70 | if model is not None
71 | # Otherwise, use an empty nn.Sequential acting as a dummy model
72 | else nn.Sequential()
73 | )
74 |
75 | # Set device to given or defaults to cuda if available
76 | is_cuda = torch.cuda.is_available()
77 | self.device = device if device else torch.device('cuda' if is_cuda else 'cpu')
78 |
79 | # If normalize is None, use identity function
80 | self.normalize = normalize if normalize else lambda x: x
81 |
82 | @classmethod
83 | def is_category(cls, category: str | AttackCategory) -> bool:
84 | """Check if the attack class belongs to the given category."""
85 | return cls.attack_category is AttackCategory.verify(category)
86 |
87 | @abstractmethod
88 | def forward(self, *args: Any, **kwds: Any) -> Any:
89 | pass
90 |
91 | def __call__(self, *args: Any, **kwds: Any) -> Any:
92 | return self.forward(*args, **kwds)
93 |
94 | def __repr__(self) -> str:
95 | name = self.__class__.__name__
96 |
97 | def repr_map(k: str, v: Any) -> str:
98 | if isinstance(v, float):
99 | return f'{k}={v:.3f}'
100 | if k in [
101 | 'model',
102 | 'normalize',
103 | 'feature_module',
104 | 'feature_maps',
105 | 'features',
106 | 'hooks',
107 | 'generator',
108 | ]:
109 | return f'{k}={v.__class__.__name__}'
110 | if isinstance(v, torch.Tensor):
111 | return f'{k}={v.shape}'
112 | return f'{k}={v}'
113 |
114 | args = ', '.join(repr_map(k, v) for k, v in self.__dict__.items())
115 | return f'{name}({args})'
116 |
117 | def __eq__(self, other: Any) -> bool:
118 | if not isinstance(other, Attack):
119 | return False
120 |
121 | eq_name_attrs = [
122 | 'model',
123 | 'normalize',
124 | 'lossfn',
125 | 'feature_module', # FIA, ILPD, NAA
126 | 'feature_maps',
127 | 'features',
128 | 'hooks', # PNAPatchOut, TGR, VDC
129 | 'sub_basis', # GeoDA
130 | 'generator', # BIA, CDA, LTP
131 | ]
132 | for attr in eq_name_attrs:
133 | if not (hasattr(self, attr) and hasattr(other, attr)):
134 | continue
135 | if (
136 | getattr(self, attr).__class__.__name__
137 | != getattr(other, attr).__class__.__name__
138 | ):
139 | return False
140 |
141 | for attr in self.__dict__:
142 | if attr in eq_name_attrs:
143 | continue
144 | self_val = getattr(self, attr)
145 | other_val = getattr(other, attr)
146 |
147 | if isinstance(self_val, torch.Tensor):
148 | if not isinstance(other_val, torch.Tensor):
149 | return False
150 | if not torch.equal(self_val, other_val):
151 | return False
152 | elif self_val != other_val:
153 | return False
154 |
155 | return True
156 |
--------------------------------------------------------------------------------
/torchattack/bfa.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 | import torch.nn.functional as f
6 |
7 | from torchattack._rgetattr import rgetattr
8 | from torchattack.attack import Attack, register_attack
9 | from torchattack.attack_model import AttackModel
10 |
11 |
12 | @register_attack()
13 | class BFA(Attack):
14 | """The BFA (Black-box Feature) attack.
15 |
16 | > From the paper: [Improving the transferability of adversarial examples through
17 | black-box feature attacks](https://www.sciencedirect.com/science/article/pii/S0925231224006349).
18 |
19 | Args:
20 | model: The model to attack.
21 | normalize: A transform to normalize images.
22 | device: Device to use for tensors. Defaults to cuda if available.
23 | eps: The maximum perturbation. Defaults to 8/255.
24 | steps: Number of steps. Defaults to 10.
25 | alpha: Step size, `eps / steps` if None. Defaults to None.
26 | decay: Decay factor for the momentum term. Defaults to 1.0.
27 | eta: The mask gradient's perturbation size. Defaults to 28.
28 | num_ens: Number of aggregate gradients. Defaults to 30.
29 | feature_layer_cfg: Name of the feature layer to attack. If not provided, tries
30 | to infer from built-in config based on the model name. Defaults to ""
31 | num_classes: Number of classes. Defaults to 1000.
32 | clip_min: Minimum value for clipping. Defaults to 0.0.
33 | clip_max: Maximum value for clipping. Defaults to 1.0.
34 | targeted: Targeted attack if True. Defaults to False.
35 | """
36 |
37 | _builtin_cfgs = {
38 | 'inception_v3': 'Mixed_5b',
39 | 'resnet50': 'layer2.3', # (not present in the paper)
40 | 'resnet152': 'layer2.7',
41 | 'vgg16': 'features.15',
42 | 'inception_v4': 'features.9',
43 | 'inception_resnet_v2': 'conv2d_4a',
44 | }
45 |
46 | def __init__(
47 | self,
48 | model: nn.Module | AttackModel,
49 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
50 | device: torch.device | None = None,
51 | eps: float = 8 / 255,
52 | steps: int = 10,
53 | alpha: float | None = None,
54 | decay: float = 1.0,
55 | eta: int = 28,
56 | num_ens: int = 30,
57 | feature_layer_cfg: str = '',
58 | num_classes: int = 1000,
59 | clip_min: float = 0.0,
60 | clip_max: float = 1.0,
61 | targeted: bool = False,
62 | ) -> None:
63 | # If `feature_layer_cfg` is not provided, try to infer used feature layer from
64 | # the `model_name` attribute (automatically attached during instantiation)
65 | if not feature_layer_cfg and isinstance(model, AttackModel):
66 | feature_layer_cfg = self._builtin_cfgs[model.model_name]
67 |
68 | # Delay initialization to avoid overriding the model's `model_name` attribute
69 | super().__init__(model, normalize, device)
70 |
71 | self.eps = eps
72 | self.steps = steps
73 | self.alpha = alpha
74 | self.decay = decay
75 | self.eta = eta
76 | self.num_ens = num_ens
77 | self.num_classes = num_classes
78 | self.clip_min = clip_min
79 | self.clip_max = clip_max
80 | self.targeted = targeted
81 | self.lossfn = nn.CrossEntropyLoss()
82 |
83 | self.feature_maps = torch.empty(0)
84 | self.feature_layer_cfg = feature_layer_cfg
85 |
86 | self._register_hook()
87 |
88 | def _register_hook(self) -> None:
89 | def hook_fn(m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
90 | self.feature_maps = o
91 |
92 | feature_mod = rgetattr(self.model, self.feature_layer_cfg)
93 | feature_mod.register_forward_hook(hook_fn)
94 |
95 | def _get_maskgrad(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
96 | x.requires_grad = True
97 | outs = self.model(self.normalize(x))
98 | loss = self.lossfn(outs, y)
99 |
100 | # Get gradient of the loss w.r.t. the masked image
101 | mg = torch.autograd.grad(loss, x)[0]
102 | mg /= torch.sum(torch.square(mg), dim=(1, 2, 3), keepdim=True).sqrt()
103 | return mg.detach()
104 |
105 | def _get_aggregate_grad(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
106 | _ = self.model(self.normalize(x))
107 | x_masked = x.clone().detach()
108 | aggregate_grad = torch.zeros_like(self.feature_maps)
109 |
110 | # Targets mask
111 | t = f.one_hot(y.type(torch.int64), self.num_classes).float().to(self.device)
112 |
113 | # Get aggregate gradients over ensembles
114 | for _ in range(self.num_ens):
115 | g = self._get_maskgrad(x_masked, y)
116 |
117 | # Get fitted image
118 | x_masked = x + self.eta * g
119 |
120 | # Get mask gradient
121 | outs = self.model(self.normalize(x_masked))
122 | loss = torch.sum(outs * t, dim=1).mean()
123 | aggregate_grad += torch.autograd.grad(loss, self.feature_maps)[0]
124 |
125 | # Compute average gradient
126 | aggregate_grad /= -torch.sqrt(
127 | torch.sum(torch.square(aggregate_grad), dim=(1, 2, 3), keepdim=True)
128 | )
129 | return aggregate_grad
130 |
131 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
132 | """Perform BFA on a batch of images.
133 |
134 | Args:
135 | x: A batch of images. Shape: (N, C, H, W).
136 | y: A batch of labels. Shape: (N).
137 |
138 | Returns:
139 | The perturbed images if successful. Shape: (N, C, H, W).
140 | """
141 |
142 | g = torch.zeros_like(x)
143 | delta = torch.zeros_like(x, requires_grad=True)
144 |
145 | # If alpha is not given, set to eps / steps
146 | if self.alpha is None:
147 | self.alpha = self.eps / self.steps
148 |
149 | aggregate_grad = self._get_aggregate_grad(x, y)
150 |
151 | # Perform BFA
152 | for _ in range(self.steps):
153 | # Compute loss
154 | _ = self.model(self.normalize(x + delta))
155 | loss = torch.sum(aggregate_grad * self.feature_maps, dim=(1, 2, 3)).mean()
156 |
157 | if self.targeted:
158 | loss = -loss
159 |
160 | # Compute gradient
161 | loss.backward()
162 |
163 | if delta.grad is None:
164 | continue
165 |
166 | # Apply momentum term
167 | g = self.decay * g + delta.grad / torch.mean(
168 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
169 | )
170 |
171 | # Update delta
172 | delta.data = delta.data + self.alpha * g.sign()
173 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
174 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
175 |
176 | # Zero out gradient
177 | delta.grad.detach_()
178 | delta.grad.zero_()
179 |
180 | return x + delta
181 |
182 |
183 | if __name__ == '__main__':
184 | from torchattack.evaluate import run_attack
185 |
186 | run_attack(
187 | attack=BFA,
188 | attack_args={'eps': 8 / 255, 'steps': 10},
189 | model_name='resnet152',
190 | victim_model_names=['resnet50', 'vgg13', 'densenet121'],
191 | )
192 |
--------------------------------------------------------------------------------
/torchattack/bia.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import torch
4 |
5 | from torchattack.attack import Attack, register_attack
6 | from torchattack.generative._weights import GeneratorWeights, GeneratorWeightsEnum
7 | from torchattack.generative.resnet_generator import ResNetGenerator
8 |
9 |
10 | class BIAWeights(GeneratorWeightsEnum):
11 | """
12 | Pretrained weights for the BIA attack generator are sourced from [the original
13 | implementation of the BIA
14 | attack](https://github.com/Alibaba-AAIG/Beyond-ImageNet-Attack#pretrained-generators).
15 | RN stands for Random Normalization, and DA stands for Domain-Agnostic.
16 | """
17 |
18 | RESNET152 = GeneratorWeights(
19 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_resnet152_0.pth',
20 | )
21 | RESNET152_RN = GeneratorWeights(
22 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_resnet152_rn_0.pth',
23 | )
24 | RESNET152_DA = GeneratorWeights(
25 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_resnet152_da_0.pth',
26 | )
27 | DENSENET169 = GeneratorWeights(
28 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_densenet169_0.pth',
29 | )
30 | DENSENET169_RN = GeneratorWeights(
31 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_densenet169_rn_0.pth',
32 | )
33 | DENSENET169_DA = GeneratorWeights(
34 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_densenet169_da_0.pth',
35 | )
36 | VGG16 = GeneratorWeights(
37 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_vgg16_0.pth',
38 | )
39 | VGG16_RN = GeneratorWeights(
40 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_vgg16_rn_0.pth',
41 | )
42 | VGG16_DA = GeneratorWeights(
43 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_vgg16_da_0.pth',
44 | )
45 | VGG19 = GeneratorWeights(
46 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_vgg19_0.pth',
47 | )
48 | VGG19_RN = GeneratorWeights(
49 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_vgg19_rn_0.pth',
50 | )
51 | VGG19_DA = GeneratorWeights(
52 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/bia_vgg19_da_0.pth',
53 | )
54 | DEFAULT = RESNET152_DA
55 |
56 |
57 | @register_attack(category='GENERATIVE')
58 | class BIA(Attack):
59 | """Beyond ImageNet Attack (BIA).
60 |
61 | > From the paper: [Beyond ImageNet Attack: Towards Crafting Adversarial Examples for
62 | Black-box Domains](https://arxiv.org/abs/2201.11528).
63 |
64 | Args:
65 | device: Device to use for tensors. Defaults to cuda if available.
66 | eps: The maximum perturbation. Defaults to 10/255.
67 | weights: Pretrained weights for the generator. Either import and use the enum,
68 | or use its name. Defaults to BIAWeights.DEFAULT.
69 | checkpoint_path: Path to a custom checkpoint. Defaults to None.
70 | clip_min: Minimum value for clipping. Defaults to 0.0.
71 | clip_max: Maximum value for clipping. Defaults to 1.0.
72 | """
73 |
74 | def __init__(
75 | self,
76 | device: torch.device | None = None,
77 | eps: float = 10 / 255,
78 | weights: BIAWeights | str | None = BIAWeights.DEFAULT,
79 | checkpoint_path: str | None = None,
80 | clip_min: float = 0.0,
81 | clip_max: float = 1.0,
82 | ) -> None:
83 | # Generative attacks do not require specifying model and normalize
84 | super().__init__(model=None, normalize=None, device=device)
85 |
86 | self.eps = eps
87 | self.checkpoint_path = checkpoint_path
88 | self.clip_min = clip_min
89 | self.clip_max = clip_max
90 |
91 | # Initialize the generator and its weights
92 | self.generator = ResNetGenerator()
93 |
94 | # Prioritize checkpoint path over provided weights enum
95 | if self.checkpoint_path is not None:
96 | self.generator.load_state_dict(
97 | torch.load(self.checkpoint_path, weights_only=True)
98 | )
99 | else:
100 | # Verify and load weights from enum if checkpoint path is not provided
101 | self.weights = BIAWeights.verify(weights)
102 | if self.weights is not None:
103 | self.generator.load_state_dict(
104 | self.weights.get_state_dict(check_hash=True)
105 | )
106 |
107 | self.generator.eval().to(self.device)
108 |
109 | def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> torch.Tensor:
110 | """Perform BIA on a batch of images.
111 |
112 | Args:
113 | x: A batch of images. Shape: (N, C, H, W).
114 |
115 | Returns:
116 | The perturbed images if successful. Shape: (N, C, H, W).
117 | """
118 |
119 | x_unrestricted = self.generator(x)
120 | delta = torch.clamp(x_unrestricted - x, -self.eps, self.eps)
121 | x_adv = torch.clamp(x + delta, self.clip_min, self.clip_max)
122 | return x_adv
123 |
124 |
125 | if __name__ == '__main__':
126 | from torchattack.evaluate import run_attack
127 |
128 | run_attack(
129 | attack=BIA,
130 | attack_args={'eps': 10 / 255, 'weights': 'VGG16_DA'},
131 | model_name='vgg16',
132 | victim_model_names=['resnet152'],
133 | )
134 |
--------------------------------------------------------------------------------
/torchattack/cda.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import torch
4 |
5 | from torchattack.attack import Attack, register_attack
6 | from torchattack.generative._weights import GeneratorWeights, GeneratorWeightsEnum
7 | from torchattack.generative.resnet_generator import ResNetGenerator
8 |
9 |
10 | class CDAWeights(GeneratorWeightsEnum):
11 | """
12 | Pretrained weights for the CDA attack generator are sourced from [the original
13 | implementation of the CDA
14 | attack](https://github.com/Muzammal-Naseer/CDA#Pretrained-Generators).
15 | """
16 |
17 | RESNET152_IMAGENET = GeneratorWeights(
18 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/cda_res152_imagenet_0_rl.pth',
19 | )
20 | INCEPTION_V3_IMAGENET = GeneratorWeights(
21 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/cda_incv3_imagenet_0_rl.pth',
22 | inception=True,
23 | )
24 | VGG16_IMAGENET = GeneratorWeights(
25 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/cda_vgg16_imagenet_0_rl.pth',
26 | )
27 | VGG19_IMAGENET = GeneratorWeights(
28 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/cda_vgg19_imagenet_0_rl.pth',
29 | )
30 | DEFAULT = RESNET152_IMAGENET
31 |
32 |
33 | @register_attack(category='GENERATIVE')
34 | class CDA(Attack):
35 | """Cross-domain Attack (CDA).
36 |
37 | > From the paper: [Cross-Domain Transferability of Adversarial Perturbations](https://arxiv.org/abs/1905.11736).
38 |
39 | Args:
40 | device: Device to use for tensors. Defaults to cuda if available.
41 | eps: The maximum perturbation. Defaults to 10/255.
42 | weights: Pretrained weights for the generator. Either import and use the enum,
43 | or use its name. Defaults to CDAWeights.DEFAULT.
44 | checkpoint_path: Path to a custom checkpoint. Defaults to None.
45 | inception: Whether to use inception (crop layer 3x300x300 to 3x299x299). Defaults to None.
46 | clip_min: Minimum value for clipping. Defaults to 0.0.
47 | clip_max: Maximum value for clipping. Defaults to 1.0.
48 | """
49 |
50 | def __init__(
51 | self,
52 | device: torch.device | None = None,
53 | eps: float = 10 / 255,
54 | weights: CDAWeights | str | None = CDAWeights.DEFAULT,
55 | checkpoint_path: str | None = None,
56 | inception: bool | None = None,
57 | clip_min: float = 0.0,
58 | clip_max: float = 1.0,
59 | ) -> None:
60 | # Generative attacks do not require specifying model and normalize
61 | super().__init__(model=None, normalize=None, device=device)
62 |
63 | self.eps = eps
64 | self.checkpoint_path = checkpoint_path
65 | self.clip_min = clip_min
66 | self.clip_max = clip_max
67 |
68 | self.weights = CDAWeights.verify(weights)
69 |
70 | # Whether is inception or not (crop layer 3x300x300 to 3x299x299)
71 | is_inception = (
72 | inception
73 | if inception is not None
74 | else (self.weights.inception if self.weights is not None else False)
75 | )
76 |
77 | # Initialize the generator
78 | self.generator = ResNetGenerator(inception=is_inception)
79 |
80 | # Load the weights
81 | if self.checkpoint_path is not None:
82 | self.generator.load_state_dict(
83 | torch.load(self.checkpoint_path, weights_only=True)
84 | )
85 | elif self.weights is not None:
86 | self.generator.load_state_dict(self.weights.get_state_dict(check_hash=True))
87 |
88 | self.generator.eval().to(self.device)
89 |
90 | def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> torch.Tensor:
91 | """Perform CDA on a batch of images.
92 |
93 | Args:
94 | x: A batch of images. Shape: (N, C, H, W).
95 |
96 | Returns:
97 | The perturbed images if successful. Shape: (N, C, H, W).
98 | """
99 |
100 | x_unrestricted = self.generator(x)
101 | delta = torch.clamp(x_unrestricted - x, -self.eps, self.eps)
102 | x_adv = torch.clamp(x + delta, self.clip_min, self.clip_max)
103 | return x_adv
104 |
105 |
106 | if __name__ == '__main__':
107 | from torchattack.evaluate import run_attack
108 |
109 | run_attack(
110 | attack=CDA,
111 | attack_args={'eps': 10 / 255, 'weights': 'INCEPTION_V3_IMAGENET'},
112 | model_name='inception_v3',
113 | victim_model_names=['resnet152'],
114 | )
115 |
--------------------------------------------------------------------------------
/torchattack/create_attack.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, Optional, Type, Union
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import ATTACK_REGISTRY, Attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | def create_attack(
11 | attack: Union[Type['Attack'], str],
12 | model: Optional[Union[nn.Module, AttackModel]] = None,
13 | normalize: Optional[Callable[[torch.Tensor], torch.Tensor]] = None,
14 | device: Optional[torch.device] = None,
15 | *,
16 | eps: Optional[float] = None,
17 | **kwargs: Any,
18 | ) -> Attack:
19 | """Create a torchattack instance based on the provided attack name and config.
20 |
21 | Args:
22 | attack: The attack to create, either by name or class instance.
23 | model: The model to be attacked. Can be an instance of nn.Module or AttackModel. Defaults to None.
24 | normalize: The normalization function specific to the model. Defaults to None.
25 | device: The device on which the attack will be executed. Defaults to None.
26 | eps: The epsilon value for the attack. Defaults to None.
27 | kwargs: Additional config parameters for the attack. Defaults to None.
28 |
29 | Returns:
30 | An instance of the specified attack.
31 |
32 | Raises:
33 | ValueError: If the specified attack name is not supported within torchattack.
34 | """
35 |
36 | # Determine attack name and check if it is supported
37 | attack_name = attack if isinstance(attack, str) else attack.attack_name
38 | if attack_name not in ATTACK_REGISTRY:
39 | raise ValueError(f"Attack '{attack_name}' is not supported within torchattack.")
40 | # Get attack class if passed as a string
41 | attack_cls = ATTACK_REGISTRY[attack] if isinstance(attack, str) else attack
42 |
43 | # `eps` is explicitly set as it is such a common argument
44 | # All other arguments should be passed as keyword arguments
45 | if eps is not None:
46 | kwargs['eps'] = eps
47 |
48 | # Special handling for generative attacks
49 | attacker: Attack = (
50 | attack_cls(device=device, **kwargs)
51 | if attack_cls.is_category('GENERATIVE')
52 | else attack_cls(model=model, normalize=normalize, device=device, **kwargs)
53 | )
54 | return attacker
55 |
56 |
57 | if __name__ == '__main__':
58 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
59 | model = AttackModel.from_pretrained('resnet18').to(device)
60 | normalize = model.normalize
61 | print(create_attack('MIFGSM', model, normalize, device, eps=0.1, steps=40))
62 | print(create_attack('CDA', device=device, weights='VGG19_IMAGENET'))
63 | print(create_attack('DeepFool', model, normalize, device, num_classes=20))
64 |
--------------------------------------------------------------------------------
/torchattack/deepfool.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack(category='NON_EPS')
11 | class DeepFool(Attack):
12 | """The DeepFool attack.
13 |
14 | > From the paper: [DeepFool: A Simple and Accurate Method to Fool Deep Neural
15 | Networks](https://arxiv.org/abs/1511.04599).
16 |
17 | Args:
18 | model: The model to attack.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | steps: Number of steps. Defaults to 100.
22 | overshoot: Overshoot parameter for noise control. Defaults to 0.02.
23 | num_classes: Number of classes to consider. Defaults to 10.
24 | clip_min: Minimum value for clipping. Defaults to 0.0.
25 | clip_max: Maximum value for clipping. Defaults to 1.0.
26 | """
27 |
28 | def __init__(
29 | self,
30 | model: nn.Module | AttackModel,
31 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
32 | device: torch.device | None = None,
33 | steps: int = 100,
34 | overshoot: float = 0.02,
35 | num_classes: int = 10,
36 | clip_min: float = 0.0,
37 | clip_max: float = 1.0,
38 | ) -> None:
39 | super().__init__(model, normalize, device)
40 |
41 | self.steps = steps
42 | self.overshoot = overshoot
43 | self.num_classes = num_classes
44 | self.clip_min = clip_min
45 | self.clip_max = clip_max
46 |
47 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
48 | """Perform DeepFool on a batch of images.
49 |
50 | Args:
51 | x: A batch of images. Shape: (N, C, H, W).
52 | y: A batch of labels. Shape: (N).
53 |
54 | Returns:
55 | The perturbed images if successful. Shape: (N, C, H, W).
56 | """
57 |
58 | x.requires_grad_()
59 | logits = self.model(self.normalize(x))
60 |
61 | # Get the classes
62 | classes = logits.argsort(axis=-1).flip(-1).detach()
63 | self.num_classes = min(self.num_classes, logits.shape[-1])
64 |
65 | n = len(x)
66 | rows = range(n)
67 |
68 | x0 = x
69 | p_total = torch.zeros_like(x)
70 | for _ in range(self.steps):
71 | # let's first get the logits using k = 1 to see if we are done
72 | diffs = [self._get_grads(x, 1, classes)]
73 |
74 | is_adv = self._is_adv(diffs[0]['logits'], y)
75 | if is_adv.all():
76 | break
77 |
78 | diffs += [
79 | self._get_grads(x, k, classes) for k in range(2, self.num_classes)
80 | ]
81 |
82 | deltas = torch.stack([d['deltas'] for d in diffs], dim=-1)
83 | grads = torch.stack([d['grads'] for d in diffs], dim=1)
84 | assert deltas.shape == (n, self.num_classes - 1)
85 | assert grads.shape == (n, self.num_classes - 1) + x0.shape[1:]
86 |
87 | # calculate the distances
88 | # compute f_k / ||w_k||
89 | distances = self._get_distances(deltas, grads)
90 | assert distances.shape == (n, self.num_classes - 1)
91 |
92 | # determine the best directions
93 | best = distances.argmin(1) # compute \hat{l}
94 | distances = distances[rows, best]
95 | deltas = deltas[rows, best]
96 | grads = grads[rows, best]
97 | assert distances.shape == (n,)
98 | assert deltas.shape == (n,)
99 | assert grads.shape == x0.shape
100 |
101 | # apply perturbation
102 | distances = distances + 1e-4 # for numerical stability
103 | p_step = self._get_perturbations(distances, grads) # =r_i
104 | assert p_step.shape == x0.shape
105 |
106 | p_total += p_step
107 |
108 | # don't do anything for those that are already adversarial
109 | x = torch.where(
110 | self._atleast_kd(is_adv, x.ndim),
111 | x,
112 | x0 + (1.0 + self.overshoot) * p_total,
113 | ) # =x_{i+1}
114 |
115 | x = (
116 | torch.clamp(x, self.clip_min, self.clip_max)
117 | .clone()
118 | .detach()
119 | .requires_grad_()
120 | )
121 |
122 | return x.detach()
123 |
124 | def _is_adv(self, logits: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
125 | # criterion
126 | y_hat = logits.argmax(-1)
127 | is_adv = y_hat != y
128 | return is_adv
129 |
130 | def _get_deltas_logits(
131 | self, x: torch.Tensor, k: int, classes: torch.Tensor
132 | ) -> dict[str, torch.Tensor]:
133 | # definition of loss_fn
134 | n = len(classes)
135 | rows = range(n)
136 | i0 = classes[:, 0]
137 |
138 | logits = self.model(self.normalize(x))
139 | ik = classes[:, k]
140 | l0 = logits[rows, i0]
141 | lk = logits[rows, ik]
142 | delta_logits = lk - l0
143 |
144 | return {
145 | 'sum_deltas': delta_logits.sum(),
146 | 'deltas': delta_logits,
147 | 'logits': logits,
148 | }
149 |
150 | def _get_grads(
151 | self, x: torch.Tensor, k: int, classes: torch.Tensor
152 | ) -> dict[str, torch.Tensor]:
153 | deltas_logits = self._get_deltas_logits(x, k, classes)
154 | deltas_logits['sum_deltas'].backward()
155 | if x.grad is not None:
156 | deltas_logits['grads'] = x.grad.clone()
157 | x.grad.data.zero_()
158 | return deltas_logits
159 |
160 | def _get_distances(self, deltas: torch.Tensor, grads: torch.Tensor) -> torch.Tensor:
161 | return deltas.abs() / (
162 | grads.flatten(start_dim=2, end_dim=-1).abs().sum(dim=-1) + 1e-8
163 | )
164 |
165 | def _get_perturbations(
166 | self, distances: torch.Tensor, grads: torch.Tensor
167 | ) -> torch.Tensor:
168 | return self._atleast_kd(distances, grads.ndim) * grads.sign()
169 |
170 | def _atleast_kd(self, x: torch.Tensor, k: int) -> torch.Tensor:
171 | shape = x.shape + (1,) * (k - x.ndim)
172 | return x.reshape(shape)
173 |
174 |
175 | if __name__ == '__main__':
176 | from torchattack.evaluate import run_attack
177 |
178 | run_attack(
179 | DeepFool,
180 | attack_args={'steps': 50, 'overshoot': 0.02},
181 | model_name='resnet152',
182 | )
183 |
--------------------------------------------------------------------------------
/torchattack/difgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 | import torch.nn.functional as f
6 |
7 | from torchattack.attack import Attack, register_attack
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | @register_attack()
12 | class DIFGSM(Attack):
13 | """The DI-FGSM (Diverse-input Iterative FGSM) attack.
14 |
15 | > From the paper: [Improving Transferability of Adversarial Examples with Input
16 | Diversity](https://arxiv.org/abs/1803.06978).
17 |
18 | Note:
19 | Key parameters include `resize_rate` and `diversity_prob`, which defines the
20 | scale size of the resized image and the probability of applying input
21 | diversity. The default values are set to 0.9 and 1.0 respectively (implying
22 | that input diversity is always applied).
23 |
24 | Args:
25 | model: The model to attack.
26 | normalize: A transform to normalize images.
27 | device: Device to use for tensors. Defaults to cuda if available.
28 | eps: The maximum perturbation. Defaults to 8/255.
29 | steps: Number of steps. Defaults to 10.
30 | alpha: Step size, `eps / steps` if None. Defaults to None.
31 | decay: Decay factor for the momentum term. Defaults to 1.0.
32 | resize_rate: The resize rate. Defaults to 0.9.
33 | diversity_prob: Applying input diversity with probability. Defaults to 1.0.
34 | clip_min: Minimum value for clipping. Defaults to 0.0.
35 | clip_max: Maximum value for clipping. Defaults to 1.0.
36 | targeted: Targeted attack if True. Defaults to False.
37 | """
38 |
39 | def __init__(
40 | self,
41 | model: nn.Module | AttackModel,
42 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
43 | device: torch.device | None = None,
44 | eps: float = 8 / 255,
45 | steps: int = 10,
46 | alpha: float | None = None,
47 | decay: float = 1.0,
48 | resize_rate: float = 0.9,
49 | diversity_prob: float = 1.0,
50 | clip_min: float = 0.0,
51 | clip_max: float = 1.0,
52 | targeted: bool = False,
53 | ) -> None:
54 | super().__init__(model, normalize, device)
55 |
56 | self.eps = eps
57 | self.steps = steps
58 | self.alpha = alpha
59 | self.decay = decay
60 | self.resize_rate = resize_rate
61 | self.diversity_prob = diversity_prob
62 | self.clip_min = clip_min
63 | self.clip_max = clip_max
64 | self.targeted = targeted
65 | self.lossfn = nn.CrossEntropyLoss()
66 |
67 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
68 | """Perform DI-FGSM on a batch of images.
69 |
70 | Args:
71 | x: A batch of images. Shape: (N, C, H, W).
72 | y: A batch of labels. Shape: (N).
73 |
74 | Returns:
75 | The perturbed images if successful. Shape: (N, C, H, W).
76 | """
77 |
78 | g = torch.zeros_like(x)
79 | delta = torch.zeros_like(x, requires_grad=True)
80 |
81 | # If alpha is not given, set to eps / steps
82 | if self.alpha is None:
83 | self.alpha = self.eps / self.steps
84 |
85 | # Perform DI-FGSM
86 | for _ in range(self.steps):
87 | # Apply input diversity to intermediate images
88 | x_adv = input_diversity(x + delta, self.resize_rate, self.diversity_prob)
89 |
90 | # Compute loss
91 | outs = self.model(self.normalize(x_adv))
92 | loss = self.lossfn(outs, y)
93 |
94 | if self.targeted:
95 | loss = -loss
96 |
97 | # Compute gradient
98 | loss.backward()
99 |
100 | if delta.grad is None:
101 | continue
102 |
103 | # Apply momentum term
104 | g = self.decay * g + delta.grad / torch.mean(
105 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
106 | )
107 |
108 | # Update delta
109 | delta.data = delta.data + self.alpha * g.sign()
110 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
111 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
112 |
113 | # Zero out gradient
114 | delta.grad.detach_()
115 | delta.grad.zero_()
116 |
117 | return x + delta
118 |
119 |
120 | def input_diversity(
121 | x: torch.Tensor, resize_rate: float = 0.9, diversity_prob: float = 0.5
122 | ) -> torch.Tensor:
123 | """Apply input diversity to a batch of images.
124 |
125 | Note:
126 | Adapted from the TensorFlow implementation (cihangxie/DI-2-FGSM):
127 | https://github.com/cihangxie/DI-2-FGSM/blob/10ffd9b9e94585b6a3b9d6858a9a929dc488fc02/attack.py#L153-L164
128 |
129 | Args:
130 | x: A batch of images. Shape: (N, C, H, W).
131 | resize_rate: The resize rate. Defaults to 0.9.
132 | diversity_prob: Applying input diversity with probability. Defaults to 0.5.
133 |
134 | Returns:
135 | The diversified batch of images. Shape: (N, C, H, W).
136 | """
137 |
138 | if torch.rand(1) > diversity_prob:
139 | return x
140 |
141 | img_size = x.shape[-1]
142 | img_resize = int(img_size * resize_rate)
143 |
144 | if resize_rate < 1:
145 | img_size = img_resize
146 | img_resize = x.shape[-1]
147 |
148 | rnd = torch.randint(low=img_size, high=img_resize, size=(1,), dtype=torch.int32)
149 | rescaled = f.interpolate(x, size=[rnd, rnd], mode='nearest')
150 |
151 | h_rem = img_resize - rnd
152 | w_rem = img_resize - rnd
153 |
154 | pad_top = torch.randint(low=0, high=h_rem.item(), size=(1,), dtype=torch.int32)
155 | pad_bottom = h_rem - pad_top
156 | pad_left = torch.randint(low=0, high=w_rem.item(), size=(1,), dtype=torch.int32)
157 | pad_right = w_rem - pad_left
158 |
159 | pad = [pad_left.item(), pad_right.item(), pad_top.item(), pad_bottom.item()]
160 | padded = f.pad(rescaled, pad=pad, mode='constant', value=0)
161 |
162 | return padded
163 |
164 |
165 | if __name__ == '__main__':
166 | from torchattack.evaluate import run_attack
167 |
168 | args = {'eps': 16 / 255, 'steps': 10, 'resize_rate': 0.9, 'diversity_prob': 1.0}
169 | run_attack(DIFGSM, attack_args=args)
170 |
--------------------------------------------------------------------------------
/torchattack/dr.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack._rgetattr import rgetattr
7 | from torchattack.attack import Attack, register_attack
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | @register_attack()
12 | class DR(Attack):
13 | """The DR (Dispersion Reduction) attack.
14 |
15 | > From the paper: [Enhancing Cross-Task Black-Box Transferability of Adversarial
16 | Examples With Dispersion Reduction](https://arxiv.org/abs/1911.11616).
17 |
18 | Args:
19 | model: The model to attack.
20 | normalize: A transform to normalize images.
21 | device: Device to use for tensors. Defaults to cuda if available.
22 | eps: The maximum perturbation. Defaults to 8/255.
23 | steps: Number of steps. Defaults to 100.
24 | alpha: Step size, `eps / steps` if None. Defaults to None.
25 | decay: Decay factor for the momentum term. Defaults to 1.0.
26 | feature_layer_cfg: Module layer name of the model to extract features from and
27 | apply dispersion reduction to. If not provided, tries to infer from built-in
28 | config based on the model name. Defaults to "".
29 | clip_min: Minimum value for clipping. Defaults to 0.0.
30 | clip_max: Maximum value for clipping. Defaults to 1.0.
31 | """
32 |
33 | # Specified in _builtin_models assume models that are loaded from,
34 | # or share the exact structure as, torchvision model variants.
35 | _builtin_cfgs = {
36 | 'vgg16': 'features.14', # conv3-3 for VGG-16
37 | 'resnet50': 'layer2.3.conv3', # conv3-4-3 for ResNet-50 (not present in the paper)
38 | 'resnet152': 'layer2.7.conv3', # conv3-8-3 for ResNet-152
39 | 'inception_v3': 'Mixed_5b', # Mixed_5b (Group A) for Inception-v3
40 | }
41 |
42 | def __init__(
43 | self,
44 | model: nn.Module | AttackModel,
45 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
46 | device: torch.device | None = None,
47 | eps: float = 8 / 255,
48 | steps: int = 100,
49 | alpha: float | None = None,
50 | decay: float = 1.0,
51 | feature_layer_cfg: str = '',
52 | clip_min: float = 0.0,
53 | clip_max: float = 1.0,
54 | ) -> None:
55 | # If `feature_layer_cfg` is not provided, try to infer used feature layer from
56 | # the `model_name` attribute (automatically attached during instantiation)
57 | if not feature_layer_cfg and isinstance(model, AttackModel):
58 | feature_layer_cfg = self._builtin_cfgs[model.model_name]
59 |
60 | # Delay initialization to avoid overriding the model's `model_name` attribute
61 | super().__init__(model, normalize, device)
62 |
63 | self.eps = eps
64 | self.steps = steps
65 | self.alpha = alpha
66 | self.decay = decay
67 | self.clip_min = clip_min
68 | self.clip_max = clip_max
69 |
70 | self.features = torch.empty(0)
71 | self.feature_layer_cfg = feature_layer_cfg
72 |
73 | self._register_model_hooks()
74 |
75 | def _register_model_hooks(self) -> None:
76 | def hook_fn(m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
77 | self.features = o
78 |
79 | feature_mod = rgetattr(self.model, self.feature_layer_cfg)
80 | feature_mod.register_forward_hook(hook_fn)
81 |
82 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
83 | """Perform DR on a batch of images.
84 |
85 | Args:
86 | x: A batch of images. Shape: (N, C, H, W).
87 | y: A batch of labels. Shape: (N).
88 |
89 | Returns:
90 | The perturbed images if successful. Shape: (N, C, H, W).
91 | """
92 |
93 | delta = torch.zeros_like(x, requires_grad=True)
94 |
95 | # If alpha is not given, set to eps / steps
96 | if self.alpha is None:
97 | self.alpha = self.eps / self.steps
98 |
99 | # Perform PGD
100 | for _ in range(self.steps):
101 | # Compute loss
102 | _ = self.model(self.normalize(x + delta))
103 | loss = -self.features.std()
104 |
105 | # Compute gradient
106 | loss.backward()
107 |
108 | if delta.grad is None:
109 | continue
110 |
111 | # Update delta
112 | g = delta.grad.data.sign()
113 |
114 | delta.data = delta.data + self.alpha * g
115 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
116 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
117 |
118 | # Zero out gradient
119 | delta.grad.detach_()
120 | delta.grad.zero_()
121 |
122 | return x + delta
123 |
124 |
125 | if __name__ == '__main__':
126 | from torchattack.evaluate.runner import run_attack
127 |
128 | run_attack(DR, model_name='vgg16', victim_model_names=['resnet18'])
129 |
--------------------------------------------------------------------------------
/torchattack/evaluate/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from torchattack.evaluate.dataset import NIPSDataset, NIPSLoader
4 | from torchattack.evaluate.meter import FoolingRateMeter
5 | from torchattack.evaluate.runner import run_attack
6 | from torchattack.evaluate.save_image import save_image_batch
7 |
8 |
9 | class DeprecatedFoolingRateMetric(FoolingRateMeter):
10 | """Deprecated class for FoolingRateMetric."""
11 |
12 | def __new__(cls, *args: Any, **kwargs: Any) -> 'DeprecatedFoolingRateMetric':
13 | import warnings
14 |
15 | warnings.warn(
16 | '`FoolingRateMetric` is deprecated. Use `FoolingRateMeter` instead.',
17 | DeprecationWarning,
18 | stacklevel=2,
19 | )
20 | return super().__new__(cls)
21 |
22 |
23 | FoolingRateMetric = DeprecatedFoolingRateMetric
24 |
25 | __all__ = [
26 | 'run_attack',
27 | 'save_image_batch',
28 | 'FoolingRateMeter',
29 | 'NIPSDataset',
30 | 'NIPSLoader',
31 | ]
32 |
--------------------------------------------------------------------------------
/torchattack/evaluate/meter.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 |
4 | class FoolingRateMeter:
5 | """Fooling rate metric tracker.
6 |
7 | Attributes:
8 | all_count: Total number of samples.
9 | cln_count: Number of correctly predicted clean samples.
10 | adv_count: Number of correctly predicted adversarial examples.
11 | targeted_count: Number of successfully attacked targeted adversarial examples.
12 |
13 | Args:
14 | targeted: Whether the current attack is targeted or not. Defaults to False.
15 | """
16 |
17 | def __init__(self, targeted: bool = False) -> None:
18 | self.targeted = targeted
19 | self.all_count = torch.tensor(0)
20 | self.cln_count = torch.tensor(0)
21 | self.adv_count = torch.tensor(0)
22 | self.targeted_count = torch.tensor(0)
23 |
24 | def update(
25 | self, labels: torch.Tensor, cln_logits: torch.Tensor, adv_logits: torch.Tensor
26 | ) -> None:
27 | """Update metric tracker during attack progress.
28 |
29 | Args:
30 | labels: Ground truth labels for non-targeted attacks, or a tuple of (ground
31 | truth labels, target labels) for targeted attacks.
32 | cln_logits: Prediction logits for clean samples.
33 | adv_logits: Prediction logits for adversarial examples.
34 | """
35 |
36 | if self.targeted:
37 | self.targeted_count += (adv_logits.argmax(dim=1) == labels[1]).sum().item()
38 | labels = labels[0]
39 |
40 | self.all_count += labels.numel()
41 | self.cln_count += (cln_logits.argmax(dim=1) == labels).sum().item()
42 | self.adv_count += (adv_logits.argmax(dim=1) == labels).sum().item()
43 |
44 | def compute(self) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
45 | """Compute the fooling rate and related metrics.
46 |
47 | Returns:
48 | A tuple of torch.Tensors containing the clean sample accuracy, adversarial
49 | example accuracy, and fooling rate (either non-targeted or targeted,
50 | depending on the attack) computed, respectively.
51 | """
52 |
53 | return (
54 | self.cln_count / self.all_count,
55 | self.adv_count / self.all_count,
56 | self.targeted_count / self.all_count
57 | if self.targeted
58 | else (self.cln_count - self.adv_count) / self.cln_count,
59 | )
60 |
61 | def reset(self) -> None:
62 | """Reset the metric tracker to initial state."""
63 |
64 | self.all_count = torch.tensor(0)
65 | self.cln_count = torch.tensor(0)
66 | self.adv_count = torch.tensor(0)
67 | self.targeted_count = torch.tensor(0)
68 |
--------------------------------------------------------------------------------
/torchattack/evaluate/save_image.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import torch
4 | from PIL import Image
5 |
6 |
7 | def save_image_batch(
8 | imgs: torch.Tensor,
9 | save_dir: str,
10 | filenames: list[str] | None = None,
11 | extension: str = 'png',
12 | kwargs: dict | None = None,
13 | ) -> None:
14 | """Losslessly (as lossless as possible) save a batch of images to disk.
15 |
16 | Args:
17 | imgs: The batch of images to save.
18 | save_dir: The directory to save the images (parent folder).
19 | filenames: The names of the images without their extensions. Defaults to None.
20 | extension: The extension of the images to save as. One of 'png', 'jpeg'. Defaults to "png".
21 | kwargs: Additional keyword arguments to pass to the image save function. Defaults to None.
22 | """
23 |
24 | if kwargs is None:
25 | kwargs = (
26 | {}
27 | # To best preserve perturbation effectiveness, we recommend saving as PNGs
28 | # See: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#png-saving
29 | if extension == 'png'
30 | # If saving as JPEGs, add additional arguments to ensure perturbation quality
31 | # See: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg-saving
32 | else {'quality': 100, 'subsampling': 0, 'keep_rgb': True}
33 | )
34 |
35 | assert extension in ['png', 'jpeg'], 'Extension must be either `png` or `jpeg`.'
36 |
37 | # Create the parent directory if it does not exist
38 | if not os.path.exists(save_dir):
39 | os.makedirs(save_dir)
40 |
41 | # Generate random filenames if none are provided
42 | if filenames is None:
43 | filenames = _generate_filenames(len(imgs))
44 |
45 | assert imgs.dim() == 4, 'Input tensor must be 4D (BCHW)'
46 | assert imgs.size(0) == len(filenames), 'Batch size must match number of filenames'
47 |
48 | for x, name in zip(imgs, filenames):
49 | img = x.detach().cpu().numpy().transpose(1, 2, 0)
50 | img = Image.fromarray((img * 255).astype('uint8'))
51 | img.save(os.path.join(save_dir, f'{name}.{extension}'), **kwargs)
52 |
53 |
54 | def _generate_filenames(num: int, name_len: int = 10) -> list[str]:
55 | """Generate a list of random filenames.
56 |
57 | Args:
58 | num: The number of filenames to generate.
59 | name_len: The length of each filename. Defaults to 10.
60 |
61 | Returns:
62 | A list of random filenames.
63 | """
64 |
65 | import random
66 | import string
67 |
68 | characters = string.ascii_letters + string.digits
69 | return [''.join(random.choices(characters, k=name_len)) for _ in range(num)]
70 |
--------------------------------------------------------------------------------
/torchattack/fgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class FGSM(Attack):
12 | """Fast Gradient Sign Method (FGSM).
13 |
14 | > From the paper: [Explaining and Harnessing Adversarial
15 | Examples](https://arxiv.org/abs/1412.6572).
16 |
17 | Args:
18 | model: A torch.nn.Module network model.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | eps: Maximum perturbation measured by Linf. Defaults to 8/255.
22 | clip_min: Minimum value for clipping. Defaults to 0.0.
23 | clip_max: Maximum value for clipping. Defaults to 1.0.
24 | targeted: Targeted attack if True. Defaults to False.
25 | """
26 |
27 | def __init__(
28 | self,
29 | model: nn.Module | AttackModel,
30 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
31 | device: torch.device | None = None,
32 | eps: float = 8 / 255,
33 | clip_min: float = 0.0,
34 | clip_max: float = 1.0,
35 | targeted: bool = False,
36 | ) -> None:
37 | super().__init__(model, normalize, device)
38 |
39 | self.eps = eps
40 | self.clip_min = clip_min
41 | self.clip_max = clip_max
42 | self.targeted = targeted
43 | self.lossfn = nn.CrossEntropyLoss()
44 |
45 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
46 | """Perform FGSM on a batch of images.
47 |
48 | Args:
49 | x: A batch of images. Shape: (N, C, H, W).
50 | y: A batch of labels. Shape: (N).
51 |
52 | Returns:
53 | The perturbed images if successful. Shape: (N, C, H, W).
54 | """
55 |
56 | # This is written in a way that is similar to iterative methods such as MIM.
57 | # The original implementation of FGSM is not written in this way.
58 | delta = torch.zeros_like(x, requires_grad=True)
59 |
60 | outs = self.model(self.normalize(x + delta))
61 | loss = self.lossfn(outs, y)
62 |
63 | if self.targeted:
64 | loss = -loss
65 |
66 | loss.backward()
67 |
68 | # If for some reason delta.grad is None, return the original image.
69 | if delta.grad is None:
70 | return x
71 |
72 | g_sign = delta.grad.data.sign()
73 |
74 | delta.data = delta.data + self.eps * g_sign
75 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
76 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
77 |
78 | return x + delta
79 |
80 |
81 | if __name__ == '__main__':
82 | from torchattack.evaluate import run_attack
83 |
84 | run_attack(FGSM, {'eps': 8 / 255, 'clip_min': 0.0, 'clip_max': 1.0})
85 |
--------------------------------------------------------------------------------
/torchattack/fia.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack._rgetattr import rgetattr
7 | from torchattack.attack import Attack, register_attack
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | @register_attack()
12 | class FIA(Attack):
13 | """The FIA (Feature Importance-aware) Attack.
14 |
15 | > From the paper: [Feature Importance-aware Transferable Adversarial
16 | Attacks](https://arxiv.org/abs/2107.14185).
17 |
18 | Args:
19 | model: The model to attack.
20 | normalize: A transform to normalize images.
21 | device: Device to use for tensors. Defaults to cuda if available.
22 | eps: The maximum perturbation. Defaults to 8/255.
23 | steps: Number of steps. Defaults to 10.
24 | alpha: Step size, `eps / steps` if None. Defaults to None.
25 | decay: Decay factor for the momentum term. Defaults to 1.0.
26 | num_ens: Number of aggregate gradients. Defaults to 30.
27 | feature_layer_cfg: Name of the feature layer to attack. If not provided, tries
28 | to infer from built-in config based on the model name. Defaults to ""
29 | drop_rate: Dropout rate for random pixels. Defaults to 0.3.
30 | clip_min: Minimum value for clipping. Defaults to 0.0.
31 | clip_max: Maximum value for clipping. Defaults to 1.0.
32 | """
33 |
34 | _builtin_cfgs = {
35 | 'inception_v3': 'Mixed_5b',
36 | 'resnet50': 'layer2.3', # (not present in the paper)
37 | 'resnet152': 'layer2.7',
38 | 'vgg16': 'features.15',
39 | 'inception_resnet_v2': 'conv2d_4a',
40 | }
41 |
42 | def __init__(
43 | self,
44 | model: nn.Module | AttackModel,
45 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
46 | device: torch.device | None = None,
47 | eps: float = 8 / 255,
48 | steps: int = 10,
49 | alpha: float | None = None,
50 | decay: float = 1.0,
51 | num_ens: int = 30,
52 | feature_layer_cfg: str = '',
53 | drop_rate: float = 0.3,
54 | clip_min: float = 0.0,
55 | clip_max: float = 1.0,
56 | ) -> None:
57 | # If `feature_layer_cfg` is not provided, try to infer used feature layer from
58 | # the `model_name` attribute (automatically attached during instantiation)
59 | if not feature_layer_cfg and isinstance(model, AttackModel):
60 | feature_layer_cfg = self._builtin_cfgs[model.model_name]
61 |
62 | # Delay initialization to avoid overriding the model's `model_name` attribute
63 | super().__init__(model, normalize, device)
64 |
65 | self.eps = eps
66 | self.steps = steps
67 | self.alpha = alpha
68 | self.decay = decay
69 | self.num_ens = num_ens
70 | self.drop_rate = drop_rate
71 | self.clip_min = clip_min
72 | self.clip_max = clip_max
73 |
74 | self.feature_layer_cfg = feature_layer_cfg
75 | self.feature_module = rgetattr(self.model, feature_layer_cfg)
76 |
77 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
78 | """Perform FIA on a batch of images.
79 |
80 | Args:
81 | x: A batch of images. Shape: (N, C, H, W).
82 | y: A batch of labels. Shape: (N).
83 |
84 | Returns:
85 | The perturbed images if successful. Shape: (N, C, H, W).
86 | """
87 |
88 | g = torch.zeros_like(x)
89 | delta = torch.zeros_like(x, requires_grad=True)
90 |
91 | # If alpha is not given, set to eps / steps
92 | if self.alpha is None:
93 | self.alpha = self.eps / self.steps
94 |
95 | hf = self.feature_module.register_forward_hook(self._forward_hook)
96 | hb = self.feature_module.register_full_backward_hook(self._backward_hook)
97 |
98 | # Gradient aggregation on ensembles
99 | agg_grad: torch.Tensor | float = 0.0
100 | for _ in range(self.num_ens):
101 | # Create variants of input with randomly dropped pixels
102 | x_dropped = self.drop_pixels(x)
103 |
104 | # Get model outputs and compute gradients
105 | outs_dropped = self.model(self.normalize(x_dropped))
106 | outs_dropped = torch.softmax(outs_dropped, dim=1)
107 |
108 | loss = torch.stack([outs_dropped[bi][y[bi]] for bi in range(x.shape[0])])
109 | loss = loss.sum()
110 | loss.backward()
111 |
112 | # Accumulate gradients
113 | agg_grad += self.mid_grad[0].detach()
114 |
115 | # for batch_i in range(x.shape[0]):
116 | # agg_grad[batch_i] /= agg_grad[batch_i].norm(p=2)
117 | agg_grad /= torch.norm(agg_grad, p=2, dim=(1, 2, 3), keepdim=True)
118 | hb.remove()
119 |
120 | # Perform FIA
121 | for _ in range(self.steps):
122 | # Pass through the model
123 | _ = self.model(self.normalize(x + delta))
124 |
125 | # Hooks are updated during forward pass
126 | loss = (self.mid_output * agg_grad).sum()
127 | loss.backward()
128 |
129 | if delta.grad is None:
130 | continue
131 |
132 | # Apply momentum term
133 | g = self.decay * g + delta.grad / torch.mean(
134 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
135 | )
136 |
137 | # Update delta
138 | delta.data = delta.data - self.alpha * g.sign()
139 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
140 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
141 |
142 | # Zero out gradient
143 | delta.grad.detach_()
144 | delta.grad.zero_()
145 |
146 | hf.remove()
147 | return x + delta
148 |
149 | def drop_pixels(self, x: torch.Tensor) -> torch.Tensor:
150 | """Randomly drop pixels from the input image.
151 |
152 | Args:
153 | x: A batch of images. Shape: (N, C, H, W).
154 |
155 | Returns:
156 | A batch of images with randomly dropped pixels.
157 | """
158 |
159 | x_dropped = torch.zeros_like(x)
160 | x_dropped.copy_(x).detach()
161 | x_dropped.requires_grad = True
162 |
163 | mask = torch.bernoulli(torch.ones_like(x) * (1 - self.drop_rate))
164 | x_dropped = x_dropped * mask
165 |
166 | return x_dropped
167 |
168 | def _forward_hook(self, m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
169 | self.mid_output = o
170 |
171 | def _backward_hook(self, m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
172 | self.mid_grad = o
173 |
174 |
175 | if __name__ == '__main__':
176 | from torchattack.evaluate import run_attack
177 |
178 | run_attack(
179 | FIA,
180 | attack_args={'feature_layer_cfg': 'layer2'},
181 | model_name='resnet50',
182 | victim_model_names=['resnet18', 'vgg13', 'densenet121'],
183 | )
184 |
--------------------------------------------------------------------------------
/torchattack/gama.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import torch
4 |
5 | from torchattack.attack import Attack, register_attack
6 | from torchattack.generative._weights import GeneratorWeights, GeneratorWeightsEnum
7 | from torchattack.generative.leaky_relu_resnet_generator import ResNetGenerator
8 |
9 |
10 | class GAMAWeights(GeneratorWeightsEnum):
11 | """
12 | We provide pretrained weights of the GAMA attack generator with training steps
13 | identical to the described settings in the paper and appendix. Specifically, we use
14 | ViT-B/16 as the backend of CLIP. Training epochs are set to 5 and 10 for the COCO
15 | and VOC datasets, respectively.
16 | """
17 |
18 | DENSENET169_COCO = GeneratorWeights(
19 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_dense169_coco_w_vitb16_epoch4.pth'
20 | )
21 | DENSENET169_VOC = GeneratorWeights(
22 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_dense169_voc_w_vitb16_epoch9.pth'
23 | )
24 | RESNET152_COCO = GeneratorWeights(
25 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_res152_coco_w_vitb16_epoch4.pth'
26 | )
27 | RESNET152_VOC = GeneratorWeights(
28 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_res152_voc_w_vitb16_epoch9.pth'
29 | )
30 | VGG16_COCO = GeneratorWeights(
31 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_vgg16_coco_w_vitb16_epoch4.pth'
32 | )
33 | VGG16_VOC = GeneratorWeights(
34 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_vgg16_voc_w_vitb16_epoch9.pth'
35 | )
36 | VGG19_COCO = GeneratorWeights(
37 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_vgg19_coco_w_vitb16_epoch4.pth'
38 | )
39 | VGG19_VOC = GeneratorWeights(
40 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/gama_vgg19_voc_w_vitb16_epoch9.pth'
41 | )
42 | DEFAULT = VGG19_COCO
43 |
44 |
45 | @register_attack(category='GENERATIVE')
46 | class GAMA(Attack):
47 | """GAMA - Generative Adversarial Multi-Object Scene Attacks.
48 |
49 | > From the paper: [GAMA: Generative Adversarial Multi-Object Scene
50 | Attacks](https://arxiv.org/abs/2209.09502).
51 |
52 | Args:
53 | device: Device to use for tensors. Defaults to cuda if available.
54 | eps: The maximum perturbation. Defaults to 10/255.
55 | weights: Pretrained weights for the generator. Either import and use the enum,
56 | or use its name. Defaults to GAMAWeights.DEFAULT.
57 | checkpoint_path: Path to a custom checkpoint. Defaults to None.
58 | clip_min: Minimum value for clipping. Defaults to 0.0.
59 | clip_max: Maximum value for clipping. Defaults to 1.0.
60 | """
61 |
62 | def __init__(
63 | self,
64 | device: torch.device | None = None,
65 | eps: float = 10 / 255,
66 | weights: GAMAWeights | str | None = GAMAWeights.DEFAULT,
67 | checkpoint_path: str | None = None,
68 | clip_min: float = 0.0,
69 | clip_max: float = 1.0,
70 | ) -> None:
71 | # Generative attacks do not require specifying model and normalize
72 | super().__init__(model=None, normalize=None, device=device)
73 |
74 | self.eps = eps
75 | self.checkpoint_path = checkpoint_path
76 | self.clip_min = clip_min
77 | self.clip_max = clip_max
78 |
79 | # Initialize the generator and its weights
80 | self.generator = ResNetGenerator()
81 |
82 | # Prioritize checkpoint path over provided weights enum
83 | if self.checkpoint_path is not None:
84 | self.generator.load_state_dict(
85 | torch.load(self.checkpoint_path, weights_only=True)
86 | )
87 | else:
88 | # Verify and load weights from enum if checkpoint path is not provided
89 | self.weights = GAMAWeights.verify(weights)
90 | if self.weights is not None:
91 | self.generator.load_state_dict(
92 | self.weights.get_state_dict(check_hash=True)
93 | )
94 |
95 | self.generator.eval().to(self.device)
96 |
97 | def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> torch.Tensor:
98 | """Perform GAMA on a batch of images.
99 |
100 | Args:
101 | x: A batch of images. Shape: (N, C, H, W).
102 |
103 | Returns:
104 | The perturbed images if successful. Shape: (N, C, H, W).
105 | """
106 |
107 | x_unrestricted = self.generator(x)
108 | delta = torch.clamp(x_unrestricted - x, -self.eps, self.eps)
109 | x_adv = torch.clamp(x + delta, self.clip_min, self.clip_max)
110 | return x_adv
111 |
112 |
113 | if __name__ == '__main__':
114 | from torchattack.evaluate import run_attack
115 |
116 | run_attack(
117 | attack=GAMA,
118 | attack_args={'eps': 10 / 255, 'weights': 'DENSENET169_COCO'},
119 | model_name='densenet169',
120 | victim_model_names=['resnet50'],
121 | )
122 |
--------------------------------------------------------------------------------
/torchattack/generative/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/torchattack/generative/__init__.py
--------------------------------------------------------------------------------
/torchattack/generative/_weights.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from enum import Enum
3 | from typing import Any, Mapping, Union
4 |
5 | from torch.hub import load_state_dict_from_url
6 |
7 |
8 | @dataclass
9 | class GeneratorWeights:
10 | url: str
11 | inception: bool = False
12 |
13 |
14 | class GeneratorWeightsEnum(Enum):
15 | @classmethod
16 | def verify(
17 | cls, obj: Union['GeneratorWeightsEnum', str, None]
18 | ) -> Union['GeneratorWeightsEnum', None]:
19 | if obj is not None:
20 | if type(obj) is str:
21 | obj = cls[obj.replace(cls.__name__ + '.', '')]
22 | elif not isinstance(obj, cls):
23 | raise TypeError(
24 | f'Invalid Weight class provided; expected {cls.__name__} '
25 | f'but received {obj.__class__.__name__}.'
26 | )
27 | return obj
28 |
29 | def get_state_dict(self, *args: Any, **kwargs: Any) -> Mapping[str, Any]:
30 | return load_state_dict_from_url(self.url, *args, **kwargs)
31 |
32 | def __repr__(self) -> str:
33 | return f'{self.__class__.__name__}.{self._name_}'
34 |
35 | def __eq__(self, other: Any) -> bool:
36 | other = self.verify(other)
37 | return isinstance(other, self.__class__) and self.name == other.name
38 |
39 | def _assert_generator_weights(self) -> GeneratorWeights:
40 | if not isinstance(self.value, GeneratorWeights):
41 | raise TypeError(
42 | f'Expected GeneratorWeights, but got {type(self.value).__name__}'
43 | )
44 | return self.value
45 |
46 | @property
47 | def url(self) -> str:
48 | return self._assert_generator_weights().url
49 |
50 | @property
51 | def inception(self) -> bool:
52 | return self._assert_generator_weights().inception
53 |
--------------------------------------------------------------------------------
/torchattack/generative/leaky_relu_resnet_generator.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | import torch.nn.functional as f
4 |
5 | # To control feature map in generator
6 | ngf = 64
7 |
8 |
9 | def fused_leaky_relu(
10 | input: torch.Tensor,
11 | bias: torch.Tensor,
12 | negative_slope: float = 0.2,
13 | scale: float = 2**0.5,
14 | ) -> torch.Tensor:
15 | return f.leaky_relu(input + bias, negative_slope) * scale
16 |
17 |
18 | class FusedLeakyReLU(nn.Module):
19 | def __init__(
20 | self, channel: int, negative_slope: float = 0.2, scale: float = 2**0.5
21 | ) -> None:
22 | super().__init__()
23 | self.bias = nn.Parameter(torch.zeros(1, channel, 1, 1))
24 | self.negative_slope = negative_slope
25 | self.scale = scale
26 |
27 | def forward(self, input: torch.Tensor) -> torch.Tensor:
28 | out = fused_leaky_relu(input, self.bias, self.negative_slope, self.scale)
29 | return out
30 |
31 |
32 | class ResNetGenerator(nn.Module):
33 | def __init__(self, inception: bool = False) -> None:
34 | """Generator network (ResNet).
35 |
36 | Args:
37 | inception: if True crop layer will be added to go from 3x300x300 to
38 | 3x299x299. Defaults to False.
39 | """
40 |
41 | super(ResNetGenerator, self).__init__()
42 |
43 | # Input_size = 3, n, n
44 | self.inception = inception
45 | self.block1 = nn.Sequential(
46 | nn.ReflectionPad2d(3),
47 | nn.Conv2d(3, ngf, kernel_size=7, padding=0, bias=False),
48 | nn.BatchNorm2d(ngf),
49 | FusedLeakyReLU(ngf),
50 | )
51 |
52 | # Input size = 3, n, n
53 | self.block2 = nn.Sequential(
54 | nn.Conv2d(ngf, ngf * 2, kernel_size=3, stride=2, padding=1, bias=False),
55 | nn.BatchNorm2d(ngf * 2),
56 | FusedLeakyReLU(ngf * 2),
57 | )
58 |
59 | # Input size = 3, n/2, n/2
60 | self.block3 = nn.Sequential(
61 | nn.Conv2d(ngf * 2, ngf * 4, kernel_size=3, stride=2, padding=1, bias=False),
62 | nn.BatchNorm2d(ngf * 4),
63 | FusedLeakyReLU(ngf * 4),
64 | )
65 |
66 | # Input size = 3, n/4, n/4
67 | # Residual Blocks: 6
68 | self.resblock1 = ResidualBlock(ngf * 4)
69 | self.resblock2 = ResidualBlock(ngf * 4)
70 | self.resblock3 = ResidualBlock(ngf * 4)
71 | self.resblock4 = ResidualBlock(ngf * 4)
72 | self.resblock5 = ResidualBlock(ngf * 4)
73 | self.resblock6 = ResidualBlock(ngf * 4)
74 |
75 | # Input size = 3, n/4, n/4
76 | self.upsampl1 = nn.Sequential(
77 | nn.ConvTranspose2d(
78 | ngf * 4,
79 | ngf * 2,
80 | kernel_size=3,
81 | stride=2,
82 | padding=1,
83 | output_padding=1,
84 | bias=False,
85 | ),
86 | nn.BatchNorm2d(ngf * 2),
87 | FusedLeakyReLU(ngf * 2),
88 | )
89 |
90 | # Input size = 3, n/2, n/2
91 | self.upsampl2 = nn.Sequential(
92 | nn.ConvTranspose2d(
93 | ngf * 2,
94 | ngf,
95 | kernel_size=3,
96 | stride=2,
97 | padding=1,
98 | output_padding=1,
99 | bias=False,
100 | ),
101 | nn.BatchNorm2d(ngf),
102 | FusedLeakyReLU(ngf),
103 | )
104 |
105 | # Input size = 3, n, n
106 | self.blockf = nn.Sequential(
107 | nn.ReflectionPad2d(3), nn.Conv2d(ngf, 3, kernel_size=7, padding=0)
108 | )
109 |
110 | self.crop = nn.ConstantPad2d((0, -1, -1, 0), 0)
111 |
112 | def forward(self, input: torch.Tensor) -> torch.Tensor:
113 | x = self.block1(input)
114 | x = self.block2(x)
115 | x = self.block3(x)
116 | x = self.resblock1(x)
117 | x = self.resblock2(x)
118 | x = self.resblock3(x)
119 | x = self.resblock4(x)
120 | x = self.resblock5(x)
121 | x = self.resblock6(x)
122 | x = self.upsampl1(x)
123 | x = self.upsampl2(x)
124 | x = self.blockf(x)
125 | if self.inception:
126 | x = self.crop(x)
127 | return (torch.tanh(x) + 1) / 2 # Output range [0 1]
128 |
129 |
130 | class ResidualBlock(nn.Module):
131 | def __init__(self, num_filters: int) -> None:
132 | super(ResidualBlock, self).__init__()
133 | self.block = nn.Sequential(
134 | nn.ReflectionPad2d(1),
135 | nn.Conv2d(
136 | in_channels=num_filters,
137 | out_channels=num_filters,
138 | kernel_size=3,
139 | stride=1,
140 | padding=0,
141 | bias=False,
142 | ),
143 | nn.BatchNorm2d(num_filters),
144 | FusedLeakyReLU(num_filters),
145 | nn.Dropout(0.5),
146 | nn.ReflectionPad2d(1),
147 | nn.Conv2d(
148 | in_channels=num_filters,
149 | out_channels=num_filters,
150 | kernel_size=3,
151 | stride=1,
152 | padding=0,
153 | bias=False,
154 | ),
155 | nn.BatchNorm2d(num_filters),
156 | )
157 |
158 | def forward(self, x: torch.Tensor) -> torch.Tensor:
159 | residual: torch.Tensor = self.block(x)
160 | return x + residual
161 |
--------------------------------------------------------------------------------
/torchattack/generative/resnet_generator.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 |
4 | # To control feature map in generator
5 | ngf = 64
6 |
7 |
8 | class ResNetGenerator(nn.Module):
9 | def __init__(self, inception: bool = False) -> None:
10 | """Generator network (ResNet).
11 |
12 | Args:
13 | inception: if True crop layer will be added to go from 3x300x300 to
14 | 3x299x299. Defaults to False.
15 | """
16 |
17 | super(ResNetGenerator, self).__init__()
18 |
19 | # Input_size = 3, n, n
20 | self.inception = inception
21 | self.block1 = nn.Sequential(
22 | nn.ReflectionPad2d(3),
23 | nn.Conv2d(3, ngf, kernel_size=7, padding=0, bias=False),
24 | nn.BatchNorm2d(ngf),
25 | nn.ReLU(True),
26 | )
27 |
28 | # Input size = 3, n, n
29 | self.block2 = nn.Sequential(
30 | nn.Conv2d(ngf, ngf * 2, kernel_size=3, stride=2, padding=1, bias=False),
31 | nn.BatchNorm2d(ngf * 2),
32 | nn.ReLU(True),
33 | )
34 |
35 | # Input size = 3, n/2, n/2
36 | self.block3 = nn.Sequential(
37 | nn.Conv2d(ngf * 2, ngf * 4, kernel_size=3, stride=2, padding=1, bias=False),
38 | nn.BatchNorm2d(ngf * 4),
39 | nn.ReLU(True),
40 | )
41 |
42 | # Input size = 3, n/4, n/4
43 | # Residual Blocks: 6
44 | self.resblock1 = ResidualBlock(ngf * 4)
45 | self.resblock2 = ResidualBlock(ngf * 4)
46 | self.resblock3 = ResidualBlock(ngf * 4)
47 | self.resblock4 = ResidualBlock(ngf * 4)
48 | self.resblock5 = ResidualBlock(ngf * 4)
49 | self.resblock6 = ResidualBlock(ngf * 4)
50 |
51 | # Input size = 3, n/4, n/4
52 | self.upsampl1 = nn.Sequential(
53 | nn.ConvTranspose2d(
54 | ngf * 4,
55 | ngf * 2,
56 | kernel_size=3,
57 | stride=2,
58 | padding=1,
59 | output_padding=1,
60 | bias=False,
61 | ),
62 | nn.BatchNorm2d(ngf * 2),
63 | nn.ReLU(True),
64 | )
65 |
66 | # Input size = 3, n/2, n/2
67 | self.upsampl2 = nn.Sequential(
68 | nn.ConvTranspose2d(
69 | ngf * 2,
70 | ngf,
71 | kernel_size=3,
72 | stride=2,
73 | padding=1,
74 | output_padding=1,
75 | bias=False,
76 | ),
77 | nn.BatchNorm2d(ngf),
78 | nn.ReLU(True),
79 | )
80 |
81 | # Input size = 3, n, n
82 | self.blockf = nn.Sequential(
83 | nn.ReflectionPad2d(3), nn.Conv2d(ngf, 3, kernel_size=7, padding=0)
84 | )
85 |
86 | self.crop = nn.ConstantPad2d((0, -1, -1, 0), 0)
87 |
88 | def forward(self, input: torch.Tensor) -> torch.Tensor:
89 | x = self.block1(input)
90 | x = self.block2(x)
91 | x = self.block3(x)
92 | x = self.resblock1(x)
93 | x = self.resblock2(x)
94 | x = self.resblock3(x)
95 | x = self.resblock4(x)
96 | x = self.resblock5(x)
97 | x = self.resblock6(x)
98 | x = self.upsampl1(x)
99 | x = self.upsampl2(x)
100 | x = self.blockf(x)
101 | if self.inception:
102 | x = self.crop(x)
103 | return (torch.tanh(x) + 1) / 2 # Output range [0 1]
104 |
105 |
106 | class ResidualBlock(nn.Module):
107 | def __init__(self, num_filters: int) -> None:
108 | super(ResidualBlock, self).__init__()
109 | self.block = nn.Sequential(
110 | nn.ReflectionPad2d(1),
111 | nn.Conv2d(
112 | in_channels=num_filters,
113 | out_channels=num_filters,
114 | kernel_size=3,
115 | stride=1,
116 | padding=0,
117 | bias=False,
118 | ),
119 | nn.BatchNorm2d(num_filters),
120 | nn.ReLU(True),
121 | nn.Dropout(0.5),
122 | nn.ReflectionPad2d(1),
123 | nn.Conv2d(
124 | in_channels=num_filters,
125 | out_channels=num_filters,
126 | kernel_size=3,
127 | stride=1,
128 | padding=0,
129 | bias=False,
130 | ),
131 | nn.BatchNorm2d(num_filters),
132 | )
133 |
134 | def forward(self, x: torch.Tensor) -> torch.Tensor:
135 | residual: torch.Tensor = self.block(x)
136 | return x + residual
137 |
--------------------------------------------------------------------------------
/torchattack/gra.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class GRA(Attack):
12 | """The GRA (Gradient Relevance) attack.
13 |
14 | > From the paper: [Boosting Adversarial Transferability via Gradient Relevance
15 | Attack](https://openaccess.thecvf.com/content/ICCV2023/html/Zhu_Boosting_Adversarial_Transferability_via_Gradient_Relevance_Attack_ICCV_2023_paper.html).
16 |
17 | Args:
18 | model: The model to attack.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | eps: The maximum perturbation. Defaults to 8/255.
22 | steps: Number of steps. Defaults to 10.
23 | alpha: Step size, `eps / steps` if None. Defaults to None.
24 | beta: The upper bound of the neighborhood. Defaults to 3.5.
25 | eta: The decay indicator factor. Defaults to 0.94.
26 | num_neighbors: Number of samples for estimating gradient variance. Defaults to 20.
27 | decay: Decay factor for the momentum term. Defaults to 1.0.
28 | clip_min: Minimum value for clipping. Defaults to 0.0.
29 | clip_max: Maximum value for clipping. Defaults to 1.0.
30 | targeted: Targeted attack if True. Defaults to False.
31 | """
32 |
33 | def __init__(
34 | self,
35 | model: nn.Module | AttackModel,
36 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
37 | device: torch.device | None = None,
38 | eps: float = 8 / 255,
39 | steps: int = 10,
40 | alpha: float | None = None,
41 | beta: float = 3.5,
42 | eta: float = 0.94,
43 | num_neighbors: int = 20,
44 | decay: float = 1.0,
45 | clip_min: float = 0.0,
46 | clip_max: float = 1.0,
47 | targeted: bool = False,
48 | ) -> None:
49 | super().__init__(model, normalize, device)
50 |
51 | self.eps = eps
52 | self.steps = steps
53 | self.alpha = alpha
54 | self.decay = decay
55 |
56 | self.beta = beta
57 | self.num_neighbors = num_neighbors
58 |
59 | # According to the paper, eta=0.94 maintains a good balance between
60 | # effectiveness to normal and defended models.
61 | self.eta = eta
62 | self.radius = beta * eps
63 |
64 | self.clip_min = clip_min
65 | self.clip_max = clip_max
66 | self.targeted = targeted
67 | self.lossfn = nn.CrossEntropyLoss()
68 |
69 | def _get_avg_grad(
70 | self, x: torch.Tensor, y: torch.Tensor, delta: torch.Tensor
71 | ) -> torch.Tensor:
72 | """Estimate the gradient using the neighborhood of the perturbed image."""
73 |
74 | grad = torch.zeros_like(x)
75 | for _ in range(self.num_neighbors):
76 | xr = torch.empty_like(delta).uniform_(-self.radius, self.radius)
77 | outs = self.model(self.normalize(x + delta + xr))
78 | loss = self.lossfn(outs, y)
79 | grad += torch.autograd.grad(loss, delta)[0]
80 | return grad / self.num_neighbors
81 |
82 | def _get_decay_indicator(
83 | self,
84 | m: torch.Tensor,
85 | delta: torch.Tensor,
86 | cur_noise: torch.Tensor,
87 | prev_noise: torch.Tensor,
88 | ) -> torch.Tensor:
89 | """Update the decay indicator based on the current and previous noise."""
90 |
91 | eq_m = torch.eq(cur_noise, prev_noise).float()
92 | di_m = torch.ones_like(delta) - eq_m
93 | m = m * (eq_m + di_m * self.eta)
94 | return m
95 |
96 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
97 | """Perform GRA on a batch of images.
98 |
99 | Args:
100 | x: A batch of images. Shape: (N, C, H, W).
101 | y: A batch of labels. Shape: (N).
102 |
103 | Returns:
104 | The perturbed images if successful. Shape: (N, C, H, W).
105 | """
106 |
107 | g = torch.zeros_like(x)
108 | delta = torch.zeros_like(x, requires_grad=True)
109 |
110 | # If alpha is not given, set to eps / steps
111 | if self.alpha is None:
112 | self.alpha = self.eps / self.steps
113 |
114 | # Initialize the decay indicator
115 | m = torch.full_like(delta, 1 / self.eta)
116 |
117 | # Perform GRA
118 | for _ in range(self.steps):
119 | # Compute loss
120 | outs = self.model(self.normalize(x + delta))
121 | loss = self.lossfn(outs, y)
122 |
123 | if self.targeted:
124 | loss = -loss
125 |
126 | # Compute gradient
127 | grad = torch.autograd.grad(loss, delta)[0]
128 | avg_grad = self._get_avg_grad(x, y, delta)
129 |
130 | # Update similarity (relevance) weighted gradient
131 | gradv = grad.reshape(grad.size(0), -1)
132 | avg_gradv = avg_grad.reshape(avg_grad.size(0), -1)
133 | s = torch.cosine_similarity(gradv, avg_gradv, dim=1).view(-1, 1, 1, 1)
134 | cur_grad = grad * s + avg_grad * (1 - s)
135 |
136 | # Save previous momentum
137 | prev_g = g.clone()
138 |
139 | # Apply momentum term
140 | g = self.decay * g + cur_grad / torch.mean(
141 | torch.abs(cur_grad), dim=(1, 2, 3), keepdim=True
142 | )
143 |
144 | # Update decay indicator
145 | m = self._get_decay_indicator(m, delta, g, prev_g)
146 |
147 | # Update delta
148 | delta.data = delta.data + self.alpha * m * g.sign()
149 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
150 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
151 |
152 | return x + delta
153 |
154 |
155 | if __name__ == '__main__':
156 | from torchattack.evaluate import run_attack
157 |
158 | run_attack(
159 | attack=GRA,
160 | attack_args={'eps': 8 / 255, 'steps': 10},
161 | model_name='resnet18',
162 | victim_model_names=['resnet50', 'vgg13', 'densenet121'],
163 | )
164 |
--------------------------------------------------------------------------------
/torchattack/ilpd.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack._rgetattr import rgetattr
7 | from torchattack.attack import Attack, register_attack
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | @register_attack()
12 | class ILPD(Attack):
13 | """The ILPD (Intermediate-level Perturbation Decay) Attack.
14 |
15 | > From the paper: [Improving Adversarial Transferability via Intermediate-level
16 | Perturbation Decay](https://arxiv.org/abs/2304.13410).
17 |
18 | Args:
19 | model: The model to attack.
20 | normalize: A transform to normalize images.
21 | device: Device to use for tensors. Defaults to cuda if available.
22 | eps: The maximum perturbation. Defaults to 8/255.
23 | steps: Number of steps. Defaults to 10.
24 | alpha: Step size, `eps / steps` if None. Defaults to None.
25 | decay: Decay factor for the momentum term. Defaults to 1.0.
26 | sigma: Standard deviation for noise. Defaults to 0.05.
27 | feature_layer_cfg: Name of the feature layer to attack. If not provided, tries
28 | to infer from built-in config based on the model name. Defaults to ""
29 | clip_min: Minimum value for clipping. Defaults to 0.0.
30 | clip_max: Maximum value for clipping. Defaults to 1.0.
31 | """
32 |
33 | _builtin_cfgs = {
34 | 'vgg19': 'features.27',
35 | 'resnet50': 'layer2.3',
36 | # below are configs not present in the paper
37 | 'inception_v3': 'Mixed_5b',
38 | 'resnet152': 'layer2.7',
39 | 'vgg16': 'features.15',
40 | 'inception_resnet_v2': 'conv2d_4a',
41 | }
42 |
43 | def __init__(
44 | self,
45 | model: nn.Module | AttackModel,
46 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
47 | device: torch.device | None = None,
48 | eps: float = 8 / 255,
49 | steps: int = 10,
50 | alpha: float | None = None,
51 | decay: float = 1.0,
52 | sigma: float = 0.05,
53 | coef: float = 0.1,
54 | feature_layer_cfg: str = '',
55 | clip_min: float = 0.0,
56 | clip_max: float = 1.0,
57 | ) -> None:
58 | # If `feature_layer_cfg` is not provided, try to infer used feature layer from
59 | # the `model_name` attribute (automatically attached during instantiation)
60 | if not feature_layer_cfg and isinstance(model, AttackModel):
61 | feature_layer_cfg = self._builtin_cfgs[model.model_name]
62 |
63 | # Delay initialization to avoid overriding the model's `model_name` attribute
64 | super().__init__(model, normalize, device)
65 |
66 | self.eps = eps
67 | self.steps = steps
68 | self.alpha = alpha
69 | self.decay = decay
70 | self.sigma = sigma
71 | self.coef = coef
72 | self.clip_min = clip_min
73 | self.clip_max = clip_max
74 | self.lossfn = nn.CrossEntropyLoss()
75 |
76 | self.feature_layer_cfg = feature_layer_cfg
77 | self.feature_module = rgetattr(self.model, feature_layer_cfg)
78 |
79 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
80 | """Perform ILPD on a batch of images.
81 |
82 | Args:
83 | x: A batch of images. Shape: (N, C, H, W).
84 | y: A batch of labels. Shape: (N).
85 |
86 | Returns:
87 | The perturbed images if successful. Shape: (N, C, H, W).
88 | """
89 |
90 | g = torch.zeros_like(x)
91 | delta = torch.zeros_like(x, requires_grad=True)
92 |
93 | # If alpha is not given, set to eps / steps
94 | if self.alpha is None:
95 | self.alpha = self.eps / self.steps
96 |
97 | # Perform ILPD
98 | for _ in range(self.steps):
99 | with torch.no_grad():
100 | ilh = self.feature_module.register_forward_hook(self._il_hook)
101 |
102 | xsig = x + self.sigma * torch.randn_like(x)
103 | self.model(self.normalize(xsig))
104 |
105 | ilo = self.feature_module.output
106 | ilh.remove()
107 |
108 | pdh = self._get_hook_pd(ilo, self.coef)
109 | self.hook = self.feature_module.register_forward_hook(pdh)
110 |
111 | # Pass through the model
112 | outs = self.model(self.normalize(x + delta))
113 | loss = self.lossfn(outs, y)
114 |
115 | # Compute gradient
116 | loss.backward()
117 |
118 | if delta.grad is None:
119 | continue
120 |
121 | # Apply momentum term
122 | g = self.decay * g + delta.grad / torch.mean(
123 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
124 | )
125 |
126 | # Update delta
127 | delta.data = delta.data + self.alpha * g.sign()
128 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
129 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
130 |
131 | # Zero out gradient
132 | delta.grad.detach_()
133 | delta.grad.zero_()
134 |
135 | # Clean up hooks
136 | self.hook.remove()
137 |
138 | return x + delta
139 |
140 | def _il_hook(self, m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
141 | """Intermediate-level hook function."""
142 | m.output = o
143 |
144 | @staticmethod
145 | def _get_hook_pd(oo: torch.Tensor, gamma: float) -> Callable:
146 | """Get the hook function for perturbation decay.
147 |
148 | Args:
149 | oo: The original output tensor of the module.
150 | gamma: The decay factor.
151 |
152 | Returns:
153 | The hook function.
154 | """
155 |
156 | def hook_pd(m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> torch.Tensor:
157 | return gamma * o + (1 - gamma) * oo
158 |
159 | return hook_pd
160 |
161 |
162 | if __name__ == '__main__':
163 | from torchattack.evaluate import run_attack
164 |
165 | run_attack(
166 | ILPD,
167 | attack_args={'feature_layer_cfg': 'layer2.3'},
168 | model_name='resnet50',
169 | victim_model_names=['resnet18', 'vgg13', 'densenet121'],
170 | )
171 |
--------------------------------------------------------------------------------
/torchattack/ltp.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import torch
4 |
5 | from torchattack.attack import Attack, register_attack
6 | from torchattack.generative._weights import GeneratorWeights, GeneratorWeightsEnum
7 | from torchattack.generative.resnet_generator import ResNetGenerator
8 |
9 |
10 | class LTPWeights(GeneratorWeightsEnum):
11 | """
12 | Pretrained weights for the LTP attack generator are sourced from [the original
13 | implementation of the LTP
14 | attack](https://github.com/krishnakanthnakka/Transferable_Perturbations#installation).
15 | """
16 |
17 | DENSENET121_IMAGENET = GeneratorWeights(
18 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/ltp_densenet121_1_net_g.pth'
19 | )
20 | INCEPTION_V3_IMAGENET = GeneratorWeights(
21 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/ltp_inception_v3_1_net_g.pth'
22 | )
23 | RESNET152_IMAGENET = GeneratorWeights(
24 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/ltp_resnet152_1_net_g.pth'
25 | )
26 | SQUEEZENET_IMAGENET = GeneratorWeights(
27 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/ltp_squeezenet_1_net_g.pth'
28 | )
29 | VGG16_IMAGENET = GeneratorWeights(
30 | url='https://github.com/spencerwooo/torchattack/releases/download/v1.0-weights/ltp_vgg16_1_net_g.pth'
31 | )
32 | DEFAULT = RESNET152_IMAGENET
33 |
34 |
35 | @register_attack(category='GENERATIVE')
36 | class LTP(Attack):
37 | """LTP Attack (Learning Transferable Adversarial Perturbations).
38 |
39 | > From the paper: [Learning Transferable Adversarial
40 | Perturbations](https://proceedings.neurips.cc/paper/2021/hash/7486cef2522ee03547cfb970a404a874-Abstract.html).
41 |
42 | Args:
43 | device: Device to use for tensors. Defaults to cuda if available.
44 | eps: The maximum perturbation. Defaults to 10/255.
45 | weights: Pretrained weights for the generator. Either import and use the enum,
46 | or use its name. Defaults to LTPWeights.DEFAULT.
47 | checkpoint_path: Path to a custom checkpoint. Defaults to None.
48 | clip_min: Minimum value for clipping. Defaults to 0.0.
49 | clip_max: Maximum value for clipping. Defaults to 1.0.
50 | """
51 |
52 | def __init__(
53 | self,
54 | device: torch.device | None = None,
55 | eps: float = 10 / 255,
56 | weights: LTPWeights | str | None = LTPWeights.DEFAULT,
57 | checkpoint_path: str | None = None,
58 | inception: bool | None = None,
59 | clip_min: float = 0.0,
60 | clip_max: float = 1.0,
61 | ) -> None:
62 | # Generative attacks do not require specifying model and normalize
63 | super().__init__(model=None, normalize=None, device=device)
64 |
65 | self.eps = eps
66 | self.checkpoint_path = checkpoint_path
67 | self.clip_min = clip_min
68 | self.clip_max = clip_max
69 |
70 | self.weights = LTPWeights.verify(weights)
71 |
72 | # Whether is inception or not (crop layer 3x300x300 to 3x299x299)
73 | is_inception = (
74 | inception
75 | if inception is not None
76 | else (self.weights.inception if self.weights is not None else False)
77 | )
78 |
79 | # Initialize the generator
80 | self.generator = ResNetGenerator(inception=is_inception)
81 |
82 | # Load the weights
83 | if self.checkpoint_path is not None:
84 | self.generator.load_state_dict(
85 | torch.load(self.checkpoint_path, weights_only=True)
86 | )
87 | elif self.weights is not None:
88 | self.generator.load_state_dict(self.weights.get_state_dict(check_hash=True))
89 |
90 | self.generator.eval().to(self.device)
91 |
92 | def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> torch.Tensor:
93 | """Perform LTP on a batch of images.
94 |
95 | Args:
96 | x: A batch of images. Shape: (N, C, H, W).
97 |
98 | Returns:
99 | The perturbed images if successful. Shape: (N, C, H, W).
100 | """
101 |
102 | x_unrestricted = self.generator(x)
103 | delta = torch.clamp(x_unrestricted - x, -self.eps, self.eps)
104 | x_adv = torch.clamp(x + delta, self.clip_min, self.clip_max)
105 | return x_adv
106 |
107 |
108 | if __name__ == '__main__':
109 | from torchattack.evaluate import run_attack
110 |
111 | run_attack(
112 | attack=LTP,
113 | attack_args={'eps': 10 / 255, 'weights': 'DENSENET121_IMAGENET'},
114 | model_name='vgg16',
115 | victim_model_names=['resnet152'],
116 | )
117 |
--------------------------------------------------------------------------------
/torchattack/mifgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class MIFGSM(Attack):
12 | """The MI-FGSM (Momentum Iterative FGSM) attack.
13 |
14 | > From the paper: [Boosting Adversarial Attacks with Momentum](https://arxiv.org/abs/1710.06081).
15 |
16 | Args:
17 | model: The model to attack.
18 | normalize: A transform to normalize images.
19 | device: Device to use for tensors. Defaults to cuda if available.
20 | eps: The maximum perturbation. Defaults to 8/255.
21 | steps: Number of steps. Defaults to 10.
22 | alpha: Step size, `eps / steps` if None. Defaults to None.
23 | decay: Decay factor for the momentum term. Defaults to 1.0.
24 | clip_min: Minimum value for clipping. Defaults to 0.0.
25 | clip_max: Maximum value for clipping. Defaults to 1.0.
26 | targeted: Targeted attack if True. Defaults to False.
27 | """
28 |
29 | def __init__(
30 | self,
31 | model: nn.Module | AttackModel,
32 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
33 | device: torch.device | None = None,
34 | eps: float = 8 / 255,
35 | steps: int = 10,
36 | alpha: float | None = None,
37 | decay: float = 1.0,
38 | clip_min: float = 0.0,
39 | clip_max: float = 1.0,
40 | targeted: bool = False,
41 | ) -> None:
42 | super().__init__(model, normalize, device)
43 |
44 | self.eps = eps
45 | self.steps = steps
46 | self.alpha = alpha
47 | self.decay = decay
48 | self.clip_min = clip_min
49 | self.clip_max = clip_max
50 | self.targeted = targeted
51 | self.lossfn = nn.CrossEntropyLoss()
52 |
53 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
54 | """Perform MI-FGSM on a batch of images.
55 |
56 | Args:
57 | x: A batch of images. Shape: (N, C, H, W).
58 | y: A batch of labels. Shape: (N).
59 |
60 | Returns:
61 | The perturbed images if successful. Shape: (N, C, H, W).
62 | """
63 |
64 | g = torch.zeros_like(x)
65 | delta = torch.zeros_like(x, requires_grad=True)
66 |
67 | # If alpha is not given, set to eps / steps
68 | if self.alpha is None:
69 | self.alpha = self.eps / self.steps
70 |
71 | # Perform MI-FGSM
72 | for _ in range(self.steps):
73 | # Compute loss
74 | outs = self.model(self.normalize(x + delta))
75 | loss = self.lossfn(outs, y)
76 |
77 | if self.targeted:
78 | loss = -loss
79 |
80 | # Compute gradient
81 | loss.backward()
82 |
83 | if delta.grad is None:
84 | continue
85 |
86 | # Apply momentum term
87 | g = self.decay * g + delta.grad / torch.mean(
88 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
89 | )
90 |
91 | # Update delta
92 | delta.data = delta.data + self.alpha * g.sign()
93 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
94 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
95 |
96 | # Zero out gradient
97 | delta.grad.detach_()
98 | delta.grad.zero_()
99 |
100 | return x + delta
101 |
102 |
103 | if __name__ == '__main__':
104 | from torchattack.evaluate import run_attack
105 |
106 | run_attack(
107 | attack=MIFGSM,
108 | attack_args={'eps': 8 / 255, 'steps': 10},
109 | model_name='resnet18',
110 | victim_model_names=['resnet50', 'vgg13', 'densenet121'],
111 | )
112 |
--------------------------------------------------------------------------------
/torchattack/mig.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class MIG(Attack):
12 | """The MIG (Momentum Integrated Gradients) attack.
13 |
14 | > From the paper: [Transferable Adversarial Attack for Both Vision Transformers and
15 | Convolutional Networks via Momentum Integrated Gradients](https://openaccess.thecvf.com/content/ICCV2023/html/Ma_Transferable_Adversarial_Attack_for_Both_Vision_Transformers_and_Convolutional_Networks_ICCV_2023_paper.html).
16 |
17 | Args:
18 | model: The model to attack.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | eps: The maximum perturbation. Defaults to 8/255.
22 | steps: Number of steps. Defaults to 10.
23 | alpha: Step size, `eps / steps` if None. Defaults to None.
24 | decay: Decay factor for the momentum term. Defaults to 1.0.
25 | s_factor: Number of scaled interpolation iterations, $T$. Defaults to 25.
26 | clip_min: Minimum value for clipping. Defaults to 0.0.
27 | clip_max: Maximum value for clipping. Defaults to 1.0.
28 | targeted: Targeted attack if True. Defaults to False.
29 | """
30 |
31 | def __init__(
32 | self,
33 | model: nn.Module | AttackModel,
34 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
35 | device: torch.device | None = None,
36 | eps: float = 8 / 255,
37 | steps: int = 10,
38 | alpha: float | None = None,
39 | decay: float = 1.0,
40 | s_factor: int = 25,
41 | clip_min: float = 0.0,
42 | clip_max: float = 1.0,
43 | targeted: bool = False,
44 | ) -> None:
45 | super().__init__(model, normalize, device)
46 |
47 | self.eps = eps
48 | self.steps = steps
49 | self.alpha = alpha
50 | self.decay = decay
51 | self.s_factor = s_factor
52 | self.clip_min = clip_min
53 | self.clip_max = clip_max
54 | self.targeted = targeted
55 | self.lossfn = nn.CrossEntropyLoss()
56 |
57 | def _get_scaled_samples(self, x: torch.Tensor) -> torch.Tensor:
58 | xb = torch.zeros_like(x)
59 | xss = [xb + (i + 1) / self.s_factor * (x - xb) for i in range(self.s_factor)]
60 | return torch.cat(xss, dim=0)
61 |
62 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
63 | """Perform MIG on a batch of images.
64 |
65 | Args:
66 | x: A batch of images. Shape: (N, C, H, W).
67 | y: A batch of labels. Shape: (N).
68 |
69 | Returns:
70 | The perturbed images if successful. Shape: (N, C, H, W).
71 | """
72 |
73 | g = torch.zeros_like(x)
74 | delta = torch.zeros_like(x, requires_grad=True)
75 | # delta.data.uniform_(-self.eps, self.eps)
76 |
77 | # If alpha is not given, set to eps / steps
78 | if self.alpha is None:
79 | self.alpha = self.eps / self.steps
80 |
81 | xbase = torch.zeros_like(x)
82 |
83 | # Perform MIG
84 | for _ in range(self.steps):
85 | # Compute loss
86 | scaled_samples = self._get_scaled_samples(x + delta)
87 | logits = self.model(self.normalize(scaled_samples))
88 |
89 | # Softmax over logits
90 | probs = nn.functional.softmax(logits, dim=1)
91 |
92 | # Get loss
93 | loss = torch.mean(probs.gather(1, y.repeat(self.s_factor).view(-1, 1)))
94 |
95 | if self.targeted:
96 | loss = -loss
97 |
98 | # Compute gradient over backprop
99 | loss.backward()
100 |
101 | if delta.grad is None:
102 | continue
103 |
104 | # Integrated gradient
105 | igrad = (x + delta - xbase) * delta.grad / self.s_factor
106 |
107 | # Apply momentum term
108 | g = self.decay * g + igrad / torch.mean(
109 | torch.abs(igrad), dim=(1, 2, 3), keepdim=True
110 | )
111 |
112 | # Update delta
113 | delta.data = delta.data + self.alpha * g.sign()
114 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
115 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
116 |
117 | # Zero out gradient
118 | delta.grad.detach_()
119 | delta.grad.zero_()
120 |
121 | return x + delta
122 |
123 |
124 | if __name__ == '__main__':
125 | from torchattack.evaluate import run_attack
126 |
127 | run_attack(
128 | attack=MIG,
129 | attack_args={'eps': 8 / 255, 'steps': 10},
130 | model_name='timm/vit_base_patch16_224',
131 | victim_model_names=['resnet50', 'vgg13', 'densenet121'],
132 | save_adv_batch=1,
133 | )
134 |
--------------------------------------------------------------------------------
/torchattack/naa.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack._rgetattr import rgetattr
7 | from torchattack.attack import Attack, register_attack
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | @register_attack()
12 | class NAA(Attack):
13 | """The NAA (Neuron Attribution-based) Attack.
14 |
15 | > From the paper: [Improving Adversarial Transferability via Neuron Attribution-Based
16 | Attacks](https://arxiv.org/abs/2204.00008).
17 |
18 | Args:
19 | model: The model to attack.
20 | normalize: A transform to normalize images.
21 | device: Device to use for tensors. Defaults to cuda if available.
22 | eps: The maximum perturbation. Defaults to 8/255.
23 | steps: Number of steps. Defaults to 10.
24 | alpha: Step size, `eps / steps` if None. Defaults to None.
25 | decay: Decay factor for the momentum term. Defaults to 1.0.
26 | num_ens: Number of aggregate gradients (NAA use `N` in the original paper
27 | instead of `num_ens` in FIA). Defaults to 30.
28 | feature_layer_cfg: Name of the feature layer to attack. If not provided, tries
29 | to infer from built-in config based on the model name. Defaults to ""
30 | clip_min: Minimum value for clipping. Defaults to 0.0.
31 | clip_max: Maximum value for clipping. Defaults to 1.0.
32 | """
33 |
34 | _builtin_cfgs = {
35 | 'inception_v3': 'Mixed_5b',
36 | 'inception_v4': 'Mixed_5e',
37 | 'inception_resnet_v2': 'conv2d_4a',
38 | 'resnet50': 'layer2.3', # ( not present in the paper)
39 | 'resnet152': 'layer2.7',
40 | }
41 |
42 | def __init__(
43 | self,
44 | model: nn.Module | AttackModel,
45 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
46 | device: torch.device | None = None,
47 | eps: float = 8 / 255,
48 | steps: int = 10,
49 | alpha: float | None = None,
50 | decay: float = 1.0,
51 | num_ens: int = 30,
52 | feature_layer_cfg: str = '',
53 | clip_min: float = 0.0,
54 | clip_max: float = 1.0,
55 | ) -> None:
56 | # If `feature_layer_cfg` is not provided, try to infer used feature layer from
57 | # the `model_name` attribute (automatically attached during instantiation)
58 | if not feature_layer_cfg and isinstance(model, AttackModel):
59 | feature_layer_cfg = self._builtin_cfgs[model.model_name]
60 |
61 | # Delay initialization to avoid overriding the model's `model_name` attribute
62 | super().__init__(model, normalize, device)
63 |
64 | self.eps = eps
65 | self.steps = steps
66 | self.alpha = alpha
67 | self.decay = decay
68 | self.num_ens = num_ens
69 | self.clip_min = clip_min
70 | self.clip_max = clip_max
71 |
72 | self.feature_layer_cfg = feature_layer_cfg
73 | self.feature_module = rgetattr(self.model, feature_layer_cfg)
74 |
75 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
76 | """Perform NAA on a batch of images.
77 |
78 | Args:
79 | x: A batch of images. Shape: (N, C, H, W).
80 | y: A batch of labels. Shape: (N).
81 |
82 | Returns:
83 | The perturbed images if successful. Shape: (N, C, H, W).
84 | """
85 |
86 | g = torch.zeros_like(x)
87 | delta = torch.zeros_like(x, requires_grad=True)
88 |
89 | # If alpha is not given, set to eps / steps
90 | if self.alpha is None:
91 | self.alpha = self.eps / self.steps
92 |
93 | hf = self.feature_module.register_forward_hook(self._forward_hook)
94 | hb = self.feature_module.register_full_backward_hook(self._backward_hook)
95 |
96 | # NAA's FIA-like gradient aggregation on ensembles
97 | # Aggregate gradients across multiple samples to estimate neuron importance
98 | agg_grad: torch.Tensor | float = 0.0
99 | for i in range(self.num_ens):
100 | # Create scaled variants of input
101 | xm = torch.zeros_like(x)
102 | xm = xm + x.clone().detach() * i / self.num_ens
103 |
104 | # Get model outputs and compute gradients
105 | outs = self.model(self.normalize(xm))
106 | outs = torch.softmax(outs, 1)
107 |
108 | loss = torch.stack([outs[bi][y[bi]] for bi in range(x.shape[0])]).sum()
109 | loss.backward()
110 |
111 | # Accumulate gradients
112 | agg_grad += self.mid_grad[0].detach()
113 |
114 | # Average the gradients
115 | agg_grad /= self.num_ens
116 | hb.remove()
117 |
118 | # Get initial feature map
119 | xp = torch.zeros_like(x) # x_prime
120 | self.model(self.normalize(xp))
121 | yp = self.mid_output.detach().clone() # y_prime
122 |
123 | # Perform NAA
124 | for _ in range(self.steps):
125 | # Pass through the model
126 | _ = self.model(self.normalize(x + delta))
127 |
128 | # Calculate loss based on feature map diff weighted by neuron importance
129 | loss = ((self.mid_output - yp) * agg_grad).sum()
130 | loss.backward()
131 |
132 | if delta.grad is None:
133 | continue
134 |
135 | # Apply momentum term
136 | g = self.decay * g + delta.grad / torch.mean(
137 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
138 | )
139 |
140 | # Update delta
141 | delta.data = delta.data - self.alpha * g.sign()
142 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
143 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
144 |
145 | # Zero out gradient
146 | delta.grad.detach_()
147 | delta.grad.zero_()
148 |
149 | hf.remove()
150 | return x + delta
151 |
152 | def _forward_hook(self, m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
153 | self.mid_output = o
154 |
155 | def _backward_hook(self, m: nn.Module, i: torch.Tensor, o: torch.Tensor) -> None:
156 | self.mid_grad = o
157 |
158 |
159 | if __name__ == '__main__':
160 | from torchattack.evaluate import run_attack
161 |
162 | run_attack(
163 | NAA,
164 | attack_args={'feature_layer_cfg': 'layer2'},
165 | model_name='resnet50',
166 | victim_model_names=['resnet18', 'vgg13', 'densenet121'],
167 | )
168 |
--------------------------------------------------------------------------------
/torchattack/nifgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class NIFGSM(Attack):
12 | """The NI-FGSM (Nesterov-accelerated Iterative FGSM) attack.
13 |
14 | Note:
15 | This attack does not apply the scale-invariant method. For the original attack
16 | proposed in the paper (SI-NI-FGSM), see `torchattack.sinifgsm.SINIFGSM`.
17 |
18 | > From the paper: [Nesterov Accelerated Gradient and Scale Invariance for Adversarial
19 | Attacks](https://arxiv.org/abs/1908.06281).
20 |
21 | Args:
22 | model: The model to attack.
23 | normalize: A transform to normalize images.
24 | device: Device to use for tensors. Defaults to cuda if available.
25 | eps: The maximum perturbation. Defaults to 8/255.
26 | steps: Number of steps. Defaults to 10.
27 | alpha: Step size, `eps / steps` if None. Defaults to None.
28 | decay: Decay factor for the momentum term. Defaults to 1.0.
29 | clip_min: Minimum value for clipping. Defaults to 0.0.
30 | clip_max: Maximum value for clipping. Defaults to 1.0.
31 | targeted: Targeted attack if True. Defaults to False.
32 | """
33 |
34 | def __init__(
35 | self,
36 | model: nn.Module | AttackModel,
37 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
38 | device: torch.device | None = None,
39 | eps: float = 8 / 255,
40 | steps: int = 10,
41 | alpha: float | None = None,
42 | decay: float = 1.0,
43 | clip_min: float = 0.0,
44 | clip_max: float = 1.0,
45 | targeted: bool = False,
46 | ) -> None:
47 | super().__init__(model, normalize, device)
48 |
49 | self.eps = eps
50 | self.steps = steps
51 | self.alpha = alpha
52 | self.decay = decay
53 | self.clip_min = clip_min
54 | self.clip_max = clip_max
55 | self.targeted = targeted
56 | self.lossfn = nn.CrossEntropyLoss()
57 |
58 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
59 | """Perform NI-FGSM on a batch of images.
60 |
61 | Args:
62 | x: A batch of images. Shape: (N, C, H, W).
63 | y: A batch of labels. Shape: (N).
64 |
65 | Returns:
66 | The perturbed images if successful. Shape: (N, C, H, W).
67 | """
68 |
69 | g = torch.zeros_like(x)
70 | delta = torch.zeros_like(x, requires_grad=True)
71 |
72 | # If alpha is not given, set to eps / steps
73 | if self.alpha is None:
74 | self.alpha = self.eps / self.steps
75 |
76 | # Perform NI-FGSM
77 | for _ in range(self.steps):
78 | # Nesterov gradient component
79 | nes = self.alpha * self.decay * g
80 | x_nes = x + delta + nes
81 |
82 | # Compute loss
83 | outs = self.model(self.normalize(x_nes))
84 | loss = self.lossfn(outs, y)
85 |
86 | if self.targeted:
87 | loss = -loss
88 |
89 | # Compute gradient
90 | loss.backward()
91 |
92 | if delta.grad is None:
93 | continue
94 |
95 | # Apply momentum term
96 | g = self.decay * delta.grad + delta.grad / torch.mean(
97 | torch.abs(delta.grad), dim=(1, 2, 3), keepdim=True
98 | )
99 |
100 | # Update delta
101 | delta.data = delta.data + self.alpha * g.sign()
102 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
103 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
104 |
105 | # Zero out gradient
106 | delta.grad.detach_()
107 | delta.grad.zero_()
108 |
109 | return x + delta
110 |
111 |
112 | if __name__ == '__main__':
113 | from torchattack.evaluate import run_attack
114 |
115 | run_attack(NIFGSM, {'eps': 8 / 255, 'steps': 10})
116 |
--------------------------------------------------------------------------------
/torchattack/pgd.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class PGD(Attack):
12 | """The Projected Gradient Descent (PGD) attack.
13 |
14 | > From the paper: [Towards Deep Learning Models Resistant to Adversarial
15 | Attacks](https://arxiv.org/abs/1706.06083).
16 |
17 | Args:
18 | model: The model to attack.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | eps: The maximum perturbation. Defaults to 8/255.
22 | steps: Number of steps. Defaults to 10.
23 | alpha: Step size, `eps / steps` if None. Defaults to None.
24 | random_start: Start from random uniform perturbation. Defaults to True.
25 | clip_min: Minimum value for clipping. Defaults to 0.0.
26 | clip_max: Maximum value for clipping. Defaults to 1.0.
27 | targeted: Targeted attack if True. Defaults to False.
28 | """
29 |
30 | def __init__(
31 | self,
32 | model: nn.Module | AttackModel,
33 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
34 | device: torch.device | None = None,
35 | eps: float = 8 / 255,
36 | steps: int = 10,
37 | alpha: float | None = None,
38 | random_start: bool = True,
39 | clip_min: float = 0.0,
40 | clip_max: float = 1.0,
41 | targeted: bool = False,
42 | ) -> None:
43 | super().__init__(model, normalize, device)
44 |
45 | self.eps = eps
46 | self.alpha = alpha
47 | self.steps = steps
48 | self.random_start = random_start
49 | self.clip_min = clip_min
50 | self.clip_max = clip_max
51 | self.targeted = targeted
52 | self.lossfn = nn.CrossEntropyLoss()
53 |
54 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
55 | """Perform PGD on a batch of images.
56 |
57 | Args:
58 | x: A batch of images. Shape: (N, C, H, W).
59 | y: A batch of labels. Shape: (N).
60 |
61 | Returns:
62 | The perturbed images if successful. Shape: (N, C, H, W).
63 | """
64 |
65 | # If random start enabled, delta (perturbation) is then randomly
66 | # initialized with samples from a uniform distribution.
67 | if self.random_start:
68 | delta = torch.empty_like(x).uniform_(-self.eps, self.eps)
69 | delta = torch.clamp(x + delta, self.clip_min, self.clip_max) - x
70 | delta.requires_grad_()
71 | else:
72 | delta = torch.zeros_like(x, requires_grad=True)
73 |
74 | # If alpha is not given, set to eps / steps
75 | if self.alpha is None:
76 | self.alpha = self.eps / self.steps
77 |
78 | # Perform PGD
79 | for _ in range(self.steps):
80 | # Compute loss
81 | outs = self.model(self.normalize(x + delta))
82 | loss = self.lossfn(outs, y)
83 |
84 | if self.targeted:
85 | loss = -loss
86 |
87 | # Compute gradient
88 | loss.backward()
89 |
90 | if delta.grad is None:
91 | continue
92 |
93 | # Update delta
94 | g = delta.grad.data.sign()
95 |
96 | delta.data = delta.data + self.alpha * g
97 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
98 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
99 |
100 | # Zero out gradient
101 | delta.grad.detach_()
102 | delta.grad.zero_()
103 |
104 | return x + delta
105 |
106 |
107 | @register_attack()
108 | class IFGSM(PGD):
109 | """Iterative Fast Gradient Sign Method (I-FGSM) attack.
110 |
111 | > From the paper: [Adversarial Examples in the Physical
112 | World](https://arxiv.org/abs/1607.02533).
113 | """
114 |
115 | def __init__(
116 | self,
117 | model: nn.Module | AttackModel,
118 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
119 | device: torch.device | None = None,
120 | eps: float = 8 / 255,
121 | steps: int = 10,
122 | alpha: float | None = None,
123 | clip_min: float = 0.0,
124 | clip_max: float = 1.0,
125 | targeted: bool = False,
126 | ) -> None:
127 | super().__init__(
128 | model,
129 | normalize,
130 | device,
131 | eps,
132 | steps,
133 | alpha,
134 | random_start=False, # key difference
135 | clip_min=clip_min,
136 | clip_max=clip_max,
137 | targeted=targeted,
138 | )
139 |
140 |
141 | if __name__ == '__main__':
142 | from torchattack.evaluate import run_attack
143 |
144 | run_attack(
145 | attack=PGD,
146 | attack_args={'eps': 8 / 255, 'steps': 20, 'random_start': True},
147 | model_name='resnet18',
148 | victim_model_names=['resnet50', 'vgg13', 'densenet121'],
149 | )
150 |
--------------------------------------------------------------------------------
/torchattack/pgdl2.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 | EPS_FOR_DIVISION = 1e-12
10 |
11 |
12 | @register_attack()
13 | class PGDL2(Attack):
14 | """The Projected Gradient Descent (PGD) attack, with L2 constraint.
15 |
16 | > From the paper: [Towards Deep Learning Models Resistant to Adversarial
17 | Attacks](https://arxiv.org/abs/1706.06083).
18 |
19 | Args:
20 | model: The model to attack.
21 | normalize: A transform to normalize images.
22 | device: Device to use for tensors. Defaults to cuda if available.
23 | eps: The maximum perturbation, measured in L2. Defaults to 1.0.
24 | steps: Number of steps. Defaults to 10.
25 | alpha: Step size, `eps / steps` if None. Defaults to None.
26 | random_start: Start from random uniform perturbation. Defaults to True.
27 | clip_min: Minimum value for clipping. Defaults to 0.0.
28 | clip_max: Maximum value for clipping. Defaults to 1.0.
29 | targeted: Targeted attack if True. Defaults to False.
30 | """
31 |
32 | def __init__(
33 | self,
34 | model: nn.Module | AttackModel,
35 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
36 | device: torch.device | None = None,
37 | eps: float = 1.0,
38 | steps: int = 10,
39 | alpha: float | None = None,
40 | random_start: bool = True,
41 | clip_min: float = 0.0,
42 | clip_max: float = 1.0,
43 | targeted: bool = False,
44 | ) -> None:
45 | super().__init__(model, normalize, device)
46 |
47 | self.eps = eps
48 | self.alpha = alpha
49 | self.steps = steps
50 | self.random_start = random_start
51 | self.clip_min = clip_min
52 | self.clip_max = clip_max
53 | self.targeted = targeted
54 | self.lossfn = nn.CrossEntropyLoss()
55 |
56 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
57 | """Perform PGD on a batch of images.
58 |
59 | Args:
60 | x: A batch of images. Shape: (N, C, H, W).
61 | y: A batch of labels. Shape: (N).
62 |
63 | Returns:
64 | The perturbed images if successful. Shape: (N, C, H, W).
65 | """
66 |
67 | # If random start enabled, delta (perturbation) is then randomly
68 | # initialized with samples from a uniform distribution.
69 | if self.random_start:
70 | delta = torch.empty_like(x).normal_()
71 | delta_flat = delta.reshape(x.size(0), -1)
72 |
73 | n = delta_flat.norm(p=2, dim=1).view(x.size(0), 1, 1, 1)
74 | r = torch.zeros_like(n).uniform_(0, 1)
75 |
76 | delta = delta * r / n * self.eps
77 | delta.requires_grad_()
78 |
79 | else:
80 | delta = torch.zeros_like(x, requires_grad=True)
81 |
82 | # If alpha is not given, set to eps / steps
83 | if self.alpha is None:
84 | self.alpha = self.eps / self.steps
85 |
86 | # Perform PGD
87 | for _ in range(self.steps):
88 | # Compute loss
89 | outs = self.model(self.normalize(x + delta))
90 | loss = self.lossfn(outs, y)
91 |
92 | if self.targeted:
93 | loss = -loss
94 |
95 | # Compute gradient
96 | loss.backward()
97 |
98 | if delta.grad is None:
99 | continue
100 |
101 | # Update delta
102 | g = delta.grad
103 |
104 | g_norms = (
105 | torch.norm(g.reshape(x.size(0), -1), p=2, dim=1) + EPS_FOR_DIVISION
106 | )
107 | g = g / g_norms.view(x.size(0), 1, 1, 1)
108 | delta.data = delta.data + self.alpha * g
109 |
110 | delta_norms = torch.norm(delta.reshape(x.size(0), -1), p=2, dim=1)
111 | factor = self.eps / delta_norms
112 | factor = torch.min(factor, torch.ones_like(delta_norms))
113 | delta.data = delta.data * factor.view(-1, 1, 1, 1)
114 |
115 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
116 |
117 | # Zero out gradient
118 | delta.grad.detach_()
119 | delta.grad.zero_()
120 |
121 | return x + delta
122 |
123 |
124 | if __name__ == '__main__':
125 | from torchattack.evaluate import run_attack
126 |
127 | run_attack(PGDL2, {'eps': 1.0, 'steps': 10, 'random_start': False})
128 |
--------------------------------------------------------------------------------
/torchattack/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/torchattack/e83d54e804fac6de23f3c4efcc1ab8e59d93a7c8/torchattack/py.typed
--------------------------------------------------------------------------------
/torchattack/sinifgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class SINIFGSM(Attack):
12 | """The SI-NI-FGSM (Scale-invariant Nesterov-accelerated Iterative FGSM) attack.
13 |
14 | > From the paper: [Nesterov Accelerated Gradient and Scale Invariance for Adversarial
15 | Attacks](https://arxiv.org/abs/1908.06281).
16 |
17 | Args:
18 | model: The model to attack.
19 | normalize: A transform to normalize images.
20 | device: Device to use for tensors. Defaults to cuda if available.
21 | eps: The maximum perturbation. Defaults to 8/255.
22 | steps: Number of steps. Defaults to 10.
23 | alpha: Step size, `eps / steps` if None. Defaults to None.
24 | decay: Decay factor for the momentum term. Defaults to 1.0.
25 | m: Number of scaled copies of the image. Defaults to 3.
26 | clip_min: Minimum value for clipping. Defaults to 0.0.
27 | clip_max: Maximum value for clipping. Defaults to 1.0.
28 | targeted: Targeted attack if True. Defaults to False.
29 | """
30 |
31 | def __init__(
32 | self,
33 | model: nn.Module | AttackModel,
34 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
35 | device: torch.device | None = None,
36 | eps: float = 8 / 255,
37 | steps: int = 10,
38 | alpha: float | None = None,
39 | decay: float = 1.0,
40 | m: int = 3,
41 | clip_min: float = 0.0,
42 | clip_max: float = 1.0,
43 | targeted: bool = False,
44 | ) -> None:
45 | super().__init__(model, normalize, device)
46 |
47 | self.eps = eps
48 | self.steps = steps
49 | self.alpha = alpha
50 | self.decay = decay
51 | self.m = m
52 | self.clip_min = clip_min
53 | self.clip_max = clip_max
54 | self.targeted = targeted
55 | self.lossfn = nn.CrossEntropyLoss()
56 |
57 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
58 | """Perform SI-NI-FGSM on a batch of images.
59 |
60 | Args:
61 | x: A batch of images. Shape: (N, C, H, W).
62 | y: A batch of labels. Shape: (N).
63 |
64 | Returns:
65 | The perturbed images if successful. Shape: (N, C, H, W).
66 | """
67 |
68 | g = torch.zeros_like(x)
69 | delta = torch.zeros_like(x, requires_grad=True)
70 |
71 | # If alpha is not given, set to eps / steps
72 | if self.alpha is None:
73 | self.alpha = self.eps / self.steps
74 |
75 | # Perform SI-NI-FGSM
76 | for _ in range(self.steps):
77 | # Nesterov gradient component
78 | nes = self.alpha * self.decay * g
79 | x_nes = x + delta + nes
80 |
81 | # Gradient is computed over scaled copies
82 | grad = torch.zeros_like(x)
83 |
84 | # Obtain scaled copies of the images
85 | for i in torch.arange(self.m):
86 | x_ness = x_nes / torch.pow(2, i)
87 |
88 | # Compute loss
89 | outs = self.model(self.normalize(x_ness))
90 | loss = self.lossfn(outs, y)
91 |
92 | if self.targeted:
93 | loss = -loss
94 |
95 | # Compute gradient
96 | loss.backward()
97 |
98 | if delta.grad is None:
99 | continue
100 |
101 | grad += delta.grad
102 |
103 | # Average gradient over scaled copies
104 | grad /= self.m
105 |
106 | # Apply momentum term
107 | g = self.decay * grad + grad / torch.mean(
108 | torch.abs(grad), dim=(1, 2, 3), keepdim=True
109 | )
110 |
111 | # Update delta
112 | delta.data = delta.data + self.alpha * g.sign()
113 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
114 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
115 |
116 | # Zero out gradient
117 | if delta.grad is not None:
118 | delta.grad.detach_()
119 | delta.grad.zero_()
120 |
121 | return x + delta
122 |
123 |
124 | if __name__ == '__main__':
125 | from torchattack.evaluate import run_attack
126 |
127 | run_attack(SINIFGSM, {'eps': 8 / 255, 'steps': 10, 'm': 3})
128 |
--------------------------------------------------------------------------------
/torchattack/ssp.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 | import torchvision as tv
6 |
7 | from torchattack.attack import Attack, register_attack
8 | from torchattack.attack_model import AttackModel
9 |
10 |
11 | class PerceptualCriteria(nn.Module):
12 | def __init__(self, ssp_layer: int) -> None:
13 | super().__init__()
14 | self.ssp_layer = ssp_layer
15 |
16 | # Use pretrained VGG16 for perceptual loss
17 | vgg16 = tv.models.vgg16(weights='DEFAULT')
18 |
19 | # Initialize perceptual model and loss function
20 | self.perceptual_model = nn.Sequential(*list(vgg16.features))[:ssp_layer]
21 | self.perceptual_model.eval()
22 | self.loss_fn = nn.MSELoss()
23 |
24 | def forward(self, x: torch.Tensor, xadv: torch.Tensor) -> torch.Tensor:
25 | x_outs = self.perceptual_model(x)
26 | xadv_outs = self.perceptual_model(xadv)
27 | loss: torch.Tensor = self.loss_fn(x_outs, xadv_outs)
28 | return loss
29 |
30 | def __repr__(self) -> str:
31 | return f'{self.__class__.__name__}(ssp_layer={self.ssp_layer})'
32 |
33 | def __eq__(self, other: Any) -> bool:
34 | return (
35 | isinstance(other, PerceptualCriteria) and self.ssp_layer == other.ssp_layer
36 | )
37 |
38 |
39 | @register_attack()
40 | class SSP(Attack):
41 | """The Self-supervised (SSP) attack.
42 |
43 | > From the paper: [A Self-supervised Approach for Adversarial
44 | Robustness](https://arxiv.org/abs/2006.04924).
45 |
46 | Note:
47 | The SSP attack requires the `torchvision` package as it uses the pretrained
48 | VGG-16 model from `torchvision.models`.
49 |
50 | Args:
51 | model: The model to attack.
52 | normalize: A transform to normalize images.
53 | device: Device to use for tensors. Defaults to cuda if available.
54 | eps: The maximum perturbation. Defaults to 8/255.
55 | steps: Number of steps. Defaults to 100.
56 | alpha: Step size, `eps / steps` if None. Defaults to None.
57 | ssp_layer: The VGG layer to use for the perceptual loss. Defaults to 16.
58 | clip_min: Minimum value for clipping. Defaults to 0.0.
59 | clip_max: Maximum value for clipping. Defaults to 1.0.
60 | targeted: Targeted attack if True. Defaults to False.
61 | """
62 |
63 | def __init__(
64 | self,
65 | model: nn.Module | AttackModel,
66 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
67 | device: torch.device | None = None,
68 | eps: float = 8 / 255,
69 | steps: int = 100,
70 | alpha: float | None = None,
71 | ssp_layer: int = 16,
72 | clip_min: float = 0.0,
73 | clip_max: float = 1.0,
74 | targeted: bool = False,
75 | ) -> None:
76 | super().__init__(model, normalize, device)
77 |
78 | self.eps = eps
79 | self.steps = steps
80 | self.alpha = alpha
81 | self.ssp_layer = ssp_layer
82 | self.clip_min = clip_min
83 | self.clip_max = clip_max
84 | self.targeted = targeted
85 |
86 | self.perceptual_criteria = PerceptualCriteria(ssp_layer).to(device)
87 |
88 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
89 | """Perform SSP on a batch of images.
90 |
91 | Args:
92 | x: A batch of images. Shape: (N, C, H, W).
93 | y: A batch of labels, not required. Shape: (N).
94 |
95 | Returns:
96 | The perturbed images if successful. Shape: (N, C, H, W).
97 | """
98 |
99 | delta = torch.randn_like(x, requires_grad=True)
100 |
101 | # If alpha is not given, set to eps / steps
102 | if self.alpha is None:
103 | self.alpha = self.eps / self.steps
104 |
105 | for _ in range(self.steps):
106 | xadv = x + delta
107 | loss = self.perceptual_criteria(self.normalize(x), self.normalize(xadv))
108 | loss.backward()
109 |
110 | if delta.grad is None:
111 | continue
112 |
113 | # Update delta
114 | g = delta.grad.data.sign()
115 |
116 | delta.data = delta.data + self.alpha * g
117 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
118 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
119 |
120 | # Zero out gradient
121 | delta.grad.detach_()
122 | delta.grad.zero_()
123 |
124 | return x + delta
125 |
126 |
127 | if __name__ == '__main__':
128 | from torchattack.evaluate import run_attack
129 |
130 | run_attack(SSP, attack_args={'eps': 16 / 255, 'ssp_layer': 16})
131 |
--------------------------------------------------------------------------------
/torchattack/tifgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import numpy as np
4 | import torch
5 | import torch.nn as nn
6 | import torch.nn.functional as f
7 |
8 | from torchattack.attack import Attack, register_attack
9 | from torchattack.attack_model import AttackModel
10 |
11 |
12 | @register_attack()
13 | class TIFGSM(Attack):
14 | """The TI-FGSM (Translation-invariant Iterative FGSM) attack.
15 |
16 | > From the paper: [Evading Defenses to Transferable Adversarial Examples by
17 | Translation-Invariant Attacks](https://arxiv.org/abs/1904.02884).
18 |
19 | Note:
20 | Key parameters include `kernel_len` and `n_sig`, which defines the size and
21 | the radius of the gaussian kernel. The default values are set to 15 and 3
22 | respectively, which are best according to the paper.
23 |
24 | Args:
25 | model: The model to attack.
26 | normalize: A transform to normalize images.
27 | device: Device to use for tensors. Defaults to cuda if available.
28 | eps: The maximum perturbation. Defaults to 8/255.
29 | steps: Number of steps. Defaults to 10.
30 | alpha: Step size, `eps / steps` if None. Defaults to None.
31 | decay: Decay factor for the momentum term. Defaults to 1.0.
32 | kern_len: Length of the kernel (should be an odd number). Defaults to 15.
33 | n_sig: Radius of the gaussian kernel. Defaults to 3.
34 | clip_min: Minimum value for clipping. Defaults to 0.0.
35 | clip_max: Maximum value for clipping. Defaults to 1.0.
36 | targeted: Targeted attack if True. Defaults to False.
37 | """
38 |
39 | def __init__(
40 | self,
41 | model: nn.Module | AttackModel,
42 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
43 | device: torch.device | None = None,
44 | eps: float = 8 / 255,
45 | steps: int = 10,
46 | alpha: float | None = None,
47 | decay: float = 1.0,
48 | kern_len: int = 15,
49 | n_sig: int = 3,
50 | clip_min: float = 0.0,
51 | clip_max: float = 1.0,
52 | targeted: bool = False,
53 | ) -> None:
54 | super().__init__(model, normalize, device)
55 |
56 | self.eps = eps
57 | self.steps = steps
58 | self.alpha = alpha
59 | self.decay = decay
60 | self.kern_len = kern_len
61 | self.n_sig = n_sig
62 | self.clip_min = clip_min
63 | self.clip_max = clip_max
64 | self.targeted = targeted
65 | self.lossfn = nn.CrossEntropyLoss()
66 |
67 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
68 | """Perform TI-FGSM on a batch of images.
69 |
70 | Args:
71 | x: A batch of images. Shape: (N, C, H, W).
72 | y: A batch of labels. Shape: (N).
73 |
74 | Returns:
75 | The perturbed images if successful. Shape: (N, C, H, W).
76 | """
77 |
78 | g = torch.zeros_like(x)
79 | delta = torch.zeros_like(x, requires_grad=True)
80 |
81 | # Get kernel
82 | kernel = self.get_kernel()
83 |
84 | # If alpha is not given, set to eps / steps
85 | if self.alpha is None:
86 | self.alpha = self.eps / self.steps
87 |
88 | # Perform TI-FGSM
89 | for _ in range(self.steps):
90 | # Compute loss
91 | outs = self.model(self.normalize(x + delta))
92 | loss = self.lossfn(outs, y)
93 |
94 | if self.targeted:
95 | loss = -loss
96 |
97 | # Compute gradient
98 | loss.backward()
99 |
100 | if delta.grad is None:
101 | continue
102 |
103 | # Apply kernel to gradient
104 | g = f.conv2d(delta.grad, kernel, stride=1, padding='same', groups=3)
105 |
106 | # Apply momentum term
107 | g = self.decay * g + g / torch.mean(
108 | torch.abs(g), dim=(1, 2, 3), keepdim=True
109 | )
110 |
111 | # Update delta
112 | delta.data = delta.data + self.alpha * g.sign()
113 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
114 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
115 |
116 | # Zero out gradient
117 | delta.grad.detach_()
118 | delta.grad.zero_()
119 |
120 | return x + delta
121 |
122 | def get_kernel(self) -> torch.Tensor:
123 | kernel = self.gkern(self.kern_len, self.n_sig).astype(np.float32)
124 |
125 | kernel = np.expand_dims(kernel, axis=0) # (W, H) -> (1, W, H)
126 | kernel = np.repeat(kernel, 3, axis=0) # -> (C, W, H)
127 | kernel = np.expand_dims(kernel, axis=1) # -> (C, 1, W, H)
128 | return torch.from_numpy(kernel).to(self.device)
129 |
130 | @staticmethod
131 | def gkern(kern_len: int = 15, n_sig: int = 3) -> np.ndarray:
132 | """Return a 2D Gaussian kernel array."""
133 |
134 | import scipy.stats as st
135 |
136 | interval = (2 * n_sig + 1.0) / kern_len
137 | x = np.linspace(-n_sig - interval / 2.0, n_sig + interval / 2.0, kern_len + 1)
138 | kern1d = np.diff(st.norm.cdf(x))
139 | kernel_raw = np.sqrt(np.outer(kern1d, kern1d))
140 | return np.array(kernel_raw / kernel_raw.sum(), dtype=np.float32)
141 |
142 |
143 | if __name__ == '__main__':
144 | from torchattack.evaluate import run_attack
145 |
146 | run_attack(TIFGSM, {'eps': 8 / 255, 'steps': 10, 'kern_len': 15, 'n_sig': 3})
147 |
--------------------------------------------------------------------------------
/torchattack/vmifgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class VMIFGSM(Attack):
12 | """The VMI-FGSM (Variance-tuned Momentum Iterative FGSM) attack.
13 |
14 | > From the paper: [Enhancing the Transferability of Adversarial Attacks through
15 | Variance Tuning](https://arxiv.org/abs/2103.15571).
16 |
17 | Note:
18 | Key parameters are `n` and `beta`, where `n` is the number of sampled
19 | examples for variance tuning and `beta` is the upper bound of the
20 | neighborhood for varying the perturbation.
21 |
22 | Args:
23 | model: The model to attack.
24 | normalize: A transform to normalize images.
25 | device: Device to use for tensors. Defaults to cuda if available.
26 | eps: The maximum perturbation. Defaults to 8/255.
27 | steps: Number of steps. Defaults to 10.
28 | alpha: Step size, `eps / steps` if None. Defaults to None.
29 | decay: Decay factor for the momentum term. Defaults to 1.0.
30 | n: Number of sampled examples for variance tuning. Defaults to 5.
31 | beta: The upper bound of the neighborhood. Defaults to 1.5.
32 | clip_min: Minimum value for clipping. Defaults to 0.0.
33 | clip_max: Maximum value for clipping. Defaults to 1.0.
34 | targeted: Targeted attack if True. Defaults to False.
35 | """
36 |
37 | def __init__(
38 | self,
39 | model: nn.Module | AttackModel,
40 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
41 | device: torch.device | None = None,
42 | eps: float = 8 / 255,
43 | steps: int = 10,
44 | alpha: float | None = None,
45 | decay: float = 1.0,
46 | n: int = 5,
47 | beta: float = 1.5,
48 | clip_min: float = 0.0,
49 | clip_max: float = 1.0,
50 | targeted: bool = False,
51 | ) -> None:
52 | super().__init__(model, normalize, device)
53 |
54 | self.eps = eps
55 | self.steps = steps
56 | self.alpha = alpha
57 | self.decay = decay
58 | self.n = n
59 | self.beta = beta
60 | self.clip_min = clip_min
61 | self.clip_max = clip_max
62 | self.targeted = targeted
63 | self.lossfn = nn.CrossEntropyLoss()
64 |
65 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
66 | """Perform VMI-FGSM on a batch of images.
67 |
68 | Args:
69 | x: A batch of images. Shape: (N, C, H, W).
70 | y: A batch of labels. Shape: (N).
71 |
72 | Returns:
73 | The perturbed images if successful. Shape: (N, C, H, W).
74 | """
75 |
76 | g = torch.zeros_like(x) # Momentum
77 | v = torch.zeros_like(x) # Gradient variance
78 | delta = torch.zeros_like(x, requires_grad=True)
79 |
80 | # If alpha is not given, set to eps / steps
81 | if self.alpha is None:
82 | self.alpha = self.eps / self.steps
83 |
84 | # Perform VMI-FGSM
85 | for _ in range(self.steps):
86 | # Compute loss
87 | outs = self.model(self.normalize(x + delta))
88 | loss = self.lossfn(outs, y)
89 |
90 | if self.targeted:
91 | loss = -loss
92 |
93 | # Compute gradient
94 | loss.backward()
95 |
96 | if delta.grad is None:
97 | continue
98 |
99 | # Apply momentum term and variance
100 | delta_grad = delta.grad
101 | g = self.decay * g + (delta_grad + v) / torch.mean(
102 | torch.abs(delta_grad + v), dim=(1, 2, 3), keepdim=True
103 | )
104 |
105 | # Compute gradient variance
106 | gv_grad = torch.zeros_like(x)
107 | for _ in range(self.n):
108 | # Get neighboring samples perturbation
109 | neighbors = delta.data + torch.randn_like(x).uniform_(
110 | -self.eps * self.beta, self.eps * self.beta
111 | )
112 | neighbors.requires_grad_()
113 | neighbor_outs = self.model(self.normalize(x + neighbors))
114 | neighbor_loss = self.lossfn(neighbor_outs, y)
115 |
116 | if self.targeted:
117 | neighbor_loss = -neighbor_loss
118 |
119 | neighbor_loss.backward()
120 |
121 | if neighbors.grad is None:
122 | continue
123 |
124 | gv_grad += neighbors.grad
125 |
126 | # Accumulate gradient variance into v
127 | v = gv_grad / self.n - delta_grad
128 |
129 | # Update delta
130 | delta.data = delta.data + self.alpha * g.sign()
131 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
132 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
133 |
134 | # Zero out gradient
135 | delta.grad.detach_()
136 | delta.grad.zero_()
137 |
138 | return x + delta
139 |
140 |
141 | if __name__ == '__main__':
142 | from torchattack.evaluate import run_attack
143 |
144 | run_attack(VMIFGSM, {'eps': 8 / 255, 'steps': 10, 'n': 5, 'beta': 1.5})
145 |
--------------------------------------------------------------------------------
/torchattack/vnifgsm.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from torchattack.attack import Attack, register_attack
7 | from torchattack.attack_model import AttackModel
8 |
9 |
10 | @register_attack()
11 | class VNIFGSM(Attack):
12 | """The VNI-FGSM (Variance-tuned Nesterov Iterative FGSM) attack.
13 |
14 | > From the paper: [Enhancing the Transferability of Adversarial Attacks through
15 | Variance Tuning](https://arxiv.org/abs/2103.15571).
16 |
17 | Note:
18 | Key parameters are `n` and `beta`, where `n` is the number of sampled
19 | examples for variance tuning and `beta` is the upper bound of the
20 | neighborhood for varying the perturbation.
21 |
22 | Args:
23 | model: The model to attack.
24 | normalize: A transform to normalize images.
25 | device: Device to use for tensors. Defaults to cuda if available.
26 | eps: The maximum perturbation. Defaults to 8/255.
27 | steps: Number of steps. Defaults to 10.
28 | alpha: Step size, `eps / steps` if None. Defaults to None.
29 | decay: Decay factor for the momentum term. Defaults to 1.0.
30 | n: Number of sampled examples for variance tuning. Defaults to 5.
31 | beta: The upper bound of the neighborhood. Defaults to 1.5.
32 | clip_min: Minimum value for clipping. Defaults to 0.0.
33 | clip_max: Maximum value for clipping. Defaults to 1.0.
34 | targeted: Targeted attack if True. Defaults to False.
35 | """
36 |
37 | def __init__(
38 | self,
39 | model: nn.Module | AttackModel,
40 | normalize: Callable[[torch.Tensor], torch.Tensor] | None = None,
41 | device: torch.device | None = None,
42 | eps: float = 8 / 255,
43 | steps: int = 10,
44 | alpha: float | None = None,
45 | decay: float = 1.0,
46 | n: int = 5,
47 | beta: float = 1.5,
48 | clip_min: float = 0.0,
49 | clip_max: float = 1.0,
50 | targeted: bool = False,
51 | ) -> None:
52 | super().__init__(model, normalize, device)
53 |
54 | self.eps = eps
55 | self.steps = steps
56 | self.alpha = alpha
57 | self.decay = decay
58 | self.n = n
59 | self.beta = beta
60 | self.clip_min = clip_min
61 | self.clip_max = clip_max
62 | self.targeted = targeted
63 | self.lossfn = nn.CrossEntropyLoss()
64 |
65 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
66 | """Perform VNI-FGSM on a batch of images.
67 |
68 | Args:
69 | x: A batch of images. Shape: (N, C, H, W).
70 | y: A batch of labels. Shape: (N).
71 |
72 | Returns:
73 | The perturbed images if successful. Shape: (N, C, H, W).
74 | """
75 |
76 | g = torch.zeros_like(x) # Momentum
77 | v = torch.zeros_like(x) # Gradient variance
78 | delta = torch.zeros_like(x, requires_grad=True)
79 |
80 | # If alpha is not given, set to eps / steps
81 | if self.alpha is None:
82 | self.alpha = self.eps / self.steps
83 |
84 | # Perform VNI-FGSM
85 | for _ in range(self.steps):
86 | # Nesterov gradient component
87 | nes = self.alpha * self.decay * g
88 | x_nes = x + delta + nes
89 |
90 | # Compute loss
91 | outs = self.model(self.normalize(x_nes))
92 | loss = self.lossfn(outs, y)
93 |
94 | if self.targeted:
95 | loss = -loss
96 |
97 | # Compute gradient
98 | loss.backward()
99 |
100 | if delta.grad is None:
101 | continue
102 |
103 | # Apply momentum term and variance
104 | delta_grad = delta.grad
105 | g = self.decay * g + (delta_grad + v) / torch.mean(
106 | torch.abs(delta_grad + v), dim=(1, 2, 3), keepdim=True
107 | )
108 |
109 | # Compute gradient variance
110 | gv_grad = torch.zeros_like(x)
111 | for _ in range(self.n):
112 | # Get neighboring samples perturbation
113 | neighbors = delta.data + torch.randn_like(x).uniform_(
114 | -self.eps * self.beta, self.eps * self.beta
115 | )
116 | neighbors.requires_grad_()
117 | neighbor_outs = self.model(self.normalize(x + neighbors))
118 | neighbor_loss = self.lossfn(neighbor_outs, y)
119 |
120 | if self.targeted:
121 | neighbor_loss = -neighbor_loss
122 |
123 | neighbor_loss.backward()
124 |
125 | if neighbors.grad is None:
126 | continue
127 |
128 | gv_grad += neighbors.grad
129 |
130 | # Accumulate gradient variance into v
131 | v = gv_grad / self.n - delta_grad
132 |
133 | # Update delta
134 | delta.data = delta.data + self.alpha * g.sign()
135 | delta.data = torch.clamp(delta.data, -self.eps, self.eps)
136 | delta.data = torch.clamp(x + delta.data, self.clip_min, self.clip_max) - x
137 |
138 | # Zero out gradient
139 | delta.grad.detach_()
140 | delta.grad.zero_()
141 |
142 | return x + delta
143 |
144 |
145 | if __name__ == '__main__':
146 | from torchattack.evaluate import run_attack
147 |
148 | run_attack(VNIFGSM, {'eps': 8 / 255, 'steps': 10, 'n': 5, 'beta': 1.5})
149 |
--------------------------------------------------------------------------------