在GeekPwn2016硅谷分会场上,来自北美工业界和学术界的顶尖安全专家针对当前流行的图形对象识别、语音识别的场景,为大家揭示了如何通过构造对抗性攻击数据,要么让其与源数据的差别细微到人类无法通过感官辨识到,要么该差别对人类感知没有本质变化,而机器学习模型可以接受并做出错误的分类决定,并且同时做了攻击演示。其中来自OpenAI的Ian Goodfellow和谷歌大脑的Alexey Kurakin分享了“对抗性图像”在现实物理世界欺骗机器学习的效果。攻击者可以针对使用图像识别的无人车,构造出一张图片,在人眼看来是一个停车的路牌,但是在汽车看来是一个限速60的标志 [1] 。下面我们将简单介绍攻击图像分类模型(见图14-1)的基本原理。
图14-1 攻击图像分类模型示意
常见的图像分类算法有AlexNet、VGG16、ResNet50和InceptionV3,下面我们简单介绍一下这些算法。
1.AlexNet
AlexNet支持双GPU结构,结构如图14-2所示。AlexNet使用了Dropout防止过拟合,使用ReLU不容易发生梯度发散。
图14-2 AlexNet结构 [2]
2.VGG16
如图14-3所示,VGG16由13个卷积层和3个全连接组成。
图14-3 VGG16的结构 [3]
3.ResNet50
深度学习网络的深度对最后的分类和识别效果有着很大的影响,所以正常想法就是能把网络设计得越深越好,但是事实上却不是这样,常规的网络的堆叠在网络越深时,效果越差。ResNet引入了残差网络结构,通过残差网络,可以把网络层弄得很深,据说现在达到了1000多层,可最终的网络分类效果还是非常好。ResNet在2015年名声大噪,而且影响了2016年深度学习在学术界和工业界的发展方向,ResNet50就是ResNet的一种,其结构如图14-4所示。
图14-4 ResNet50的结构 [4]
4.InceptionV3
一般的卷积层只是一味地增加卷积层的深度,但是在单层上卷积核却只有一种,这样特征提取的功能可能会比较弱。Google增加单层卷积层的宽度,即在单层卷积层上使用不同尺度的卷积核,他们构建了Inception这个基本单元,基本的Inception中有1×1卷积核、3×3卷积核、5×5卷积核,还有一个3×3下采样,从而产生了InceptionV1模型,如图14-5所示。InceptionV3的改进是使用了2层3×3的小卷积核替代了5×5卷积核。图书《Web安全之深度学习实战》中识别WebShell时也用到了类似的思路,使用大小分别为3、4和5的一维卷积处理PHP的opcode序列,效果也非常不错。
图14-5 Inception单元结构 [5]
若要很好地理解针对图像分类模型的攻击,需要重温一下梯度算法和损失函数。在深度学习模型里面,经常需要使用梯度算法,针对损失函数的反馈不断调整各层的参数,使得损失函数最小化。损失函数可以理解为理想和现实之间的差距,通常定义一个函数来描述真实值和预测值之间的差异,在训练阶段,真实值就是样本对应的真实标签,预测值就是机器学习模型预测的标签值,这些都是明确的,所以损失函数是可以定义和计算的。机器学习模型训练的过程就是不断调整参数追求损失函数最小化的过程。梯度可以理解为多元函数的指定点上升的坡度,假设多元函数可以表示为f(x,y),那么对应的梯度的定义为:
可见梯度可以用偏导数来定义,通常损失函数就是这个多元函数,特征向量可以看成这个多元函数的某个点。在训练过程中,针对参数的调整可以使用梯度和学习率来定义,其中学习率也叫作学习步长,物理含义就是变量在梯度方向上移动的长度。学习率是一个非常重要的参数,过大会导致损失函数的震荡难以收敛,过小会导致计算缓慢。目前还没有很成熟的理论来推导最合适的学习率,经验值在0.001~0.1。以表示学习率,那么迭代更新参数x的方法为:
当我们求函数的最大值时,我们会向梯度向上的方向移动,所以使用加号,也称为梯度上升算法。如果我们想求函数的最小值时,则需要向梯度向下的方向移动,也称为梯度下降算法。所以使用减号,比如求损失函数最小值,对应迭代求解的方法为:
我们通过一个非常简单的例子演示这个过程,假设我们只有一个变量x,对应的损失函数定义为:
根据梯度的定义,可以获得对应的梯度为:
我们随机初始化x,学习率设置为0.1,整个过程如下:
def demo(): import random a=0.1 x=random.randint(1,10) y = x * x + 2 index=1 while index < 100 and abs(y-2) > 0.01 : y=x*x+2 print "batch={} x={} y={}".format(index,x,y) x=x-2*x*a index+=1
整个迭代过程最多100步,由于我们预先知道函数的最小值为2,所以如果当计算获得的函数值非常接近2,我们也可以提前退出迭代过程,比如绝对值相差不超过0.01。最后果然没让我们失望,在迭代20次后就找到了接近理论上的最小点,代码如下:
batch=14 x=0.329853488333 y=2.10880332377 batch=15 x=0.263882790666 y=2.06963412721 batch=16 x=0.211106232533 y=2.04456584141 batch=17 x=0.168884986026 y=2.02852213851 batch=18 x=0.135107988821 y=2.01825416864 batch=19 x=0.108086391057 y=2.01168266793 batch=20 x=0.0864691128455 y=2.00747690748
Keras里面提供相应的工具返回loss函数关于variables的梯度,variables为张量变量的列表,这里的loss函数即损失函数:
from keras import backend as K k.gradients(loss, variables)
Keras也提供了function用于实例化一个Keras函数,inputs是输入列表,其元素为占位符或张量变量,outputs为输出张量的列表
k.function(inputs, outputs, updates=[])
在使用机器学习进行图像分类时,通常获得的是针对不同分类标签的概率值,我们加载Keras预训练好的InceptionV3模型进行图片识别与分类,代码如下。这部分代码在GitHub的code/hackImage.py。
def demo2(): from keras.applications.resnet50 import preprocess_input, decode_predictions model = inception_v3.InceptionV3() img = image.load_img("pig.jpg", target_size=(299, 299)) original_image = image.img_to_array(img) original_image /= 255. original_image -= 0.5 original_image *= 2. original_image = np.expand_dims(original_image, axis=0) preds = model.predict(original_image) print('Predicted:', decode_predictions(preds, top=3)[0])
小猪佩琪的图片如图14-6所示,识别结果为:
('Predicted:', [(u'n04116512', u'rubber_eraser', 0.18097804), (u'n04254120', u'soap_dispenser', 0.15356822), (u'n04579432', u'whistle', 0.060938589)])
图14-6 小猪佩琪图片
家猪照片如图14-7所示,识别结果为:
('Predicted:', [(u'n02395406', u'hog', 0.65429366), (u'n03935335', u'piggy_bank', 0.23287569), (u'n02396427', u'wild_boar', 0.007565801)])
图14-7 家猪照片
可见使用自然动物的照片识别准确率还是不错的,使用漫画动物识别率比较低。我们可以看到使用深度学习进行图像识别与分类,本质上是把一个高维的向量放到深度学习模型里面进行计算,获得不同分类结果的概率。我们把图像分类抽象成一个二分类问题,如图14-8所示,模型通过迭代计算获得一个分割线,把两类事物分开。攻击这类分类问题的原理就是,通过微小改变特征的值,越过分割线,然后获得一个错误的分类结果,如图14-9所示。尤其是在图像分类领域,特征的维度特别多,通过细小的更改,有一定的可能导致最后计算结果的较大改变。假如现在我们手上有一个家猪的照片,我们想伪造成烤面包机的照片,我们可以把损失函数定义为1减去烤面包机标签的概率,那么就可以使用梯度下降算法,或者把损失函数定义为烤面包机标签的概率,使用梯度上升算法,迭代调整图片的内容(也就是多维向量的数值)进行训练。这里需要强调的是,机器学习训练的过程是调整参数,攻击机器学习的过程是调整图片的内容。
图14-8 分类问题原理图 [6]
图14-9 攻击分类问题的原理图
我们这次攻击的目标是InceptionV3模型,Keras内置了这个模型,我们直接使用就可以了。从模型中直接获取第一层的输入作为输入层,最后一层的输出为输出层。代码如下:
model = inception_v3.InceptionV3() model_input_layer = model.layers[0].input model_output_layer = model.layers[-1].output
然后加载我们攻击的图片,比如我们的小猪。这里需要特别强调的是,NumPy出于性能考虑,默认的变量赋值会引用同样一份内存,所以我们需要使用np.copy手工强制复制一份图像数据,代码如下:
img = image.load_img("pig.jpg", target_size=(299, 299)) original_image = image.img_to_array(img) hacked_image = np.copy(original_image)
为了避免图像变化过大,超过肉眼可以接受的程度,我们需要定义阈值:
max_change_above = original_image + 0.01 max_change_below = original_image - 0.01
下面我们要定义最关键的3个函数了。我们定义损失函数是识别为烤面包机的概率,因此我们需要使用梯度上升算法,不断追求损失函数的最大化,变量object_type_to_fake定义的就是烤面包机对应的标签。有了损失函数以后,我们就可以通过Keras的接口获取到对应的梯度函数。最后通过K.function获取一个Keras函数实例,该函数的输入列表分别为输入层和当前是训练模式还是测试模式的标记learning_phase(),输出列表是损失函数和梯度。关于K.function的使用建议阅读Keras的在线文档 [7] ,代码如下:
cost_function = model_output_layer[0, object_type_to_fake] gradient_function = K.gradients(cost_function, model_input_layer)[0] grab_cost_and_gradients_from_model = K.function([model_input_layer, K.learning_phase()], [cost_function, gradient_function])
下面我们就可以开始通过训练迭代最终获得我们需要的图片了,我们认为烤面包机的概率超过60%即可,所以我们定义损失函数的值超过0.6即可以完成训练。我们设置使用训练模式,learning_phase()标记为0,使用梯度上升算法迭代获取新的图片内容。为了不影响肉眼识别,超过阈值的部分会截断,这部分功能使用NumPy的np.clip即可完成,代码如下:
while cost < 0.60: cost, gradients = grab_cost_and_gradients_from_model([hacked_image, 0]) hacked_image += gradients * learning_rate print gradients hacked_image = np.clip(hacked_image, max_change_below, max_change_above) hacked_image = np.clip(hacked_image, -1.0, 1.0)
输出梯度的内容,便于我们理解:
[[ 2.29095144e-06 4.88560318e-07 -1.26309533e-06] [ -1.21029143e-06 -7.01245654e-06 -9.00149917e-06] [ -8.28917791e-07 -3.46928073e-06 3.33982143e-06] ..., [ -2.91559354e-06 -8.72657665e-07 6.22621087e-07] [ 2.66754637e-06 1.84044097e-06 -2.53160965e-06] [ -4.96620885e-07 3.94217068e-07 -7.95937069e-07]]]]
训练完成后,保存图片即可。这里需要说明的是,图像保存到NumPy变量后,每个维度都是0~255之间的整数,需要转换成-1~1之间的小数便于模型处理。保存成图像的时候需要再转换回以前的范围,代码如下:
img = hacked_image[0] img /= 2. img += 0.5 img *= 255. im = Image.fromarray(img.astype(np.uint8)) im.save("hacked-pig-image.png")
在我的Mac本经过接近2小时3070次迭代训练,我们获得了新的家猪图像,如图14-10所示。但是机器学习模型识别它为烤面包机的概率却达到了95.61%,我们攻击成功。在GPU服务器上大致运行5分钟可以得到一样的结果。
图14-10 基于梯度下降算法被识别为烤面包机的家猪
在基于梯度上升的攻击算法中,我们对图像内容的更新是基于下列公式:
正如梯度下降算法在深度学习训练中收敛缓慢一样,基于梯度上升的图像攻击也非常缓慢,需要进行多次迭代才能获取结果。Ian Goodfellow在文献 [8] 中指出,可以通过优化迭代算法来加速训练过程,他提出了FGSM(Fast Gradient Sign Method)方法,其核心公式为:
其中,∈代表一个很小的系数,在文献中为0.007,sign为阶梯函数,定义如下:
除了迭代环节,FGSM与基于梯度上升的算法完全相同。在迭代环节,我们通过NumPy的sign函数对梯度进行处理,然后迭代更新图片内容,代码如下:
e=0.007 while cost < 0.60: cost, gradients = grab_cost_and_gradients_from_model([hacked_image, 0]) #fast gradient sign method #EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES n=np.sign(gradients) hacked_image +=n*e hacked_image = np.clip(hacked_image, max_change_below, max_change_above) hacked_image = np.clip(hacked_image, -1.0, 1.0) print("batch:{} Cost: {:.8}%".format(index,cost * 100)) index+=1
在我的Mac本经过2分钟16次迭代训练,我们获得了新的家猪图像,如图14-11所示。但是机器学习模型识别它为烤面包机的概率却达到了74.31%,迭代次数明显减少,代码如下:
batch:11 Cost: 2.7044188% batch:12 Cost: 16.616838% batch:13 Cost: 38.806009% batch:14 Cost: 52.693129% batch:15 Cost: 38.372087% batch:16 Cost: 74.312818%
由于我们设置的退出条件是概率大于60%,所以FGSM没有继续迭代下去,我们通过设置阈值可以得到概率更大的图片,在进一步的实验中我们通过37次迭代得到了概率为99.56%的攻击图片(见图14-12):
batch:34 Cost: 97.030985% batch:35 Cost: 90.346575% batch:36 Cost: 63.920081% batch:37 Cost: 99.558592%
图14-11 基于FGSM算法被识别为烤面包机的家猪(16次迭代训练)
图14-12 基于FGSM算法被识别为烤面包机的家猪(37次迭代训练)
Ian Goodfellow在他的论文 [9] 中指出,针对图像的攻击方式在现实生活中也可以发生,如图14-13所示,攻击图片经过拍照打印后依然可以欺骗图像分类模型,系统错把“洗衣机”标签识别为“保险箱”。
图14-13 攻击图片经过拍照打印后依然可以欺骗图像分类模型
[1] https://www.leiphone.com/news/201701/1mbCZTulsqi1XOI4.html
[2] https://www.cnblogs.com/52machinelearning/p/5821591.html
[3] https://www.cs.toronto.edu/~frossard/post/vgg16/
[4] http://blog.csdn.net/mao_feng/article/details/52734438
[5] http://blog.csdn.net/numeria/article/details/73611456
[6] 图14-8和图14-9均引自网址https://medium.com/@ageitgey/machine-learning-is-fun-part-8-how-tointentionally-trick-neural-networks-b55da32b7196。
[7] http://keras-cn.readthedocs.io/en/latest/backend/
[8] https://arxiv.org/abs/1412.6572
[9] https://arxiv.org/abs/1607.02533