Perceptual Attack

Apply neural perceptual attack to images taken from: Cassidy Laidlaw, Sahil Singla, and Soheil Feizi. “Perceptual adversarial robustness: Defense against unseen threat models.” arXiv preprint arXiv:2006.12655 (2020).

Problem Description

Given a classifier \(f\) which maps any input image \(x \in X\) to its label \(y = f(x) \in Y\). The goal of neural perceptual attack is to find an input \(\widetilde{x}\) that is perceptually similar to the original input \(x\) but can fool the classifier \(f\). This can be formulated as:

\[\max_{\widetilde{x}} L (f(\widetilde{x}),y),\]
\[\text{s.t.}\;\; d(x,\widetilde{x}) = ||\phi(x) - \phi (\tilde{x}) ||_{2} \leq \epsilon\]

Here

\[L (f({x}),y) = \max_{i\neq y} (z_i(x) - z_y(x) ),\]

where \(z_i(x)\) is the \(i\)-th logit output of \(f(x)\), and \(\phi(\cdot)\) is a function that maps the input \(x\) to normalized, flattened activations

Modules Importing

Import all necessary modules and add PyGRANSO src folder to system path.

NOTE: the perceptual advex package (https://github.com/cassidylaidlaw/perceptual-advex.git) is required to calculate the distance

[1]:
# install required package
try:
    import perceptual_advex
except ImportError:
    !pip install perceptual-advex
[2]:
import time
import torch
import sys
from pygranso.pygranso import pygranso
from pygranso.pygransoStruct import pygransoStruct
from pygranso.private.getNvar import getNvarTorch
from perceptual_advex.utilities import get_dataset_model
from perceptual_advex.perceptual_attacks import get_lpips_model
from perceptual_advex.distances import normalize_flatten_features
import gc

Download Pretrained Model

[3]:
import os

# Download ResNet model
if not os.path.exists('data/checkpoints/cifar_pgd_l2_1.pt'):
    !mkdir -p data/checkpoints
    !curl -o data/checkpoints/cifar_pgd_l2_1.pt https://perceptual-advex.s3.us-east-2.amazonaws.com/cifar_pgd_l2_1_cpu.pt

Data Initialization

Specify torch device, neural network architecture, and generate data.

NOTE: please specify path for downloading data.

Use GPU for this problem. If no cuda device available, please set device = torch.device(‘cpu’)

[4]:
device = torch.device('cuda')

dataset, model = get_dataset_model(
dataset='cifar',
arch='resnet50',
checkpoint_fname='data/checkpoints/cifar_pgd_l2_1.pt',
)
model = model.to(device=device, dtype=torch.double)
# Create a validation set loader.
batch_size = 1
_, val_loader = dataset.make_loaders(1, batch_size, only_val=True, shuffle_val=False)

inputs, labels = next(iter(val_loader))

# All the user-provided data (vector/matrix/tensor) must be in torch tensor format.
# As PyTorch tensor is single precision by default, one must explicitly set `dtype=torch.double`.
# Also, please make sure the device of provided torch tensor is the same as opts.torch_device.
inputs = inputs.to(device=device, dtype=torch.double)
labels = labels.to(device=device)

# externally-bounded attack: AlexNet for constraint while ResNet for objective
lpips_model = get_lpips_model('alexnet_cifar', model).to(device=device, dtype=torch.double)

# Don't reccoment use in the current version. self-bounded attack: AlexNet for both constraint and objective
# model = get_lpips_model('alexnet_cifar', model).to(device=device, dtype=torch.double)

# attack_type = 'L_2'
# attack_type = 'L_inf'
attack_type = 'Perceptual'
=> loading checkpoint 'data/checkpoints/cifar_pgd_l2_1.pt'
==> Preparing dataset cifar..
Files already downloaded and verified
Traceback (most recent call last):
  File "/home/buyun/anaconda3/envs/cuosqp_pygranso/lib/python3.9/multiprocessing/queues.py", line 251, in _feed
    send_bytes(obj)
  File "/home/buyun/anaconda3/envs/cuosqp_pygranso/lib/python3.9/multiprocessing/connection.py", line 205, in send_bytes
    self._send_bytes(m[offset:offset + size])
  File "/home/buyun/anaconda3/envs/cuosqp_pygranso/lib/python3.9/multiprocessing/connection.py", line 416, in _send_bytes
    self._send(header + buf)
  File "/home/buyun/anaconda3/envs/cuosqp_pygranso/lib/python3.9/multiprocessing/connection.py", line 373, in _send
    n = write(self._handle, buf)
BrokenPipeError: [Errno 32] Broken pipe

Function Set-Up

Encode the optimization variables, and objective and constraint functions.

Note: please strictly follow the format of comb_fn, which will be used in the PyGRANSO main algortihm.

[5]:
# variables and corresponding dimensions.
var_in = {"x_tilde": list(inputs.shape)}

def MarginLoss(logits,labels):
    correct_logits = torch.gather(logits, 1, labels.view(-1, 1))
    max_2_logits, argmax_2_logits = torch.topk(logits, 2, dim=1)
    top_max, second_max = max_2_logits.chunk(2, dim=1)
    top_argmax, _ = argmax_2_logits.chunk(2, dim=1)
    labels_eq_max = top_argmax.squeeze().eq(labels).float().view(-1, 1)
    labels_ne_max = top_argmax.squeeze().ne(labels).float().view(-1, 1)
    max_incorrect_logits = labels_eq_max * second_max + labels_ne_max * top_max
    loss = -(max_incorrect_logits - correct_logits).clamp(max=1).squeeze().sum()
    return loss

def user_fn(X_struct,inputs,labels,lpips_model,model):
    adv_inputs = X_struct.x_tilde

    # objective function
    # 8/255 for L_inf, 1 for L_2, 0.5 for PPGD/LPA
    if attack_type == 'L_2':
        epsilon = 1
    elif attack_type == 'L_inf':
        epsilon = 8/255
    else:
        epsilon = 0.5

    logits_outputs = model(adv_inputs)

    f = MarginLoss(logits_outputs,labels)

    # inequality constraint
    ci = pygransoStruct()
    if attack_type == 'L_2':
        ci.c1 = torch.norm((inputs - adv_inputs).reshape(inputs.size()[0], -1)) - epsilon
    elif attack_type == 'L_inf':
        ci.c1 = torch.norm((inputs - adv_inputs).reshape(inputs.size()[0], -1), float('inf')) - epsilon
    else:
        input_features = normalize_flatten_features( lpips_model.features(inputs)).detach()
        adv_features = lpips_model.features(adv_inputs)
        adv_features = normalize_flatten_features(adv_features)
        lpips_dists = (adv_features - input_features).norm(dim=1)
        ci.c1 = lpips_dists - epsilon

    # equality constraint
    ce = None

    return [f,ci,ce]

comb_fn = lambda X_struct : user_fn(X_struct,inputs,labels,lpips_model,model)

User Options

Specify user-defined options for PyGRANSO

[6]:
opts = pygransoStruct()
opts.torch_device = device
opts.maxit = 100
opts.opt_tol = 1e-6
opts.print_frequency = 1
opts.x0 = torch.reshape(inputs,(torch.numel(inputs),1))

Main Algorithm

[7]:
start = time.time()
soln = pygranso(var_spec = var_in,combined_fn = comb_fn,user_opts = opts)
end = time.time()
print("Total Wall Time: {}s".format(end - start))
/home/buyun/anaconda3/envs/cuosqp_pygranso/lib/python3.9/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /opt/conda/conda-bld/pytorch_1623448255797/work/c10/core/TensorImpl.h:1156.)
  return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)


