"); //-->
接下来,让我们解析下指令参数:
# construct the argument parser and parsethe arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image",required=True,
help="pathto input image")
args = vars(ap.parse_args())
我们在这里只需要一个命令行参数,--image,即输入图像存放在硬盘上的路径。
如果你从来没有处理过命令行参数和argparse ,我建议你看一下这篇教程。
接下来让载入输入图像并进行预处理:
# load image fromdisk and make a clone for annotation
print("[INFO] loadingimage...")
image = cv2.imread(args["image"])
output = image.copy()
# preprocess the input image
output = imutils.resize(output, width=400)
preprocessedImage = preprocess_image(image)
通过调用cv2.imread来载入输入图片。在第4行对这张图片进行了复制,以便后期在输出结果上面画框并标注上预测结果类别标签。
我们调整下输出图片的尺寸,让它的宽变为400像素,这样可以适配我们的电脑屏幕。在这里同样使用preprocess_image函数将其处理为可用于ResNet进行分类的输入图片。
加上我们预处理好的图片,接着载入ResNet并对图片进行分类:
# load thepre-trained ResNet50 model
print("[INFO] loadingpre-trained ResNet50 model...")
model = ResNet50(weights="imagenet")
# makepredictions on the input image and parse the top-3 predictions
print("[INFO] makingpredictions...")
predictions =model.predict(preprocessedImage)
predictions = decode_predictions(predictions, top=3)[0]
第3行,载入ResNet和该模型用ImageNet数据集预训练好的权重。
第6和7行,针对预处理好的图片进行预测,然后再用 Keras/TensorFlow 中的decode_predictions辅助函数对图片进行解码。
现在让我们看看神经网络预测的Top 3 (置信度前三)类别以及所展示的类别标签:
# loop over thetop three predictions
for(i, (imagenetID, label,prob))inenumerate(predictions):
# print the ImageNet class label ID of the top prediction to our
# terminal (we'll need thislabel for our next script which will
# perform the actual adversarial attack)
if i == 0:
print("[INFO] {} => {}".format(label, get_class_idx(label)))
# display the prediction to our screen
print("[INFO] {}.{}: {:.2f}%".format(i + 1, label, prob * 100))
第2行开始循环Top-3预测结果。
如果这是第一个预测结果(即Top-1预测结果),在输出终端显示可读的标签,然后利用 get_class_idx函数找出该标签对应的ImageNet的整数值索引。
还可以在终端上展示Top-3的标签和对应的概率值。
最终一步就是将Top-1预测结果标注在输出图片中:
# draw thetop-most predicted label on the image along with the
# confidence score
text = "{}:{:.2f}%".format(predictions[0][1],
predictions[0][2] * 100)
cv2.putText(output, text, (3, 20),cv2.FONT_HERSHEY_SIMPLEX, 0.8,
(0, 255, 0), 2)
# show the output image
cv2.imshow("Output", output)
cv2.waitKey(0)
输出图像在终端显示,如果你单击OpenCV窗口或按下任意键,输出图像将会关闭。
非对抗图像的分类结果
现在可以用ResNet来执行基本的图像分类(非对抗攻击)了。
首先在“下载“页获取源代码和图像范例。
从这开始,打开一个终端并执行以下命令:
$ pythonpredict_normal.py --image pig.jpg
[INFO] loading image...
[INFO] loadingpre-trained ResNet50 model...
[INFO] making predictions...
[INFO] hog => 341
[INFO] 1. hog: 99.97%
[INFO] 2.wild_boar: 0.03%
[INFO] 3. piggy_bank: 0.00%
图五:预训练好的ResNet模型可以正确地将这张图片分类为“猪(hog)”。
在这里你可以看到我们将一张猪的图片进行了分类,置信度为99.97%。
另外,这里还加上了hog标签的ID(341)。我们将会在下一章用到这个标签ID,我们会针对这张猪的输入图片进行一次对抗攻击。
在Keras和TensorFlow上实现对抗图像和对抗攻击
接下来就要学习如何在Keras和TensorFlow上实现对抗图像和对抗攻击。
打开generate_basic_adversary.py文件,插入以下代码:
# import necessary packages
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.losses importSparseCategoricalCrossentropy
from tensorflow.keras.applications.resnet50 import decode_predictions
from tensorflow.keras.applications.resnet50 import preprocess_input
import tensorflow as tf
import numpy as np
import argparse
import cv2
在第2-10行中引入我们所需的Python包。你会注意到我们再次用到了ResNet50 架构,以及对应的preprocess_input函数(用于预处理/缩放输入图像)和 decode_predictions用于解码预测输出和显示可读的ImageNet标签。
SparseCategoricalCrossentropy 用于计算标签和预测值之间的分类交叉熵损失。利用稀疏版本的分类交叉熵,我们不需要像使用scikit-learn的LabelBinarizer或者Keras/TensorFlow的to_categorical功能一样用one-hot对类标签编码。
例如在predict_normal.py脚本中有preprocess_image 功能,我们在这个脚本上同样需要:
defpreprocess_image(image):
# swap color channels, resizethe input image, and add a batch
# dimension
image = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
image = cv2.resize(image, (224, 224))
image = np.expand_dims(image, axis=0)
# return the preprocessedimage
return image
除了省略了调用preprocess_input函数,这一段代码与上一段代码相同,当我们开始创建对抗图像时,你们马上就会明白为什么省去调用这一函数。
接下来,我们有一个简单的辅助程序,clip_eps:
defclip_eps(tensor, eps):
# clip the values of thetensor to a given range and return it
return tf.clip_by_value(tensor,clip_value_min=-eps,
clip_value_max=eps)
这个函数的目的就是接受一个输入张量tensor,然后在范围值 [-eps, eps]内对输入进行截取。
被截取后的tensor会被返回到调用函数。
接下来看看generate_adversaries 函数,这是对抗攻击的灵魂:
defgenerate_adversaries(model, baseImage,delta, classIdx, steps=50):
# iterate over the number ofsteps
for step inrange(0, steps):
# record our gradients
with tf.GradientTape()as tape:
# explicitly indicate thatour perturbation vector should
# be tracked for gradient updates
tape.watch(delta)
generate_adversaries 方法是整个脚本的核心。这个函数接收四个必需的参数,以及第五个可选参数:
model:ResNet50模型(如果你愿意,你可以换成其他预训练好的模型,例如VGG16,MobileNet等等);
baseImage:原本没有被干扰的输入图像,我们有意针对这张图像创建对抗攻击,导致model参数对它进行错误的分类。
delta:噪声向量,将会被加入到baseImage中,最终导致错误分类。我们将会用梯度下降均值来更新这个delta 向量。
classIdx:通过predict_normal.py脚本所获得的类别标签整数值索引。
steps:梯度下降执行的步数(默认为50步)。
第3行开始循环设定好的步数。
接下来用GradientTape来记录梯度。在tape上调用 .watch方法指出扰动向量是可以用来追踪更新的。
现在可以建造对抗图像了:
# add our perturbation vector to the base image and
# preprocess the resulting image
adversary = preprocess_input(baseImage + delta)
# run this newly constructed image tensor through our
# model and calculate theloss with respect to the
# *original* class index
predictions = model(adversary,training=False)
loss = -sccLoss(tf.convert_to_tensor([classIdx]),
predictions)
# check to see if we arelogging the loss value, and if
# so, display it to our terminal
if step % 5 == 0:
print("step: {},loss: {}...".format(step,
loss.numpy()))
# calculate the gradients ofloss with respect to the
# perturbation vector
gradients = tape.gradient(loss, delta)
# update the weights, clipthe perturbation vector, and
# update its value
optimizer.apply_gradients([(gradients, delta)])
delta.assign_add(clip_eps(delta, eps=EPS))
# return the perturbationvector
return delta
第3行将delta扰动向量加入至baseImage的方式来组建对抗图片,所得到的结果将放入ResNet50的preprocess_input函数中来进行比例缩放和结果对抗图像进行归一化。
接下来几行的意义是:
第7行用model参数导入的模型对新创建的对抗图像进行预测。
第8和9行针对原有的classIdx(通过运行predict_normal.py得到的top-1 ImageNet类别标签整数值索引)计算损失。
第12-14行表示每5步就显示一次损失值。
第17行,在with声明外根据扰动向量计算梯度损失。
接着,可以更新delta向量,截取掉超出 [-EPS,EPS] 范围外的值。
最终,把得到的扰动向量返回至调用函数——即最终的delta值,该值能让我们建立用来欺骗模型的对抗攻击。
在对抗脚本的核心实现后,接下来就是解析命令行参数:
# construct the argumentparser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--input", required=True,
help="path tooriginal input image")
ap.add_argument("-o", "--output", required=True,
help="path tooutput adversarial image")
ap.add_argument("-c", "--class-idx", type=int,required=True,
help="ImageNetclass ID of the predicted label")
args = vars(ap.parse_args())
我们的对抗攻击Python脚本需要三个指令行参数:
--input: 输入图像的磁盘路径(例如pig.jpg);
--output: 在构建进攻后的对抗图像输出(例如adversarial.png);
--class-idex:ImageNet数据集中的类别标签整数值索引。我们可以通过执行在“非对抗图像的分类结果”章节中提到的predict_normal.py来获得这一索引。
接下来是几个变量的初始化,以及加载/预处理--input图像:
# define theepsilon and learning rate constants
EPS = 2 / 255.0
LR = 0.1
# load the inputimage from disk and preprocess it
print("[INFO] loadingimage...")
image = cv2.imread(args["input"])
image = preprocess_image(image)
第2行定义了用于在构建对抗图像时来裁剪tensor的epsilon值(EPS)。2 / 255.0 是EPS的一个用于对抗类刊物或教程的标准值和指导值(如果你想要了解更多的默认值,你可以参考这份指导)。
在第3行定义了学习速率。经验之谈,LR的初始值一般设为0.1,在创建你自己的对抗图像时可能需要调整这个值。
最后两行载入输入图像,利用preprocess_image辅助函数来对其进行预处理。
接下来,可以载入ResNet模型:
# load thepre-trained ResNet50 model for running inference
print("[INFO] loadingpre-trained ResNet50 model...")
model = ResNet50(weights="imagenet")
# initializeoptimizer and loss function
optimizer = Adam(learning_rate=LR)
sccLoss = SparseCategoricalCrossentropy()
第3行载入在ImageNet数据集上训练好的ResNet50模型。
我们将会用到Adam优化器,以及稀疏的分类损失用于更新我们的扰动向量。
让我们来构建对抗图像:
# create a tensorbased off the input image and initialize the
# perturbation vector (we will update this vector via training)
baseImage = tf.constant(image,dtype=tf.float32)
delta = tf.Variable(tf.zeros_like(baseImage), trainable=True)
# generate the perturbation vector to create an adversarialexample
print("[INFO]generating perturbation...")
deltaUpdated = generate_adversaries(model, baseImage,delta,
args["class_idx"])
# create theadversarial example, swap color channels, and save the
# output image to disk
print("[INFO]creating adversarial example...")
adverImage = (baseImage +deltaUpdated).numpy().squeeze()
adverImage = np.clip(adverImage, 0, 255).astype("uint8")
adverImage = cv2.cvtColor(adverImage,cv2.COLOR_RGB2BGR)
cv2.imwrite(args["output"], adverImage)
第3行根据输入图像构建了一个tensor,第4行初始化扰动向量delta。
我们可以把ResNet50、输入图像、初始化后的扰动向量、及类标签整数值索引作为参数,用来调用generate_adversaries并更新delta向量。
在 generate_adversaries函数执行时,会一直更新delta扰动向量,生成最终的噪声向量deltaUpdated。
在倒数第4行,在baseImage上加入deltaUpdated 向量,就生成了最终的对抗图像(adverImage)。
然后,对生成的对抗图像进行以下三步后处理:
将超出[0,255] 范围的值裁剪掉;
将图片转化成一个无符号8-bit(unsigned 8-bit)整数(这样OpenCV才能对图片进行处理);
将通道顺序从RGB转换成BGR。
在经过这些处理步骤后,就可以把对抗图像写入到硬盘里了。
真正的问题来了,我们新创建的对抗图像能够欺骗我们的ResNet模型吗?
下一段代码就可以回答这一问题:
# run inferencewith this adversarial example, parse the results,
# and display the top-1 predicted result
print("[INFO]running inference on the adversarial example...")
preprocessedImage = preprocess_input(baseImage +deltaUpdated)
predictions =model.predict(preprocessedImage)
predictions = decode_predictions(predictions, top=3)[0]
label = predictions[0][1]
confidence = predictions[0][2] * 100
print("[INFO] label:{} confidence: {:.2f}%".format(label,
confidence))
# draw the top-most predicted label on the adversarial imagealong
# with theconfidence score
text = "{}: {:.2f}%".format(label, confidence)
cv2.putText(adverImage, text, (3, 20),cv2.FONT_HERSHEY_SIMPLEX, 0.5,
(0, 255, 0), 2)
# show the output image
cv2.imshow("Output", adverImage)
cv2.waitKey(0)
在第4行又一次创建了一个对抗图像,方式还是在原始输入图像中加入delta噪声向量,但这次我们利用ResNet的preprocess_input功能来处理。
生成的预处理图像进入到ResNet,然后会得到top-3预测结果并对他们进行解码(第5和6行)。
接着我们获取到top-1标签和对应的概率/置信度,并将这些值显示在终端上(第7-10行)。
最后一步就是把最高的预测值标在输出的对抗图像上,并展示在屏幕上。
对抗图像和攻击的结果
准备好见证一次对抗攻击了吗?
从这里开始,你就可以打开终端并执行下列代码了:
$ python generate_basic_adversary.py --inputpig.jpg --output adversarial.png --class-idx 341
[INFO] loading image...
[INFO] loading pre-trained ResNet50 model...
[INFO] generatingperturbation...
step: 0, loss:-0.0004124982515349984...
step: 5, loss:-0.0010656398953869939...
step: 10, loss:-0.005332294851541519...
step: 15, loss: -0.06327803432941437...
step: 20, loss: -0.7707189321517944...
step: 25, loss: -3.4659299850463867...
step: 30, loss: -7.515471935272217...
step: 35, loss: -13.503922462463379...
step: 40, loss: -16.118188858032227...
step: 45, loss: -16.118192672729492...
[INFO] creating adversarial example...
[INFO] running inference on theadversarial example...
[INFO] label: wombat confidence: 100.00%
图六:之前,这张输入图片被正确地分在了“猪(hog)”类别中,但现在因为对抗攻击而被分在了“袋熊(wombat)”类别里!
我们的输入图片 pig.jpg 之前被正确地分在了“猪(hog)”类别中,但现在它的标签却成为了“袋熊(wombat)”!
将原始图片和用generate_basic_adversary.py脚本生成的对抗图片放在一起进行对比:
图片
图七:在左边是原始图片,分类结果正确。右边将是对抗图片,被错误地分在了“袋熊(wombat)”类别中。而对于人类的眼睛来看完全分辨不出两张图片的有什么区别。
左边是最初猪的图像,在右边是输出的对抗图像,这张图像被错误的分在了“袋熊(wombat)”类别。
就像你看到的一样,这两张图片没有任何可感知的差别,我们人类的眼睛看不出任何区别,但对于ResNet来说确实完全不同的。
这很好,但我们无法清晰地掌控对抗图像被最终识别的类别标签。这会引起以下问题:
我们有可能掌控输入图片的最终类别标签吗?答案是肯定的,这会成为我下一篇教程的主题。
总结来说,对抗图像和对抗攻击真的是令人细思极恐。但如果等我们看到下一篇教程,就可以提前防御这种类型的进攻。稍后再详细说明下。
致谢
如果没有Goodfellow, Szegedy和其他深度学习的研究者的工作,这篇教程就无法完成。
另外,这篇教程所用到的实现代码灵感来自于TensorFlow官方实现的《Fast Gradient Signed Method》。我强烈建议你去看看其他的示例,每一段代码都比这篇教程中的在理论和数学上更加明确。
总结
在这篇教程中,你学到关于对抗攻击的知识,关于他们是怎样工作的,以及随着人工智能和深度神经网络与这个世界的关联越来越高,对抗攻击就会构成更大的威胁。
接着我们利用深度学习库Keras 和TensorFlow实现了基本的对抗攻击算法。
利用对抗攻击,我们可以蓄意扰乱一张输入图片,例如:
1、这张输入图片会被错误分类。
2、然而,肉眼看上去被扰乱的图片还是和之前一样。
利用这篇教程所使用的方法,我们并不能控制图片最终被判别的类别标签——我们所做的只是创造一个噪声向量,并将其嵌入到输入图像中,导致深度神经网络对其错误分类。
如果我们能够控制最终的类别标签会怎样呢?比如说,我们拿一张“狗”的图片,然后制造一次对抗攻击,让卷积神经网络认为这是一张“猫”的图片,这有没有可能呢?
答案是肯定的——我们会在下一篇教程中来谈论这一话题。
原文链接:
https://www.pyimagesearch.com/2020/10/19/adversarial-images-and-attacks-with-keras-and-tensorflow/
原文标题:
Adversarial images andattacks with Keras and TensorFlow
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。