georgesale 2020-06-05
图像识别作为人工智能最成熟的应用领域,已经大规模落地并服务于人们的日常生活。但在大规模商业化的同时,也面临更多方面的威胁。对抗样本通过对图像做微小的改动,在用户无感知的情况下,会导致AI系统被入侵、错误命令被执行。欺骗AI系统做出错误的决断,将会给社会造成重大的损失。通过研究如何欺骗AI系统,对现有的薄弱点进行修补,使得AI系统更健壮。
本文提供了一个强劲的攻击方案,该方案可以对ResNeXt50白盒模型、人工加固的灰盒模型、AutoDL模型均有显著的效果,并在飞桨AI安全对抗赛中取得较好成绩。在该比赛中,选手需要尽量小的扰动下骗过更多的模型,才能拿到更好的得分,一起来看看他是如何实现的吧。
01.方案部分结果展示
02.解题思路
相比初赛,决赛的难点在于多了一个人工加固的模型(灰盒),黑盒模型增加为三个,包括由AutoDL技术训练的模型,因此需要针对人工加固模型训练自己加固的模型。针对黑盒模型,我主要集成多样化的模型来逼近;针对AutoDL技术训练的模型,我主要集成AutoDL搜索出的网络结构来迁移攻击。
此外相比初赛,我在方案中添加了多样的越过局部最优的策略和限定变动像素点的限制,同时对生成的图片进行了小扰动截断,保证在提升迁移性能的同时降低MSE。
03.攻击方法
攻击所用算法主体为MomentumIteratorAttack,代码实现参照了advbox。在此基础之上,尝试了不同的目标函数,集成了不同的模型,添加了多样的越过局部最优的策略。
3.1目标函数
损失函数1:
其中,m为模型个数,为标签y的独热编码,为第i个模型的参数。此损失函数引用于[1]
代码如下:
loss_logp = -1*fluid.layers.log(1-fluid.layers.matmul(out1,one_hot_label[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out2,one_hot_label[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out3,one_hot_label2[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out4,one_hot_label2[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out5,one_hot_label2[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out6,one_hot_label2[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out8,one_hot_label2[0],transpose_y=True))\ -1*fluid.layers.log(1-fluid.layers.matmul(out9,one_hot_label2[0],transpose_y=True))
损失函数2:
其中,N为类别数量,M为模型数量,为当输入为X时,模型j判定类别i的概率。
代码如下:
out_total1 = fluid.layers.softmax(out_logits1[0]+out_logits2[0]) out_total2 = fluid.layers.softmax(out_logits3[0]+out_logits4[0]+out_logits5[0]+out_logits6[0]+out_logits7[0]+out_logits8[0]+out_logits9[0]) loss2 = fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out_total1, labellabel=label[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out_total2, label=label2[0]))
损失函数3:
符号含义同损失函数2,不再赘述。
代码如下:
ze = fluid.layers.fill_constant(shape=[1], value=-1, dtype='float32') loss = 1.2*fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out1, labellabel=label[0]))\ + 0.2*fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out2, labellabel=label[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out3, label=label2[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out4, label=label2[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out5, label=label2[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out6, label=label2[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out7, label=label2[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out8, label=label2[0]))\ + fluid.layers.matmul(ze, fluid.layers.cross_entropy(input=out9, label=label2[0]))
经过实测,发现三者效果接近,损失函数3效果最好。损失函数1的特点是程序运行快,猜测可能是求梯度简单。使用损失函数2时,攻击成功后的分类会接近一致。
3.2模型集成
集成模型选取思路为多元,尽可能多的不同模型,才可能逼近赛题背后的黑盒模型,所用模型总体描述如图3-1所示。
图3-1 模型集成细节
3.3灰盒模型攻击思路
决赛中的灰盒模型为人工加固的模型,结构为ResNeXt50。为了攻击黑盒模型,我在本地训练了一个加固模型,作为灰盒模型的逼近。训练加固模型涉及到训练集的选取和训练方法的选取,训练集的构成主要包含如下两部分:
图3-2 对抗训练部分样本集构建思路
对抗模型的训练方法与一般模型训练无异。为了探索更优的对抗模型,我们进行了几组不同训练集的对比(其他参数固定),如下表所示。
其中,86.45等分数是决赛评测分数,经过实验对比第一组实验设置效果最好。
3.4 越过山丘
下面主要介绍为了解决陷入局部最优采取的一系列策略。
3.4.1 图片粒度梯度反向
此方法受启发于[2],论文中采取双路寻优,一路采用常规方法梯度上升,如图3-3中绿线所示。另一路先采取梯度下降到达这一分类局部最优再进行梯度上升,以期找到更快的上升路径,如图3-3中蓝线所示。我在实现过程中对其进行简化,仅在迭代的第一步进行梯度下降。
图3-3 梯度策略示意图(图片来自论文[2])
核心代码如下:
if i==0:#第一次迭代梯度反向 advadv=adv+epsilon*norm_m_m else: advadv=adv-epsilon*norm_m_m
3.4.2 像素粒度梯度反向
此方法承接于3.4.1,可将3.4.1视为整个图片粒度的梯度反向。同样是为了越过局部最优,采用3.4.2方法,随机选取梯度中5%进行取反,可视为像素粒度的梯度反向,反转比例为一超参数。同样为了在迭代后期趋于稳定,反向的梯度的比例会随着迭代次数增加而减少。比例为一需要调节的超参数。
核心代码如下:
if i<50: #前50步,2%的梯度反响,随着i递减 试试5% dir_mask = np.random.rand(3,224,224) dir_maskdir_mask = dir_mask>(0.15-i/900) dir_mask[dir_mask==0] = -1 norm_m_m = np.multiply(norm_m_m,dir_mask)
3.4.3 每步迭代前,对原始图片添加高斯噪声
此方法受启发于[6],论文作者认为攻击模型的梯度具有噪声,损害了迁移能力。论文作者用一组原始图片加噪声后的梯度的平均代替原来的梯度,效果得到提升,形式化表述如下:
而我与论文作者理解不同,添加噪声意在增加梯度的噪声,以越过局部最优,再者多次计算梯度非常耗时,因此我选用了只加一次噪声,均值为0,方差为超参数,形式化表述如下:
同时,为在迭代后期趋于稳定,在添加噪声时,噪声的方差会随着迭代次数的增加而减小。
核心代码如下:
if i<50: adv_noise = (adv+np.random.normal(loc=0.0, scale=0.8+epsilon/90,size = (3,224,224))).astype('float32') else: adv_noise = (adv+np.random.normal(loc=0.0, scale=0.2,size = (3,224,224))).astype('float32')
3.4.4 将图片变动的像素点限定在梯度最大的5%
该方法受启发于[3], 随机在图像中选择max_pixels个点。而我采用的方法是计算所有像素的梯度,选取梯度最大的5%进行变动,此举可以有效降低MSE。
3.4.5 三通道梯度平均
该方法受启发于[3],文中,作者随机在图像中选择max_pixels个点 在多个信道中同时进行修改。因此我尝试了两种方法,一是只计算R或G或B通道的梯度,三个通道减去相同的梯度。二是将三通道的梯度进行平均。从结果来看,后者更有效,提分明显。
3.4.6 攻击后进行目标攻击
此方法受启发于[2],作者在成功越过分界线后进行消除无用噪声操作,作者认为此举可以加强对抗样本的迁移能力。
我的做法与此不同,我认为不仅要越过边界,还要走向这个错误分类的低谷。此举依据的假设是:尽管不同模型的分界线存在差异,模型学到的特征应是相似的。思路如图3-4中红色箭头所示,带有圆圈的数字表示迭代步数。
因此在成功攻击之后,我又添加了两步定向攻击,目标为攻击成功时被错分的类别。在集成攻击时,目标为被错分类别的众数。
图3-4 目标攻击示意图(修改自论文[2])
核心代码如下:
for i in range(2):#迭代两次进行目标攻击 adv_noise = (adv+np.random.normal(loc=0.0, scale=0.1,size = (3,224,224))).astype('float32') target_label=np.array([t_label]).astype('int64') target_label=np.expand_dims(target_label, axis=0) target_label2=np.array([origdict[t_label]]).astype('int64') target_label2=np.expand_dims(target_label2, axis=0) g,resul1,resul2,resul3,resul4,resul5,resul6,resul7,resul8,resul9 = exe.run(adv_program, fetch_list=[gradients,out1,out2,out3,out4,out5,out6,out7,out8,out9], feed={'label2':target_label2,'adv_image':adv_noise,'label': target_label } ) g = (g[0][0]+g[0][1]+g[0][2])/3 #三通道梯度平均 velocity = g / (np.linalg.norm(g.flatten(),ord=1) + 1e-10) momentum = decay_factor * momentum + velocity norm_m = momentum / (np.linalg.norm(momentum.flatten(),ord=2) + 1e-10) _max = np.max(abs(norm_m)) tmp = np.percentile(abs(norm_m), [25, 99.45, 99.5])#将图片变动的像素点限定在0.5% thres = tmp[2] mask = abs(norm_m)>thres norm_m_m = np.multiply(norm_m,mask) advadv=adv+epsilon*norm_m_m #实施linf约束 adv=linf_img_tenosr(img,adv,epsilon)
3.5 临门一脚
然而这不够,作为一个竞赛,人人都虎视眈眈盯着奖金的时候还需要不断的提升,因此还需要临门一脚,一种后处理方法。
小扰动截断:
使用上述方法后,我的结果在95-96分之间波动,为进一步提升成绩,我选用最高分96.53分图片进行后处理。后处理方法为:将攻击后的图片与原图片进行对比,对一定阈值以下的扰动进行截断。经过不断上探阈值,发现阈值为17(图片的像素范围为0-255)的时候效果最好。此方法提分0.3左右。
核心代码如下:
org_img = tensor2img(img) adv_img = tensor2img(adv) #17/256 以下的扰动全部截断 diff = abs(org_img-adv_img)<drop_thres #<17的为1 diff_max = abs(org_img-adv_img)>=drop_thres #>=17的为1 #<17的保留org_img tmp1 = np.multiply(org_img,diff) #>17的保留adv_img tmp2 = np.multiply(adv_img,diff_max) final_img = tmp1+tmp2
04.尝试过的方法
本小节介绍在比赛过程中尝试过但没效果过或者效果不明显的方法。
4.1 中间层攻击
依据论文[4]中所述,我用飞桨复现了文中方法,采用模型为ResNeXt50_32x4d,参数为初赛提供白盒模型参数,处理的中间层为res5a,学习率0.01,迭代次数为5次,损失函数为ILA。
图4-1 没有经过后处理生成的对抗样本与原样本对比图
图4-2 经过后处理的生成的对抗样本与原样本对比图
图4-2中Pos_Adversarial Image为图4-1中Adversarial Image图片使用中间层攻击生成的图片,使用该后处理方法后确如论文中所说图片更平滑了,但是迁移性能却下降了,因此弃用。
4.2 标签平滑
效果下降。
4.3 推后原始类别排名
以6分类问题为例说明推后原始类别排名想法,如图4-3所示。
图4-3 推后类别排名思路示意图
假设攻击前某张图片概率分布如图4-3第一行所示,属于第一类的概率最高。一旦攻击成功就停止的情况下,如图4-3第二行所示,属于第一类的概率下降为0.2,模型将原图误分类为第二类。
但是第一类的类别排序仍然靠前,在其他不同分界线模型下,很可能仍然能够正确分类(猜测)。因此将终止条件改进,不但要能够攻击成功,还要原始类别的排序靠后,如图4-3第三行,原始第一类的类别概率降至0.1,排序降至第四,也许会提升迁移性能。
在实验中,这种做法效果不是很明显。与此想法类似,我还尝试了定向攻击为原始分类概率最低的类别,同样效果不明显。
05.写在赛后
1. 以上就是本人在AI安全对抗赛取得第二名的全部方案,完成运行代码可以访问AI Studio项目:https://aistudio.baidu.com/aistudio/projectdetail/296291
2. 决赛赛程中我霸榜半个月有余,绞尽脑汁尝试各种攻击方法,不断阅读论文,不断尝试效果,并且照猫画虎快速入门了飞桨,收获很多。
3. 致读者,这是一个非常好的入门对抗样本的机会,细嚼baseline和我的方案将让你入门多种这个领域的算法。
4. 感谢AI Studio让我用到了v100,基本不用担心显存不够用,我自己的台式机装的gtx 1050连n年前的vgg都跑不动。
5. 一点脑洞:受启发于[5],为人脸添加一个眼睛可以达到错分目的,类似的给狗添加一幅眼镜。
图5.1 人脸添加眼镜示意图(截取自[5])