作业所需数据 akti
作业参考答案
作业要求
本此作业是一个“adversarial attack”的简单实践。要求我们使用两种方法攻击神经网络,一种是“FGSM”,另一种自选
对抗攻击
“adversarial attack”被翻译为“对抗攻击”,它的手段是对输入样本故意添加一些人无法察觉的细微的干扰,导致模型以高置信度给出一个错误的输出。直观的理解是:对训练数据添加细微的扰动使其不被发现,同时使得神经网络误判,影响其性能。
对抗攻击分类
对抗攻击大体分为四类:
白盒攻击,称为White-box attack,也称为open-box 对模型和训练集完全了解,这种情况比较简单,但是和实际情况不符合。
黑盒攻击,称为Black-box attack,对模型不了解,对训练集不了解或了解很少。这种攻击和实际情况比较符合,主要也是主要研究方向。
定向攻击,称为targeted attack,对于一个多分类网络,把输入分类误判到一个指定的类上
非定向攻击,称为non-target attack,只需要生成对抗样本来欺骗神经网络,可以看作是上面的一种特例。
FGSM
FGSM,全称Fast Gradient Sign Method(快速梯度下降法),FGSM会生成一个噪声,在原图像上加上这个噪声,会使得图像分类错误。
上图是论文中的示例,原本是熊猫的图加上噪声,被模型识别成了长臂猿。
噪声是如何得到的呢?我们原本训练网络,图像作为输入,优化的是模型的参数,而FGSM则把模型参数当作输入,改变的图像的输入,并且改变的方向与其损失函数的方向相反。使得损失函数越来越大。
表达式:
X ′ = X + ε ∗ s i g n ( ∇ x J ( X , y ) ) X^{'}=X+\varepsilon *sign(\nabla_{x} J(X,y)) X′=X+ε∗sign(∇xJ(X,y))
x x x是原图像, x ′ x^{'} x′表示被攻击后的图像, ε \varepsilon ε 是一个调节系数, s i g n ( ) sign() sign() 是一个符号函数,代表的意思也很简单,就是取一个值的符号,当值大于 0 时取 1,当值等于 0 时取 0,当值小于 0 时取 -1,▽ 表示求 x 的梯度,可以理解为偏导,J 是训练模型的损失函数。
从函数可以看出,FGSM攻击且只采用方向,不直接采用梯度值, ε \varepsilon ε会控制扰动的大小。
FGSM算法简单有效,在图像攻击领域扮演着非常重要的角色,后续还有类似迭代版FGSM等改进算法。
任务1示例代码
下面给出的参考答案中的代码:
class Attacker:
def __init__(self, img_dir, label):
# 讀入預訓練模型 vgg16(也可选择其它模型)
self.model = models.vgg16(pretrained = True)
self.model.cuda()
self.model.eval()
self.mean = [0.485, 0.456, 0.406]
self.std = [0.229, 0.224, 0.225]
# 把圖片 normalize 到 0~1 之間 mean 0 variance 1
self.normalize = transforms.Normalize(self.mean, self.std, inplace=False)
transform = transforms.Compose([
transforms.Resize((224, 224), interpolation=3),
transforms.ToTensor(),
self.normalize
])
# 利用 Adverdataset 這個 class 讀取資料
self.dataset = Adverdataset('./data/images', label, transform)
self.loader = torch.utils.data.DataLoader(
self.dataset,
batch_size = 1,
shuffle = False)
####################FGSM 核心代码
# FGSM 攻擊
def fgsm_attack(self, image, epsilon, data_grad):
# 找出 gradient 的方向
sign_data_grad = data_grad.sign()
# 將圖片加上 gradient 方向乘上 epsilon 的 noise(对应公式)
perturbed_image = image + epsilon * sign_data_grad
# 將圖片超過 1 或是小於 0 的部分 clip 掉
# perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
def attack(self, epsilon):
# 存下一些成功攻擊後的圖片 以便之後顯示
adv_examples = []
wrong, fail, success = 0, 0, 0
for (data, target) in self.loader:
data, target = data.to(device), target.to(device)
data_raw = data;
data.requires_grad = True
# 將圖片丟入 model 進行測試 得出相對應的 class
output = self.model(data)
init_pred = output.max(1, keepdim=True)[1]
# 如果 class 錯誤 就不進行攻擊
if init_pred.item() != target.item():
wrong += 1
continue
# 如果 class 正確 就開始計算 gradient 進行 FGSM 攻擊
loss = F.nll_loss(output, target)
self.model.zero_grad()
loss.backward()
data_grad = data.grad.data
perturbed_data = self.fgsm_attack(data, epsilon, data_grad)
# 再將加入 noise 的圖片丟入 model 進行測試 得出相對應的 class
output = self.model(perturbed_data)
final_pred = output.max(1, keepdim=True)[1]
if final_pred.item() == target.item():
# 辨識結果還是正確 攻擊失敗
fail += 1
else:
# 辨識結果失敗 攻擊成功
success += 1
# 將攻擊成功的圖片存入
if len(adv_examples) < 5:
adv_ex = perturbed_data * torch.tensor(self.std, device = device).view(3, 1, 1) + \
torch.tensor(self.mean, device= device).view(3, 1, 1)
adv_ex = adv_ex.squeeze().detach().cpu().numpy()
data_raw = data_raw * torch.tensor(self.std, device = device).view(3, 1, 1) + \
torch.tensor(self.mean, device = device).view(3, 1, 1)
data_raw = data_raw.squeeze().detach().cpu().numpy()
adv_examples.append( (init_pred.item(), final_pred.item(), data_raw , adv_ex) )
final_acc = (fail / (wrong + success + fail))
print("Epsilon: {}\tTest Accuracy = {} / {} = {}\n".format(epsilon, fail, len(self.loader), final_acc))
return adv_examples, final_acc
最后攻击生成的图像
任务2尝试
除了使用FGSM,作业还要求使用另一种算法,这里我使用I-FGSM,是基于FGSM的一个改进算法。
FGSM每次更新只更新一次,而有时候单次更新不足以成功,因此在原来的基础上提出迭代式的FGSM, 也就是I-FGSM(iterative FGSM)
I-FGSM的公式如下:
X 0 ′ = X , X N + 1 ′ = C l i p x , ε { X N ′ + α s i g n ( ∇ x J ( X N ′ , y ) ) } X_{0}^{'}=X, X_{N+1}^{'}=Clip_{x}, _{\varepsilon}\{X_{N}^{'}+\alpha sign(\nabla_{x} J(X_{N}^{'},y)) \} X0′=X,XN+1′=Clipx,ε{XN′+αsign(∇xJ(XN′,y))}
这个公式中,α表示权重,论文中取1,迭代次数用N表示,论文中N取min(e+4, 1.25e),这部分其实就是将总的噪声幅值分配到每一次迭代中,因此在给定噪声幅值e的前提下,还可以直接用α=e/N来设置α和N参数。另外式子中的Clip表示将溢出的数值用边界值(0或1)代替,以解决部分像素值可能会溢出(比如超出0到1的范围)的问题,保证生成有效的图像。其具体表达式如下:
c l i p x , ε { X ′ } = { X + ε , 如 果 X ′ > X + ε X − ε , 如 果 X ′ < X − ε X ′ , 如 果 X ′ ∈ ( X − ε , X + ε ) 1 , 数 值 超 过 像 素 数 值 上 限 0 , 数 值 超 过 像 素 数 值 下 限 clip_{x,\varepsilon}\{X^{'} \}=\left\{ \begin{aligned} X+\varepsilon,如果X^{'}>X+\varepsilon \\ X-\varepsilon,如果X^{'}<X-\varepsilon \\ X^{'},如果X^{'}\in(X-\varepsilon,X+\varepsilon)\\ 1,数值超过像素数值上限\\ 0,数值超过像素数值下限 \end{aligned} \right. clipx,ε{X′}=⎩⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎧X+ε,如果X′>X+εX−ε,如果X′<X−εX′,如果X′∈(X−ε,X+ε)1,数值超过像素数值上限0,数值超过像素数值下限
代码尝试
def where(self,cond, x, y):
cond = cond.float()
return (cond*x) + ((1-cond)*y)
def i_fgsm_attack(self,image,epsilon, data_grad,alpha,iteration):
perturbed_image=image
for i in range(iteration):
sign_data_grad = data_grad.sign()
perturbed_image = perturbed_image + alpha * sign_data_grad
# for j in range(len(perturbed_image)):
# perturbed_image[j]=self.clip(image[j],epsilon,perturbed_image[j])
perturbed_image=self.where(perturbed_image > (image + epsilon),image + epsilon,perturbed_image)
perturbed_image=self.where(perturbed_image < (image - epsilon),image - epsilon,perturbed_image)
perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
最终结果,alpha 选择的是1像素.
参考
https://zhuanlan.zhihu.com/p/104532285?utm_source=qq