經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

李澤南發表於2017-08-11

用於識別圖片中物體的神經網路可以被精心設計的對抗樣本欺騙,而這些在人類看起來沒有什麼問題的圖片是如何產生的呢?最近,來自 OpenAI 的研究者 Anish Athalye 等人撰文介紹了他們使用 TensorFlow 製作「假圖片」的方法。

為神經網路加入對抗樣本是一個簡單而有意義的工作:仔細設計的「擾動」輸入可以讓神經網路受到分類任務上的挑戰。這些對抗樣本如果應用到現實世界中可能會導致安全問題,因此非常值得關注。

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

僅僅加入一些特殊的噪點,影象識別系統就會把大熊貓認作是長臂猿——而且是 99% 確信度。想象一下,如果無人駕駛汽車遇到了這種情況……


在本文中,我們將簡要介紹用於合成對抗樣本的演算法,並將演示如何將其應用到 TensorFlow 中,構建穩固對抗樣本的方法。

Jupyter notebook 可執行檔案:http://www.anishathalye.com/media/2017/07/25/adversarial.ipynb

設定

我們選擇攻擊用 ImageNet 訓練的 Inception v3 模型。在這一節裡,我們會從 TF-slim 影象分類庫中載入一個預訓練的神經網路。

import tensorflow as tf
import tensorflow.contrib.slim as slim
import tensorflow.contrib.slim.nets as nets
tf.logging.set_verbosity(tf.logging.ERROR)
sess = tf.InteractiveSession()

首先,我們需要設定一張輸入圖片。我們使用 tf.Variable 而非 tf.placeholder 是因為我們會需要讓資料可被訓練,這樣我們就可以在需要時繼續輸入。

image = tf.Variable(tf.zeros((299, 299, 3)))

隨後,我們載入 Inception v3 模型。

def inception(image, reuse):
    preprocessed = tf.multiply(tf.subtract(tf.expand_dims(image, 0), 0.5), 2.0)
    arg_scope = nets.inception.inception_v3_arg_scope(weight_decay=0.0)
    with slim.arg_scope(arg_scope):
        logits, _ = nets.inception.inception_v3(
            preprocessed, 1001, is_training=False, reuse=reuse)
        logits = logits[:,1:] # ignore background class
        probs = tf.nn.softmax(logits) # probabilities
    return logits, probs

logits, probs = inception(image, reuse=False)

接下來,我們載入預訓練權重。這種 Inception v3 模型的 top-5 正確率為 93.9%。

import tempfile
from urllib.request import urlretrieve
import tarfile
import os
data_dir = tempfile.mkdtemp()
inception_tarball, _ = urlretrieve(
    'http://download.tensorflow.org/models/inception_v3_2016_08_28.tar.gz')
tarfile.open(inception_tarball, 'r:gz').extractall(data_dir)
restore_vars = [
    var for var in tf.global_variables()
    if var.name.startswith('InceptionV3/')
]
saver = tf.train.Saver(restore_vars)
saver.restore(sess, os.path.join(data_dir, 'inception_v3.ckpt'))

接下來,我們編寫程式碼來顯示一張圖片,對其進行分類,並顯示分類結果。

import json
import matplotlib.pyplot as plt
imagenet_json, _ = urlretrieve(
    'http://www.anishathalye.com/media/2017/07/25/imagenet.json')
with open(imagenet_json) as f:
    imagenet_labels = json.load(f)
def classify(img, correct_class=None, target_class=None):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 8))
    fig.sca(ax1)
    p = sess.run(probs, feed_dict={image: img})[0]
    ax1.imshow(img)
    fig.sca(ax1)
    
    topk = list(p.argsort()[-10:][::-1])
    topprobs = p[topk]
    barlist = ax2.bar(range(10), topprobs)
    if target_class in topk:
        barlist[topk.index(target_class)].set_color('r')
    if correct_class in topk:
        barlist[topk.index(correct_class)].set_color('g')
    plt.sca(ax2)
    plt.ylim([0, 1.1])
    plt.xticks(range(10),
               [imagenet_labels[i][:15] for i in topk],
               rotation='vertical')
    fig.subplots_adjust(bottom=0.2)
    plt.show()


示例圖片

我們載入示例圖片,並確認它們已被正確分類。

import PIL
import numpy as np
img_path, _ = urlretrieve('http://www.anishathalye.com/media/2017/07/25/cat.jpg')
img_class = 281
img = PIL.Image.open(img_path)
big_dim = max(img.width, img.height)
wide = img.width > img.height
new_w = 299 if not wide else int(img.width * 299 / img.height)
new_h = 299 if wide else int(img.height * 299 / img.width)
img = img.resize((new_w, new_h)).crop((0, 0, 299, 299))
img = (np.asarray(img) / 255.0).astype(np.float32)
classify(img, correct_class=img_class)

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本


