强化学习也可以被攻击,根据UC伯克利大学,OpenAI和宾夕法尼亚大学的研究人员发表的论文《Adversarial Attacks on Neural Network Policies》 [1] 以及内华达大学的论文《Vulnerability of Deep Reinforcement Learning to Policy Induction Attacks》 [2] 显示,广泛使用的强化学习算法,比如DQN、TRPO和A3C,在这种攻击面前都十分脆弱。即便是人类难以观察出来的微妙的干扰因素,也能导致系统性能减弱。比如引发一个智能体让乒乓球拍在本该下降时反而上升 [3] 。
Dawn Song在文献 [4] 中进行了定量的描述,场景为使用强化学习玩Pong游戏,如图14-14所示。通常玩Pong这类型游戏,需要使用强化学习里面的DQN,如图14-15所示,本质上也是处理图像然后进行神经网络计算得出各个动作对应的概率值,这点非常类似于图像分类的过程,所以可以使用FGSM算法对强化学习模型进行攻击。通过实验表明,使用FGSM可以显著影响其得分,如图14-16所示。
图14-14 使用强化学习玩Pong游戏
图14-15 玩Pong游戏使用的强化学习DQN
图14-16 使用FGSM攻击强化学习模型的效果 [5]
手写数字识别在生活中经常会遇到,比如银行领域识别用户手写的数字判断金额等。通常手写数字的识别依赖机器学习模型,下面我们模拟攻击手写数字识别模型。数据集依然使用MNIST数据集,机器学习模型使用CNN。这部分代码在GitHub的code/hackMnistImage.py。
1.构造手写数字识别的CNN模型
我们构造手写数字识别的CNN模型,架构如图14-17所示,参数如下:
·输入层大小为(28,28,1)。
·32个大小为(3,3)的卷积处理。
·64个大小为(3,3)的卷积处理。
·使用大小为(2,2)的池化处理,取最大值。
·压平为一维向量。
·节点数为128的全连接。
图14-17 手写数字识别的CNN模型
使用激活函数sigmoid,输出大小为10的一维向量,标记0~9各个数字的分类概率,代码如下:
model = Sequential() model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape)) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(128, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(num_classes, activation='softmax')) model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy']) model.summary() plot_model(model, show_shapes=True, to_file='hackImage/keras-cnn.png')
为了提高调试阶段的效率,我们训练好模型后就将训练好的参数保留,下次运行时可以直接加载对应的参数,省略训练过程,这也是最接近实际情况的,代码如下:
#保证只有第一次调用的时候会训练参数 if os.path.exists('hackImage/keras-cnn.h5'): model.load_weights('hackImage/keras-cnn.h5') else: model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(x_test, y_test)) score = model.evaluate(x_test, y_test, verbose=0) print('Test loss:', score[0]) print('Test accuracy:', score[1]) model.save_weights("hackImage/keras-cnn.h5")
2.选择被攻击的图片样本
简单起见,我们从MNIST数据集中选择100个数字1~9的图片,尝试欺骗机器学习模型,让它识别为数字0。MNIST数据集是黑底白字的图像,其中纯黑用0表示,纯白用255表示,每个像素的取值范围为0~255,为了处理方便我们需要转换到-1~1。最终我们将获得的100个手写数字的样本合成一张图片,如图14-18所示。为了美观,也可以转成白底黑字,如图14-19所示。代码如下:
#获取100个非0样本 def getDataFromMnist(): (x_train, y_train), (x_test, y_test) = mnist.load_data() #原有范围在0-255转换到 0-1 #x_train = (x_train.astype(np.float32) - 127.5)/127.5 #原有范围在0-255转换调整到-1和1之间 x_train = x_train.astype(np.float32)/255.0 x_train-=0.5 x_train*=2.0 x_train = x_train[:, :, :, None] x_test = x_test[:, :, :, None] #筛选非0的图片的索引 index=np.where(y_train!=0) print "Raw train data shape:{}".format(x_train.shape) x_train=x_train[index] print "All 1-9 train data shape:{}".format(x_train.shape) x_train=x_train[-100:] print "Selected 100 1-9 train data shape:{}".format(x_train.shape)
图14-18 被挑选出的手写数字图案(黑底白字)
图14-19 被挑选出的手写数字图案(白底黑字)
3.训练产生攻击样本
图像攻击算法我们依然使用效率较高的FGSM算法。我们构造CNN模型,获取整个模型的输入和输出层,定义损失函数和梯度的获取方式,设置我们要伪装成的数字的索引,代码如下:
cnn=trainCNN() #都伪装成0 object_type_to_fake=0 model_input_layer = cnn.layers[0].input model_output_layer = cnn.layers[-1].output 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])
依次训练100个样本,其中需要重点强调的是,对于彩色图片,可以设置较小的调整范围,比如0.01,但是对于灰度图像,尤其是MNIST这种,需要调整更大的范围,甚至是黑白颠倒,代码如下:
for index in range(100): progress_bar.update(index) mnist_image_raw=generator_images[index] mnist_image_hacked = np.copy(mnist_image_raw) mnist_image_hacked = np.expand_dims(mnist_image_hacked, axis=0) #调整的极限 彩色图片 #max_change_above = mnist_image_raw + 0.01 #max_change_below = mnist_image_raw - 0.01 #调整的极限 灰度图片 max_change_above = mnist_image_raw + 1.0 max_change_below = mnist_image_raw - 1.0
使用FGSM算法对图像进行迭代调整,直到损失函数的值达到0.8以上,即被识别为数字0的概率超过80%:
cost=0 while cost < 0.80: cost, gradients = grab_cost_and_gradients_from_model([mnist_image_hacked, 0]) # fast gradient sign method # EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES # hacked_image += gradients * learning_rate n = np.sign(gradients) mnist_image_hacked += n * e mnist_image_hacked = np.clip(mnist_image_hacked, max_change_below, max_change_above) mnist_image_hacked = np.clip(mnist_image_hacked, -1.0, 1.0) #覆盖原有图片 generator_images[index]=mnist_image_hacked
4.攻击结果
整个过程非常快,即使是在Mac本上也可以在5分钟内完成训练。训练产生的原始攻击样本如图14-20所示,为了查看方便可以转换成对应的白底黑字,如图14-21所示。MNIST图片是灰度图像,可以进一步优化处理,把图像中灰色的像素进一步处理,处理后的图像如图14-22所示。具体代码如下:
#灰度图像里面黑是0 白是255 可以把中间状态的处理下 image[image>127]=255 Image.fromarray(image.astype(np.uint8)).save("hackImage/100mnist-hacked-w-good.png")
图14-20 训练产生的攻击样本(黑底白字)
图14-21 训练产生的攻击样本(白底黑字)
图14-22 优化后的攻击样本(白底黑字)
自编码器(Auto Encoders)是深度学习中常见的一种模型。人类在理解复杂事物的时候,总是先总结初级的特征,然后从初级特征中总结出高级的特征。如图14-23所示,以识别手写数字为例,通过学习总结,发现可以把手写字母表示为几个非常简单的子图案的组合。
图14-23 将数字图案拆分成几个小图案的组合 [6]
通过对大量黑白风景照片提取16×16的图像碎片分析,研究发现几乎所有的图像碎片都可以由64种正交的边组合得到。声音也有同样的情况,大量未标注的音频中可以得到20种基本结构,绝大多数声音都可以由这些基本的结构线性组合得到。这就是特征的稀疏表达,通过少量的基本特征组合、拼装得到更高层抽象的特征 [7] 。自编码器模型正可以用于自动化地完成这种特征提取和表达的过程,而且整个过程是无监督的。基本的自编码器模型是一个简单的三层神经网络结构,如图14-24所示,一个输入层、一个隐藏层和一个输出层,其中输出层和输入层具有相同的维数。
图14-24 自编码器神经结构
自编码器的原理如图14-25所示,输入层和输出层分别代表神经网络的输入和输出层,隐藏层承担了编码器和解码器的工作,编码过程就是从高维度的输入层转化到是低维度的隐藏层的过程,反之,解码过程就是从低维度的隐藏层到高维度的输出层的转化过程。可见,自编码器是个有损转化的过程,可以通过对比输入和输出的差别来定义损失函数。训练的过程不需要对数据进行标记,整个过程就是不断求解损失函数最小化的过程,这也是自编码器名字的由来。这部分代码在GitHub的code/hackAutoEncode.py。
图14-25 自编码器原理图
1.构造自编码器
我们构造手写数字识别的自编码器模型,架构如图14-26所示,参数如下:
·输入层大小为784。
·节点数为100的全连接。
·输出层维度为784。
具体代码如下:
input_shape = (28*28,) input_img = Input(shape=input_shape) encoded = Dense(100, activation='relu')(input_img) decoded = Dense(784, activation='sigmoid')(encoded) model = Model(inputs=[input_img], outputs=[decoded]) model.compile(loss='binary_crossentropy',optimizer='adam') model.summary() plot_model(model, show_shapes=True, to_file='hackAutoEncode/keras-ae.png')
图14-26 手写数字识别的自编码器
为了提高调试阶段的效率,我们训练好模型后就将训练好的参数保留,下次运行时可以直接加载对应的参数,代码如下:
#保证只有第一次调用的时候会训练参数 if os.path.exists(h5file): model.load_weights(h5file) else: model.fit(x_train_nosiy, x_train, epochs=epochs, batch_size=batch_size, verbose=1, validation_data=(x_test, x_test)) model.save_weights(h5file)
2.选择被攻击的图片样本
选择被攻击的图片样本的方法和攻击手写数字识别模型的一样,我们将获得的100个手写数字的样本合成一张图片(见图14-27)。
图14-27 被挑选出的手写数字图案(黑底白字)
攻击自编码器模型时,我们还需要选择伪装成的图案,因为自编码产生的结果也是图片。可以直接在MNIST选择一个数字0对应的图案。为了后继处理方便,需要把图案的像素点的取值转换到-1~1,代码如下:
#获取数字0的图案 def getZeroFromMnist(): (x_train, y_train), (x_test, y_test) = mnist.load_data() #原有范围在0-255转换到 0-1 #x_train = (x_train.astype(np.float32) - 127.5)/127.5 #原有范围在0-255转换调整到-1和1之间 x_train = x_train.astype(np.float32)/255.0 x_train-=0.5 x_train*=2.0 x_train = x_train[:, :, :, None] x_test = x_test[:, :, :, None] index=np.where(y_train==0) x_train=x_train[index] x_train=x_train[-1:] return x_train
训练过程中,损失函数使用交叉熵,进过20轮训练,损失值在-13左右:
Epoch 19/20 60000/60000 [==============================] - 5s - loss: -13.1321 - val_loss: -13.1099 Epoch 20/20 60000/60000 [==============================] - 6s - loss: -13.1332 - val_loss: -13.1104
3.训练产生攻击样本
图像攻击算法我们依然使用效率较高的FGSM算法。我们构造自编码模型,获取整个模型的输入和输出层,定义损失函数和梯度的获取方式。其中需要重点强调的是,这次需要欺骗模型,伪装成图案0,而且模型的输出就是一个图案,所以损失函数定义为输出图案和需要伪装成的图案之间的交叉熵。另外我们构造的自编码器的输入是维度为784的向量,所以需要把图案进行形状转换。
代码如下:
model = trainAutoEncode() # 都伪装成0 object_type_to_fake = getZeroFromMnist() object_type_to_fake=object_type_to_fake.reshape(28*28) object_type_to_fake = np.expand_dims(object_type_to_fake, axis=0) model_input_layer = model.layers[0].input model_output_layer = model.layers[-1].output #生成的图像与图案0之间的差为损失函数 cost_function = K.mean(K.binary_crossentropy(model_output_layer,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])
依次训练100个样本,设置图像调整的范围,由于是灰度图案,所以需要把可调整的范围设置较大,比如-1~1之间,代码如下:
progress_bar = Progbar(target=100) for index in range(100): progress_bar.update(index) mnist_image_raw = generator_images[index] mnist_image_hacked = np.copy(mnist_image_raw) mnist_image_hacked=mnist_image_hacked.reshape(28*28) mnist_image_hacked = np.expand_dims(mnist_image_hacked, axis=0) #调整的极限 灰度图片 max_change_above = mnist_image_raw + 1.0 max_change_below = mnist_image_raw - 1.0
使用FGSM算法对图像进行迭代调整,由于这次目标是最小化损失函数,所以使用梯度下降的算法。为了防止死循环,设置最大迭代计算的次数,超过阈值退出。核心的判断条件是损失函数的值,即交叉熵的值,实际调试发现-12.0也可以生成不错的效果,有兴趣的读者也可以调整该值。
代码如下:
while cost > -12.0 and i < 500: cost, gradients = grab_cost_and_gradients_from_model([mnist_image_hacked, 0]) # fast gradient sign method # EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES # hacked_image += gradients * learning_rate n = np.sign(gradients) mnist_image_hacked -= n * e mnist_image_hacked = np.clip(mnist_image_hacked, max_change_below, max_change_above) mnist_image_hacked = np.clip(mnist_image_hacked, -1.0, 1.0) i += 1
4.攻击结果
整个过程非常快,即使是在Mac本上也可以在5分钟内完成训练。我们先观察自解码还原图片的效果,如图14-28和图14-29所示。
我们训练攻击样本,产生的攻击样本如图14-30和图14-31所示。
图14-28 自解码还原的图片(黑底白字)
图14-29 自解码还原的图片(白底黑字)
图14-30 攻击样本(黑底白字)
图14-31 攻击样本(白底黑字)
使用自解码器还原攻击样本,产生的图片如图14-32和图14-33所示,可见自解码器把我们的攻击样本全部还原成了数字0。
图14-32 自解码还原的攻击图片(黑底白字)
图14-33 自解码还原的攻击图片(白底黑字)
差分自编码器(Variational AutoEncoder,VAE)是自编码器的一种变体。VAE在编码器和解码器之间增加了一个采样环节,如图14-34所示。编码器编码的结果同时输出给标准差向量和均值向量,通过一个满足正态分布的采样量乘以标准差再加上均值,就形成了一个新的满足正态分布的采样。通常,如果一个随机变量x满足正态分布,表示为:
其中,μ表示均值,也可以使用mean表示,σ表示标准差,也可以用std表示。
比较直观的理解是,在自编码器中是使用一个多维离散向量表示图像,在VAE中是用多维的连续向量。我们重点介绍与自编码器存在差异的环节,这部分代码在GitHub的code/keras-vae.py。
图14-34 VAE原理图
1.构造差分自编码器
训练数据同样使用MNIST,与之前处理不同的是,我们把像素的值转换到0~1之间,这样可以更好地使用交叉熵和KL距离定义计算损失函数,代码如下:
(x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.reshape(x_train.shape[0], -1) x_test = x_test.reshape(x_test.shape[0], -1) #图像转换到0到1之间 x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255.
我们构造MNIST识别的自编码器模型,架构如图14-35所示,参数如下:
·输入层大小为784。
·结点数为256的全连接。
·全连接对应两个输出,均是结点数为2的全连接,分别代表均值向量和标准差向量。
·Lambda层,通过均值向量和标准差向量的计算获得新的满足正态分布的采样。
·结点数为256的全连接。
·输出层结点数为784。
具体代码如下:
x = Input(shape=(original_dim,)) h = Dense(intermediate_dim, activation='relu')(x) z_mean = Dense(latent_dim)(h) z_std = Dense(latent_dim)(h)
这里需要特别介绍的是Lambda层,它完成了非常重要的采样过程,Lambda支持通过函数定义采样的过程。我们定义采样函数sampling,它的输入是均值向量和标准差向量,均是长度为2的向量,也就是说我们使用一个长度为2取值连续的向量表示了MNIST数据集的全部图像,在上节的自编码器中,我们使用长度为100取值离散的向量表示。z_mean表示均值向量,z_std表示标准差向量,为了计算方便,这里z_std其实是std的平方取了自然对数:
z_std=2ln(std)
图14-35 VAE结构
epsilon表示一个标准差为0,均值为1,满足正态分布的多维向量。针对z_mean和z_std的采样就可以理解为:
sampling=z_mean+std*epsilon=z_mean+ezstd/2* epsilon
具体代码如下:
def sampling(args): z_mean, z_std = args epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0., stddev=epsilon_std) return z_mean + K.exp(z_std/2) * epsilon
使用Lambda函数完成采样过程:
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_std])
构造对应的解码器:
decoder_h = Dense(intermediate_dim, activation='relu') decoder_mean = Dense(original_dim, activation='sigmoid') h_decoded = decoder_h(z) x_decoded_mean = decoder_mean(h_decoded)
VAE的损失函数稍微复杂一点:一方面,需要衡量输入输出图像的差距,这个使用交叉熵就可以完成。另一方面,由于引入了满足正态分布的采样环节,我们需要衡量生成的分布与正态分布的差别,这里就需要使用KL距离。KL距离是Kullback-Leibler差异(Kullback-Leibler Divergence)的简称,也叫作相对熵(Relative Entropy),它衡量的是相同事件空间里的两个概率分布的差异情况。我们用D(P||Q)表示KL距离,计算公式如下:
具体到VAE,使用损失函数定义为两部分,分别是交叉熵和输出向量的大小乘积与KL距离的和。Diederik P Kingma和Max Welling在VAE的经典论文《Auto-Encoding Variational Bayes》 [8] 中完整介绍了VAE的原理以及数学推导过程,有兴趣的读者可以仔细去了解。经过推导后,KL举例可以简化为:
代码如下:
def vae_loss(x, x_decoded_mean): encode_decode_loss=original_dim * metrics.binary_crossentropy(x, x_decoded_mean) kl_loss = -0.5 * K.sum(1 + z_std - K.square(z_mean) - K.exp(z_std), axis=-1) return K.mean(kl_loss+encode_decode_loss)
2.训练产生攻击样本
图像攻击算法我们依然使用效率较高的FGSM算法。我们构造自编码模型,获取整个模型的输入和输出层,定义损失函数和梯度的获取方式。整个训练过程与自编码完全相同,唯一需要注意的是,图像的像素在0~1之间,所以调整范围时的最大范围也是0~1,而不是-1~1。
代码如下:
mnist_image_hacked = np.clip(mnist_image_hacked, 0.0, 1.0)
3.攻击结果
整个过程非常快,即使是在Mac本上也可以在10分钟内完成训练。我们先观察VAE还原图片的效果,图14-36所示为原始数据集,图14-37和图14-38所示为还原数据集。
图14-36 原始的MNIST数据集
图14-37 通过VAE还原的MNIST数据集(黑底白字)
图14-38 通过VAE还原的MNIST数据集(白底黑字)
通过FGSM产生出攻击样本,如图14-39和图14-40所示。
图14-39 针对VAE的攻击图片(黑底白字)
图14-40 针对VAE的攻击图片(白底黑字)
使用VAE还原攻击图片,如图14-41和图14-42所示,可见VAE被欺骗了。
图14-41 VAE还原的攻击图片(黑底白字)
图14-42 VAE还原的攻击图片(白底黑字)
Dawn Song教授和她的团队在这方面做了非常深入的研究,在她的论文“Adversarial examples for generative models” [9] 中,她针对VAE进行了攻击。她首先是在MNIST数据集上做了实验,正常情况下,VAE可以把原始图像(见图14-43)还原出来(见图14-44),通过FGSM攻击VAE,企图欺骗VAE,把1~9的数字都伪装成0,生成的攻击图片如图14-45所示,还原后的效果如图14-46所示,可以发现VAE被成功欺骗了。
图14-43 原始的MNIST数据集
图14-44 通过VAE还原的MNIST数据集
图14-45 针对VAE的攻击图片
图14-46 VAE还原后攻击图片
Dawn Song教授在稍微复杂些的SVHN数据集上使用相同的方式也攻击成功了。SVHN(the Street View House Numbers) [10] 数据集是一个真实世界的街道门牌号数字识别数据集(见图14-47),在此要感谢以下科学家提供这个数据集给机器学习领域:
Yuval Netzer, Tao Wang, Adam Coates, Alessandro Bissacco, Bo Wu, Andrew Y. Ng Reading Digits in Natural Images with Unsupervised Feature Learning NIPS Workshop on Deep Learning and Unsupervised Feature Learning 2011.
图14-47 SVHN数据集
正常情况下,VAE可以把原始图像(见图14-48)还原出来(见图14-49),通过FGSM攻击VAE,企图欺骗VAE,把1~9的门牌数字都伪装成0,生成的攻击图片如图14-50所示,还原后的效果如图14-51所示,可以发现VAE又被成功欺骗了。
图14-48 原始的SVHN数据集
图14-49 通过VAE还原的SVHN数据集
图14-50 针对SVHN数据集的攻击图片
图14-51 VAE还原的攻击图片
Dawn Song教授在更复杂的人脸图像上也取得了成功,她使用的数据集为CelebA(见图14-52)。CelebA是香港中文大学的Ziwei Liu、Ping Luo、Xiaogang Wang和Xiaoou Tang对外提供一份人脸数据集,包含近20万张图片。
图14-52 CelebA数据集
针对CelebA的攻击图片如图14-53所示,VAE还原的图像如图14-54所示,可见VAE又被欺骗了。以上的图片都来自Dawn Song教授的论文。
图14-53 针对CelebA数据集的攻击图片
图14-54 VAE还原的图片
[1] https://arxiv.org/abs/1702.02284
[2] https://arxiv.org/abs/1701.04143
[3] https://www.leiphone.com/news/201702/Jpb0uiOt9RTwcB8E.html
[4] https://arxiv.org/abs/1705.06452
[5] 图14-14至图14-16均引自网址https://arxiv.org/abs/1705.06452。
[6] http://blog.csdn.net/u010089444/article/details/52601193
[7] http://blog.csdn.net/happyhorizion/article/details/77894049
[8] https://arxiv.org/abs/1312.6114
[9] https://arxiv.org/abs/1702.06832
[10] http://ufldl.stanford.edu/housenumbers/