Li Honyi 機械学習の宿題 10 - 敵対的攻撃、FGSM、IFGSM

理論的な部分については、Li Honyi の機械学習 - Adversarial Attack_iwill323 のブログ - CSDN ブログを参照してください。

目次

目標と方法

評価方法

ガイドパッケージ

全体設定

データ

変身

データセット

プロキシモデルとターゲットモデル

非攻撃的なイメージでのターゲット モデルのパフォーマンスを評価する

攻撃アルゴリズム

FGSM

I-FGSM

MI-FGSM

多様な入力 (DIM)

攻撃機能

攻撃イメージ生成機能

アンサンブルアタック

統合モデル機能

アンサンブルモデルを構築する

攻撃結果を可視化する

攻撃

FGSM法

I-FGSM方式+アンサンベルアタック

MIFGSM + アンサンブルアタック(適切なモデルを選択)

DIM-MIFGSM + アンサンブルアタック(適切なモデルを選択)

パッシブディフェンス - JPEG圧縮

攻撃

防衛

拡張子:ファイル読み込み


目標と方法

ターゲット ネットワークのトレーニング データを使用して 1 つまたはいくつかのプロキシ ネットワークをトレーニングし (このジョブはトレーニングする必要はなく、トレーニングされたモデルを使用するだけです)、プロキシ ネットワークを攻撃のターゲットとして扱い、プロキシ ネットワークを使用して攻撃的な攻撃を生成します。プロキシネットワーク上でホワイトボックス攻撃を行い、パラメータが不明な学習済み画像をネットワークに入力することで攻撃が実現します。

○攻撃目的:非標的型攻撃

○攻撃アルゴリズム:FGSM/I-FGSM

○攻撃スキーム:ブラックボックス攻撃(プロキシネットワーク上で攻撃を行う)

○多様な入力(DIM)による攻撃伝達性の向上

○ 複数のプロキシ モデルを攻撃する - アンサンブル攻撃

この課題に参加する国立台湾大学の学生でない場合、提出結果と実際のスコアを確認することはできません。

評価方法

画像のピクセル値は 0 ~ 255 ですが、このジョブでは画像の変化があまり目立たないように、変更できる最大ピクセル サイズ ε を 8 に制限しています。ε が 16 に等しい場合、画像の変化はより明白になります。

○εは8固定
○距離測定:L-inf.ノルム
○モデル精度(低下)のみが評価基準

ガイドパッケージ

import torch
import torch.nn as nn
import torchvision
import os
import glob
import shutil
import numpy as np
from PIL import Image
from torchvision.transforms import transforms
from torch.utils.data import Dataset, DataLoader
import matplotlib.pyplot as plt

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
batch_size = 8

全体設定

主に、画像の標準化に使用される平均値と標準偏差 std、および ε です。ε は 255 と std で除算され、次のように説明されます。

良性の画像: 敵対的な摂動を含まない画像
敵対的な画像: 敵対的な摂動を含む画像

# the mean and std are the calculated statistics from cifar_10 dataset
cifar_10_mean = (0.491, 0.482, 0.447) # mean for the three channels of cifar_10 images
cifar_10_std = (0.202, 0.199, 0.201) # std for the three channels of cifar_10 images

# convert mean and std to 3-dimensional tensors for future operations
mean = torch.tensor(cifar_10_mean).to(device).view(3, 1, 1)
std = torch.tensor(cifar_10_std).to(device).view(3, 1, 1)

epsilon = 8/255/std

root = './data' # directory for storing benign images

データ

変身

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(cifar_10_mean, cifar_10_std)
])

データセット

Li Honyi の 2022 Machine Learning HW10 Analysis_Machine Learning Craftsman's Blog-CSDN Blog からダウンロードでき、合計 200 枚の写真が 10 のフォルダーに分かれており、各カテゴリには 20 枚の写真があります。

        data_dir
        §── class_dir
        │ ├── class1.png
        │ §── ...
        │ §── class20.png

このディレクトリ構造を見ると、ImageFolder 関数が使用できることがわかります。ImageFolder 関数はデータセットの読み取りと Division_iwill323 のブログを参照- CSDN ブログ