對抗樣本

給定圖片 X,我們的神經網路輸出 P(y|x)上的概率分佈。當我們製作對抗性輸入時,我們想找到一個 x ^,其中 logP(y ^ | x ^)對於目標標籤 y ^是最大化的:這樣,我們的輸入就會被誤分類為目標類別中。我們可以通過把自己約束在半徑為ϵ的ℓ∞框中來確保 x ^看起來與原始的 x 看起來差不多,這就需要∥x−x^∥∞≤ϵ了。

在這個框架中,一個對抗性樣本是約束優化問題的解,我們可以使用反向傳播和預測梯度下降來求得它,這基本上是用於訓練網路本身的相同技術。演算法很簡單:

我們首先將對抗樣本初始化為 x^←x。然後重複以下過程直到收斂為止:

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

初始化

我們先從最簡單的開始:寫一個用於初始化的 TensorFlow op。

x = tf.placeholder(tf.float32, (299, 299, 3))

x_hat = image # our trainable adversarial input
assign_op = tf.assign(x_hat, x)

梯度下降

接下來,我們編寫梯度下降步驟來最大化目標類的記錄可能性(或等價地,最小化交叉熵)。

learning_rate = tf.placeholder(tf.float32, ())
y_hat = tf.placeholder(tf.int32, ())

labels = tf.one_hot(y_hat, 1000)
loss = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=[labels])
optim_step = tf.train.GradientDescentOptimizer(
    learning_rate).minimize(loss, var_list=[x_hat])

投影步驟

最後,我們編寫投影步驟來使對抗樣本和原始圖片看起來更接近。此外,我們會將圖片剪輯為 [0,1],以讓圖片保持有效。

epsilon = tf.placeholder(tf.float32, ())

below = x - epsilon
above = x + epsilon
projected = tf.clip_by_value(tf.clip_by_value(x_hat, below, above), 0, 1)
with tf.control_dependencies([projected]):
    project_step = tf.assign(x_hat, projected)

執行

我們已準備好合成一個對抗樣本圖了,我們準備選擇「鱷梨醬」(ImageNet 分類 924)來作為它的錯誤分類。

demo_epsilon = 2.0/255.0 # a really small perturbation
demo_lr = 1e-1
demo_steps = 100
demo_target = 924 # "guacamole"

# initialization step
sess.run(assign_op, feed_dict={x: img})

# projected gradient descent
for i in range(demo_steps):
    # gradient descent step
    _, loss_value = sess.run(
        [optim_step, loss],
        feed_dict={learning_rate: demo_lr, y_hat: demo_target})
    # project step
    sess.run(project_step, feed_dict={x: img, epsilon: demo_epsilon})
    if (i+1) % 10 == 0:
        print('step %d, loss=%g' % (i+1, loss_value))
    

adv = x_hat.eval() # retrieve the adversarial example
step 10, loss=4.18923
step 20, loss=0.580237
step 30, loss=0.0322334
step 40, loss=0.0209522
step 50, loss=0.0159688
step 60, loss=0.0134457
step 70, loss=0.0117799
step 80, loss=0.0105757
step 90, loss=0.00962179
step 100, loss=0.00886694

這個對抗圖片看起來和原圖沒什麼區別,沒有任何可見的人造痕跡。然而,它被明確地分類為「鱷梨醬」,而且確信度極高!

classify(adv, correct_class=img_class, target_class=demo_target)

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

穩固的對抗樣本

現在,讓我們看看更高階的例子。我們遵循論文《Synthesizing Robust Adversarial Examples》的方法來尋找一個強大的對抗性樣本,希望尋找到這張貓圖片的單一擾動,讓它在某些轉變後仍然保持「假分類」。我們可以選擇任何可微分變換的分佈,在本文中,我們將演示圖片如何在旋轉θ∈[−π/4,π/4] 後保持對抗性輸入。

在開始前,我們先檢查一下剛才的對抗性樣本在旋轉後還管不管用,讓我們旋轉θ=π/8。

ex_angle = np.pi/8

angle = tf.placeholder(tf.float32, ())
rotated_image = tf.contrib.image.rotate(image, angle)
rotated_example = rotated_image.eval(feed_dict={image: adv, angle: ex_angle})
classify(rotated_example, correct_class=img_class, target_class=demo_target)

看起來我們的原始對抗性樣本不太管用了!

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

