1 引言
该论文是关于对抗样本可迁移性攻击的文章。现有的特征级攻击通常会采用不准确的神经元重要性估计,这样会降低了对抗样本的可迁移性。在该论文中,作者提出了基于的神经元属性的对抗攻击,它通过更准确的神经元重要性估计来进行特征级攻击。首先将模型的输出完全归因于中间层的每个神经元。然后,作者推导了神经元属性的近似方案,以极大地减少计算开销。最后,根据神经元的属性结果对神经元进行加权,并发起特征级攻击。实验结果也证实了论文中方法优越性。论文的代码已经开源。
论文链接: https://arxiv.org/abs/2204.00008
代码链接: https://github.com/jpzhang1810/NAA
2 论文方法
特征级别的攻击在生成对抗样本的过程中会破坏掉积极的特征从而扩大消极的特征。因此,由特征级别生成的对抗样本可有继承误导其它深度学习模型的高迁移性特征。特征级别攻击的关键在于找到一个合适的方式去度量每一个神经元的重要程度。在该论文中作者引入了一个度量神经元重要程度的度量方式,名为神经元属性,此外作者还基于神经元重要程度提出了基于神经元属性的攻击方式。
令
x
x
x为干净样本,它对应的真实标签为
z
z
z。
F
(
⋅
)
F(\cdot)
F(⋅)为一个分类模型,则
F
(
x
)
F(x)
F(x)表示为
x
x
x的输出。
y
y
y表示第
y
y
y层的激活值,其中
y
j
y_j
yj表示在这个特征图中第
j
j
j个神经单元的激活值。
x
a
d
v
x^{adv}
xadv表示对抗样本,且有
∥
x
−
x
a
d
v
∥
p
<
ε
\|x-x^{adv}\|_p<\varepsilon
∥x−xadv∥p<ε,其中
∥
⋅
∥
p
\|\cdot\|_p
∥⋅∥p表示
p
p
p范数,
ε
\varepsilon
ε为对抗扰动。
给定一个基准图像
x
′
x^{\prime}
x′,作者可以定义输入图片
x
∈
R
N
×
N
x\in \mathbb{R}^{N\times N}
x∈RN×N的属性为
A
:
=
∑
i
=
1
N
2
(
x
i
−
x
i
′
)
∫
0
1
∂
F
∂
x
i
(
x
′
+
α
(
x
−
x
′
)
)
d
α
A:=\sum\limits_{i=1}^{N^2}(x_i-x^{\prime}_i)\int_0^1\frac{\partial F}{\partial x_i}(x^{\prime}+\alpha(x-x^{\prime}))d\alpha
A:=i=1∑N2(xi−xi′)∫01∂xi∂F(x′+α(x−x′))dα其中
∂
F
∂
x
i
(
⋅
)
\frac{\partial F}{\partial x_i}(\cdot)
∂xi∂F(⋅)表示
F
F
F关于第
i
i
i个像素的偏导数。上公式说明
F
F
F的梯度是沿着给定的直线路径
(
x
′
+
α
(
x
−
x
′
)
)
(x^{\prime}+\alpha(x-x^{\prime}))
(x′+α(x−x′))进行积分的。根据路径积分的微积分基本定理可以得到,只有当
F
(
x
′
)
≈
0
F(x^{\prime})\approx 0
F(x′)≈0时,会有
A
≈
F
(
x
)
A\approx F(x)
A≈F(x),即
x
′
=
0
x^{\prime}=0
x′=0是一张全黑图像,上述结论成立。
令
x
α
=
x
′
+
α
(
x
−
x
′
)
x_\alpha=x^{\prime}+\alpha(x-x^{\prime})
xα=x′+α(x−x′),则第
y
y
y层的第
y
j
y_j
yj个神经元的属性为
A
y
j
=
∑
i
=
1
N
2
(
x
i
−
x
i
′
)
∫
0
1
∂
F
∂
y
j
(
y
(
x
α
)
)
∂
y
j
∂
x
i
(
x
α
)
d
α
A_{y_j}=\sum\limits_{i=1}^{N^2}(x_i-x_i^{\prime})\int^1_0\frac{\partial F}{\partial y_j}(y(x_\alpha))\frac{\partial y_j}{\partial x_i}(x_\alpha)d\alpha
Ayj=i=1∑N2(xi−xi′)∫01∂yj∂F(y(xα))∂xi∂yj(xα)dα其中
∑
y
j
∈
y
A
y
j
=
A
\sum\limits_{y_j\in y}A_{y_j}=A
yj∈y∑Ayj=A在选定每一层都成立。因此,神经元属性反映了每个神经元到输出真实的影响。为了实际中复杂的积分计算,作者在直线路径中采样了
n
n
n个虚拟样本,并使用黎曼加和去估计该积分,然后改变加和的顺序则有:
A
y
j
≈
1
n
∑
m
=
1
n
(
∂
F
∂
y
j
(
y
(
x
m
)
)
)
(
∑
i
=
1
N
2
(
x
i
−
x
i
′
)
∂
y
j
∂
x
i
(
x
m
)
)
A_{y_j}\approx \frac{1}{n}\sum\limits_{m=1}^n\left(\frac{\partial F}{\partial y_j}(y(x_m))\right)\left(\sum\limits_{i=1}^{N^2}(x_i-x^{\prime}_i)\frac{\partial y_j}{\partial x_i}(x_m)\right)
Ayj≈n1m=1∑n(∂yj∂F(y(xm)))⎝
⎛i=1∑N2(xi−xi′)∂xi∂yj(xm)⎠
⎞其中
x
m
=
x
′
+
m
n
(
x
−
x
′
)
x_m=x^{\prime}+\frac{m}{n}(x-x^{\prime})
xm=x′+nm(x−x′)是路径上虚拟的图像。
考虑到神经网络中神经元的数量非常大,所以计算成本会非常高,为了降低计算复杂度,作者在上公式中做了一个简单的假设进而简化公式。一开始时,
∂
F
∂
y
j
(
y
(
x
m
)
)
\frac{\partial F}{\partial y_j}(y(x_m))
∂yj∂F(y(xm))是
F
(
x
)
F(x)
F(x)关于
y
j
y_j
yj的梯度,同时
∑
i
=
1
N
2
(
x
i
−
x
i
′
)
∂
y
j
∂
x
i
(
x
m
)
\sum\limits_{i=1}^{N^2}(x_i-x_i^{\prime})\frac{\partial y_j}{\partial x_i}(x_m)
i=1∑N2(xi−xi′)∂xi∂yj(xm)是
y
j
y_j
yj关于每个像素
x
i
x_i
xi梯度的加和,作者假定这两个梯度是线性独立的。
给定两个相互独立的序列
a
i
a_i
ai和
b
i
b_i
bi,则有
∑
i
=
1
n
(
a
i
−
a
ˉ
)
(
b
i
−
b
ˉ
)
=
0
\sum\limits_{i=1}^n(a_i-\bar{a})(b_i-\bar{b})=0
i=1∑n(ai−aˉ)(bi−bˉ)=0,其中
(
⋅
ˉ
)
(\bar{\cdot})
(⋅ˉ)表示的是序列均值。进一步则有,
∑
i
=
1
n
a
i
⋅
b
i
=
1
n
∑
i
=
1
n
a
i
∑
i
=
1
n
b
i
\sum\limits_{i=1}^na_i\cdot b_i=\frac{1}{n}\sum\limits_{i=1}^na_i\sum\limits_{i=1}^nb_i
i=1∑nai⋅bi=n1i=1∑naii=1∑nbi,可以将以上公式的两个大括号分别看成
a
a
a和
b
b
b,于是可以得到如下公式
A
y
j
≈
1
n
∑
m
=
1
n
∂
F
∂
y
j
(
y
(
x
m
)
)
1
n
∑
m
=
1
n
∑
i
=
1
N
2
(
x
i
−
x
i
′
)
∂
y
j
∂
x
i
(
x
m
)
A_{y_j}\approx \frac{1}{n}\sum\limits_{m=1}^n\frac{\partial F}{\partial y_j}(y(x_m))\frac{1}{n}\sum\limits_{m=1}^n\sum\limits_{i=1}^{N^2}(x_i-x_i^{\prime})\frac{\partial y_j}{\partial x_i}(x_m)
Ayj≈n1m=1∑n∂yj∂F(y(xm))n1m=1∑ni=1∑N2(xi−xi′)∂xi∂yj(xm)根据路径积分的基本定理可知
1
n
∑
m
=
1
n
∑
i
=
1
N
2
(
x
i
−
x
i
′
)
∂
y
j
∂
x
i
(
x
m
)
=
(
y
j
−
y
′
)
\frac{1}{n}\sum\limits_{m=1}^n\sum\limits_{i=1}^{N^2}(x_i-x^\prime_i)\frac{\partial y_j}{\partial x_i}(x_m)=(y_j-y^{\prime})
n1m=1∑ni=1∑N2(xi−xi′)∂xi∂yj(xm)=(yj−y′)其中
y
j
′
y^{\prime}_j
yj′是第
j
j
j个神经元的激活值,其此时输入的图像是黑色图像。令
y
j
−
y
j
′
y_j-y^{\prime}_j
yj−yj′为
Δ
y
j
\Delta y_j
Δyj,并且
1
n
∑
m
=
1
n
∂
F
∂
y
j
(
y
(
x
m
)
)
\frac{1}{n}\sum\limits_{m=1}^n\frac{\partial F}{\partial y_j}(y(x_m))
n1m=1∑n∂yj∂F(y(xm))被看做融合注意力
I
A
(
y
j
)
IA(y_j)
IA(yj),一个简单的形式为
A
y
j
=
Δ
y
j
⋅
I
A
(
y
j
)
A_{y_j}=\Delta y_j\cdot IA(y_j)
Ayj=Δyj⋅IA(yj)。
I
A
(
y
j
)
IA(y_j)
IA(yj)反映了从基准图像到输入的直线上梯度的积分与神经元
y
j
y_j
yj的关系。
综上所述可以发现,原本神经元属性的计算复杂度为
O
(
H
∗
W
∗
C
)
\mathcal{O}(H*W*C)
O(H∗W∗C),其中
H
H
H是目标层的高,
W
W
W是目标层的宽,
C
C
C是目标层通道数,而作者提出的方法计算复杂度为
O
(
1
)
\mathcal{O}(1)
O(1),在每一个梯度整合的步骤中只需要一个梯度操作,如果不进行简化,则在每一个步骤中则需要进行1百万次梯度操作;因此可知论文中的简化方法大大降低了计算复杂度。考虑第
y
y
y层所有神经元属性的计算公式为
A
y
=
∑
y
j
∈
y
A
y
j
=
∑
y
j
∈
y
Δ
y
j
⋅
I
A
(
y
j
)
=
(
y
−
y
′
)
⋅
I
A
(
y
)
A_y=\sum\limits_{y_j\in y}A_{y_j}=\sum\limits_{y_j\in y}\Delta y_j \cdot IA(y_j)=(y-y^{\prime})\cdot IA(y)
Ay=yj∈y∑Ayj=yj∈y∑Δyj⋅IA(yj)=(y−y′)⋅IA(y)
生成对抗样本的过程中有用的特征被抑制,有害的特征则会被放大。为了分析这两种特征的影响,作者试图找出哪一种特征主导了对抗样本的可迁移性,利用一个超参数
γ
\gamma
γ来平衡正面和负面属性,此外,作者还区分不同值的神经元属性的显著程度。例如,当调查减少一个大的积极属性神经元是否比增加一个小的消极属性神经元更有利于攻击。为此,作者设计了多个线性或非线性变换函数,即
f
p
(
A
y
j
)
f_p(A_{y_j})
fp(Ayj),用于积极的神经元属性和
−
f
n
(
−
A
y
j
)
−f_n(−A_{y_j})
−fn(−Ayj)用消极神经元属性。因此,目标层
y
y
y上所有神经元的加权属性
W
A
y
W A_y
WAy可以被计算为
W
A
y
=
∑
A
y
j
≥
0
,
y
j
∈
y
f
p
(
A
y
j
)
−
γ
⋅
∑
A
y
j
<
0
,
y
j
∈
y
f
n
(
−
A
y
j
)
WA_y=\sum\limits_{A_{y_j}\ge 0,y_j\in y}f_p(A_{y_j})-\gamma\cdot\sum\limits_{A_{y_j}<0,y_j\in y}f_n(-A_{y_j})
WAy=Ayj≥0,yj∈y∑fp(Ayj)−γ⋅Ayj<0,yj∈y∑fn(−Ayj)最小化目标函数
W
A
y
W A_y
WAy是要比直接最小化函数
A
y
A_y
Ay要好,因为
W
A
y
W A_y
WAy同时考虑了神经元极性和数值两方面。因此优化目标可以整理为如下所示:
min
x
a
d
v
W
A
y
s
.
t
.
∥
x
−
x
a
d
v
∥
∞
<
ε
\min\limits_{x^{\mathrm{adv}}}WA_y \quad \mathrm{s.t.}\text{ }\|x-x^{\mathrm{adv}}\|_{\infty}<\varepsilon
xadvminWAys.t. ∥x−xadv∥∞<ε其中作者利用动量迭代的方法去求解以上优化问题,具体的算法流程图如下所示:
3 实验结果
如下表所示为论文中的方法与与baseline方法在无防御模型,对抗训练模型和加载防御模型的分别在白盒和黑盒条件下的攻击效果。可以发现在白盒条件下,论文中的方法的攻击成功率接近100%;在黑盒条件下,该方法也比其它的方法有更高的攻击迁移率。
另外作者还比较了带输入变换的攻击方法的效果,其中输入变换方法分别是PIM和DIM。如下表所示为带输入变换的不同的攻击方法在无防御模型,对抗训练模型和加载防御模型的分别在白盒和黑盒条件下的攻击效果。可以发现在黑盒条件下,论文中方法比其它的方法有更高的攻击迁移率。
如下图所示为消融实验的实验结果,作者主要分析了目标特征层,积分步数
n
n
n和权重系数
γ
\gamma
γ对迁移攻击成功率的影响。右下图的结果可知,对于不同的目标特征层可以发现,中间的目标特征层的迁移攻击成功率最高,说明中间特征层的特征对迁移攻击成功率有更大的影响。对于积分步数
n
n
n来说,当
n
=
30
n=30
n=30时可以达到最好的效果。对于权重系数
γ
\gamma
γ来说,可以发现当
γ
=
1
\gamma=1
γ=1时,迁移攻击成功率达到最优的效果,由此可知,正面特征和负面特征同等重要。
4 代码
"""Implementation of attack."""
# coding: utf-8
import os
import torch
import torchvision.transforms as T
import torch.nn as nn
import torchvision
from torch.utils.data import Dataset
import csv
import numpy as np
import pretrainedmodels
import PIL.Image as Image
import ssl
from matplotlib import pyplot as plt
import numpy as np
import argparse
ssl._create_default_https_context = ssl._create_unverified_context
os.environ['CUDA_VISIBLE_DEVICES']='0'
parser = argparse.ArgumentParser()
parser.add_argument('--epsilon', type=float, default=16)
parser.add_argument('--T_niters', type=int, default=10)
parser.add_argument('--alpha', type=float, default=1.6)
parser.add_argument('--steps', type=int, default=10)
parser.add_argument('--mu', type=float, default=1.0)
parser.add_argument('--batch_size', type=int, default=4)
parser.add_argument('--gamma', type=float, default=1.0)
parser.add_argument('--target_layer', type=str, default='layer2')
parser.add_argument('--save_dir', type=str, default = 'test/')
parser.add_argument('--model_name', type=str, default='resnet')
args = parser.parse_args()
os.environ['TORCH_HOME']=r'F:\code\Imagenet\model'
def compute_bound(mean, std):
channel1_low_bound = (0 - mean[0]) / std[0]
channel1_hign_bound = (1 - mean[0]) / std[0]
channel2_low_bound = (0 - mean[1]) / std[1]
channel2_hign_bound = (1 - mean[1]) / std[1]
channel3_low_bound = (0 - mean[2]) / std[2]
channel3_hign_bound = (1 - mean[2]) / std[2]
bound = [channel1_low_bound, channel1_hign_bound, channel2_low_bound, channel2_hign_bound, channel3_low_bound, channel3_hign_bound]
return bound
def inv_img(img, mean, std):
img[:,0,:,:] = img[:,0,:,:] * std[0] + mean[0]
img[:,1,:,:] = img[:,1,:,:] * std[1] + mean[1]
img[:,2,:,:] = img[:,2,:,:] * std[2] + mean[2]
return img
class SelectedImagenet(Dataset):
def __init__(self, imagenet_val_dir, selected_images_csv, transform=None):
super(SelectedImagenet, self).__init__()
self.imagenet_val_dir = imagenet_val_dir
self.selected_images_csv = selected_images_csv
self.transform = transform
self._load_csv()
def _load_csv(self):
reader = csv.reader(open(self.selected_images_csv, 'r'))
next(reader)
self.selected_list = list(reader)[0:100]
def __getitem__(self, item):
target, target_name, image_name, _ = self.selected_list[item]
image = Image.open(os.path.join(self.imagenet_val_dir, image_name))
if image.mode != 'RGB':
image = image.convert('RGB')
if self.transform is not None:
image = self.transform(image)
return image, int(target)
def __len__(self):
return len(self.selected_list)
class NAA(object):
def __init__(self, epsilon, alpha, steps, T_niters, mu, gamma, target_layer, bound, device):
self.epsilon = epsilon
self.steps = steps
self.T_niters = T_niters
self.alpha = alpha
self.bound = bound
self.device = device
self.mu = mu
self.gamma = gamma
self.target_layer = target_layer
self.bound = bound
def attack(self, model, ori_img, label):
gradients = []
def hook_backward(module, grad_input, grad_output):
gradients.append(grad_output[0].clone().detach())
Handle_backward = []
for layer_name, layer in model.named_children():
if layer_name == self.target_layer:
backward_hook = layer.register_full_backward_hook(hook_backward)
# Handle_backward.append(backward_hook)
baseline = torch.zeros_like(ori_img).to(self.device)
for i in range(self.steps):
img_tmp = baseline + (float(i) / self.steps) * (ori_img - baseline)
img_tmp.requires_grad_(True)
output = model(img_tmp)
output_label = output.gather(1, label.view(-1,1)).squeeze(1)
grad = torch.autograd.grad(output_label, img_tmp, grad_outputs = torch.ones_like(output_label), retain_graph = True, create_graph=True)
Intergrated_Attention = sum(gradients) / self.steps
backward_hook.remove()
# for handle in Handle_backward:
# handle.remove()
# features = []
# def hook_forward(module, input, output):
# features.append(output.clone().detach())
# forward_hook = model.layer2.register_forward_hook(hook_forward)
# _ = model(baseline)
# base_feature = features[0]
# forward_hook.remove()
x_input = ori_img.clone().detach()
loss = 0
g_t = torch.zeros_like(x_input)
model_children = torchvision.models._utils.IntermediateLayerGetter(model, {self.target_layer: 'feature'})
x_adv = x_input.clone().detach()
for t in range(self.T_niters):
x_adv.requires_grad_(True)
base_feature = model_children(baseline)['feature']
x_feature = model_children(x_adv)['feature']
attribution = (x_feature - base_feature) * Intergrated_Attention
blank = torch.zeros_like(attribution)
positive = torch.where(attribution >= 0, attribution, blank)
negative = torch.where(attribution < 0, attribution, blank)
## Transformation: Linear transformation performs the best
positive = positive
negative = negative
balance_attribution = positive + self.gamma * negative
loss = torch.sum(balance_attribution)
loss.backward()
grad = x_adv.grad
g_t = self.mu * g_t + grad / torch.norm(grad, p=1, dim = [1,2,3], keepdim = True)
x_adv = x_adv - self.alpha * torch.sign(g_t)
x_adv = torch.where(x_adv > ori_img + self.epsilon, ori_img + self.epsilon, x_adv)
x_adv = torch.where(x_adv < ori_img - self.epsilon, ori_img - self.epsilon, x_adv)
x_adv[:,0,:,:]= torch.clamp(x_adv[:,0,:,:], self.bound[0], self.bound[1])
x_adv[:,1,:,:]= torch.clamp(x_adv[:,1,:,:], self.bound[2], self.bound[3])
x_adv[:,2,:,:]= torch.clamp(x_adv[:,2,:,:], self.bound[4], self.bound[5])
x_adv = x_adv.detach()
print(x_adv - ori_img)
return x_adv
if __name__ == '__main__':
steps = args.steps
T_niters = args.T_niters
alpha = args.alpha / (255 * 0.225)
epsilon = args.epsilon /(255 * 0.225)
mu = args.mu
gamma = args.gamma
target_layer = args.target_layer
model_name = args.model_name
batch_size = args.batch_size
save_dir = args.save_dir
if torch.cuda.is_available():
device = torch.device('cuda')
else:
device = torch.device('cpu')
if model_name == 'squeezenet':
model = torchvision.models.squeezenet1_0(pretrained=True)
elif model_name == 'vgg':
model = torchvision.models.vgg19(pretrained=True)
elif model_name == 'inception':
model = torchvision.models.inception_v3(pretrained=True)
elif model_name == 'senet':
model = pretrainedmodels.__dict__['senet154'](num_classes=1000, pretrained='imagenet')
elif model_name == 'resnet':
model = torchvision.models.resnet50(pretrained=True)
elif model_name == 'densenet':
model = torchvision.models.densenet121(pretrained=True)
else:
print('No implemation')
if model_name in ['squeezenet', 'resnet', 'densenet', 'senet' ,'vgg']:
input_size = [3, 224, 224]
else:
input_size = [3, 299, 299]
model.eval()
model.to(device)
mean = (0.485, 0.456, 0.406)
std = (0.229, 0.224, 0.225)
norm = T.Normalize(tuple(mean), tuple(std))
resize = T.Resize(tuple((input_size[1:])))
bound = compute_bound(mean, std)
trans = T.Compose([
T.Resize((256,256)),
T.CenterCrop((224,224)),
resize,
T.ToTensor(),
norm
])
dataset = SelectedImagenet(imagenet_val_dir='data/imagenet/ILSVRC2012_img_val',
selected_images_csv='data/imagenet/selected_imagenet.csv',
transform=trans
)
ori_loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers = 0, pin_memory = False)
attack_type = NAA(epsilon, alpha, steps, T_niters, mu, gamma, target_layer, bound, device)
label_ls = []
for ind, (ori_img , label) in enumerate(ori_loader):
label_ls.append(label)
ori_img = ori_img.to(device)
label = label.to(device)
img_adv = attack_type.attack(model, ori_img, label)
print('successful')
predict = model(ori_img)
predict_adv = model(img_adv)
predicted = torch.max(predict.data, 1)[1]
predicted_adv = torch.max(predict_adv.data, 1)[1]
print('label:', label)
print('predict:', predicted)
print('predict_adv:', predicted_adv)
img_adv = inv_img(img_adv, mean, std)
np.save(save_dir + '/batch_{}.npy'.format(ind), torch.round(img_adv.data*255).cpu().numpy().astype(np.uint8()))
del img, ori_img, img_adv
print('batch_{}.npy saved'.format(ind))
label_ls = torch.cat(label_ls)
np.save(save_dir + '/labels.npy', label_ls.numpy())
print('images saved')