5.6.2 使用PyTorch实现JSMA
5.6.2 使用pytorch实现jsma
下面介绍在pytorch平台实现jsma算法的基本过程,示例代码位于:
https://github.com/duoergun0729/adversarial_examples/blob/master/code/
5-jsma-pytorch.ipynb
在示例中主要流程为,用原始图像的值初始化对抗样本,通过前向函数计算梯度,然后根据jsma算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值等于攻击目标的标签为止,如图5-21所示。这里需要指出的是,jsma中梯度是直接通过前向计算获得的logits的输出结果与输入之间计算产生的,不依赖于损失函数,这是jsma与fgm、deepfool的区别。
图5-21 使用pytorch实现jsma示例
下面我们介绍使用pytorch实现jsma的核心代码,首先设置仅输入数据可以计算梯度,模型的其他参数不计算梯度。
#图像数据梯度可以获取
img.requires_grad = true
#设置为不保存梯度值,自然也无法修改
for param in model.parameters():
param.requires_grad = false
定义一些全局参数,包括最大迭代次数、扰动系数和像素值的边界。
epochs=500
#扰动系数
theta=0.3
#定义边界
max_=3.0
min_=-3.0
pytorch在进行图像数据标准化时,imagenet2012的处理方式略有不同,均值和标准差分别为[0.485,0.456,0.406]和[0.229,0.224,0.225],因此像素值的边界不是常见的[0,1]或者[–1,1],而是[–3,3]。事实上像素值的边界会比[–3,3]范围稍小,但是不会超过这个范围。
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
img /= 255.0
img = (img - mean) / std
定义目标标签和损失函数,这里的损失函数仅仅是为了便于显示迭代计算过程中是否收敛,并不是用于反向传递计算梯度。
#攻击目标
target_label=288
target=variable(torch.tensor([float(target_label)]).to(device).long())
loss_func = torch.nn.crossentropyloss()
定义搜索空间mask,mask大小与输入一致,初始化时认为输入的每个像素都在搜索空间中,当迭代过程中某个像素的值超过边界,就把mask中对应的位置设置为0。
mask = np.ones_like(img.data.cpu().numpy())
下面开始迭代计算过程,迭代退出条件是达到最大迭代次数或者分类标签与攻击目标的标签相同,定向攻击成功。
for epoch in range(epochs):
# 向前计算过程
output = model(img)
label=np.argmax(output.data.cpu().numpy())
loss = loss_func(output, target)
print("epoch={} label={} loss={}".format(epoch,label,loss))
#如果定向攻击成功
if label == target_label:
break
迭代计算的过程就是不断提高预测为攻击目标的概率。通过saliency_map可以计算出当前最有利于提高该概率的像素坐标idx以及对应的方向pix_sign。
#梯度清零
zero_gradients(img)
idx, pix_sign=saliency_map(output, img,target_label, mask)
#进行扰动
img.data[idx]=img.data[idx]+pix_sign * theta * (max_ - min_)
当修改的像素的数值超过边界时,从搜索空间mask中去掉该像素,即在mask中将其设置为0,后继的迭代将不再修改该像素。
#达到极限的点不再参与更新
if (img.data[idx]<=min_) or (img.data[idx]>=max_):
print("idx={} over {}".format(idx,img.data[idx]))
mask[idx]=0
img.data[idx]=np.clip(img.data[idx], min_, max_)
saliency_map的具体实现方式为,根据前向计算获得的logits的输出结果f与输入x、攻击目标标签t,计算出f相对于x的梯度derivative。根据derivative计算出α参数,这里简化了β参数的计算,相当于只关注了α参数。
def saliency_map(f, x,t, mask):
#通过梯度函数,计算出对目标分类贡献最大的像素
f[0, t].backward(retain_graph=true)
derivative=x.grad.data.cpu().numpy().copy()
alphas = derivative * mask
#用于记录哪些像素点被修改过
betas = -np.ones_like(alphas)
α参数和β参数本身是带有符号的,即derivative大的为正,反之为负,因此可将求解预测为攻击目标的概率贡献最大的点的问题,转换为求解α参数和β参数乘积最小的问题,即有:
sal_map = np.abs(alphas) * np.abs(betas) * np.sign(alphas * betas)
#查找对攻击最有利的像素点
idx = np.argmin(sal_map)
idx是一维坐标,还需要通过np.unravel_index将其转换为像素位置坐标。
#转换成(p1,p2)格式
idx = np.unravel_index(idx, mask.shape)
pix_sign = np.sign(alphas)[idx]
return idx, pix_sign
如图5-22所示,经过166轮迭代,攻击成功,l0为106即只修改了106个像素,l2为对抗样本与原始图像之间的差别。
epoch=164 label=51 loss=1.828399658203125
epoch=165 label=51 loss=1.7898340225219727
epoch=166 label=288 loss=1.7511415481567383
l0=(106,) l2=1428.7942469089103
图5-22 原始数据和对抗样本的对比示意图
下面介绍在pytorch平台实现jsma算法的基本过程,示例代码位于:
https://github.com/duoergun0729/adversarial_examples/blob/master/code/
5-jsma-pytorch.ipynb
在示例中主要流程为,用原始图像的值初始化对抗样本,通过前向函数计算梯度,然后根据jsma算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值等于攻击目标的标签为止,如图5-21所示。这里需要指出的是,jsma中梯度是直接通过前向计算获得的logits的输出结果与输入之间计算产生的,不依赖于损失函数,这是jsma与fgm、deepfool的区别。
图5-21 使用pytorch实现jsma示例
下面我们介绍使用pytorch实现jsma的核心代码,首先设置仅输入数据可以计算梯度,模型的其他参数不计算梯度。
#图像数据梯度可以获取
img.requires_grad = true
#设置为不保存梯度值,自然也无法修改
for param in model.parameters():
param.requires_grad = false
定义一些全局参数,包括最大迭代次数、扰动系数和像素值的边界。
epochs=500
#扰动系数
theta=0.3
#定义边界
max_=3.0
min_=-3.0
pytorch在进行图像数据标准化时,imagenet2012的处理方式略有不同,均值和标准差分别为[0.485,0.456,0.406]和[0.229,0.224,0.225],因此像素值的边界不是常见的[0,1]或者[–1,1],而是[–3,3]。事实上像素值的边界会比[–3,3]范围稍小,但是不会超过这个范围。
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
img /= 255.0
img = (img - mean) / std
定义目标标签和损失函数,这里的损失函数仅仅是为了便于显示迭代计算过程中是否收敛,并不是用于反向传递计算梯度。
#攻击目标
target_label=288
target=variable(torch.tensor([float(target_label)]).to(device).long())
loss_func = torch.nn.crossentropyloss()
定义搜索空间mask,mask大小与输入一致,初始化时认为输入的每个像素都在搜索空间中,当迭代过程中某个像素的值超过边界,就把mask中对应的位置设置为0。
mask = np.ones_like(img.data.cpu().numpy())
下面开始迭代计算过程,迭代退出条件是达到最大迭代次数或者分类标签与攻击目标的标签相同,定向攻击成功。
for epoch in range(epochs):
# 向前计算过程
output = model(img)
label=np.argmax(output.data.cpu().numpy())
loss = loss_func(output, target)
print("epoch={} label={} loss={}".format(epoch,label,loss))
#如果定向攻击成功
if label == target_label:
break
迭代计算的过程就是不断提高预测为攻击目标的概率。通过saliency_map可以计算出当前最有利于提高该概率的像素坐标idx以及对应的方向pix_sign。
#梯度清零
zero_gradients(img)
idx, pix_sign=saliency_map(output, img,target_label, mask)
#进行扰动
img.data[idx]=img.data[idx]+pix_sign * theta * (max_ - min_)
当修改的像素的数值超过边界时,从搜索空间mask中去掉该像素,即在mask中将其设置为0,后继的迭代将不再修改该像素。
#达到极限的点不再参与更新
if (img.data[idx]<=min_) or (img.data[idx]>=max_):
print("idx={} over {}".format(idx,img.data[idx]))
mask[idx]=0
img.data[idx]=np.clip(img.data[idx], min_, max_)
saliency_map的具体实现方式为,根据前向计算获得的logits的输出结果f与输入x、攻击目标标签t,计算出f相对于x的梯度derivative。根据derivative计算出α参数,这里简化了β参数的计算,相当于只关注了α参数。
def saliency_map(f, x,t, mask):
#通过梯度函数,计算出对目标分类贡献最大的像素
f[0, t].backward(retain_graph=true)
derivative=x.grad.data.cpu().numpy().copy()
alphas = derivative * mask
#用于记录哪些像素点被修改过
betas = -np.ones_like(alphas)
α参数和β参数本身是带有符号的,即derivative大的为正,反之为负,因此可将求解预测为攻击目标的概率贡献最大的点的问题,转换为求解α参数和β参数乘积最小的问题,即有:
sal_map = np.abs(alphas) * np.abs(betas) * np.sign(alphas * betas)
#查找对攻击最有利的像素点
idx = np.argmin(sal_map)
idx是一维坐标,还需要通过np.unravel_index将其转换为像素位置坐标。
#转换成(p1,p2)格式
idx = np.unravel_index(idx, mask.shape)
pix_sign = np.sign(alphas)[idx]
return idx, pix_sign
如图5-22所示,经过166轮迭代,攻击成功,l0为106即只修改了106个像素,l2为对抗样本与原始图像之间的差别。
epoch=164 label=51 loss=1.828399658203125
epoch=165 label=51 loss=1.7898340225219727
epoch=166 label=288 loss=1.7511415481567383
l0=(106,) l2=1428.7942469089103
图5-22 原始数据和对抗样本的对比示意图