C&W Attack 実装

# はじめに

「C&W Attack」という敵対的サンプル攻撃をPyTorchで実装しました。
画像データはCIFAR10を用いました。
実装にあたり書籍「AIセキュリティから学ぶディープラーニング[技術]入門」に掲載のソースコードを参考にしました。

参考元ソースコード 

https://github.com/tagomaru/ai_security/blob/master/notebooks/Chapter10/C%26W_with_MNIST.ipynb


# 実装

本稿では L2 Attack、Targeted Attack を実装します。

## モジュールのインポート

```python
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.models as models
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
```

## データの用意

```python
mu=0.5
sigma=0.5

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((mu, mu, mu), (sigma, sigma, sigma)), 
])
trainset = torchvision.datasets.CIFAR10(
    root='./data', 
    train=True,
    download=True,
    transform=transform
)
trainloader = torch.utils.data.DataLoader(
    trainset,
    batch_size=100,
    shuffle=True,
    num_workers=2
)
testset = torchvision.datasets.CIFAR10(
    root='./data', 
    train=False, 
    download=True, 
    transform=transform
)
testloader = torch.utils.data.DataLoader(
    testset, 
    batch_size=1, 
    shuffle=False, 
    num_workers=2
)
```

## モデルの用意

```python
def Net():
    model_ft=models.resnet50(pretrained=True)
    model_ft.fc=nn.Linear(model_ft.fc.in_features, 10)
    return  model_ft

# Define what device we are using
use_cuda=True
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
path="./exp/bestloss.pth"
model.load_state_dict(torch.load(path))

# Set the model in evaluation mode. In this case this is for the Dropout layers
model.eval()
```

## 前準備

```python
class2name={
    0: "airplane", 
    1: "automobile", 
    2: "bird", 
    3: "cat", 
    4: "deer", 
    5: "dog", 
    6: "frog", 
    7: "horse", 
    8: "ship", 
    9: "truck", 
}
```

## 攻撃