adv_set = torchvision.datasets.ImageFolder(os.path.join(root), transform=transform) 
adv_loader = DataLoader(adv_set, batch_size=batch_size, shuffle=False)

興味深いのは、元のコードが Dataset 関数をカスタマイズしていることです。これは短く簡潔であり、学ぶ価値があります。

class AdvDataset(Dataset):
    def __init__(self, data_dir, transform):
        self.images = []
        self.labels = []
        self.names = []
        '''
        data_dir
        ├── class_dir
        │   ├── class1.png
        │   ├── ...
        │   ├── class20.png
        '''
        for i, class_dir in enumerate(sorted(glob.glob(f'{data_dir}/*'))):
            images = sorted(glob.glob(f'{class_dir}/*'))
            self.images += images
            self.labels += ([i] * len(images))  # 第i个读到的类文件夹,类别就是i
            self.names += [os.path.relpath(imgs, data_dir) for imgs in images]  # 返回imgs相对于data_dir的相对路径
        self.transform = transform
    def __getitem__(self, idx):
        image = self.transform(Image.open(self.images[idx]))
        label = self.labels[idx]
        return image, label
    def __getname__(self):
        return self.names
    def __len__(self):
        return len(self.images)

adv_set = AdvDataset(root, transform=transform)
adv_names = adv_set.__getname__()
adv_loader = DataLoader(adv_set, batch_size=batch_size, shuffle=False)

print(f'number of images = {adv_set.__len__()}')

プロキシモデルとターゲットモデル

この割り当てでは、トレーニングされたモデルをプロキシ ネットワークおよび攻撃ターゲット モデルとして使用します。これらのネットワークは CIFAR-10 で事前トレーニングされており、Pytorchcv からインポートできます。モデル一覧はこちらです_cifar10 サフィックスが付いたモデルを選択するには。

対象モデルはresnet110_cifar10です。後者のプロキシ モデルは、nin_cifar10、resnet20_cifar10、preresnet20_cifar10 を選択します。つまり、これらのネットワーク上での攻撃イメージのトレーニングと生成が、resnet110_cifar10 への攻撃に適用されます。

from pytorchcv.model_provider import get_model as ptcv_get_model

model = ptcv_get_model('resnet110_cifar10', pretrained=True).to(device)
loss_fn = nn.CrossEntropyLoss()

非攻撃的なイメージでのターゲット モデルのパフォーマンスを評価する

def epoch_benign(model, loader, loss_fn):
    model.eval()
    train_acc, train_loss = 0.0, 0.0
    with torch.no_grad():
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            yp = model(x)
            loss = loss_fn(yp, y)
            train_acc += (yp.argmax(dim=1) == y).sum().item()
            train_loss += loss.item() * x.shape[0]
    return train_acc / len(loader.dataset), train_loss / len(loader.dataset)

攻撃された画像内の resnet110_cifar10 の精度は、benign_acc=0.95、benign_loss=0.22678 です。

benign_acc, benign_loss = epoch_benign(model, adv_loader, loss_fn)
print(f'benign_acc = {benign_acc:.5f}, benign_loss = {benign_loss:.5f}')

攻撃アルゴリズム

FGSM

高速勾配符号法 (FGSM)。FGSM はイメージに対して 1 回だけ攻撃を実行します。

def fgsm(model, x, y, loss_fn, epsilon=epsilon):    
    x_adv = x.detach().clone() # 克隆x是因为x的值会随着x_adv的改变而改变
    x_adv.requires_grad = True # need to obtain gradient of x_adv, thus set required grad
    loss = loss_fn(model(x_adv), y) 
    loss.backward()    
    # fgsm: use gradient ascent on x_adv to maximize loss
    grad = x_adv.grad.detach() 
    x_adv = x_adv + epsilon * grad.sign()  # 不会越界,所以不用clip
    return x_adv

I-FGSM

反復高速勾配符号法 (I-FGSM)。fgsm と比較して、ifgsm 方式では複数の fgsm ループ攻撃が使用され、追加パラメータ α が存在します。