╔═════ QP SOLVER NOTICE ════════════════════════════════════════════════════════════════════════╗
║  PyGRANSO requires a quadratic program (QP) solver that has a quadprog-compatible interface,  ║
║  the default is osqp. Users may provide their own wrapper for the QP solver.                  ║
║  To disable this notice, set opts.quadprog_info_msg = False                                   ║
╚═══════════════════════════════════════════════════════════════════════════════════════════════╝
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
PyGRANSO: A PyTorch-enabled port of GRANSO with auto-differentiation                                             ║
Version 1.0.0                                                                                                    ║
Licensed under the AGPLv3, Copyright (C) 2021 Tim Mitchell and Buyun Liang                                       ║
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
Problem specifications:                                                                                          ║
 # of variables                     :   3072                                                                     ║
 # of inequality constraints        :      1                                                                     ║
 # of equality constraints          :      0                                                                     ║
═════╦═══════════════════════════╦════════════════╦═════════════════╦═══════════════════════╦════════════════════╣
     ║ <--- Penalty Function --> ║                ║ Total Violation ║ <--- Line Search ---> ║ <- Stationarity -> ║
Iter ║    Mu    │      Value     ║    Objective   ║   Ineq   │  Eq  ║ SD │ Evals │     t    ║ Grads │    Value   ║
═════╬═══════════════════════════╬════════════════╬═════════════════╬═══════════════════════╬════════════════════╣
   0 ║ 1.000000 │  0.50909392739 ║  0.50909392739 ║ 0.000000 │   -  ║ -  │     1 │ 0.000000 ║     1 │ 0.752374   ║
   1 ║ 1.000000 │  0.13139580076 ║  0.13139580076 ║ 0.000000 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.569827   ║
   2 ║ 1.000000 │ -0.01353464659 ║ -0.11231957917 ║ 0.098785 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.364019   ║
   3 ║ 1.000000 │ -0.03305865013 ║ -0.23057790915 ║ 0.197519 │   -  ║ S  │     2 │ 0.500000 ║     1 │ 0.391431   ║
   4 ║ 1.000000 │ -0.07512002132 ║ -0.36057738209 ║ 0.285457 │   -  ║ S  │     2 │ 0.500000 ║     1 │ 0.436613   ║
   5 ║ 1.000000 │ -0.11627463660 ║ -0.49942430204 ║ 0.383150 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.349554   ║
   6 ║ 1.000000 │ -0.17664891011 ║ -0.58140452016 ║ 0.404756 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.484603   ║
   7 ║ 1.000000 │ -0.29911013557 ║ -0.68329983929 ║ 0.384190 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.357295   ║
   8 ║ 1.000000 │ -0.35472121343 ║ -0.75604941979 ║ 0.401328 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.760431   ║
   9 ║ 1.000000 │ -0.46871129466 ║ -0.80585740448 ║ 0.337146 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 1.010051   ║
  10 ║ 1.000000 │ -0.50872108700 ║ -0.88639971711 ║ 0.377679 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.548706   ║
  11 ║ 1.000000 │ -0.62605323050 ║ -0.88862023590 ║ 0.262567 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.638928   ║
  12 ║ 1.000000 │ -0.64317710100 ║ -0.93760108907 ║ 0.294424 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.852888   ║
  13 ║ 1.000000 │ -0.74714953697 ║ -0.98111740251 ║ 0.233968 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.616422   ║
  14 ║ 1.000000 │ -0.79240880586 ║ -1.00000000000 ║ 0.207591 │   -  ║ S  │     2 │ 0.500000 ║     1 │ 0.509754   ║
  15 ║ 1.000000 │ -0.83347523436 ║ -1.00000000000 ║ 0.166525 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.253215   ║
  16 ║ 1.000000 │ -0.86121027483 ║ -0.95196903482 ║ 0.090759 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 1.270118   ║
  17 ║ 1.000000 │ -0.91384321895 ║ -1.00000000000 ║ 0.086157 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.187817   ║
  18 ║ 1.000000 │ -0.93207484436 ║ -0.97803478945 ║ 0.045960 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.748240   ║
  19 ║ 1.000000 │ -0.94498666126 ║ -1.00000000000 ║ 0.055013 │   -  ║ S  │     2 │ 0.500000 ║     1 │ 0.112843   ║
