Skip to content

Commit

Permalink
Merge pull request #1884 from twosixlabs/develop
Browse files Browse the repository at this point in the history
Armory v0.16.5 release candidate
  • Loading branch information
lcadalzo authored Feb 6, 2023
2 parents 7d15f9a + a3aa999 commit 562f150
Show file tree
Hide file tree
Showing 80 changed files with 319 additions and 210 deletions.
1 change: 1 addition & 0 deletions armory/art_experimental/attacks/poison_loader_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def insert(self, x: np.ndarray) -> np.ndarray:
raise ValueError("Shift + Backdoor length is greater than audio's length.")

audio[shift : shift + bd_length] += self.scaled_trigger
audio = np.clip(audio, -1.0, 1.0)
return audio.astype(original_dtype)


Expand Down
59 changes: 59 additions & 0 deletions armory/baseline_models/pytorch/resnet18.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ def get_art_model_sgd(
model_kwargs: dict, wrapper_kwargs: dict, weights_path: Optional[str] = None
) -> PyTorchClassifier:

"""Returns the same model as get_art_model, but with the SGD optimizer.
Some poisoning attacks were found to be brittle with regard to optimizer.
"""

model = OuterModel(weights_path=weights_path, **model_kwargs)

lr = wrapper_kwargs.pop("learning_rate", 0.1)
Expand All @@ -107,3 +111,58 @@ def get_art_model_sgd(
clip_values=(0.0, 1.0),
)
return wrapped_model


class ResnetForCifarSleeperAgent(torch.nn.Module):
def __init__(
self,
**model_kwargs,
):
"""This version of the Resnet imitates that found in the ART example notebook for the Sleeper Agent attack:
https://github.com/Trusted-AI/adversarial-robustness-toolbox/blob/main/notebooks/poisoning_attack_sleeper_agent_pytorch.ipynb
Sleeper Agent is somewhat brittle and is not successful against the above torchvision.models.resnet18
with the current attack parameters; hence the inclusion of this version.
"""

data_means = model_kwargs.pop("data_means", CIFAR10_MEANS)
data_stds = model_kwargs.pop("data_stds", CIFAR10_STDS)

super().__init__()
self.inner_model = models.ResNet(
models.resnet.BasicBlock, [2, 2, 2, 2], num_classes=10
)

self.data_means = torch.tensor(data_means, dtype=torch.float32, device=DEVICE)
self.data_stdev = torch.tensor(data_stds, dtype=torch.float32, device=DEVICE)

def forward(self, x: torch.Tensor) -> torch.Tensor:
x_norm = ((x - self.data_means) / self.data_stdev).permute(0, 3, 1, 2)
output = self.inner_model(x_norm)

return output


def get_art_model_cifar_sleeper_agent(
model_kwargs: dict, wrapper_kwargs: dict, weights_path: Optional[str] = None
) -> PyTorchClassifier:
"""Return ART-wrapped Resnet version specific for sleeper agent poisoning on Cifar10"""

model = ResnetForCifarSleeperAgent(weights_path=weights_path, **model_kwargs)
lr = wrapper_kwargs.pop("learning_rate", 0.1)
optimizer = torch.optim.SGD(
model.parameters(), lr=lr, momentum=0.9, weight_decay=5e-4, nesterov=True
)

wrapped_model = PyTorchClassifier(
model,
loss=torch.nn.CrossEntropyLoss(),
optimizer=optimizer,
input_shape=wrapper_kwargs.pop(
"input_shape", (224, 224, 3)
), # default to imagenet shape
channels_first=False,
**wrapper_kwargs,
clip_values=(0.0, 1.0),
)
return wrapped_model
26 changes: 17 additions & 9 deletions armory/baseline_models/tf_graph/audio_resnet50.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def get_spectrogram(audio):
return spectrogram # shape (124, 129, 1)


def make_audio_resnet(**kwargs) -> tf.keras.Model:
def make_audio_resnet(sequential=True, **kwargs) -> tf.keras.Model:

inputs = keras.Input(shape=(16000,))
spectrogram = Lambda(lambda audio: get_spectrogram(audio))(inputs)
Expand All @@ -32,8 +32,10 @@ def make_audio_resnet(**kwargs) -> tf.keras.Model:
)

model = keras.Model(resnet.inputs, resnet.outputs)
# ART's TensorFlowV2Classifier get_activations() requires a Sequential model
model = keras.Sequential([model])
if sequential:
# ART's TensorFlowV2Classifier get_activations() requires a Sequential model
model = keras.Sequential([model])

