├── .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 | ![torchattack](./images/torchattack.png){: style="width:600px"} 18 |
19 | 20 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/refs/heads/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 21 | [![pypi python versions](https://img.shields.io/pypi/pyversions/torchattack.svg?logo=pypi&logoColor=white&labelColor=2D3339)](https://pypi.python.org/pypi/torchattack) 22 | [![pypi version](https://img.shields.io/pypi/v/torchattack.svg?logo=pypi&logoColor=white&labelColor=2D3339)](https://pypi.python.org/pypi/torchattack) 23 | [![pypi weekly downloads](https://img.shields.io/pypi/dm/torchattack?logo=pypi&logoColor=white&labelColor=2D3339)](https://pypi.python.org/pypi/torchattack) 24 | [![lint](https://github.com/spencerwooo/torchattack/actions/workflows/ci.yml/badge.svg)](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 | ![Benign image](../images/usage/xs.png){ width=200 } 6 |
Benign Image
7 |
8 |
9 | ![Adversarial example via MI-FGSM](../images/usage/advs-mifgsm-resnet50-eps-8.png){ width=200 } 10 |
[MIFGSM](../attacks/mifgsm.md) :octicons-arrow-right-24: ResNet50
11 |
12 |
13 | ![Adversarial example via TGR](../images/usage/advs-tgr-vitb16-eps-8.png){ 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 | --------------------------------------------------------------------------------