═════╬═══════════════════════════╬════════════════╬═════════════════╬═══════════════════════╬════════════════════╣
     ║ <--- Penalty Function --> ║                ║ Total Violation ║ <--- Line Search ---> ║ <- Stationarity -> ║
Iter ║    Mu    │      Value     ║    Objective   ║   Ineq   │  Eq  ║ SD │ Evals │     t    ║ Grads │    Value   ║
═════╬═══════════════════════════╬════════════════╬═════════════════╬═══════════════════════╬════════════════════╣
  20 ║ 1.000000 │ -0.96019197251 ║ -0.98795223931 ║ 0.027760 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 1.182802   ║
  21 ║ 1.000000 │ -0.96657925961 ║ -1.00000000000 ║ 0.033421 │   -  ║ S  │     3 │ 0.250000 ║     1 │ 0.065142   ║
  22 ║ 1.000000 │ -0.99053087965 ║ -1.00000000000 ║ 0.009469 │   -  ║ S  │     1 │ 1.000000 ║     1 │ 0.032242   ║
  23 ║ 1.000000 │ -1.00000000000 ║ -1.00000000000 ║ 0.000000 │   -  ║ S  │     2 │ 2.000000 ║     1 │ 0.000000   ║
═════╩═══════════════════════════╩════════════════╩═════════════════╩═══════════════════════╩════════════════════╣
F = final iterate, B = Best (to tolerance), MF = Most Feasible                                                   ║
Optimization results:                                                                                            ║
═════╦═══════════════════════════╦════════════════╦═════════════════╦═══════════════════════╦════════════════════╣
   F ║          │                ║ -1.00000000000 ║ 0.000000 │   -  ║    │       │          ║       │            ║
   B ║          │                ║ -1.00000000000 ║ 0.000000 │   -  ║    │       │          ║       │            ║
  MF ║          │                ║ -1.00000000000 ║ 0.000000 │   -  ║    │       │          ║       │            ║