```python
class CW2:
    """ C&W Attack (L2) による敵対的サンプルを生成
    Attributes:
        classifier (Model) : logits を出力するモデル
        k (float): 自信を調整するパラメータ
        learning_rate (float): Adam の学習率
        binary_search_steps (int): バイナリサーチの回数
        max_iterations (int): Adam の最大イテレーション回数
        initial_c (float): c の初期値
    """

    def __init__(self, model, k = 0, learning_rate = 0.01,
                            binary_search_steps = 9, max_iterations = 1000,
                            initial_c = 0.001):

        # 引数をすべてインスタンス変数にセット
        self.model = model
        self.k = k
        self.learning_rate = learning_rate
        self.max_iterations = max_iterations
        self.binary_search_steps = binary_search_steps
        self.initial_c = initial_c
            
    def is_satisfied_with_k(self, logits, target_class):
        """
        「 k を加味したターゲットクラスの logit が最大」という制約を満たすか確認する。
        ターゲットクラスの logit から k　を引いた値が他のどのクラスの logit よりも大きければ　True を返し、
        そうでなければ、False を返す。
        Args:
            logits (ndarray): logits
            target_class (ndarray): ターゲットクラス
        Returns:
            satisfied (bool): 制約を満たす場合は True、そうでなければ False
        """
        # logits = np.copy(logits)
        logits[target_class] -= self.k
        satisfied = torch.argmax(logits) == target_class
        return satisfied    
    
    def generate(self, original_image, target_class_ohe):
        """
        敵対的サンプルを生成
        Args:
            original_image (tensor): オリジナル画像 `(28, 28)`
            target_class_ohe (tensor): ターゲットクラスの One-Hot `(10, )`
        Returns:
            o_best_adv_image (tensor): 敵対的サンプル `(28, 28)`
        """

        # ターゲットクラスを変数にセット
        target_class = np.argmax(target_class_ohe)
        
        # オリジナル画像の型を変数にセット
        shape = original_image.shape
               
        # c とその下限値と上限値をセット
        c = self.initial_c
        c_lower = 0
        c_upper = 1e10

        #  (tanh(w) + 1) / 2 の w
        w = torch.zeros(shape, dtype=torch.float32)
        w.requires_grad = True
        
        #  オリジナル画像を Tensor にキャスト
        # original_image = torch.tensor(original_image, dtype=torch.float32)

        # 目的関数を生成する
        def build_objective(c, w):

            # 敵対的サンプルを格納する変数
            # adv_image = (torch.tanh(w) + 1) / 2
            adv_image = torch.tanh(w) # 正規化後の画像は[-1,1]

            # 目的関数1
            objective1 = torch.sum(torch.square(adv_image - original_image))

            # 敵対的サンプルの logits
            logits = self.model(adv_image)[0]

            # ターゲットクラスの logit
            target_logit = torch.sum(target_class_ohe * logits)
            
            # ターゲットクラス以外の logit の最大値
            other_max_logit = torch.max((1 - target_class_ohe) * logits + (target_class_ohe * torch.min(logits)))
          
            # 目的関数2
            objective2 = c * torch.maximum(torch.tensor(0.0), other_max_logit - target_logit + self.k)

            # 目的関数
            objective = objective1 + objective2

            return objective

        # Adam のインスタンスを生成
        opt = torch.optim.Adam([w], lr=self.learning_rate)
        
        # 目的関数の最小化の過程で見つかった最小の objective1、敵対的サンプル、分類結果を格納する変数
        o_best_objective1 = np.inf # 初期値として無限をセット
        o_best_adv_image = np.zeros(shape) # 初期値としてすべての要素に0をセット
        o_best_class = -1 # 初期値としてダミークラスをセット
        
        # バイナリサーチ用のループ
        for outer_step in range(self.binary_search_steps):

            # print("outer_step: ", outer_step)
            
            # バイナリサーチのステップ内で見つかった最小の objective1 と分類結果を格納する変数
            best_objective1 = np.inf # 初期値として無限をセット
            best_class = -1 # 初期値としてダミークラスをセット
            
            # objective を保存しておく変数
            prev_objective = np.inf # 初期値として無限をセット
            
            # Adam を実行するループ
            for iteration in range(self.max_iterations):

                # print("    iteration: ", iteration)
                
                # Adam を実行
                opt.zero_grad()
                objective = build_objective(c, w)
                objective.backward()
                opt.step()
                
                # 敵対的サンプルの logits 取得
                # adv_image = (torch.tanh(w) + 1) / 2
                adv_image = torch.tanh(w) # 正規化後の画像は[-1,1]
                logits = self.model(adv_image)[0]

                objective1 = torch.sum(torch.square(adv_image - original_image))

                # max_iterations の 10% ごとに objective を確認
                if iteration % (self.max_iterations // 10) == 0:
                    # objective にほとんど変化がない、もしくは増えている場合は Adam のループを抜ける
                    if build_objective(c, w) > prev_objective * 0.9999:
                        break
                    # 次のステップの比較用に現在の objective を保存
                    prev_objective = build_objective(c, w)
                
                # 制約を満たすか確認
                satisfied = self.is_satisfied_with_k(logits, target_class)

                # objective1 が、best_objective1 より小さく、制約も満たせば、best_objective1 と best_class を更新する
                if objective1 < best_objective1 and satisfied:
                    best_objective1 = objective1
                    best_class = target_class
                    
                # objective1 が、o_best_objective1 より小さく、制約も満たせば、o_best_objective1, o_best_class, o_best_adv_image を更新する
                if objective1 < o_best_objective1 and satisfied:
                    o_best_objective1 = objective1
                    o_best_class = target_class
                    o_best_adv_image = adv_image

            # 現在のバイナリサーチのステップで制約を満たしている場合
            if best_class == target_class:
                # c の上限値に現在の c をセットする
                c_upper = c

                # c に　「現在の c」　と　「c の下限値」　の平均値をセットする
                c = (c_lower + c_upper) / 2

            # 見つからなかった場合
            else:
                # c の下限値に現在の c をセットする
                c_lower = c
                if c_upper < 1e9:
                    # c の上限値が 1e9 未満の場合は、c に　「現在の c」　と　「c の上限値」　の平均値をセットする
                    c = (c_lower + c_upper) / 2
                else:
                    # それ以外は、c が大きくなりすぎないように、c を 10 倍する
                    c *= 10
            
            # ログ出力
            print('Binary Search {0}/{1}'.format(outer_step + 1, self.binary_search_steps))
            print('  L2 square: {0:.2f} - c: {1:.2f} - class: {2}'.format(o_best_objective1, c, o_best_class))

        # 敵対的サンプルを ndarray に変換して返す
        return o_best_adv_image
```

