Skip to content

Commit b427440

Browse files
author
Beat Buesser
committed
Merge remote-tracking branch 'origin/dev_1.8.0' into development_documentation
2 parents d54576e + 31f7931 commit b427440

File tree

70 files changed

+4965
-1341
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+4965
-1341
lines changed

.github/workflows/ci-pytorch-fasterrcnn.yml renamed to .github/workflows/ci-pytorch-object-detectors.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI PyTorchFasterRCNN
1+
name: CI PyTorchObjectDetectors
22
on:
33
# Run on manual trigger
44
workflow_dispatch:
@@ -20,7 +20,7 @@ on:
2020

2121
jobs:
2222
test_pytorch_fasterrcnn:
23-
name: PyTorchFasterRCNN
23+
name: PyTorchObjectDetectors
2424
runs-on: ubuntu-20.04
2525
strategy:
2626
fail-fast: false
@@ -43,7 +43,9 @@ jobs:
4343
pip install torch==1.6.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
4444
pip install torchvision==0.7.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
4545
pip install torchaudio==0.6.0 -f https://download.pytorch.org/whl/torch_stable.html
46-
- name: Run Test Action
46+
- name: Run Test Action - test_pytorch_object_detector
47+
run: pytest --cov-report=xml --cov=art --cov-append -q -vv tests/estimators/object_detection/test_pytorch_object_detector.py --framework=pytorch --durations=0
48+
- name: Run Test Action - test_pytorch_faster_rcnn
4749
run: pytest --cov-report=xml --cov=art --cov-append -q -vv tests/estimators/object_detection/test_pytorch_faster_rcnn.py --framework=pytorch --durations=0
4850
- name: Upload coverage to Codecov
4951
uses: codecov/[email protected]

.github/workflows/ci-style-checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
run: mypy art
5252
- name: pytest-flake8
5353
if: ${{ always() }}
54-
run: pytest --flake8 -v -m flake8
54+
run: pytest --flake8 -v -m flake8 --ignore=contrib
5555
- name: black
5656
if: ${{ always() }}
5757
run: |

art/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from art import preprocessing
1313

1414
# Semantic Version
15-
__version__ = "1.7.2"
15+
__version__ = "1.8.0.dev0"
1616

1717
# pylint: disable=C0103
1818

art/attacks/attack.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ class Attack(abc.ABC):
9191
"""
9292

9393
attack_params: List[str] = list()
94+
# The _estimator_requirements define the requirements an estimator must satisfy to be used as a target for an
95+
# attack. They should be a tuple of requirements, where each requirement is either a class the estimator must
96+
# inherit from, or a tuple of classes which define a union, i.e. the estimator must inherit from at least one class
97+
# in the requirement tuple.
9498
_estimator_requirements: Optional[Union[Tuple[Any, ...], Tuple[()]]] = None
9599

96100
def __init__(
@@ -111,7 +115,7 @@ def __init__(
111115
if self.estimator_requirements is None:
112116
raise ValueError("Estimator requirements have not been defined in `_estimator_requirements`.")
113117

114-
if not all(t in type(estimator).__mro__ for t in self.estimator_requirements):
118+
if not self.is_estimator_valid(estimator):
115119
raise EstimatorError(self.__class__, self.estimator_requirements, estimator)
116120

117121
self._estimator = estimator
@@ -155,6 +159,24 @@ def _check_params(self) -> None:
155159
if not isinstance(self.tensor_board, (bool, str)):
156160
raise ValueError("The argument `tensor_board` has to be either of type bool or str.")
157161

162+
def is_estimator_valid(self, estimator) -> bool:
163+
"""
164+
Checks if the given estimator satisfies the requirements for this attack.
165+
166+
:param estimator: The estimator to check.
167+
:return: True if the estimator is valid for the attack.
168+
"""
169+
170+
for req in self.estimator_requirements:
171+
# A requirement is either a class which the estimator must inherit from, or a tuple of classes and the
172+
# estimator is required to inherit from at least one of the classes
173+
if isinstance(req, tuple):
174+
if all(p not in type(estimator).__mro__ for p in req):
175+
return False
176+
elif req not in type(estimator).__mro__:
177+
return False
178+
return True
179+
158180

159181
class EvasionAttack(Attack):
160182
"""
@@ -373,7 +395,7 @@ class MembershipInferenceAttack(InferenceAttack):
373395
Abstract base class for membership inference attack classes.
374396
"""
375397

376-
def __init__(self, estimator: Union["CLASSIFIER_TYPE"]):
398+
def __init__(self, estimator):
377399
"""
378400
:param estimator: A trained estimator targeted for inference attack.
379401
:type estimator: :class:`.art.estimators.estimator.BaseEstimator`