═════╩═══════════════════════════╩════════════════╩═════════════════╩═══════════════════════╩════════════════════╣
Iterations:              23                                                                                      ║
Function evaluations:    31                                                                                      ║
PyGRANSO termination code: 0 --- converged to stationarity and feasibility tolerances.                           ║
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
Total Wall Time: 4.690735816955566s

Batch Attacks

Apply attacks to multiple images by repeating above steps and calculate the success rate

[8]:
total_count = 50
total_diff = 0
original_count = 0
attack_count = 0
total_time = 0
total_iterations = 0
i = 0
it = iter(val_loader)

for i in range(total_count):
    # Get a batch from the validation set.
    inputs, labels = next(it)
    inputs = inputs.to(device=device, dtype=torch.double)
    labels = labels.to(device=device)

    # variables and corresponding dimensions.
    var_in = {"x_tilde": list(inputs.shape)}

    opts.x0 = torch.reshape(inputs,(torch.numel(inputs),1))
    # suppress output
    opts.print_level = 0

    pred_labels = model(inputs).argmax(1)
    if pred_labels == labels:
        original_count += 1
    else:
        continue

    start = time.time()
    soln = pygranso(var_spec = var_in,combined_fn = comb_fn,user_opts = opts)
    end = time.time()

    # Garbage Collector
    gc.collect()

    print("i = %d"%i)

    total_time += end - start
    total_iterations += soln.fn_evals

    final_adv_input = torch.reshape(soln.final.x,inputs.shape)
    pred_labels2 = model(final_adv_input.to(device=device, dtype=torch.double)).argmax(1)

    if pred_labels2 == labels:
        attack_count += 1

    if attack_type == 'L_2':
            diff = torch.norm((inputs.to(device=device, dtype=torch.double) - final_adv_input).reshape(inputs.size()[0], -1))
    elif attack_type == 'L_inf':
        diff = ( torch.norm((inputs.to(device=device, dtype=torch.double) - final_adv_input).reshape(inputs.size()[0], -1), float('inf') ) )
    else:
        input_features = normalize_flatten_features( lpips_model.features(inputs)).detach()
        adv_features = lpips_model.features(final_adv_input)
        adv_features = normalize_flatten_features(adv_features)
        lpips_dists = torch.mean((adv_features - input_features).norm(dim=1))
        diff = lpips_dists

    total_diff += diff

print("\n\n\nModel train acc on the original image = {}".format(( original_count/total_count )))
print("Success rate of attack = {}".format( (original_count-attack_count)/original_count ))
print("Average distance between attacked image and original image = {}".format(total_diff/original_count))
print("Average run time of PyGRANSO = {}s, mean f_eval = {} iters".format(total_time/original_count,total_iterations/original_count))
[W pthreadpool-cpp.cc:90] Warning: Leaking Caffe2 thread-pool after fork. (function pthreadpool)
i = 0
i = 8
i = 11
i = 14
i = 18
i = 23
i = 28
i = 34
i = 38
i = 42
i = 45
i = 46



Model train acc on the original image = 0.24
Success rate of attack = 1.0
Average distance between attacked image and original image = 0.49154774143691704
Average run time of PyGRANSO = 3.7453556855519614s, mean f_eval = 30.833333333333332 iters