はじめに
前回に引き続き、PyTorch 公式チュートリアル の第10弾です。
今回は Adversarial Example Generation を進めます。
Adversarial Example Generation
このチュートリアルでは機械学習のセキュリティを紹介します。
Adversarial Examples(敵対的サンプル)は、モデルを誤認識させるノイズを生成し、入力データに含めることです。
Fast Gradient Sign Attack(FGSM)を使用して画像を加工し、MNIST分類器(手書き画像分類器)を誤分類させる様子を見ていきましょう。
Threat Model
敵対的攻撃には多くのカテゴリがありますが、多くは入力データに最小限のノイズを追加し、モデルに誤分類させる点で共通しています。大別すると、ホワイトボックスとブラックボックスに分けられます。
ホワイトボックス攻撃は、攻撃者が入力、出力、重みやアーキテクチャなどモデルに関する情報を把握している前提の攻撃です。 ブラックボックス攻撃は、攻撃者がモデルの入力と出力のみアクセスでき、モデルに関する情報は知らない場合の攻撃です。 誤分類の種類として、誤ったクラスに分類させる「通常の誤分類」(misclassification)の他、特定の入力のみ誤分類させる「source/target misclassification」という種類もあります。
今回取り上げる Fast Gradient Sign Attack(FGSM)は「通常の誤分類」(misclassification)を目的にしたホワイトボックス攻撃です。
FGSM を例に攻撃の方法を詳しく見ていきましょう。
Fast Gradient Sign Attack
有名な敵対的攻撃の1つに、イアン・グッドフェロー氏らの Explaining and Harnessing Adversarial Examples(敵対的な例の説明と活用) に記述されている、Fast Gradient Sign Attack(FGSM)があります。攻撃は非常に強力ですが、難しくはありません。ニューラルネットワークの勾配を利用して攻撃するように設計されています。
アイデアは単純です。通常は逆伝播された勾配をもとに損失を最小化しますが、入力データを調整して(ノイズを入れて)、逆伝播された勾配に基づいて損失を最大化します。
コードに入る前に、FGSM のパンダの例を見て、確認します。
図から、$x$ は「パンダ」として正しく分類された元の入力画像、$y$ は $x$ の正解ラベル、$\mathbf{\theta}$ はモデルパラメータ、 $J(\mathbf{\theta}, \mathbf{x}, y)$ はネットワークのトレーニングに使用される損失です。攻撃はまず、勾配を入力データに逆伝播して $\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$ を計算します。次に、損失を最大化する方向(つまり、 $sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))$ )に小さなステップ(図では $\epsilon$ : 0.007)で入力データを調整します。結果として生じる画像 $x'$ (右側の画像)は、明らかに「パンダ」であるにもかかわらず、「テナガザル」として誤って分類されます。
import os
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
Implementation(実装)
攻撃を実装していきます。
最初に入力パラメータについて説明します。
次に攻撃を受けるモデルを定義し、攻撃を実装して検証します。
Inputs(入力)
このチュートリアルの入力は3つだけです。次のように定義します。:
-
epsilons - 実行に使用するイプシロン値のリスト。イプシロン値はノイズに掛ける係数です。攻撃しない(ノイズがない)場合の精度を確認するため、リストに0を保持することが重要です。
また、一般的には、イプシロンが大きいほど元画像へのノイズが目立ちますが、
モデルの精度を低下させるという点で、より効果が高くなります。
イプシロン値のデータ範囲は[0,1]であるため、1を超えてはなりません。 -
pretrained_model - pytorch/examples/mnist で事前トレーニングしたMNISTモデルへのパス。
事前にトレーニングされたモデルをここからダウンロードしてください。 -
use_cuda - 必要に応じてCUDAを使用するフラグ。
このチュートリアルは処理に時間がかからないため、CUDAを備えたGPUは
重要ではありません。
# data ディレクトリを作成します
os.makedirs('data', exist_ok=True)
%%shell
# 事前トレーニング済みモデルをダウンロードします
wget 'https://drive.google.com/uc?export=download&id=1KVOHbHnjCd1L-ookcd7CxDqb7rb8-DSx' -O './data/lenet_mnist_model.pth'
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_loader は、MNISTからコピーされています。
以下で、モデルとデータローダーを定義してから、モデルを初期化し、事前にトレーニングされた重みをロードします。
# LeNet モデル定義
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_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=False, download=True, transform=transforms.Compose([
transforms.ToTensor(),
])),
batch_size=1, shuffle=True)
# 使用しているデバイスを定義します
print("CUDA Available: ",torch.cuda.is_available())
device = torch.device("cuda" if (use_cuda and torch.cuda.is_available()) else "cpu")
# ネットワークを初期化します
model = Net().to(device)
# 事前トレーニング済みモデルをロードします
model.load_state_dict(torch.load(pretrained_model, map_location='cpu'))
# モデルを評価モードに設定します。この場合、これはドロップアウトレイヤー用です
model.eval()
CUDA Available: False
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 Attack
次に Adversarial Examples(敵対的サンプル)を作成する関数を定義します。
fgsm_attack 関数は3つの入力を受け取ります。
imageは元のクリーンな画像($x$)、
イプシロン ( $\epsilon$ )はピクセル単位の摂動(ノイズ)量、
data_grad は 入力画像に対する損失の勾配($\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$) です。
摂動画像(ノイズを含めた画像)は次の式で作成されます。
$
\begin{align}
perturbed_image = image + epsilon*sign(data_grad) = x + \epsilon * sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))
\end{align}
$
最後に、データの元の範囲を維持するため torch.clamp で範囲を [0,1] にクリッピングします。
# FGSM 攻撃コード
def fgsm_attack(image, epsilon, data_grad):
# データ勾配の要素ごとの符号を収集します
sign_data_grad = data_grad.sign()
# 入力画像の各ピクセルを調整して、摂動画像を作成します
perturbed_image = image + epsilon*sign_data_grad
# [0,1]の範囲を維持するためのクリッピングの追加
# 0 より小さい場合は 0 にします、1より大きい場合は 1 にします
perturbed_image = torch.clamp(perturbed_image, 0, 1)
# 摂動画像を返却します
return perturbed_image
Testing Function
最後に、攻撃の実行と結果を確認するテスト関数を作成します。
関数を実行すると MNIST データセットをもとに、ノイズ画像の作成とモデルによる予測が実行され、結果が出力されます。
テスト関数はイプシロンも受け取り、モデルの精度とともにノイズの強さ(イプシロン値)を出力します。
具体的な処理の流れはを記述します。
テストセット内の各サンプルについて以下を実行します。
- 入力データ(data_grad)で損失の勾配を計算します。
- fgsm_attack(perturbed_data)でノイズ画像を作成します。
- ノイズ画像をモデルで予測し、誤分類かどうか確認します。
- 後で視覚化するため、誤分類になったノイズ画像を保存して返します。
Run Attack
最後は攻撃を実行するコードです。
指定されたイプシロンのリスト(0, .05, .1, .15, .2, .25, .3)に対してテスト関数を実行します。
イプシロンごとに、精度と誤分類になったいくつか(5つ)の画像を保存します。
$\epsilon=0$ の場合は、ノイズがない(攻撃していない)場合の精度になります。
accuracies = []
examples = []
# 各イプシロンのテストを実行します
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
Results
Accuracy vs Epsilon
結果を確認します。
最初に、イプシロンごとの精度をプロットします。
前に触れたように、イプシロンが大きくなるとモデルの精度が低下します。
これは、イプシロンが大きいほど損失を最大化する方向にパラメータが調整されるためです。
イプシロンと精度の関係は線形(直線)にはなっていません。例えば、 $\epsilon= 0.05$ での精度は $\epsilon= 0$ よりも約4%低くなっていますが、$\epsilon= 0.2$ での精度は$\epsilon= 0.15$ よりも25%低くなっています。
$\epsilon= 0.25$ 〜 $\epsilon= 0.3$ で全く学習できていない状態(正解率10%)に達します。(10クラス分類のため、学習できていなくても正解率は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()
Sample Adversarial Examples
イプシロンが増加するとテストの精度は低下しますが、ノイズはより簡単に認識できるようになります。
攻撃者が考慮しなければならない「精度を低下させること」と「知覚性(ノイズを分からないようにすること)」の間にはトレードオフがあります。
ここで、各イプシロン値で誤分類になった画像をプロットします。
最初の行は、ノイズのない元の「クリーンな」画像を表す $\epsilon$ = 0 の画像です。各画像のタイトルは、「正解の数字 -> 誤分類の数字」です。
ノイズは $\epsilon= 0.15$ で明らかになり始め、$\epsilon= 0.3$ で非常に明白になります。
ただし、すべての場合において、人間はノイズが追加されているにもかかわらず、正しいクラスを識別することができます。
(とありますが、人間にも判断しにくい数字もあるようにも見えます。一番右下の画像など)
Where to go next?
このチュートリアルが敵対的機械学習(adversarial machine learning)の導入になるでしょう。
機械学習のセキュリティは、まさに始まったばかりで、攻撃や防御の方法に関するアイデアがいくつも提言されています。
実際 NIPS 2017(NIPSは機械学習のカンファレンスです。)で攻撃と防御のコンペイベントもあり、そこで使用された多くの方法はこのペーパーに記載されています:Adversarial Attacks and Defences Competition。
また、攻撃の対象は画像だけではありません。 音声からテキストへ変換するモデルに対する攻撃がここに記載されています。
敵対的な機械学習をさらに学ぶための最良の方法は、自分で試してみることです。 NIPS 2017のコンペとは異なる攻撃を実装して、FGSMとの違いを確認してください。そして、自分の攻撃からモデルを守ってみてください。
終わりに
今回のチュートリアルでは、敵対的機械学習(adversarial machine learning)の導入を学びました。
次回は「DCGAN Tutorial」を進めてみたいと思います。
履歴
2020/12/20 初版公開
2020/12/27 イアン・グッドフェロー氏の記述修正
2021/02/03 次回のリンク追加