PyTorch 1.3 Tutorials : 画像 : 敵対的サンプルの生成 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 12/21/2019 (1.3.1)
* 本ページは、PyTorch 1.3 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:
- Image : Adversarial Example Generation
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
画像 : 敵対的サンプルの生成
これを読んでいれば、願わくば貴方は幾つかの機械学習モデルがどれほど効果的であるか高く評価できるでしょう。研究は ML モデルをより高速に、より正確に、そしてより効率的に常に推し進めています。けれども、モデルの設計と訓練のしばしば見落とされる側面はセキュリティと堅牢性で、特にモデルを騙そうと望む敵対者に直面することにおいてです。
このチュートリアルは ML モデルのセキュリティ脆弱性への認識を高め、敵対的機械学習のホットなトピックへの洞察を与えます。貴方は、画像への知覚できない摂動の追加が劇的に異なるモデルパフォーマンスを引き起こせることを見出して驚くかもしれません。これがチュートリアルであることを考えて、画像分類器上のサンプルを通してこのトピックを探究します。特に、MNIST 分類器を騙すために、最初の最もポピュラーな攻撃メソッドの一つ、高速勾配 Sign 攻撃 (FGSM) を使用します。
脅威モデル (= Threat Model)
コンテキストのために、敵対的攻撃の多くのカテゴリーがあり、それぞれ異なる目標と攻撃者の知識の前提を持ちます。けれども、一般に包括的な目標は望まれる誤分類を引き起こすために入力データに最小量の摂動を追加することです。攻撃者の知識の幾つかの種類の前提があり、そのうちの 2 つは : ホワイトボックス と ブラックボックス です。ホワイトボックス 攻撃は攻撃者がアーキテクチャ、入力、出力、そして重みを含む、モデルへの完全な知識とアクセスを持つことを仮定します。ブラックボックス 攻撃は攻撃者がモデルの入力と出力へのアクセスだけを持ち、基礎的なアーキテクチャや重みについては何も知らないことを仮定します。誤分類 と ソース/ターゲット誤分類 を含む、幾つかのタイプの目標もまたあります。誤分類 の目標は敵対者は出力分類が間違っていることだけを望み、新しい分類が何であるかは気にしないことを意味します。ソース/ターゲット誤分類 は敵対者は元は特定のソースクラスである画像をそれが特定のターゲットクラスとして分類されるように変更することを望むことを意味します。
このケースでは、FGSM 攻撃は 誤分類 の目標を持つ ホワイトボックス 攻撃です。この背景情報とともに、今私達は攻撃を詳細に議論できます。
高速勾配 Sign 攻撃
今までで最初の最もポピュラーな敵対的攻撃の一つは 高速勾配 Sign 攻撃 (FGSM) として参照され Explaining and Harnessing Adversarial Examples で Goodfellow et. al. により説明されています。攻撃は著しくパワフルですが、しかし直感的です。それはニューラルネットワークをそれらが学習する方法、勾配 を利用して攻撃するように設計されています。アイデアは単純で、逆伝播勾配に基づいて重みを調整することにより損失を最小化するために動作するのではなく、攻撃は同じ逆伝播勾配に基づいて 損失を最大化するために入力データを調整します。換言すれば、攻撃は入力データに関する損失の勾配を使用します、それから損失を最大化するために入力データを調整します。
コードに飛び込む前に、有名な FGSM パンダ・サンプルを見て幾つかの記法を引用しましょう。
図から、$\mathbf{x}$ は「パンダ」として正しく分類される元の入力画像で、$y$ は $\mathbf{x}$ のための正解ラベルで、$\mathbf{\theta}$ はモデル・パラメータを表し、$J(\mathbf{\theta}, \mathbf{x}, y)$ はネットワークを訓練するために使用される損失です。攻撃は $\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$ を計算するために勾配を入力データに逆伝播し戻します。それから、それは損失を最大化する方向 (i.e. $sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))$) の小さいステップ (図では $\epsilon$ あるいは $0.007$) で入力データを調整します。結果として摂動された画像, $x’$, はそれからターゲット・ネットワークにより (それは依然として明らかに「パンダ」であるとき) 「テナガザル (= gibbon)」として誤分類されます。
願わくばこのチュートリアルのための動機が明確になった今、実装に飛び込みましょう。
from __future__ import print_function import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torchvision import datasets, transforms import numpy as np import matplotlib.pyplot as plt
実装
このセクションでは、チュートリアルのための入力パラメータを議論し、攻撃を受けるモデルを定義し、それから攻撃をコーディングして幾つかのテストを実行します。
入力
このチュートリアルのためには 3 つだけ入力があり、次のように定義されます :
- epsilons – 実行のために使用する epsilon 値のリストです。リストに 0 を保持することは重要です、何故ならばそれは元のテストセット上のモデル・パフォーマンスを表わすからです。また、直感的には epsilon が大きくなればなるほど、摂動は顕著になりますがモデル精度を低下させるという点で攻撃がより効果的になることが期待できるでしょう。ここでのデータ範囲は $[0, 1]$ ですから、epsilon 値は 1 を超えないはずです。
- pretrained_model – 事前訓練された MNIST モデルへのパス、これは pytorch/examples/mnist で訓練されました。単純化のために、事前訓練されたモデルは ここで ダウンロードします。
- use_cuda – 望まれて利用可能であれば CUDA を使用するためのブーリアン・フラグ。注意してください、CUDA を伴う GPU はこのチュートリアルのためには重要ではありません、何故ならば CPU でそれほど時間がかかりません。
epsilons = [0, .05, .1, .15, .2, .25, .3] pretrained_model = "data/lenet_mnist_model.pth" use_cuda=True
攻撃されるモデル
言及したように、攻撃されるモデルは pytorch/examples/mnist からの同じ MNIST モデルです。貴方自身の MNIST モデルを訓練してセーブしても良いですし、あるいは提供されるモデルをダウンロードして使用することもできます。ここでの Net 定義と test dataloader は MNIST サンプルからコピーされています。このセクションの目的はモデルと dataloader を定義してから、モデルを初期化して事前訓練された重みをロードすることです。
# LeNet Model definition class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.conv2_drop = nn.Dropout2d() self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = F.relu(F.max_pool2d(self.conv1(x), 2)) x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320) x = F.relu(self.fc1(x)) x = F.dropout(x, training=self.training) x = self.fc2(x) return F.log_softmax(x, dim=1) # MNIST Test dataset and dataloader declaration test_loader = torch.utils.data.DataLoader( datasets.MNIST('../data', train=False, download=True, transform=transforms.Compose([ transforms.ToTensor(), ])), batch_size=1, shuffle=True) # Define what device we are using print("CUDA Available: ",torch.cuda.is_available()) device = torch.device("cuda" if (use_cuda and torch.cuda.is_available()) else "cpu") # Initialize the network model = Net().to(device) # Load the pretrained model model.load_state_dict(torch.load(pretrained_model, map_location='cpu')) # Set the model in evaluation mode. In this case this is for the Dropout layers model.eval()
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to ../data/MNIST/raw/train-images-idx3-ubyte.gz Extracting ../data/MNIST/raw/train-images-idx3-ubyte.gz to ../data/MNIST/raw Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to ../data/MNIST/raw/train-labels-idx1-ubyte.gz Extracting ../data/MNIST/raw/train-labels-idx1-ubyte.gz to ../data/MNIST/raw Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to ../data/MNIST/raw/t10k-images-idx3-ubyte.gz Extracting ../data/MNIST/raw/t10k-images-idx3-ubyte.gz to ../data/MNIST/raw Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to ../data/MNIST/raw/t10k-labels-idx1-ubyte.gz Extracting ../data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ../data/MNIST/raw Processing... Done! CUDA Available: True
FGSM 攻撃
今、元の入力を摂動することにより敵対的サンプルを作成する関数を定義できます。fgsm_attack 関数は 3 つの入力を取ります、image は元のクリーンな画像 ($x$)、epsilon はピクセル-wise な摂動総量 ($\epsilon$)、そして data_grad は入力画像に関する損失の勾配です ($\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$)。それから関数は摂動された画像を以下として作成します :
\[perturbed\_image = image + epsilon*sign(data\_grad) = x + \epsilon * sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))\]
最後に、データの元の範囲を維持するために、摂動された画像は範囲 $[0, 1]$ にクリップされます。
# FGSM attack code def fgsm_attack(image, epsilon, data_grad): # Collect the element-wise sign of the data gradient sign_data_grad = data_grad.sign() # Create the perturbed image by adjusting each pixel of the input image perturbed_image = image + epsilon*sign_data_grad # Adding clipping to maintain [0,1] range perturbed_image = torch.clamp(perturbed_image, 0, 1) # Return the perturbed image return perturbed_image
テスト関数
最後に、このチュートリアルの中心的な結果は test 関数からのものです。この test 関数への各呼び出しは MNIST テストセット上の完全なテストステップを遂行して最終的な精度を報告します。けれども、この関数はまた epsilon 入力を取ることに気が付いてください。これは test 関数が強度 (= strength) $\epsilon$ を持つ敵対者からの攻撃を受けるモデルの精度を報告するからです。より具体的には、テストセットの各サンプルに対して、関数は入力データに関する損失の勾配を計算し ($data\_grad$)、fgsm_attack で摂動された画像を作成し ($perturbed\_data$)、それから摂動されたサンプルが敵対的かどうかを見るために確認します。モデル精度をテストすることに加えて、関数はまた後で可視化されるためにある成功的な敵対的サンプルをセーブして返します。
def test( model, device, test_loader, epsilon ): # Accuracy counter correct = 0 adv_examples = [] # Loop over all examples in test set for data, target in test_loader: # Send the data and label to the device data, target = data.to(device), target.to(device) # Set requires_grad attribute of tensor. Important for Attack data.requires_grad = True # Forward pass the data through the model output = model(data) init_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability # If the initial prediction is wrong, dont bother attacking, just move on if init_pred.item() != target.item(): continue # Calculate the loss loss = F.nll_loss(output, target) # Zero all existing gradients model.zero_grad() # Calculate gradients of model in backward pass loss.backward() # Collect datagrad data_grad = data.grad.data # Call FGSM Attack perturbed_data = fgsm_attack(data, epsilon, data_grad) # Re-classify the perturbed image output = model(perturbed_data) # Check for success final_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability if final_pred.item() == target.item(): correct += 1 # Special case for saving 0 epsilon examples if (epsilon == 0) and (len(adv_examples) < 5): adv_ex = perturbed_data.squeeze().detach().cpu().numpy() adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) ) else: # Save some adv examples for visualization later if len(adv_examples) < 5: adv_ex = perturbed_data.squeeze().detach().cpu().numpy() adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) ) # Calculate final accuracy for this epsilon final_acc = correct/float(len(test_loader)) print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader), final_acc)) # Return the accuracy and an adversarial example return final_acc, adv_examples
攻撃を実行する
実装の最後のパートは実際に攻撃を実行することです。ここで、epsilons 入力の各 epsilon 値に対して完全なテストステップを実行します。各 epsilon に対して次のセクションでプロットされるために最終的な精度と幾つかの成功的な敵対的サンプルもセーブします。プリントされた精度が epsilon 値が増加するにつれてどのように減少するか気がつくでしょう。また、$\epsilon=0$ のケースは攻撃のない、元のテスト精度を表わすことに注意してください。
accuracies = [] examples = [] # Run test for each epsilon for eps in epsilons: acc, ex = test(model, device, test_loader, eps) accuracies.append(acc) examples.append(ex)
Epsilon: 0 Test Accuracy = 9810 / 10000 = 0.981 Epsilon: 0.05 Test Accuracy = 9426 / 10000 = 0.9426 Epsilon: 0.1 Test Accuracy = 8510 / 10000 = 0.851 Epsilon: 0.15 Test Accuracy = 6826 / 10000 = 0.6826 Epsilon: 0.2 Test Accuracy = 4301 / 10000 = 0.4301 Epsilon: 0.25 Test Accuracy = 2082 / 10000 = 0.2082 Epsilon: 0.3 Test Accuracy = 869 / 10000 = 0.0869
結果
精度 vs Epsilon
最初の結果は精度 versus epsilon プロットです。前に言及したように、epsilon が増加するにつれてテスト精度が減少することを期待します。これはより大きい epsilon は損失を最大化する方向により大きなステップを取ることを意味するからです。epsilon 値が線形に区分されていても曲線の傾向は線形ではないことに気がつくでしょう。例えば、$\epsilon=0.05$ における精度は $\epsilon=0$ よりもおよそ 4 % だけ低いですが、$\epsilon=0.2$ における精度は $\epsilon=0.15$ よりも 25% 低いです。また、モデルの精度は $\epsilon=0.25$ と $\epsilon=0.3$ の間で 10-クラス分類器のためにランダム精度に行き当たることに気がつくでしょう。
plt.figure(figsize=(5,5)) plt.plot(epsilons, accuracies, "*-") plt.yticks(np.arange(0, 1.1, step=0.1)) plt.xticks(np.arange(0, .35, step=0.05)) plt.title("Accuracy vs Epsilon") plt.xlabel("Epsilon") plt.ylabel("Accuracy") plt.show()
敵対的サンプルをサンプリングする
ただで手に入るものはないという考えを覚えていますか?このケースでは、epsilon が増加するにつれてテスト精度は減少しますが、しかし 摂動はより容易に知覚できるようになります。実際には、精度の低下と知覚できることの間には、攻撃者が考慮しなければならないトレードオフがあります。ここでは、各 epsilon 値における成功的な敵対的サンプルの幾つかの例を示します。プロットの各行は異なる epsilon 値を示します。最初の行は $\epsilon=0$ の例で、これは摂動がない元の「クリーンな」画像を表します。各画像のタイトルは「元の分類 -> 敵対的分類」を示します。気付いてください、摂動は $\epsilon=0.15$ で明白になり始めてそして $\epsilon=0.3$ で完全に明らかです。けれども、総てのケースで人間は追加されたノイズにもかかわらず依然として正しいクラスを識別できます。
# Plot several examples of adversarial samples at each epsilon cnt = 0 plt.figure(figsize=(8,10)) for i in range(len(epsilons)): for j in range(len(examples[i])): cnt += 1 plt.subplot(len(epsilons),len(examples[0]),cnt) plt.xticks([], []) plt.yticks([], []) if j == 0: plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14) orig,adv,ex = examples[i][j] plt.title("{} -> {}".format(orig, adv)) plt.imshow(ex, cmap="gray") plt.tight_layout() plt.show()
Where to go next?
望ましくはこのチュートリアルは敵対的機械学習のトピックへの幾つかの洞察を与えます。ここから進むための多くの可能性のある方向があります。この攻撃は敵対的攻撃研究の初っ端を表しています、ML モデルをどのように攻撃するかそして敵対者から守るかについての多くの続く考えがあるからです。実際に、NIPS 2017 では敵対的攻撃と防御コンペティションがありコンペティションで使用された方法の多くはこのペーパーで記述されています : Adversarial Attacks and Defences Competition。防御上のワークもまた機械学習モデルを自然に摂動されるのと敵対的に加工された入力の両者に対して一般により堅牢にする考えに導きます。
もう一つの進む方向は異なるドメインにおける敵対的攻撃と防御です。敵対的研究は画像ドメインに制限されません、speech-to-text モデル上の この 攻撃を確認してください。しかしおそらく敵対的機械学習についてより学習するための最善の方法は貴方の手を汚すことです。NIPS 2017 コンペティションからの異なる攻撃を実装して、そしてそれが FGSM とどのように異なるかを見てみましょう。それから、貴方自身の攻撃からモデルを防御してみましょう。
以上