# set alpha as the step size in Global Settings section
# alpha and num_iter can be decided by yourself
alpha = 0.8/255/std
def ifgsm(model, x, y, loss_fn, epsilon=epsilon, alpha=alpha, num_iter=20):
    x_adv = x    
    for i in range(num_iter):
        # x_adv = fgsm(model, x_adv, y, loss_fn, alpha) # call fgsm with (epsilon = alpha) to obtain new x_adv        
        x_adv = x_adv.detach().clone()
        x_adv.requires_grad = True # need to obtain gradient of x_adv, thus set required grad
        loss = loss_fn(model(x_adv), y) 
        loss.backward()
        # fgsm: use gradient ascent on x_adv to maximize loss
        grad = x_adv.grad.detach()
        x_adv = x_adv + alpha * grad.sign()

        x_adv = torch.max(torch.min(x_adv, x+epsilon), x-epsilon) # clip new x_adv back to [x-epsilon, x+epsilon]
    return x_adv

MI-FGSM

https://arxiv.org/pdf/1710.06081.pdf

ifgsm と比較して、mifgsm は攻撃が極大値に陥るのを防ぐために勢いを追加します (これはオプティマイザーの勢いの原理に似ています)

def mifgsm(model, x, y, loss_fn, epsilon=epsilon, alpha=alpha, num_iter=20, decay=0.9):
    x_adv = x
    # initialze momentum tensor
    momentum = torch.zeros_like(x).detach().to(device)
    # write a loop of num_iter to represent the iterative times
    for i in range(num_iter):
        x_adv = x_adv.detach().clone()
        x_adv.requires_grad = True # need to obtain gradient of x_adv, thus set required grad
        loss = loss_fn(model(x_adv), y) # calculate loss
        loss.backward() # calculate gradient
        # Momentum calculation
        grad = x_adv.grad.detach() 
        grad = decay * momentum +  grad / (grad.abs().sum() + 1e-8)        
        momentum = grad
        x_adv = x_adv + alpha * grad.sign()
        x_adv = torch.max(torch.min(x_adv, x+epsilon), x-epsilon) # clip new x_adv back to [x-epsilon, x+epsilon]
    return x_adv

多様な入力 (DIM)

生成されたイメージがプロキシ モデルにオーバーフィットすると、ターゲット モデルに対するこれらのイメージの攻撃力が低下する可能性があります。

