tokenfool 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tokenfool-0.0.1/LICENSE +21 -0
- tokenfool-0.0.1/PKG-INFO +95 -0
- tokenfool-0.0.1/README.md +71 -0
- tokenfool-0.0.1/pyproject.toml +46 -0
- tokenfool-0.0.1/setup.cfg +4 -0
- tokenfool-0.0.1/tests/test_att.py +237 -0
- tokenfool-0.0.1/tests/test_patchfool.py +174 -0
- tokenfool-0.0.1/tests/test_timm_vit_adapter.py +226 -0
- tokenfool-0.0.1/tokenfool/__init__.py +0 -0
- tokenfool-0.0.1/tokenfool/adapters/__init__.py +0 -0
- tokenfool-0.0.1/tokenfool/adapters/interfaces.py +66 -0
- tokenfool-0.0.1/tokenfool/adapters/timm_vit.py +237 -0
- tokenfool-0.0.1/tokenfool/attacks/__init__.py +0 -0
- tokenfool-0.0.1/tokenfool/attacks/att.py +427 -0
- tokenfool-0.0.1/tokenfool/attacks/patchfool.py +378 -0
- tokenfool-0.0.1/tokenfool/attacks/utils.py +14 -0
- tokenfool-0.0.1/tokenfool.egg-info/PKG-INFO +95 -0
- tokenfool-0.0.1/tokenfool.egg-info/SOURCES.txt +19 -0
- tokenfool-0.0.1/tokenfool.egg-info/dependency_links.txt +1 -0
- tokenfool-0.0.1/tokenfool.egg-info/requires.txt +16 -0
- tokenfool-0.0.1/tokenfool.egg-info/top_level.txt +1 -0
tokenfool-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TrustThink
|
|
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.
|
tokenfool-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tokenfool
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Author-email: Allison Lane <lane_allison@trustthink.net>
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: torch
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest; extra == "test"
|
|
13
|
+
Requires-Dist: timm; extra == "test"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: ruff; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest; extra == "dev"
|
|
18
|
+
Requires-Dist: timm; extra == "dev"
|
|
19
|
+
Requires-Dist: torchvision; extra == "dev"
|
|
20
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
21
|
+
Requires-Dist: tqdm; extra == "dev"
|
|
22
|
+
Requires-Dist: ipywidgets; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# tokenfool
|
|
26
|
+
|
|
27
|
+
Widely recognized methods for producing adversarial examples (e.g., PGD, FGSM) were developed and popularized in the context of convolutional neural network (CNN)-based image models. These attacks operate in continuous input space and rely on end-to-end differentiability; in practice, they are typically applied as pixel-level perturbations to induce misclassification. Existing open-source adversarial toolkits such as [Foolbox](https://github.com/bethgelab/foolbox), [CleverHans](https://github.com/cleverhans-lab/cleverhans), and [Adversarial Robustness Toolbox (ART)](https://github.com/Trusted-AI/adversarial-robustness-toolbox) consolidate these implementations, enabling users to benchmark robustness and evaluate defenses through standardized interfaces.
|
|
28
|
+
|
|
29
|
+
In recent years, transformer-based models have emerged as a dominant alternative to CNNs in computer vision. Architectures such as ViT, DeiT, Swin, and DETR replace convolutional feature extraction with patch tokenization and self-attention mechanisms. These models are now widely deployed across classification, detection, and segmentation tasks. Like CNNs, vision transformers are vulnerable to adversarial examples under standard white-box gradient attacks. However, their token-based representation and global self-attention introduce different structural sensitivities compared to convolutional architectures. In addition to conventional pixel-space perturbations, transformers are susceptible to structured patch-level attacks, token/embedding-space perturbations, and attacks that exploit attention distributions or object query representations in detection models.
|
|
30
|
+
|
|
31
|
+
## About
|
|
32
|
+
|
|
33
|
+
TokenFool is an open-source toolkit for adversarial attacks on image transformers. It is modular and extensible, enabling straightforward integration of new attack algorithms. Attacks operate under gradient-based white-box assumptions and integrate directly with standard PyTorch training and evaluation workflows. Users can support their own models by implementing the interface contract expected by the attacks. This keeps attack code separate from provider-specific or model-specific wiring and makes it possible to extend TokenFool beyond the built-in adapters.
|
|
34
|
+
|
|
35
|
+
## Current Status
|
|
36
|
+
|
|
37
|
+
TokenFool currently includes:
|
|
38
|
+
- a core `tokenfool` package for attack implementations
|
|
39
|
+
- an adapter/interface layer for transformer classifier integrations
|
|
40
|
+
- example usage notebooks
|
|
41
|
+
- automated test coverage
|
|
42
|
+
- repository CI workflows
|
|
43
|
+
|
|
44
|
+
Current attack coverage includes:
|
|
45
|
+
- **Patch-Fool** ([paper](https://arxiv.org/abs/2203.08392))
|
|
46
|
+
- **Adaptive Token Tuning (ATT)** ([paper](https://proceedings.neurips.cc/paper_files/paper/2024/hash/24f8dd1b8f154f1ee0d7a59e368eccf3-Abstract-Conference.html))
|
|
47
|
+
|
|
48
|
+
Current model support is focused on:
|
|
49
|
+
- **timm-style ViT/DeiT classifier implementations**
|
|
50
|
+
- **user-defined models that implement the required interface contract via a custom adapter**
|
|
51
|
+
|
|
52
|
+
## Next Steps
|
|
53
|
+
|
|
54
|
+
Planned expansion areas include:
|
|
55
|
+
- additional transformer-specific attack methods
|
|
56
|
+
- broader model-family and provider coverage
|
|
57
|
+
- support for non-classification transformer settings through new interfaces and adapters
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## TokenFool Architecture
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
The library is structured as a layered system: attacks depend on stable interface contracts rather than directly on model implementations. Concrete adapters bridge those interfaces to supported model implementations, while custom adapters provide the path for integrating user-owned models without changing attack logic.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
1. Clone the repository
|
|
69
|
+
(**External contributors**: fork the repository first, then clone your fork)
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/TrustThink/tokenfool.git
|
|
72
|
+
cd tokenfool
|
|
73
|
+
```
|
|
74
|
+
2. (Recommended) Create a virtual environment
|
|
75
|
+
|
|
76
|
+
3. Install the project in editable mode with development dependencies
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e ".[dev]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
4. Make changes on a new branch, including tests were appropriate
|
|
82
|
+
|
|
83
|
+
5. Run tests
|
|
84
|
+
```bash
|
|
85
|
+
pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
6. Run lint checks
|
|
89
|
+
```bash
|
|
90
|
+
ruff check
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
7. Open a Pull Request
|
|
94
|
+
|
|
95
|
+
Repository checks also run through GitHub Actions.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# tokenfool
|
|
2
|
+
|
|
3
|
+
Widely recognized methods for producing adversarial examples (e.g., PGD, FGSM) were developed and popularized in the context of convolutional neural network (CNN)-based image models. These attacks operate in continuous input space and rely on end-to-end differentiability; in practice, they are typically applied as pixel-level perturbations to induce misclassification. Existing open-source adversarial toolkits such as [Foolbox](https://github.com/bethgelab/foolbox), [CleverHans](https://github.com/cleverhans-lab/cleverhans), and [Adversarial Robustness Toolbox (ART)](https://github.com/Trusted-AI/adversarial-robustness-toolbox) consolidate these implementations, enabling users to benchmark robustness and evaluate defenses through standardized interfaces.
|
|
4
|
+
|
|
5
|
+
In recent years, transformer-based models have emerged as a dominant alternative to CNNs in computer vision. Architectures such as ViT, DeiT, Swin, and DETR replace convolutional feature extraction with patch tokenization and self-attention mechanisms. These models are now widely deployed across classification, detection, and segmentation tasks. Like CNNs, vision transformers are vulnerable to adversarial examples under standard white-box gradient attacks. However, their token-based representation and global self-attention introduce different structural sensitivities compared to convolutional architectures. In addition to conventional pixel-space perturbations, transformers are susceptible to structured patch-level attacks, token/embedding-space perturbations, and attacks that exploit attention distributions or object query representations in detection models.
|
|
6
|
+
|
|
7
|
+
## About
|
|
8
|
+
|
|
9
|
+
TokenFool is an open-source toolkit for adversarial attacks on image transformers. It is modular and extensible, enabling straightforward integration of new attack algorithms. Attacks operate under gradient-based white-box assumptions and integrate directly with standard PyTorch training and evaluation workflows. Users can support their own models by implementing the interface contract expected by the attacks. This keeps attack code separate from provider-specific or model-specific wiring and makes it possible to extend TokenFool beyond the built-in adapters.
|
|
10
|
+
|
|
11
|
+
## Current Status
|
|
12
|
+
|
|
13
|
+
TokenFool currently includes:
|
|
14
|
+
- a core `tokenfool` package for attack implementations
|
|
15
|
+
- an adapter/interface layer for transformer classifier integrations
|
|
16
|
+
- example usage notebooks
|
|
17
|
+
- automated test coverage
|
|
18
|
+
- repository CI workflows
|
|
19
|
+
|
|
20
|
+
Current attack coverage includes:
|
|
21
|
+
- **Patch-Fool** ([paper](https://arxiv.org/abs/2203.08392))
|
|
22
|
+
- **Adaptive Token Tuning (ATT)** ([paper](https://proceedings.neurips.cc/paper_files/paper/2024/hash/24f8dd1b8f154f1ee0d7a59e368eccf3-Abstract-Conference.html))
|
|
23
|
+
|
|
24
|
+
Current model support is focused on:
|
|
25
|
+
- **timm-style ViT/DeiT classifier implementations**
|
|
26
|
+
- **user-defined models that implement the required interface contract via a custom adapter**
|
|
27
|
+
|
|
28
|
+
## Next Steps
|
|
29
|
+
|
|
30
|
+
Planned expansion areas include:
|
|
31
|
+
- additional transformer-specific attack methods
|
|
32
|
+
- broader model-family and provider coverage
|
|
33
|
+
- support for non-classification transformer settings through new interfaces and adapters
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## TokenFool Architecture
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
The library is structured as a layered system: attacks depend on stable interface contracts rather than directly on model implementations. Concrete adapters bridge those interfaces to supported model implementations, while custom adapters provide the path for integrating user-owned models without changing attack logic.
|
|
41
|
+
|
|
42
|
+
## Development
|
|
43
|
+
|
|
44
|
+
1. Clone the repository
|
|
45
|
+
(**External contributors**: fork the repository first, then clone your fork)
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/TrustThink/tokenfool.git
|
|
48
|
+
cd tokenfool
|
|
49
|
+
```
|
|
50
|
+
2. (Recommended) Create a virtual environment
|
|
51
|
+
|
|
52
|
+
3. Install the project in editable mode with development dependencies
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e ".[dev]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
4. Make changes on a new branch, including tests were appropriate
|
|
58
|
+
|
|
59
|
+
5. Run tests
|
|
60
|
+
```bash
|
|
61
|
+
pytest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
6. Run lint checks
|
|
65
|
+
```bash
|
|
66
|
+
ruff check
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
7. Open a Pull Request
|
|
70
|
+
|
|
71
|
+
Repository checks also run through GitHub Actions.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tokenfool"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Allison Lane", email="lane_allison@trustthink.net"}
|
|
10
|
+
]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license="MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"torch",
|
|
17
|
+
"numpy",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
test = [
|
|
22
|
+
"pytest",
|
|
23
|
+
"timm",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dev = [
|
|
27
|
+
"ruff",
|
|
28
|
+
"mypy",
|
|
29
|
+
"pytest",
|
|
30
|
+
"timm",
|
|
31
|
+
"torchvision",
|
|
32
|
+
"matplotlib",
|
|
33
|
+
"tqdm",
|
|
34
|
+
"ipywidgets",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.mypy]
|
|
38
|
+
python_version = "3.11"
|
|
39
|
+
ignore_missing_imports = true
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["."]
|
|
43
|
+
include = ["tokenfool*"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from tokenfool.attacks.att import ATT
|
|
6
|
+
|
|
7
|
+
MEAN = (0.485, 0.456, 0.406)
|
|
8
|
+
STD = (0.229, 0.224, 0.225)
|
|
9
|
+
|
|
10
|
+
class MockAttn(nn.Module):
|
|
11
|
+
def __init__(self, dim: int):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.qkv = nn.Linear(dim, dim)
|
|
14
|
+
self.attn_drop = nn.Identity()
|
|
15
|
+
|
|
16
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
17
|
+
return self.attn_drop(self.qkv(x))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MockBlock(nn.Module):
|
|
21
|
+
def __init__(self, dim: int):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.attn = MockAttn(dim)
|
|
24
|
+
self.mlp = nn.Linear(dim, dim)
|
|
25
|
+
|
|
26
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
27
|
+
x = x + self.attn(x)
|
|
28
|
+
x = x + self.mlp(x)
|
|
29
|
+
return x
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TinyBackbone(nn.Module):
|
|
33
|
+
def __init__(self, image_size=32, patch_size=8, dim=8, num_classes=5, num_prefix_tokens=1):
|
|
34
|
+
super().__init__()
|
|
35
|
+
assert image_size % patch_size == 0
|
|
36
|
+
patch_dim = 3 * patch_size * patch_size
|
|
37
|
+
|
|
38
|
+
self.num_prefix_tokens = num_prefix_tokens
|
|
39
|
+
self.patch_size = patch_size
|
|
40
|
+
self.patch_proj = nn.Linear(patch_dim, dim)
|
|
41
|
+
self.prefix = nn.Parameter(torch.zeros(1, num_prefix_tokens, dim))
|
|
42
|
+
self.blocks = nn.ModuleList([MockBlock(dim), MockBlock(dim)])
|
|
43
|
+
self.head = nn.Linear(dim, num_classes)
|
|
44
|
+
|
|
45
|
+
def _to_tokens(self, x: torch.Tensor) -> torch.Tensor:
|
|
46
|
+
B, C, H, W = x.shape
|
|
47
|
+
p = self.patch_size
|
|
48
|
+
g = H // p
|
|
49
|
+
patches = x.unfold(2, p, p).unfold(3, p, p)
|
|
50
|
+
patches = patches.permute(0, 2, 3, 1, 4, 5).reshape(B, g * g, C * p * p)
|
|
51
|
+
patch_tokens = self.patch_proj(patches)
|
|
52
|
+
prefix = self.prefix.expand(B, -1, -1)
|
|
53
|
+
return torch.cat([prefix, patch_tokens], dim=1)
|
|
54
|
+
|
|
55
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
56
|
+
x = self._to_tokens(x)
|
|
57
|
+
for blk in self.blocks:
|
|
58
|
+
x = blk(x)
|
|
59
|
+
return self.head(x[:, 0])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DummyHookableViT:
|
|
63
|
+
def __init__(self, backbone: TinyBackbone):
|
|
64
|
+
self.model = backbone
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def num_prefix_tokens(self) -> int:
|
|
68
|
+
return self.model.num_prefix_tokens
|
|
69
|
+
|
|
70
|
+
def zero_grad(self, set_to_none: bool = True) -> None:
|
|
71
|
+
self.model.zero_grad(set_to_none=set_to_none)
|
|
72
|
+
|
|
73
|
+
def logits(self, x: torch.Tensor) -> torch.Tensor:
|
|
74
|
+
return self.model(x)
|
|
75
|
+
|
|
76
|
+
def hook_modules(self):
|
|
77
|
+
return {
|
|
78
|
+
"attn_drop": [blk.attn.attn_drop for blk in self.model.blocks],
|
|
79
|
+
"qkv": [blk.attn.qkv for blk in self.model.blocks],
|
|
80
|
+
"mlp": [blk.mlp for blk in self.model.blocks],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def att_feature_module(self):
|
|
84
|
+
return self.model.blocks[-2]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("patch_out", [False, True])
|
|
89
|
+
@pytest.mark.parametrize("num_prefix_tokens", [1, 2])
|
|
90
|
+
def test_att_shapes_and_bounds(patch_out, num_prefix_tokens):
|
|
91
|
+
torch.manual_seed(0)
|
|
92
|
+
device = torch.device("cpu")
|
|
93
|
+
|
|
94
|
+
B, C, H, W = 2, 3, 32, 32
|
|
95
|
+
x = torch.rand(B, C, H, W, device=device)
|
|
96
|
+
|
|
97
|
+
backbone = TinyBackbone(
|
|
98
|
+
image_size=H,
|
|
99
|
+
patch_size=8,
|
|
100
|
+
dim=32,
|
|
101
|
+
num_classes=7,
|
|
102
|
+
num_prefix_tokens=num_prefix_tokens,
|
|
103
|
+
).to(device)
|
|
104
|
+
model = DummyHookableViT(backbone)
|
|
105
|
+
|
|
106
|
+
x_adv = ATT(
|
|
107
|
+
model,
|
|
108
|
+
x,
|
|
109
|
+
y=None,
|
|
110
|
+
epsilon=8 / 255,
|
|
111
|
+
iters=3,
|
|
112
|
+
decay=1.0,
|
|
113
|
+
patch_out=patch_out,
|
|
114
|
+
patch_size=8,
|
|
115
|
+
image_size=H,
|
|
116
|
+
progress=False,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert x_adv.shape == x.shape
|
|
120
|
+
assert torch.isfinite(x_adv).all()
|
|
121
|
+
|
|
122
|
+
mu_t = torch.tensor(MEAN, device=device, dtype=x.dtype).view(1, 3, 1, 1)
|
|
123
|
+
std_t = torch.tensor(STD, device=device, dtype=x.dtype).view(1, 3, 1, 1)
|
|
124
|
+
lo = (0 - mu_t) / std_t
|
|
125
|
+
hi = (1 - mu_t) / std_t
|
|
126
|
+
|
|
127
|
+
assert torch.all(x_adv >= lo - 1e-6)
|
|
128
|
+
assert torch.all(x_adv <= hi + 1e-6)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_att_changes_input():
|
|
132
|
+
torch.manual_seed(0)
|
|
133
|
+
device = torch.device("cpu")
|
|
134
|
+
|
|
135
|
+
x = torch.rand(1, 3, 32, 32, device=device)
|
|
136
|
+
|
|
137
|
+
backbone = TinyBackbone(
|
|
138
|
+
image_size=32,
|
|
139
|
+
patch_size=8,
|
|
140
|
+
dim=32,
|
|
141
|
+
num_classes=5,
|
|
142
|
+
num_prefix_tokens=1,
|
|
143
|
+
).to(device)
|
|
144
|
+
model = DummyHookableViT(backbone)
|
|
145
|
+
|
|
146
|
+
x_adv = ATT(
|
|
147
|
+
model,
|
|
148
|
+
x,
|
|
149
|
+
epsilon=8 / 255,
|
|
150
|
+
iters=3,
|
|
151
|
+
patch_out=True,
|
|
152
|
+
patch_size=8,
|
|
153
|
+
image_size=32,
|
|
154
|
+
progress=False,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
diff = (x_adv - x).abs().max().item()
|
|
158
|
+
assert diff > 0.0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_att_targeted_smoke():
|
|
162
|
+
torch.manual_seed(0)
|
|
163
|
+
device = torch.device("cpu")
|
|
164
|
+
|
|
165
|
+
x = torch.rand(2, 3, 32, 32, device=device)
|
|
166
|
+
target = torch.tensor([1, 2], device=device)
|
|
167
|
+
|
|
168
|
+
backbone = TinyBackbone(
|
|
169
|
+
image_size=32,
|
|
170
|
+
patch_size=8,
|
|
171
|
+
dim=32,
|
|
172
|
+
num_classes=5,
|
|
173
|
+
num_prefix_tokens=1,
|
|
174
|
+
).to(device)
|
|
175
|
+
model = DummyHookableViT(backbone)
|
|
176
|
+
|
|
177
|
+
x_adv = ATT(
|
|
178
|
+
model,
|
|
179
|
+
x,
|
|
180
|
+
y=target,
|
|
181
|
+
epsilon=8 / 255,
|
|
182
|
+
iters=2,
|
|
183
|
+
targeted=True,
|
|
184
|
+
patch_out=True,
|
|
185
|
+
patch_size=8,
|
|
186
|
+
image_size=32,
|
|
187
|
+
progress=False,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert x_adv.shape == x.shape
|
|
191
|
+
assert torch.isfinite(x_adv).all()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_att_respects_pixel_epsilon_in_unnormalized_space():
|
|
195
|
+
torch.manual_seed(0)
|
|
196
|
+
device = torch.device("cpu")
|
|
197
|
+
|
|
198
|
+
x = torch.rand(1, 3, 32, 32, device=device)
|
|
199
|
+
|
|
200
|
+
backbone = TinyBackbone(
|
|
201
|
+
image_size=32,
|
|
202
|
+
patch_size=8,
|
|
203
|
+
dim=32,
|
|
204
|
+
num_classes=5,
|
|
205
|
+
num_prefix_tokens=1,
|
|
206
|
+
).to(device)
|
|
207
|
+
model = DummyHookableViT(backbone)
|
|
208
|
+
|
|
209
|
+
x_adv = ATT(
|
|
210
|
+
model,
|
|
211
|
+
x,
|
|
212
|
+
epsilon=8 / 255,
|
|
213
|
+
iters=3,
|
|
214
|
+
patch_out=True,
|
|
215
|
+
patch_size=8,
|
|
216
|
+
image_size=32,
|
|
217
|
+
progress=False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
mu_t = torch.tensor(MEAN, device=device, dtype=x.dtype).view(1, 3, 1, 1)
|
|
221
|
+
std_t = torch.tensor(STD, device=device, dtype=x.dtype).view(1, 3, 1, 1)
|
|
222
|
+
x_pix = (x * std_t + mu_t).clamp(0, 1)
|
|
223
|
+
x_adv_pix = (x_adv * std_t + mu_t).clamp(0, 1)
|
|
224
|
+
|
|
225
|
+
diff = (x_adv_pix - x_pix).abs()
|
|
226
|
+
assert torch.all(diff <= 8 / 255 + 1e-5)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_att_missing_hook_methods_raises():
|
|
230
|
+
class BadModel:
|
|
231
|
+
def logits(self, x):
|
|
232
|
+
return x.mean(dim=(2, 3))
|
|
233
|
+
|
|
234
|
+
x = torch.rand(1, 3, 32, 32)
|
|
235
|
+
|
|
236
|
+
with pytest.raises(TypeError):
|
|
237
|
+
ATT(BadModel(), x, iters=1, progress=False)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from tokenfool.attacks.patchfool import PatchFool, _infer_special_tokens_from_attn
|
|
6
|
+
|
|
7
|
+
class DummyViT(nn.Module):
|
|
8
|
+
"""
|
|
9
|
+
Minimal differentiable model TransformerClassifier for tests
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, num_classes=10, heads=2, grid=4, specials=1, layers=6):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.num_classes = num_classes
|
|
14
|
+
self.heads = heads
|
|
15
|
+
self.grid = grid
|
|
16
|
+
self.specials = specials
|
|
17
|
+
self.N = specials + grid * grid
|
|
18
|
+
self.layers = layers
|
|
19
|
+
self.proj = nn.Linear(3, num_classes)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def logits(self, x: torch.Tensor) -> torch.Tensor:
|
|
23
|
+
feat = x.mean(dim=(2, 3))
|
|
24
|
+
return self.proj(feat)
|
|
25
|
+
|
|
26
|
+
def logits_and_attn(self, x: torch.Tensor):
|
|
27
|
+
logits = self.logits(x)
|
|
28
|
+
B = x.size(0)
|
|
29
|
+
device = x.device
|
|
30
|
+
base = x.mean(dim=(2, 3), keepdim=False)
|
|
31
|
+
s = base.sum(dim=1, keepdim=True)
|
|
32
|
+
A = torch.sigmoid(s).view(B, 1, 1, 1) * torch.ones(B, self.heads, self.N, self.N, device=device)
|
|
33
|
+
A = A / (A.sum(dim=-1, keepdim=True) + 1e-12)
|
|
34
|
+
|
|
35
|
+
attn_list = [A for _ in range(self.layers)]
|
|
36
|
+
return logits, attn_list
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalized_bounds(mu, std, device):
|
|
40
|
+
mu_t = torch.tensor(mu, device=device).view(1, 3, 1, 1)
|
|
41
|
+
std_t = torch.tensor(std, device=device).view(1, 3, 1, 1)
|
|
42
|
+
lo = (0 - mu_t) / std_t
|
|
43
|
+
hi = (1 - mu_t) / std_t
|
|
44
|
+
return lo, hi, std_t
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_infer_special_tokens_basic():
|
|
48
|
+
assert _infer_special_tokens_from_attn(197) == 1
|
|
49
|
+
assert _infer_special_tokens_from_attn(198) == 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("patch_select", ["Rand", "Saliency", "Attn"])
|
|
53
|
+
def test_patchfool_shapes_and_bounds(patch_select):
|
|
54
|
+
torch.manual_seed(0)
|
|
55
|
+
device = torch.device("cpu")
|
|
56
|
+
|
|
57
|
+
B, C, H, W = 2, 3, 64, 64
|
|
58
|
+
x = torch.rand(B, C, H, W, device=device)
|
|
59
|
+
model = DummyViT(grid=H // 16, specials=1).to(device)
|
|
60
|
+
|
|
61
|
+
x_adv, mask = PatchFool(
|
|
62
|
+
model,
|
|
63
|
+
x,
|
|
64
|
+
y=None,
|
|
65
|
+
patch_size=16,
|
|
66
|
+
num_patch=1,
|
|
67
|
+
sparse_pixel_num=0,
|
|
68
|
+
patch_select=patch_select,
|
|
69
|
+
attack_mode="CE_loss",
|
|
70
|
+
iters=3, # fast
|
|
71
|
+
lr=0.1,
|
|
72
|
+
mild_l_inf=0.0,
|
|
73
|
+
mild_l_2=0.0,
|
|
74
|
+
device=device,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert x_adv.shape == x.shape
|
|
78
|
+
assert mask.shape == (B, 1, H, W)
|
|
79
|
+
|
|
80
|
+
# bounds check
|
|
81
|
+
lo, hi, _ = normalized_bounds((0.485,0.456,0.406), (0.229,0.224,0.225), device)
|
|
82
|
+
assert torch.all(x_adv >= lo - 1e-6)
|
|
83
|
+
assert torch.all(x_adv <= hi + 1e-6)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_patchfool_mask_locality_sparse0():
|
|
87
|
+
torch.manual_seed(0)
|
|
88
|
+
device = torch.device("cpu")
|
|
89
|
+
|
|
90
|
+
B, C, H, W = 1, 3, 64, 64
|
|
91
|
+
x = torch.rand(B, C, H, W, device=device)
|
|
92
|
+
model = DummyViT(grid=H // 16, specials=1).to(device)
|
|
93
|
+
|
|
94
|
+
x_adv, mask = PatchFool(
|
|
95
|
+
model,
|
|
96
|
+
x,
|
|
97
|
+
y=None,
|
|
98
|
+
patch_size=16,
|
|
99
|
+
num_patch=1,
|
|
100
|
+
sparse_pixel_num=0,
|
|
101
|
+
patch_select="Rand",
|
|
102
|
+
attack_mode="CE_loss",
|
|
103
|
+
iters=3,
|
|
104
|
+
lr=0.1,
|
|
105
|
+
device=device,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
diff = (x_adv - x).abs()
|
|
109
|
+
outside = diff * (1 - mask)
|
|
110
|
+
assert outside.max().item() <= 1e-5
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_patchfool_linf_constraint():
|
|
114
|
+
torch.manual_seed(0)
|
|
115
|
+
device = torch.device("cpu")
|
|
116
|
+
|
|
117
|
+
B, C, H, W = 1, 3, 64, 64
|
|
118
|
+
x = torch.rand(B, C, H, W, device=device)
|
|
119
|
+
model = DummyViT(grid=H // 16, specials=1).to(device)
|
|
120
|
+
|
|
121
|
+
mild_l_inf = 0.05
|
|
122
|
+
x_adv, mask = PatchFool(
|
|
123
|
+
model,
|
|
124
|
+
x,
|
|
125
|
+
y=None,
|
|
126
|
+
patch_size=16,
|
|
127
|
+
num_patch=1,
|
|
128
|
+
sparse_pixel_num=0,
|
|
129
|
+
patch_select="Rand",
|
|
130
|
+
attack_mode="CE_loss",
|
|
131
|
+
iters=5,
|
|
132
|
+
lr=0.2,
|
|
133
|
+
mild_l_inf=mild_l_inf,
|
|
134
|
+
mild_l_2=0.0,
|
|
135
|
+
device=device,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
_, _, std_t = normalized_bounds((0.485,0.456,0.406), (0.229,0.224,0.225), device)
|
|
139
|
+
eps = mild_l_inf / std_t
|
|
140
|
+
diff = (x_adv - x).abs()
|
|
141
|
+
assert torch.all(diff <= eps + 1e-5)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_patchfool_l2_constraint():
|
|
145
|
+
torch.manual_seed(0)
|
|
146
|
+
device = torch.device("cpu")
|
|
147
|
+
|
|
148
|
+
B, C, H, W = 1, 3, 64, 64
|
|
149
|
+
x = torch.rand(B, C, H, W, device=device)
|
|
150
|
+
model = DummyViT(grid=H // 16, specials=1).to(device)
|
|
151
|
+
|
|
152
|
+
mild_l_2 = 1.0
|
|
153
|
+
x_adv, mask = PatchFool(
|
|
154
|
+
model,
|
|
155
|
+
x,
|
|
156
|
+
y=None,
|
|
157
|
+
patch_size=16,
|
|
158
|
+
num_patch=1,
|
|
159
|
+
sparse_pixel_num=0,
|
|
160
|
+
patch_select="Rand",
|
|
161
|
+
attack_mode="CE_loss",
|
|
162
|
+
iters=5,
|
|
163
|
+
lr=0.2,
|
|
164
|
+
mild_l_inf=0.0,
|
|
165
|
+
mild_l_2=mild_l_2,
|
|
166
|
+
device=device,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
_, _, std_t = normalized_bounds((0.485,0.456,0.406), (0.229,0.224,0.225), device)
|
|
170
|
+
radius = (mild_l_2 / std_t).view(1, C)
|
|
171
|
+
|
|
172
|
+
pert = (x_adv - x) * mask
|
|
173
|
+
l2 = torch.linalg.norm(pert.view(B, C, -1), dim=-1)
|
|
174
|
+
assert torch.all(l2 <= radius + 1e-4)
|