Project Gradient Descent Attack 実装

# はじめに

「Project Gradient Descent Attack」という敵対的サンプル攻撃をPyTorchで実装しました。
画像データはCIFAR10を用いました。
実装にあたり書籍「AIセキュリティから学ぶディープラーニング[技術]入門」に掲載のソースコードを一部引用しました。書籍ではARTというライブラリを用いて実装していますがここではナイーブに実装していきます。ARTを用いた実装は以下のページからご覧ください。

https://github.com/tagomaru/ai_security/blob/master/notebooks/Chapter10/PGD_attack_with_MNIST.ipynb

# 実装

## ライブラリのインポート

```python
import torch
import torch.nn as nn
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
# 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
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
eps=0.1
eps_step=0.005
max_step=40
```

```python
criterion = nn.CrossEntropyLoss()
```

## 攻撃

```python
def fgsm_attack(image, epsilon, data_grad):
    """
    INPUT
    image: (B, C, H, W), [-1,1]
    epsilon: (1, )
    data_grad: (B, C, H, W)
    OUTPUT
    perturbed_image: (B, C, H, W), [-1,1]
    """
    # 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 [-1,1] range
    perturbed_image = torch.clamp(perturbed_image, -1, 1)
    # Return the perturbed image
    return perturbed_image
```

```python
def test(
        model, 
        data, 
        target, 
        device, 
        max_step, 
        eps_step, 
        eps
    ):
    """
    """
    success = True
    data, target = data.to(device), target.to(device)
    orig_img = data.clone()
    data.requires_grad = True
    output=model(data)
    pred=output.max(1, keepdim=True)[1]

    # print(pred.item(), target.item())
    if pred.item() == target.item():
        for t in range(max_step):
            # calcurate loss and backward
            loss=criterion(output, target)
            model.zero_grad()
            loss.backward()
            data_grad=data.grad.data

            # Attack and clip
            data=fgsm_attack(data, eps_step, data_grad)
            data=torch.clamp(
                data, 
                orig_img-eps, 
                orig_img+eps
            )
            data=data.detach().to(device)
            data.requires_grad = True
            output=model(data)

            # pred
            pred=output.max(1, keepdim=True)[1]

            # Check for success
            if pred.item() != target.item():
                break
    
    if pred.item() == target.item():
        success = False

    return data, pred, success
```

クリッピングとdetach()処理の実装を忘れずに。（忘れると予期せぬ結果になったりエラーになったりします）

関数名が fgsm_attack になっているのはPGDAの内部で用いられるFGSMを実装しているためです。

## 可視化

```python
data, target = next(iter(testloader))
![uploading...0]()

orig_img=data[0]
orig_img=orig_img.detach().numpy()
orig_img=orig_img.transpose(1,2,0)
orig_img=mu+sigma*orig_img

pred=torch.softmax(model(data), dim=1)
original_class=torch.argmax(pred).item()
original_name=class2name[original_class]
original_score=torch.max(pred).item()*100

adv_img=adv_data[0]
adv_img=adv_img.detach().numpy()
adv_img=adv_img.transpose(1,2,0)
adv_img=mu+sigma*adv_img

adv_pred=torch.softmax(model(adv_data), dim=1)
adv_class=torch.argmax(adv_pred).item()
adv_name=class2name[adv_class]
adv_score=torch.max(adv_pred).item()*100

plt.figure(figsize=(11,5.5))

plt.subplot(1, 2, 1)
plt.axis("off")
plt.title(f"original (class={original_name}, score={original_score:.2f}%)")
plt.imshow(orig_img)

plt.subplot(1, 2, 2)
plt.axis("off")
plt.title(f"adversarial (class={adv_name}, score={adv_score:.2f}%)")
plt.imshow(adv_img)
```


![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3481383/2ec29945-73e8-2215-7efb-6c0aff0c5bc1.png)

右側の画像は cat に見えますが dog と認識されてしまいました。

# 終わりに

PGDAのイメージは「FGSMを細かく繰り返す」。
考え方はシンプルですが、FGSMよりも見た目に違和感のない敵対的サンプルが生成できています。（その分時間はかかりますが）