mifgsm に基づいて、dim-mifgsm は過剰適合を避けるために攻撃された画像に変換を追加します。この手法は、記事「入力多様性による敵対的例の転送可能性の向上」(https://arxiv.org/pdf/1803.06978.pdf) から引用されています。この記事の変換では、最初に画像のサイズをランダムに変更し、次に画像を元のサイズにランダムにパディングします。

def dmi_mifgsm(model, x, y, loss_fn, epsilon=epsilon, alpha=alpha, num_iter=50, decay=0.9, p=0.5):
    x_adv = x
    # initialze momentum tensor
    momentum = torch.zeros_like(x).detach().to(device)
    # write a loop of num_iter to represent the iterative times
    for i in range(num_iter):
        x_adv = x_adv.detach().clone()
        x_adv_raw = x_adv.clone()
        if torch.rand(1).item() >= p:  # 以一定几率进行数据增广
            #resize img to rnd X rnd
            rnd = torch.randint(29, 33, (1,)).item()
            x_adv = transforms.Resize((rnd, rnd))(x_adv)
            #padding img to 32 X 32 with 0
            left = torch.randint(0, 32 - rnd + 1, (1,)).item()
            top = torch.randint(0, 32 - rnd + 1, (1,)).item()
            right = 32 - rnd - left
            bottom = 32 - rnd - top
            x_adv = transforms.Pad([left, top, right, bottom])(x_adv)
        x_adv.requires_grad = True # need to obtain gradient of x_adv, thus set required grad
        loss = loss_fn(model(x_adv), y)
        loss.backward() 
        # Momentum calculation        
        grad = x_adv.grad.detach()
        grad = decay * momentum + grad/(grad.abs().sum() + 1e-8)
        momentum = grad
        x_adv = x_adv_raw + alpha * grad.sign()
        x_adv = torch.max(torch.min(x_adv, x+epsilon), x-epsilon) # clip new x_adv back to [x-epsilon, x+epsilon]
    return x_adv

攻撃機能

攻撃イメージ生成機能

関数 gen_adv_examples を使用して、攻撃アルゴリズムを呼び出し、攻撃画像を生成し、攻撃効果 (攻撃画像を識別するためのプロキシ モデルの精度) を計算します。

変換された画像のピクセルは [0-1] に位置し、チャネルも変更されているため、攻撃画像を生成するには逆演算が必要です。ここのコードは教科書レベルです

# perform adversarial attack and generate adversarial examples
def gen_adv_examples(model, loader, attack, loss_fn):
    model.eval()
    adv_names = []
    train_acc, train_loss = 0.0, 0.0
    for i, (x, y) in enumerate(loader):
        x, y = x.to(device), y.to(device)
        x_adv = attack(model, x, y, loss_fn) # obtain adversarial examples
        yp = model(x_adv)
        loss = loss_fn(yp, y)
        _, pred = torch.max(yp, 1)        
        train_acc += (pred == y.detach()).sum().item()
        train_loss += loss.item() * x.shape[0]
        # store adversarial examples
        adv_ex = ((x_adv) * std + mean).clamp(0, 1) # to 0-1 scale
        adv_ex = (adv_ex * 255).clamp(0, 255) # 0-255 scale
        adv_ex = adv_ex.detach().cpu().data.numpy().round() # round to remove decimal part
        adv_ex = adv_ex.transpose((0, 2, 3, 1)) # transpose (bs, C, H, W) back to (bs, H, W, C)
        adv_examples = adv_ex if i == 0 else np.r_[adv_examples, adv_ex]
    return adv_examples, train_acc / len(loader.dataset), train_loss / len(loader.dataset)

# create directory which stores adversarial examples
def create_dir(data_dir, adv_dir, adv_examples, adv_names):
    if os.path.exists(adv_dir) is not True:
        _ = shutil.copytree(data_dir, adv_dir)
    for example, name in zip(adv_examples, adv_names):
        im = Image.fromarray(example.astype(np.uint8)) # image pixel value should be unsigned int
        im.save(os.path.join(adv_dir, name))

アンサンブルアタック

複数のプロキシ モデルに対する同時攻撃。「転送可能な敵対的な例とブラックボックス攻撃の詳細を調べる」を参照してください。

ModuleList は、サブモジュール (または、nn.Module クラスに属する必要があるレイヤー) のリストを入力として受け取り、List と同様の追加および拡張操作を実行できます。同時に、サブモジュールまたはレイヤーの重みがネットワークに自動的に追加されます。nn.ModuleList はネットワークを定義するものではなく、異なるモジュールをまとめて保存するだけであることに注意することが重要です。ModuleList 内の要素の順序は、ネットワーク内での実際の位置順序を表すものではなく、モデルの定義は、forward 関数によって各層の順序を指定した後にのみ完了します。

統合モデル機能

class ensembleNet(nn.Module):
    def __init__(self, model_names):
        super().__init__()
        # ModuleList 接收一个子模块(或层,需属于nn.Module类)的列表作为输入,可以类似List那样进行append和extend操作
        self.models = nn.ModuleList([ptcv_get_model(name, pretrained=True) for name in model_names])
        # self.models.append(undertrain_resnet18) 可以append自己训练的代理网络
        
    def forward(self, x):
        emsemble_logits = None
        # sum up logits from multiple models  
        for i, m in enumerate(self.models):
            emsemble_logits = m(x) if i == 0 else emsemble_logits + m(x)        
        return emsemble_logits/len(self.models)

アンサンブルモデルを構築する

エージェントモデル

model_names = [
    'nin_cifar10',
    'resnet20_cifar10',
    'preresnet20_cifar10'
]
ensemble_model = ensembleNet(model_names).to(device)
ensemble_model.eval()

攻撃結果を可視化する

攻撃画像は攻撃ごとに生成され、保存されます。以下の攻撃画像フォルダのパスを変更して、攻撃画像を読み込み、ターゲットネットワークに渡し、攻撃効果を可視化します。

classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']    
def show_attck(adv_dir, classes=classes):    
    plt.figure(figsize=(10, 20))
    cnt = 0
    for i, cls_name in enumerate(classes):
        path = f'{cls_name}/{cls_name}1.png'
        # benign image
        cnt += 1
        plt.subplot(len(classes), 4, cnt)
        im = Image.open(os.path.join(adv_dir, path))
        logit = model(transform(im).unsqueeze(0).to(device))[0]
        predict = logit.argmax(-1).item()
        prob = logit.softmax(-1)[predict].item()
        plt.title(f'benign: {cls_name}1.png\n{classes[predict]}: {prob:.2%}')
        plt.axis('off')
        plt.imshow(np.array(im))
        # adversarial image
        cnt += 1
        plt.subplot(len(classes), 4, cnt)
        im = Image.open(os.path.join(root, path))
        logit = model(transform(im).unsqueeze(0).to(device))[0]
        predict = logit.argmax(-1).item()
        prob = logit.softmax(-1)[predict].item()
        plt.title(f'adversarial: {cls_name}1.png\n{classes[predict]}: {prob:.2%}')
        plt.axis('off')
        plt.imshow(np.array(im))
    plt.tight_layout()
    plt.show()

攻撃

FGSM法

adv_examples, ifgsm_acc, ifgsm_loss = gen_adv_examples(ensemble_model, adv_loader, ifgsm, loss_fn)
print(f'ensemble_ifgsm_acc = {ifgsm_acc:.5f}, ensemble_ifgsm_loss = {ifgsm_loss:.5f}')

adv_dir = 'ifgsm'
create_dir(root, adv_dir, adv_examples, adv_names)
show_attck(adv_dir)
fgsm_acc = 0.59000、fgsm_loss = 2.49304

ターゲット ネットワークの元の認識パフォーマンスは benign_acc = 0.95000、benign_loss = 0.22678 であり、簡易ベースラインを通過しました。

ターゲット ネットワーク resnet110_cifar10 に対する攻撃の影響を確認してください (前の視覚化コードを使用)、いくつかの成功といくつかの失敗があります。ホワイトボックス攻撃

I-FGSM方式+アンサンベルアタック

まず、良性の画像で統合モデルの精度を観察します。

from pytorchcv.model_provider import get_model as ptcv_get_model

benign_acc, benign_loss = epoch_benign(ensemble_model, adv_loader, loss_fn)
print(f'benign_acc = {benign_acc:.5f}, benign_loss = {benign_loss:.5f}')
良性_acc = 0.95000、良性_損失 = 0.15440 

攻撃

adv_examples, ifgsm_acc, ifgsm_loss = gen_adv_examples(ensemble_model, adv_loader, ifgsm, loss_fn)
print(f'ensemble_ifgsm_acc = {ifgsm_acc:.5f}, ensemble_ifgsm_loss = {ifgsm_loss:.5f}')

adv_dir = 'ensemble_ifgsm'
create_dir(root, adv_dir, adv_examples, adv_names)
show_attck(adv_dir)
アンサンブル_ifgsm_acc = 0.00000、アンサンブル_ifgsm_loss = 13.41135

中ベースライン (acc <= 0.50) を超えました。ターゲット ネットワーク resnet110_cifar10 に対する攻撃の影響を確認します (次の視覚化コードを使用します)。

MIFGSM + アンサンブルアタック(適切なモデルを選択)

Li Honyi の 2022 Machine Learning HW10 Analysis_Machine Learning Craftsman's Blog-CSDN Blogによると、中程度のベースラインでは、いくつかのプロキシ モデルがランダムに選択されており、非常に盲目的です。記事によると、Query-Free Adversarial Transfer via Undertrained Surrogates (https:// arxiv.org/abs/2007.00806) の説明では、トレーニングが不十分なモデルをいくつか選択できます。トレーニングが不十分であることの意味には 2 つの側面が含まれます: 1 つはモデルのトレーニング エポックが少ないこと、もう 1 つはモデルが到達していないことです。検証セット (val set) 内の最小損失。論文の例によると、https://github.com/kuangliu/pytorch-cifar のトレーニング方法を使用し、resnet18 モデルを選択し、30 エポックでトレーニングします (通常のトレーニングで最高の状態に達するには約 200 エポックが必要です)結果)を取得し、それを Join ensmbleNet に変換します。(このトレーニングが不十分なモデルは以下では行われていません)

adv_examples, ifgsm_acc, ifgsm_loss = gen_adv_examples(ensemble_model, adv_loader, mifgsm, loss_fn)
print(f'ensemble_mifgsm_acc = {ifgsm_acc:.5f}, ensemble_mifgsm_loss = {ifgsm_loss:.5f}')

adv_dir = 'ensemble_mifgsm'
create_dir(root, adv_dir, adv_examples, adv_names)
show_attck(adv_dir)
ensemble_mifgsm_acc = 0.00500、ensemble_mifgsm_loss = 13.23710

ターゲット ネットワーク resnet110_cifar10 に対する攻撃の影響を確認します (次の視覚化コードを使用します)。

DIM-MIFGSM + アンサンブルアタック(適切なモデルを選択)

adv_examples, ifgsm_acc, ifgsm_loss = gen_adv_examples(ensemble_model, adv_loader, dmi_mifgsm, loss_fn)
print(f'ensemble_dmi_mifgsm_acc = {ifgsm_acc:.5f}, ensemble_dim_mifgsm_loss = {ifgsm_loss:.5f}')

adv_dir = 'ensemble_dmi_mifgsm'
create_dir(root, adv_dir, adv_examples, adv_names)
show_attck(adv_dir)
ensemble_dmi_mifgsm_acc = 0.00000、ensemble_dim_mifgsm_loss = 15.16159

ターゲット ネットワーク resnet110_cifar10 に対する攻撃の影響を確認します (次の視覚化コードを使用します)。

パッシブディフェンス - JPEG圧縮

imgaug パッケージによる JPEG 圧縮、圧縮率は 70 に設定

参考: imgaug.augmenters.arithmetic — imgaug 0.4.0 ドキュメント

攻撃

# original image
path = f'dog/dog2.png'
im = Image.open(f'./data/{path}')
logit = model(transform(im).unsqueeze(0).to(device))[0]
predict = logit.argmax(-1).item()
prob = logit.softmax(-1)[predict].item()
plt.title(f'benign: dog2.png\n{classes[predict]}: {prob:.2%}')
plt.axis('off')
plt.imshow(np.array(im))
plt.tight_layout()
plt.show()

# adversarial image 
adv_im = Image.open(f'./ensemble_dmi_mifgsm/{path}')
logit = model(transform(adv_im).unsqueeze(0).to(device))[0]
predict = logit.argmax(-1).item()
prob = logit.softmax(-1)[predict].item()
plt.title(f'adversarial: dog2.png\n{classes[predict]}: {prob:.2%}')
plt.axis('off')
plt.imshow(np.array(adv_im))
plt.tight_layout()
plt.show()

 

防衛

import imgaug.augmenters as iaa

# pre-process image
x = transforms.ToTensor()(adv_im)*255
x = x.permute(1, 2, 0).numpy()
x = x.astype(np.uint8)

# TODO: use "imgaug" package to perform JPEG compression (compression rate = 70)
compressed_x =  iaa.arithmetic.compress_jpeg(x, compression=70)

logit = model(transform(compressed_x).unsqueeze(0).to(device))[0]
predict = logit.argmax(-1).item()
prob = logit.softmax(-1)[predict].item()
plt.title(f'JPEG adversarial: dog2.png\n{classes[predict]}: {prob:.2%}')
plt.axis('off')


plt.imshow(compressed_x)
plt.tight_layout()
plt.show()

防御は成功した

拡張子:ファイル読み込み

元のコードの手書きデータセット関数は研究する価値があります。まず、ルート フォルダー内のすべてのファイルを読み取り、並べ替えて、リスト変数を返します。

>>dir_list =sorted(glob.glob(f'{root}/*'))
>>print(dir_list)

['./data\\airplane'、'./data\\automobile'、'./data\\bird'、'./data\\cat'、'./data\\deer'、'./data \\犬'、'./data\\frog'、'./data\\horse'、'./data\\ship'、'./data\\truck']

リスト変数の最初のフォルダーを読み取り、最初のファイル名を取り出します。これらのファイル名は Image.open 関数で使用できます。

>>images =sorted(glob.glob(f'{dir_list[0]}/*'))
>>print(images[0])

./data\飛行機\飛行機1.png

相対パスを削除する

>>print(os.path.relpath(images[0], root))

飛行機\飛行機1.png

おすすめ

転載: blog.csdn.net/iwill323/article/details/128031965