model.compile(
optimizer=tf.keras.optimizers.Adam(),
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
Expand All @@ -47,12 +49,7 @@ def get_art_model(
model_kwargs: dict, wrapper_kwargs: dict, weights_path: Optional[str] = None
):

if weights_path:
raise ValueError(
"This model is implemented for poisoning and does not (yet) load saved weights."
)

model = make_audio_resnet(**model_kwargs)
model = make_audio_resnet(sequential=True, **model_kwargs)

loss_object = losses.SparseCategoricalCrossentropy()

Expand All @@ -73,3 +70,14 @@ def train_step(model, samples, labels):
)

return art_classifier


def get_unwrapped_model(
weights_path: str,
**model_kwargs,
):
# This is used for the explanatory model for the poisoning fairness metrics
model = make_audio_resnet(sequential=False, **model_kwargs)
model.load_weights(weights_path)

return model
120 changes: 85 additions & 35 deletions armory/metrics/poisoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,46 @@
from PIL import Image
import numpy as np
import torch
import tensorflow as tf

from armory.data.utils import maybe_download_weights_from_s3

# An armory user may request one of these models under 'adhoc'/'explanatory_model'
EXPLANATORY_MODEL_CONFIGS = explanatory_model_configs = {
"cifar10_silhouette_model": {
"speech_commands_explanatory_model": {
"activation_layer": "avg_pool",
"data_modality": "audio",
"model_framework": "tensorflow",
"module": "armory.baseline_models.tf_graph.audio_resnet50",
"name": "get_unwrapped_model",
"weights_file": "speech_commands_explanatory_model_resnet50_bean.h5",
},
"cifar10_explanatory_model": {
"model_kwargs": {
"data_means": [0.4914, 0.4822, 0.4465],
"data_stds": [0.2471, 0.2435, 0.2616],
"num_classes": 10,
},
"module": "armory.baseline_models.pytorch.resnet18_bean_regularization",
"name": "get_model",
"resize_image": False,
"weights_file": "cifar10_explanatory_model_resnet18_bean.pt",
},
"gtsrb_silhouette_model": {
"model_kwargs": {},
"gtsrb_explanatory_model": {
"module": "armory.baseline_models.pytorch.micronnet_gtsrb_bean_regularization",
"name": "get_model",
"resize_image": False,
"weights_file": "gtsrb_explanatory_model_micronnet_bean.pt",
},
"resisc10_silhouette_model": {
"resisc10_explanatory_model": {
"model_kwargs": {
"data_means": [0.39382024, 0.4159701, 0.40887499],
"data_stds": [0.18931773, 0.18901625, 0.19651154],
"num_classes": 10,
},
"module": "armory.baseline_models.pytorch.resnet18_bean_regularization",
"name": "get_model",
"preprocess_kwargs": {
"resize_image": True,
},
"weights_file": "resisc10_explanatory_model_resnet18_bean.pt",
},
}
Expand All @@ -46,18 +55,44 @@ class ExplanatoryModel:
def __init__(
self,
explanatory_model,
resize_image=True,
size=(224, 224),
resample=Image.BILINEAR,
device=DEVICE,
data_modality="image",
model_framework="pytorch",
activation_layer=None,
preprocess_kwargs=None,
):
"""
explanatory_model: A callable pytorch or tensorflow model used to produce
activations for silhouette analysis
data_modality: one of "image" or "audio" (more options to be added as needed)
model_framework: "pytorch" or "tensorflow"
activation_layer: name of the layer of the model from which to draw activations
(currently only for tensorflow models).
If None, uses the final output layer.
preprocess_kwargs: keyword arguments for the preprocessing function
"""
if not callable(explanatory_model):
raise ValueError(f"explanatory_model {explanatory_model} is not callable")
if model_framework not in ("pytorch", "tensorflow"):
raise ValueError(
f"model_framework should be 'pytorch' or 'tensorflow', not '{model_framework}'"
)
self.explanatory_model = explanatory_model
self.resize_image = bool(resize_image)
self.size = size
self.resample = resample
self.device = device
self.data_modality = data_modality
self.model_framework = model_framework
self.activation_layer = activation_layer
self.preprocess_kwargs = preprocess_kwargs if preprocess_kwargs else {}

if self.activation_layer is not None:
if self.model_framework == "tensorflow":
# Set explanatory_model to return activations from internal layer
self.explanatory_model = tf.keras.Model(
explanatory_model.layers[0].input,
explanatory_model.get_layer(self.activation_layer).output,
)
else:
raise ValueError(
"Currently, 'activation_layer' can only be specified for a tensorflow model, not pytorch."
)

@classmethod
def from_config(cls, model_config, **kwargs):
Expand Down Expand Up @@ -87,7 +122,10 @@ def from_config(cls, model_config, **kwargs):
model_fn = getattr(model_module, name)
explanatory_model = model_fn(weights_path, **model_kwargs)

return cls(explanatory_model, **model_config)
return cls(
explanatory_model,
**model_config,
)

def get_activations(self, x, batch_size: int = None):
"""
Expand All @@ -96,25 +134,32 @@ def get_activations(self, x, batch_size: int = None):
if batch_size, batch inputs and then concatenate
"""
activations = []
with torch.no_grad():
if batch_size:
batch_size = int(batch_size)
if batch_size < 1:
raise ValueError("batch_size must be false or a positive int")
else:
batch_size = len(x)
if batch_size:
batch_size = int(batch_size)
if batch_size < 1:
raise ValueError("batch_size must be false or a positive int")
else:
batch_size = len(x)

for i in range(0, len(x), batch_size):
x_batch = x[i : i + batch_size]

for i in range(0, len(x), batch_size):
x_batch = x[i : i + batch_size]
if self.model_framework == "pytorch":
with torch.no_grad():
x_batch = self.preprocess(x_batch)
activation, _ = self.explanatory_model(x_batch)
activations.append(activation.detach().cpu().numpy())

elif self.model_framework == "tensorflow":
x_batch = self.preprocess(x_batch)
activation, _ = self.explanatory_model(x_batch)
activations.append(activation.detach().cpu().numpy())
activation = self.explanatory_model(x_batch, training=False)
activations.append(activation.numpy())

return np.concatenate(activations)

@staticmethod
def _preprocess(
x, resize_image=True, size=(224, 224), resample=Image.BILINEAR, device=DEVICE
def _preprocess_image(
x, resize_image=False, size=(224, 224), resample=Image.BILINEAR, device=DEVICE
):
if np.issubdtype(x.dtype, np.floating):
if x.min() < 0.0 or x.max() > 1.0:
Expand Down Expand Up @@ -145,10 +190,15 @@ def preprocess(self, x):
"""
Preprocess a batch of images
"""
return type(self)._preprocess(
x,
self.resize_image,
self.size,
resample=self.resample,
device=self.device,
)
if self.data_modality == "image":
return type(self)._preprocess_image(
x,
**self.preprocess_kwargs,
)
elif self.data_modality == "audio":
return x

else:
raise ValueError(
f"There is no preprocessing function for data_modality '{self.data_modality}'. Please set data_modality to 'image' or 'audio', or implement preprocessing for data_modality '{self.data_modality}'"
)
16 changes: 8 additions & 8 deletions armory/metrics/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -1098,7 +1098,7 @@ def _object_detection_get_tpr_mr_dr_hr(
)


@populationwise
@batchwise
def object_detection_true_positive_rate(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5, class_list=None
):
Expand Down Expand Up @@ -1129,7 +1129,7 @@ def object_detection_true_positive_rate(
return true_positive_rate_per_img


@populationwise
@batchwise
def object_detection_misclassification_rate(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5, class_list=None
):
Expand Down Expand Up @@ -1160,7 +1160,7 @@ def object_detection_misclassification_rate(
return misclassification_rate_per_image


@populationwise
@batchwise
def object_detection_disappearance_rate(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5, class_list=None
):
Expand Down Expand Up @@ -1192,7 +1192,7 @@ def object_detection_disappearance_rate(
return disappearance_rate_per_img


@populationwise
@batchwise
def object_detection_hallucinations_per_image(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5, class_list=None
):
Expand Down Expand Up @@ -1223,7 +1223,7 @@ def object_detection_hallucinations_per_image(
return hallucinations_per_image


@populationwise
@batchwise
def carla_od_hallucinations_per_image(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5
):
Expand All @@ -1241,7 +1241,7 @@ def carla_od_hallucinations_per_image(
)


@populationwise
@batchwise
def carla_od_disappearance_rate(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5
):
Expand All @@ -1259,7 +1259,7 @@ def carla_od_disappearance_rate(
)


@populationwise
@batchwise
def carla_od_true_positive_rate(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5
):
Expand All @@ -1277,7 +1277,7 @@ def carla_od_true_positive_rate(
)


@populationwise
@batchwise
def carla_od_misclassification_rate(
y_list, y_pred_list, iou_threshold=0.5, score_threshold=0.5
):
Expand Down
5 changes: 0 additions & 5 deletions armory/scenarios/carla_object_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ def run_attack(self):

self.x_adv, self.y_target, self.y_pred_adv = x_adv, y_target, y_pred_adv

def load_metrics(self):
super().load_metrics()
# measure adversarial results using benign predictions as labels
self.metrics_logger.add_tasks_wrt_benign_predictions()

def _load_sample_exporter_with_boxes(self):
return ObjectDetectionExporter(
self.export_dir,
Expand Down
Loading

0 comments on commit 562f150

Please sign in to comment.