art/attacks/evasion/projected_gradient_descent/projected_gradient_descent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
)
4646

4747
if TYPE_CHECKING:
48-
from art.utils import CLASSIFIER_LOSS_GRADIENTS_TYPE
48+
from art.utils import CLASSIFIER_LOSS_GRADIENTS_TYPE, OBJECT_DETECTOR_TYPE
4949

5050
logger = logging.getLogger(__name__)
5151

@@ -76,7 +76,7 @@ class ProjectedGradientDescent(EvasionAttack):
7676

7777
def __init__(
7878
self,
79-
estimator: "CLASSIFIER_LOSS_GRADIENTS_TYPE",
79+
estimator: Union["CLASSIFIER_LOSS_GRADIENTS_TYPE", "OBJECT_DETECTOR_TYPE"],
8080
norm: Union[int, float, str] = np.inf,
8181
eps: Union[int, float, np.ndarray] = 0.3,
8282
eps_step: Union[int, float, np.ndarray] = 0.1,

art/attacks/inference/attribute_inference/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from art.attacks.inference.attribute_inference.black_box import AttributeInferenceBlackBox
55
from art.attacks.inference.attribute_inference.baseline import AttributeInferenceBaseline
6+
from art.attacks.inference.attribute_inference.true_label_baseline import AttributeInferenceBaselineTrueLabel
67
from art.attacks.inference.attribute_inference.white_box_decision_tree import AttributeInferenceWhiteBoxDecisionTree
78
from art.attacks.inference.attribute_inference.white_box_lifestyle_decision_tree import (
89
AttributeInferenceWhiteBoxLifestyleDecisionTree,

art/attacks/inference/attribute_inference/black_box.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@
2929
from art.estimators.estimator import BaseEstimator
3030
from art.estimators.classification.classifier import ClassifierMixin
3131
from art.attacks.attack import AttributeInferenceAttack
32+
from art.estimators.regression import RegressorMixin
3233
from art.utils import check_and_transform_label_format, float_to_categorical, floats_to_one_hot
3334

3435
if TYPE_CHECKING:
35-
from art.utils import CLASSIFIER_TYPE
36+
from art.utils import CLASSIFIER_TYPE, REGRESSOR_TYPE
3637

3738
logger = logging.getLogger(__name__)
3839

@@ -47,23 +48,27 @@ class AttributeInferenceBlackBox(AttributeInferenceAttack):
4748
used as a proxy.
4849
"""
4950

50-
_estimator_requirements = (BaseEstimator, ClassifierMixin)
51+
attack_params = AttributeInferenceAttack.attack_params + ["prediction_normal_factor"]
52+
_estimator_requirements = (BaseEstimator, (ClassifierMixin, RegressorMixin))
5153

5254
def __init__(
5355
self,
54-
classifier: "CLASSIFIER_TYPE",
56+
estimator: Union["CLASSIFIER_TYPE", "REGRESSOR_TYPE"],
5557
attack_model: Optional["CLASSIFIER_TYPE"] = None,
5658
attack_feature: Union[int, slice] = 0,
59+
prediction_normal_factor: float = 1,
5760
):
5861
"""
5962
Create an AttributeInferenceBlackBox attack instance.
6063
61-
:param classifier: Target classifier.
64+
:param estimator: Target estimator.
6265
:param attack_model: The attack model to train, optional. If none is provided, a default model will be created.
6366
:param attack_feature: The index of the feature to be attacked or a slice representing multiple indexes in
6467
case of a one-hot encoded feature.
68+
:param prediction_normal_factor: If supplied, predictions of the model are multiplied by the factor when used as
69+
inputs to the attack-model. Only applicable when `estimator` is a regressor.
6570
"""
66-
super().__init__(estimator=classifier, attack_feature=attack_feature)
71+
super().__init__(estimator=estimator, attack_feature=attack_feature)
6772
if isinstance(self.attack_feature, int):
6873
self.single_index_feature = True
6974
else:
@@ -99,6 +104,9 @@ def __init__(
99104
n_iter_no_change=10,
100105
max_fun=15000,
101106
)
107+
108+
self.prediction_normal_factor = prediction_normal_factor
109+
102110
self._check_params()
103111

104112
def fit(self, x: np.ndarray) -> None:
@@ -111,12 +119,15 @@ def fit(self, x: np.ndarray) -> None:
111119
# Checks:
112120
if self.estimator.input_shape is not None:
113121
if self.estimator.input_shape[0] != x.shape[1]:
114-
raise ValueError("Shape of x does not match input_shape of classifier")
122+
raise ValueError("Shape of x does not match input_shape of model")
115123
if self.single_index_feature and self.attack_feature >= x.shape[1]:
116124
raise ValueError("attack_feature must be a valid index to a feature in x")
117125

118126
# get model's predictions for x
119-
predictions = np.array([np.argmax(arr) for arr in self.estimator.predict(x)]).reshape(-1, 1)
127+
if ClassifierMixin in type(self.estimator).__mro__:
128+
predictions = np.array([np.argmax(arr) for arr in self.estimator.predict(x)]).reshape(-1, 1)
129+
else: # Regression model
130+
predictions = self.estimator.predict(x).reshape(-1, 1) * self.prediction_normal_factor
120131

121132
# get vector of attacked feature
122133
y = x[:, self.attack_feature]
@@ -155,9 +166,12 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
155166
raise ValueError("Number of rows in x and y do not match")
156167
if self.estimator.input_shape is not None:
157168
if self.single_index_feature and self.estimator.input_shape[0] != x.shape[1] + 1:
158-
raise ValueError("Number of features in x + 1 does not match input_shape of classifier")
169+
raise ValueError("Number of features in x + 1 does not match input_shape of model")
159170

160-
x_test = np.concatenate((x, y), axis=1).astype(np.float32)
171+
if RegressorMixin in type(self.estimator).__mro__:
172+
x_test = np.concatenate((x, y * self.prediction_normal_factor), axis=1).astype(np.float32)
173+
else:
174+
x_test = np.concatenate((x, y), axis=1).astype(np.float32)
161175

162176
if self.single_index_feature:
163177
if "values" not in kwargs.keys():
@@ -182,3 +196,7 @@ def _check_params(self) -> None:
182196
raise ValueError("Attack feature must be either an integer or a slice object.")
183197
if isinstance(self.attack_feature, int) and self.attack_feature < 0:
184198
raise ValueError("Attack feature index must be positive.")
199+
200+
if RegressorMixin not in type(self.estimator).__mro__:
201+
if self.prediction_normal_factor != 1:
202+
raise ValueError("Prediction normal factor is only applicable to regressor models.")

art/attacks/inference/attribute_inference/meminf_based.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
from art.estimators.estimator import BaseEstimator
2929
from art.estimators.classification.classifier import ClassifierMixin
3030
from art.attacks.attack import AttributeInferenceAttack, MembershipInferenceAttack
31+
from art.estimators.regression import RegressorMixin
3132
from art.exceptions import EstimatorError
3233

3334
if TYPE_CHECKING:
34-
from art.utils import CLASSIFIER_TYPE
35+
from art.utils import CLASSIFIER_TYPE, REGRESSOR_TYPE
3536

3637
logger = logging.getLogger(__name__)
3738

@@ -44,26 +45,26 @@ class AttributeInferenceMembership(AttributeInferenceAttack):
4445
as a member with the highest confidence.
4546
"""
4647

47-
_estimator_requirements = (BaseEstimator, ClassifierMixin)
48+
_estimator_requirements = (BaseEstimator, (ClassifierMixin, RegressorMixin))
4849

4950
def __init__(
5051
self,
51-
classifier: "CLASSIFIER_TYPE",
52+
estimator: Union["CLASSIFIER_TYPE", "REGRESSOR_TYPE"],
5253
membership_attack: MembershipInferenceAttack,
5354
attack_feature: Union[int, slice] = 0,
5455
):
5556
"""
5657
Create an AttributeInferenceMembership attack instance.
5758
58-
:param classifier: Target classifier.
59+
:param estimator: Target estimator.
5960
:param membership_attack: The membership inference attack to use. Should be fit/calibrated in advance, and
60-
should support returning probabilities.
61+
should support returning probabilities. Should also support the target estimator.
6162
:param attack_feature: The index of the feature to be attacked or a slice representing multiple indexes in
6263
case of a one-hot encoded feature.
6364
"""
64-
super().__init__(estimator=classifier, attack_feature=attack_feature)
65-
if not all(t in type(classifier).__mro__ for t in membership_attack.estimator_requirements):
66-
raise EstimatorError(membership_attack, membership_attack.estimator_requirements, classifier)
65+
super().__init__(estimator=estimator, attack_feature=attack_feature)
66+
if not membership_attack.is_estimator_valid(estimator):
67+
raise EstimatorError(membership_attack.__class__, membership_attack.estimator_requirements, estimator)
6768

6869
self.membership_attack = membership_attack
6970
self._check_params()
@@ -84,7 +85,7 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
8485
"""
8586
if self.estimator.input_shape is not None:
8687
if isinstance(self.attack_feature, int) and self.estimator.input_shape[0] != x.shape[1] + 1:
87-
raise ValueError("Number of features in x + 1 does not match input_shape of classifier")
88+
raise ValueError("Number of features in x + 1 does not match input_shape of the estimator")
8889

8990
if "values" not in kwargs.keys():
9091
raise ValueError("Missing parameter `values`.")
@@ -96,11 +97,11 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
9697
if y.shape[0] != x.shape[0]:
9798
raise ValueError("Number of rows in x and y do not match")
9899

99-
# assumes single index
100+
# single index
100101
if isinstance(self.attack_feature, int):
101102
first = True
102103
for value in values:
103-
v_full = np.full((x.shape[0], 1), value).astype(np.float32)
104+
v_full = np.full((x.shape[0], 1), value).astype(x.dtype)
104105
x_value = np.concatenate((x[:, : self.attack_feature], v_full), axis=1)
105106
x_value = np.concatenate((x_value, x[:, self.attack_feature :]), axis=1)
106107

@@ -112,7 +113,7 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
112113
probabilities = np.hstack((probabilities, predicted))
113114

114115
# needs to be of type float so we can later replace back the actual values
115-
value_indexes = np.argmax(probabilities, axis=1).astype(np.float32)
116+
value_indexes = np.argmax(probabilities, axis=1).astype(x.dtype)
116117
pred_values = np.zeros_like(value_indexes)
117118
for index, value in enumerate(values):
118119
pred_values[value_indexes == index] = value
@@ -134,7 +135,7 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
134135
else:
135136
probabilities = np.hstack((probabilities, predicted))
136137
first = False
137-
value_indexes = np.argmax(probabilities, axis=1).astype(np.float32)
138+
value_indexes = np.argmax(probabilities, axis=1).astype(x.dtype)
138139
pred_values = np.zeros_like(probabilities)
139140
for index, value in enumerate(values):
140141
curr_value = np.zeros(len(values))

0 commit comments

Comments
 (0)