所以,如何讓我們的對抗樣本在旋轉後仍有效果?給定變換 T 的分佈,我們可以最大化 Et∼TlogP(y^∣t(x^)),使其服從∥x−x^∥∞≤ϵ。我們可以通過投影梯度下降來解決這個優化問題。注意,∇Et∼TlogP(y^∣t(x^)) 是 Et∼T∇logP(y^∣t(x^)) 並在每個梯度下降步驟中與樣本近似。

與手動實現梯度取樣不同,我們可以使用一個 trick 來讓 TesnorFlow 為我們做到這點:我們可以對基於取樣的梯度下降建模,作為隨機分類器的集合中的梯度下降——在輸入內容被分類前隨機從分佈和變化中取樣。

num_samples = 10
average_loss = 0
for i in range(num_samples):
    rotated = tf.contrib.image.rotate(
        image, tf.random_uniform((), minval=-np.pi/4, maxval=np.pi/4))
    rotated_logits, _ = inception(rotated, reuse=True)
    average_loss += tf.nn.softmax_cross_entropy_with_logits(
        logits=rotated_logits, labels=labels) / num_samples

我們可以重新使用 assign_op 和 project_step 指令,儘管我們必須為這個新的目標寫一個新的 optim_step。

optim_step = tf.train.GradientDescentOptimizer(
    learning_rate).minimize(average_loss, var_list=[x_hat])

最後,我們準備執行 PGD 來生成新的對抗輸入。和剛才的例子一樣,我們選擇「鱷梨醬」來作為目標分類。

demo_epsilon = 8.0/255.0 # still a pretty small perturbation
demo_lr = 2e-1
demo_steps = 300
demo_target = 924 # "guacamole"

# initialization step
sess.run(assign_op, feed_dict={x: img})

# projected gradient descent
for i in range(demo_steps):
    # gradient descent step
    _, loss_value = sess.run(
        [optim_step, average_loss],
        feed_dict={learning_rate: demo_lr, y_hat: demo_target})
    # project step
    sess.run(project_step, feed_dict={x: img, epsilon: demo_epsilon})
    if (i+1) % 50 == 0:
        print('step %d, loss=%g' % (i+1, loss_value))
    

adv_robust = x_hat.eval() # retrieve the adversarial example
step 50, loss=0.0804289
step 100, loss=0.0270499
step 150, loss=0.00771527
step 200, loss=0.00350717
step 250, loss=0.00656128
step 300, loss=0.00226182

完成了,這種對抗圖片被分類為「鱷梨醬」,置信度很高,即使是在被旋轉後也沒問題!

rotated_example = rotated_image.eval(feed_dict={image: adv_robust, angle: ex_angle})
classify(rotated_example, correct_class=img_class, target_class=demo_target)

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

讓我們評估一下所有角度旋轉後的圖片「魯棒性」,看看從 P(y^∣x^) 在θ∈[−π/4,π/4] 角度中的效果。

thetas = np.linspace(-np.pi/4, np.pi/4, 301)

p_naive = []
p_robust = []
for theta in thetas:
    rotated = rotated_image.eval(feed_dict={image: adv_robust, angle: theta})
    p_robust.append(probs.eval(feed_dict={image: rotated})[0][demo_target])
    
    rotated = rotated_image.eval(feed_dict={image: adv, angle: theta})
    p_naive.append(probs.eval(feed_dict={image: rotated})[0][demo_target])

robust_line, = plt.plot(thetas, p_robust, color='b', linewidth=2, label='robust')
naive_line, = plt.plot(thetas, p_naive, color='r', linewidth=2, label='naive')
plt.ylim([0, 1.05])
plt.xlabel('rotation angle')
plt.ylabel('target class probability')
plt.legend(handles=[robust_line, naive_line], loc='lower right')
plt.show()

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

如圖所示,效果非常棒!

論文:Synthesizing Robust Adversarial Examples

經得住考驗的「假圖片」:用TensorFlow為神經網路生成對抗樣本

論文連結:https://arxiv.org/abs/1707.07397

神經網路容易受到對抗性樣本的影響:一點點精巧的擾動資訊就可以讓網路完全錯判。然而,一些研究證明了這些對抗性樣本在面對小小的轉換之後就會失效:例如,放大圖片就會讓影象分類重新迴歸正確。這為我們帶來了新的課題:在實踐中,是否存在對抗性樣本?因為在現實世界中,影象的遠近和視角總是在不斷變化的。

本論文證明了對抗性樣本可以是魯棒的——能夠經受住各種變換的考驗。我們的新方法可以讓一張圖片在經歷所有變換之後仍然保持錯誤分類。實驗證明,我們不能依賴縮放、轉換和旋轉來解決對抗性樣本的問題。

相關文章