はじめに
Adversarial Attackをご存知でしょうか?日本語で敵対的攻撃ですかね。
以下のパンダがテナガザルに誤判定される例は有名かと思います。人間には分からない微小なノイズを画像に重ねることでAIを誤判定させています。
引用元:EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES
こういったAIに対する攻撃を応用すると、例えば自動運転AIに対し、道路標識を誤認識させて、止まらないといけないところで直進させる、なんてこともできてしまうかもしれません。
上図の右側のようにAIに誤判定させるような画像のことをAdversarial Exampleとかいったりして、Adversarial Exampleの作り方も、上図のように微小なノイズ(Adversarial Exampleの文脈ではよく摂動と言われます)を加えたり、画像の一部を意図的に欠損させたり、背景に幾何学模様を組み込んだりと、様々な方法が研究されているようです。
今回はAdversarial Attackに対し、AIがどのように判断を誤ったのかを前回の記事で勉強したGrad-CAMの手法を使って可視化し、異変に気付くことができるか実装しながら試してみようと思います。
参考文献
以下がとても参考になりました!
- EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES
- Adversarial Examples をやってみる。
- はじめてのAdversarial Example
- 今更聞けない!? Adversarial Examples
- AI Safety — How Do you Prevent Adversarial Attacks?
Fast Gradient Sign Method (FGSM)
今回はパンダの誤判定で使われた同様の方法でAdversarial Exampleを作って見ようと思います。
この方法はFast Gradient Sign Method (FGSM)という名前の手法で、名前の通り高速にAdversarial Exampleを作ることが可能です。
パンダの図のところにも記載がありましたが、根幹となる数式は以下になります。(元論文から引用)
$\eta$が元画像に与える摂動を表します。$\epsilon$は微小な固定値で、$\boldsymbol x$は入力画像、$y$は正解ラベル、$J$は損失関数を表しています。$\boldsymbol \theta$はモデルのパラメータで、今回の手法はパラメータの更新を行わないため固定値です。$sign$は符号関数になります。
数式を日本語にざっくりと翻訳すると、入力画像の正解ラベルとの損失を大きくなる方向におけるノイズを生成している、ということになります。
これだけで誤判定されられちゃうのかとびっくりですが、さっそく実装を通して上式の理解を深めてみようと思います。
実装
まずはどの画像にAdversarial Attackするかですが、今回はパンダの例にもあるようにパンダの画像を使おうと思います。モデルはVGG19を使います。
とりあえず、画像を準備して表示するところまでの実装は以下の通りです。
※以降のソースコードはGoogle Colab上で動かしており、検証画像もGoogle Driveに格納しています。事前にGoogle Driveをcolabにマウントしておく必要があります。
# 各種ライブラリインポート
%matplotlib inline
import urllib
import pickle
import cv2
import numpy as np
from PIL import Image
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import transforms
from torchvision import models
import matplotlib.pyplot as plt
import seaborn as sns
# colabをダークモードにしていると、グラフ表示したときに目盛が見えなくなってしまうことに対する対処
sns.set_style("white")
# 検証画像格納先
drive_dir = "drive/My Drive/Colab Notebooks/grad-cam/"
# 検証画像の読み込み&表示
panda_image = Image.open(drive_dir + "panda3.jpg")
plt.imshow(panda_image)
plt.show()
画像はこちらから拝借しました!
まぁ誰がどう見てもパンダですよね。次はこのパンダの画像をVGG19に通して予測させてみます。VGG19の予測カテゴリには「giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca」がありますので、こちらのラベルを予測してほしいです。
ついでに前回記事のようにGrad-CAMで判断根拠の可視化まで行います。Grad-CAMのソースコードの詳細は前回記事をご参照ください。
# 画像の縦横を224x224にしてtensor型に変換
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
])
# 前処理
panda_tensor = transform(panda_image) # panda_tensor.size() = torch.Size([3, 224, 224])
panda_tensor = panda_tensor.unsqueeze(0)
# hook関数定義
def forward_hook(module, inputs, outputs):
global feature
feature = outputs[0]
def backward_hook(module, grad_inputs, grad_outputs):
global feature_grad
feature_grad = grad_outputs[0]
# モデルを検証モードにして、特徴マップを計算するレイヤーにhook関数を登録
model = model.eval()
model.features.register_forward_hook(forward_hook)
model.features.register_backward_hook(backward_hook)
# 検証画像をVGG19で予測させてみる
y_pred = model(panda_tensor)
pred_index = y_pred.argmax().item()
print("回答", pred_index ,labels[pred_index])
print("確率", F.softmax(y_pred)[0][pred_index].item())
# 回答 388 giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca
# 確率 0.7071896195411682
# ここから下はGrad-CAMの計算用です。
y_pred[0][pred_index].backward()
feature_vec = feature_grad.view(512, 7*7)
alpha = torch.mean(feature_vec, axis=1)
feature = feature.squeeze(0)
L = F.relu(torch.sum(feature*alpha.view(-1,1,1),0))
L = L.detach().numpy()
L_min = np.min(L)
L_max = np.max(L - L_min)
L = (L - L_min)/L_max
L = cv2.resize(L, (224, 224))
# heat map に変換
def toHeatmap(x):
x = (x*255).reshape(-1)
cm = plt.get_cmap('jet')
x = np.array([cm(int(np.round(xi)))[:3] for xi in x])
return x.reshape(224,224,3)
img1 = panda_tensor.squeeze(0).permute(1,2,0)
img2 = toHeatmap(L)
alpha = 0.5
grad_cam_image = img1*alpha + img2*(1-alpha)
plt.imshow(grad_cam_image)
約70%の確率でパンダと予測してくれましたし、きれいにGrad-CAMできました。顔を見て、パンダと判断したようです。そりゃそうですよねって感じで納得感ある結果です。
Adversarial Exampleを作る
次に上で紹介した数式を参考にこのパンダ画像に摂動を与えてAdversarial Exampleを作ってみます。まずは$\eta$(摂動)を作って表示してみましょう。
# パンダ画像の勾配を計算することになるので、勾配計算をONにする
panda_tensor = panda_tensor.requires_grad_(True)
# パンダの正解ラベルidは388でした。正解との損失を計算したいので、直に388をしています。
y_pred = model(panda_tensor)
criterion = nn.CrossEntropyLoss()
loss = criterion(y_pred, torch.tensor([388]))
loss.backward()
epsilon = 0.007
eta = epsilon*np.sign(panda_tensor.grad) # eta.size() = torch.Size([1, 3, 224, 224])
# εを施す前の画像(画像左)
plt.imshow(np.sign(panda_tensor.grad).squeeze(0).permute(1,2,0).detach().numpy())
# η(画像右)
plt.imshow(eta.squeeze(0).permute(1,2,0).detach().numpy())
なかなか気持ち悪いノイズが生成されました。
$\epsilon$施すと真っ黒になるんですね。この辺の感覚がないの画像慣れしてないからなんでしょうね。。。
次にこの摂動を元画像のパンダに加えればAdversarial Exampleの完成。これは単純に足し算するだけでOK
# 元画像に摂動を与えてAdversarial Exampleを作る
panda_tensor = panda_tensor + eta
plt.imshow(panda_tensor.squeeze(0).permute(1,2,0).detach().numpy())
若干背後がざらついた感じになってなくもないですが、少なくともこの画像は紛れもなくパンダですね。
Adversarial ExampleをGrad-CAMで判断根拠を可視化してみる
後は$\eta$を加えたパンダ画像を先程と同じようにGrad-CAMで可視化するところまで流すのですが、先程と全く同じ実装なので、ソースコードは割愛します。
実際の予測結果とGrad-CAMの可視化結果は以下の通りとなりました。
回答 728 plastic bag
確率 0.3606475591659546
パンダがビニール袋(ポリ袋)と判定されてしまいました。。。こればやばい。
判定確率が約36%なので、VGG19としてはやや迷いがちではあるようですが、パンダとかけ離れた予測をしてしまいました。もはや生物ですらない。。。
ただGrad-CAMの可視化結果を見てみると、パンダの下半身あたりを見てビニール袋と判定したようですね。どう考えても人が見ればこれはパンダだし、パンダなら顔とかを根拠にするだろうと考えるのは自然なので、パンダの下半身を見ているのは変だぞ?と異変に気付くことができそうな気がします。
別のパンダ画像も同様にAdversarial Example作ってGrad-CAMで可視化した結果見てみましょう。
(こちらからパンダを拝借しました!)
まずこちらが元画像と元画像に対するGrad-CAMの結果
回答 388 giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca
確率 0.9090976715087891
こちらがAdversarial ExampleとそのGrad-CAMの結果
回答 296 ice bear, polar bear, Ursus Maritimus, Thalarctos maritimus
確率 0.25158318877220154
こちらのパンダは元画像が先程のパンダよりも確率が高く(約90%)パンダと判定しており、口あたりを根拠としています。
しかし、摂動を与えることで、見た目は先程と同様に変化がわからないのに、パンダのお腹あたりを見て、ice bear(ホッキョクグマ)と自信なさげ(約25%)に回答しました。
白いもふもふ的なところでホッキョクグマと判断したのなら、先程のビニール袋よりかはまだありえなくはない、って感じですが、こちらもどう考えてもパンダなのに顔を全然見てないのはなにかおかしいぞ?って気づけそうですね。
おわりに
AIの判断を誤らせるAdversarial Attackを実際に試してみて、Grad-CAMで異変に気づけそうか可視化して確認してみました。今回の手法のAdversarial Exampleの作り方なら、Grad-CAMで異変に気づけそうかなって思える結果となりました。
こういうの楽しいな
おわり