```python
original_image, _ = testset[0]
original_image=original_image.unsqueeze(0)
target_class_ohe = F.one_hot(torch.tensor(0), num_classes=10) # ターゲットクラスを 0 とする
```

k=0 の攻撃
```python
attack = CW2(model, k=0) # k = 0 で CW2 のインスタンスを生成
adv_image_k0 = attack.generate(original_image, target_class_ohe) # 敵対的サンプルを生成

# 推論結果。 model_logits ではなく、model での推論である点に注意
Y_hat_k0 = model(adv_image_k0)

# スコア
score_k0 = torch.max(torch.softmax(Y_hat_k0, dim=1)).item()

# 分類結果
class_k0 = torch.argmax(Y_hat_k0[0]).item()
advk0_name = class2name[class_k0]

# L2
adv_img_k0 = adv_image_k0[0].detach().numpy()
orig_img = original_image[0].numpy()
L2_k0 = np.linalg.norm(adv_img_k0 - orig_img)

adv_img_k0 = mu+sigma*adv_img_k0
adv_img_k0 = adv_img_k0.transpose(1,2,0)
```

k=7 の攻撃
```python
attack = CW2(model, k=7) # k = 0 で CW2 のインスタンスを生成
adv_image_k7 = attack.generate(original_image, target_class_ohe) # 敵対的サンプルを生成

# 推論結果。 model_logits ではなく、model での推論である点に注意
Y_hat_k7 = model(adv_image_k7)

# スコア
score_k7 = torch.max(torch.softmax(Y_hat_k7, dim=1)).item()

# 分類結果
class_k7 = torch.argmax(Y_hat_k7[0]).item()
advk7_name = class2name[class_k7]

# L2
adv_img_k7 = adv_image_k7[0].detach().numpy()
orig_img = original_image[0].numpy()
L2_k7 = np.linalg.norm(adv_img_k7 - orig_img)

adv_img_k7 = adv_img_k7.transpose(1,2,0)
adv_img_k7 = mu+sigma*adv_img_k7
```

## 可視化

```python
plt.figure(figsize=(15, 15))

# オリジナル画像を表示
plt.subplot(1, 3, 1)
plt.gca().axes.xaxis.set_visible(False)
plt.gca().axes.yaxis.set_visible(False)
plt.title('Original\n {0} - {1:.2f}%'.format(class2name[original_class], original_score * 100))
plt.imshow(img)

# k = 0 の敵対的サンプルを表示
plt.subplot(1, 3, 2)
plt.gca().axes.xaxis.set_visible(False)
plt.gca().axes.yaxis.set_visible(False)
plt.title('Adversarial (k = 0)\n {0} - {1:.2f}%, l2 = {2:.2f}'.format(class2name[class_k0], score_k0 * 100, L2_k0))
plt.imshow(adv_img_k0)

# k = 7 の敵対的サンプルを表示
plt.subplot(1, 3, 3)
plt.gca().axes.xaxis.set_visible(False)
plt.gca().axes.yaxis.set_visible(False)
plt.title('Adversarial (k = 7)\n {0} - {1:.2f}%, l2 = {2:.2f}'.format(class2name[class_k7], score_k7 * 100, L2_k7))
plt.imshow(adv_img_k7)
```

可視化は以下のようになります。
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3481383/c3e8576b-3950-83c4-53dc-954aa15ecca1.png)
k=0, k=7 ともに airplane と誤認識させることに成功しています。
ここでよく見ると、k=0 ではスコア（airplaneの確率）が50.71%であるのに対して、k=7 のほうがスコアは99.88%と高くなっています。（その分l2ノルムも大きくなっています）


# 終わりに
現論文によると C&W Attack は当時有効とされていた Distillation Defense を破った手法とのこと。
時間を見つけて防御手法も調査していきたい。