PyTorch 2.0 チュートリアル : 画像と動画 : 敵対的サンプルの生成

PyTorch 2.0 チュートリアル : 画像と動画 : 敵対的サンプルの生成 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 05/03/2023 (2.0.0)

* 本ページは、PyTorch 2.0 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

クラスキャット 人工知能 研究開発支援サービス

クラスキャット は人工知能・テレワークに関する各種サービスを提供しています。お気軽にご相談ください :

◆ 人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。スケジュール
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

  • 株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
  • sales-info@classcat.com  ;  Website: www.classcat.com  ;   ClassCatJP

 

PyTorch 2.0 チュートリアル : 画像と動画 : 敵対的サンプルの生成

これを読んでいれば、願わくば貴方は幾つかの機械学習モデルがどれほど効果的であるか高く評価できるでしょう。研究は ML モデルをより高速に、より正確に、そしてより効率的に常に推し進めています。けれども、モデルの設計と訓練のしばしば見落とされる側面はセキュリティと堅牢性で、特にモデルを騙すことを望む敵対者に直面することにおいてです。

このチュートリアルは ML モデルのセキュリティ脆弱性への認識を高め、敵対的機械学習のホットなトピックへの洞察を与えます。貴方は、画像への知覚できない摂動の追加が異なるモデル性能を劇的に引き起こせることを見出して驚くかもしれません。これがチュートリアルであることを考慮して、画像分類器上のサンプルを通してこのトピックを探究します。特に、MNIST 分類器を騙すために、最初の最もポピュラーな攻撃メソッドの一つ、高速勾配 Sign 攻撃 (FGSM) を使用します。

 

脅威モデル (= Threat Model)

コンテキストのために、敵対的攻撃の多くのカテゴリーがあり、それぞれ異なる目標と攻撃者の知識の前提を持ちます。けれども、一般に包括的な目標は望まれる誤分類を引き起こすために入力データに最小量の摂動を追加することです。攻撃者の知識の幾つかの種類の前提があり、そのうちの 2 つは : ホワイトボックスブラックボックス です。ホワイトボックス 攻撃は攻撃者がアーキテクチャ、入力、出力と重みを含む、モデルへの完全な知識とアクセスを持つことを仮定します。ブラックボックス 攻撃は攻撃者がモデルの入力と出力へのアクセスだけを持ち、基礎的なアーキテクチャや重みについては何も知らないことを仮定します。誤分類ソース/ターゲット誤分類 を含む、幾つかのタイプの目標もまたあります。誤分類 の目標は敵対者は出力分類が間違っていることだけを望み、新しい分類が何であるかは気にしないことを意味します。ソース/ターゲット誤分類 は敵対者は元は特定のソースクラスである画像をそれが特定のターゲットクラスとして分類されるように変更することを望むことを意味します。

このケースでは、FGSM 攻撃は 誤分類 の目標を持つ ホワイトボックス 攻撃です。この背景情報とともに、今私達は攻撃を詳細に議論できます。

 

高速勾配 Sign 攻撃

今までで最初の最もポピュラーな敵対的攻撃の一つは 高速勾配 Sign 攻撃 (FGSM) として参照され Explaining and Harnessing 敵対的サンプル で 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

# NOTE: This is a hack to get around "User-agent" limitations when downloading MNIST datasets
#       see, https://github.com/pytorch/vision/issues/3497 for more information
from six.moves import urllib
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)

 

実装

このセクションでは、チュートリアルのための入力パラメータを議論し、攻撃を受けるモデルを定義し、それから攻撃をコーディングして幾つかのテストを実行します。

 

入力

このチュートリアルのためには 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

 

攻撃されるモデル (= Model Under Attack)

言及したように、攻撃されるモデルは 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
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to ../data/MNIST/raw/train-images-idx3-ubyte.gz

  0%|          | 0/9912422 [00:00<?, ?it/s]
 78%|#######8  | 7733248/9912422 [00:00<00:00, 77220431.96it/s]
100%|##########| 9912422/9912422 [00:00<00:00, 92673019.17it/s]
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
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to ../data/MNIST/raw/train-labels-idx1-ubyte.gz

  0%|          | 0/28881 [00:00<?, ?it/s]
100%|##########| 28881/28881 [00:00<00:00, 121989621.17it/s]
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
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to ../data/MNIST/raw/t10k-images-idx3-ubyte.gz

  0%|          | 0/1648877 [00:00<?, ?it/s]
100%|##########| 1648877/1648877 [00:00<00:00, 25276087.47it/s]
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
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to ../data/MNIST/raw/t10k-labels-idx1-ubyte.gz

  0%|          | 0/4542 [00:00<?, ?it/s]
100%|##########| 4542/4542 [00:00<00:00, 27176217.93it/s]
Extracting ../data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ../data/MNIST/raw

CUDA Available:  True

Net(
  (conv1): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))
  (conv2_drop): Dropout2d(p=0.5, inplace=False)
  (fc1): Linear(in_features=320, out_features=50, bias=True)
  (fc2): Linear(in_features=50, out_features=10, bias=True)
)

 

FGSM 攻撃

今、元の入力を摂動することにより敵対的サンプルを作成する関数を定義できます。fgsm_attack 関数は 3 つの入力を取ります、image は元のクリーンな画像 ($x$)、epsilon はピクセル単位の摂動総量 ($\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, don't 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

最初の結果は精度 vs 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 とどのように異なるかを見てみましょう。それから、貴方自身の攻撃からモデルを防御してみましょう。

 

以上