https://adversarial-ml-tutorial.org/introduction/ [NeurIPS 2018 tutorial, “Adversarial Robustness: Theory and Practice”]를 기반으로 한 Article입니다.

서론


실제 현실에 인공지능(Artificial Intelligence) 모델을 활용할 때에는 ‘대체로’ 잘 작동하는 것은 최소 조건이 되며, 나아가 ‘항상’ 우리가 신뢰할 수 있는지가 중요해집니다. 현재 인공지능 분야에서는 신뢰성에 관련된 많은 개념이 존재하지만, 그 중에서도 최근 몇 년간 큰 관심을 모은 주제는 적대적 강건성(Adversarial Robustness)입니다.

적대적 강건성(Adversarial Robustness)이란 “악의적인 공격자가 입력에 단순 노이즈(Noise)와 같은 다양한 섭동(Perturbation)을 주었을 때에도 모델이 잘 작동할 수 있는가?”를 의미합니다. 물론 이러한 정의는 강건성이란 학문 내에서 굉장히 작은 의미의 개념일 수 있지만, 현재 딥러닝(Deep Learning)을 포함한 인공지능 기술이 직면하고 있는 많은 결점들 중 가장 큰 결점이기도 합니다.

본 칼럼에서는 딥러닝에서의 적대적 강건성에 대한 광범위한 소개를 제공하고자 합니다. 기본적인 수학과 간단한 코드를 통해 적대적 강건성의 개념을 알아보고, 강건성 개념의 중요성을 알아보고자 합니다.

자세한 코드는 https://adversarial-ml-tutorial.org/introduction/introduction.tar.gz에서 확인 가능합니다.

딥러닝 모델의 분류 과정


딥러닝의 가장 큰 장점 중 하나는, 실제 데이터에서 결과를 보는 것이 굉장히 쉽다는 것입니다. 이를 활용해서, 첫 번째 적대적 예제를 만들어 보겠습니다. PyTorch 내의 사전 훈련된 ResNet50 모델을 사용하여 아래의 돼지 사진을 분류해 보겠습니다.

'Show Pig' licensed under CC BY 2.0

대다수의 PyTorch 이미지 분류 모델들은 우선적으로 (대략 평균이 0이고 분산이 1이 되도록) torchvision.transforms 모듈을 사용하여 이미지를 정규화(Normalization)합니다. 그러나 본 예제에서는 먼저, 이미지를 로드하고 224x224로 크기만 조절 후 나중에 변환하도록 하겠습니다.

from PIL import Image
from torchvision import transforms

# read the image, resize to 224 and convert to PyTorch Tensor
pig_img = Image.open("pig.jpg")
preprocess = transforms.Compose([
   transforms.Resize(224),
   transforms.ToTensor(),
])
pig_tensor = preprocess(pig_img)[None,:,:,:]

# plot image (note that numpy using HWC whereas Pytorch user CHW, so we need to convert)
plt.imshow(pig_tensor[0].numpy().transpose(1,2,0))
'Show Pig' licensed under CC BY 2.0

이제 크기가 조절된 이미지에 정규화 과정을 거친 후, 학습된 ResNet50 모델울 통해 돼지 사진을 분류해보겠습니다.

import torch
import torch.nn as nn
from torchvision.models import resnet50

# simple Module to normalize an image
class Normalize(nn.Module):
    def __init__(self, mean, std):
        super(Normalize, self).__init__()
        self.mean = torch.Tensor(mean)
        self.std = torch.Tensor(std)
    def forward(self, x):
        return (x - self.mean.type_as(x)[None,:,None,None]) / self.std.type_as(x)[None,:,None,None]

# values are standard normalization for ImageNet images, 
# from https://github.com/pytorch/examples/blob/master/imagenet/main.py
norm = Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

# load pre-trained ResNet50, and put into evaluation mode (necessary to e.g. turn off batchnorm)
model = resnet50(pretrained=True)
model.eval()

# interpret the prediction
pred = model(norm(pig_tensor))

import json
with open("imagenet_class_index.json") as f:
    imagenet_classes = {int(i):x[1] for i,x in json.load(f).items()}
print(imagenet_classes[pred.max(dim=1)[1].item()])
hog

“hog”는 돼지를 의미하는 단어로 정답입니다. 즉, 위의 간단한 코드만으로도 딥러닝 모델을 활용해 임의의 사진을 분류할 수 있게 됩니다.

적대적 예제의 생성


그렇다면, 악의적인 사용자는 어떻게 이 이미지를 돼지(hog)가 아닌 다른 것으로 분류되게 할 수 있을까요? 이를 이해하기 위해서는 우리가 딥러닝 모델을 보통 어떻게 학습 시키는지 살펴보아야 합니다.

우선 딥러닝 모델 \(h\)는 여러 개의 매개변수(parameter) \(\theta\)를 학습하여 얻어집니다. 이 때, 우리가 하고자 하는 것은 특정 이미지 \(x\)에 대한 결과값인 \(h_\theta(x)\)와 정답인 \(y\)을 똑같이 만들고자 하는 것입니다. 따라서, \(h_\theta(x)\)와 \(y\) 사이의 차이를 정의하는 손실함수(loss function) \(\ell\)에 대해, 딥러닝 모델의 학습 목표는 아래와 같이 정의될 수 있습니다.

\begin{equation} \label{eq:min} \min_\theta \ell(h_\theta(x), y) \end{equation}

위의 식 \eqref{eq:min}이 의미하는 바는 “특정 이미지 \(x\)에 대한 결과값인 \(h_\theta(x)\)와 정답인 \(y\)이 주어졌을 때, \(\ell(h_\theta(x), y)\)를 최소화하는 매개변수 \(\theta\)를 찾는 것 입니다.” 거의 모든 딥러닝 모델은 위와 같이 학습되며, 적당한 값 미만의 손실함수 \(\ell(h_\theta(x), y)\) 값을 가지게 되면, 정답인 \(y\)를 맞출 수 있게 됩니다.

그렇다면, 이미 작은 손실함수 \(\ell(h_\theta(x), y)\) 값을 가지도록 학습된 모델 \(h_\theta\)을 “악의적”으로 이를 방해하기 위해서는 어떻게 해야할까요? 정답은, 최소화했던 손실함수를 역으로 최대화하는 것입니다.

\begin{equation} \label{eq:max} \max_{\hat{x}} \ell(h_\theta(\hat{x}), y) \end{equation}

\eqref{eq:max}가 하고자 하는 것은 \(\ell(h_\theta(\hat{x}), y)\)을 최대화하는 \(\hat{x}\)를 찾는 것입니다. 즉, 정답인 \(y\)와 거리가 먼 예측값을 내도록하는 이미지인 \(\hat{x}\)를 찾는 것입니다. 이를 우리는 적대적 예제(adversarial example)라고 합니다. 그러나 \(\hat{x}\)가 실제로 정답인 \(y\) 관련이 없다면, 예를 들어 돼지 이미지가 아닌 강아지 이미지라면, ‘공격’이라고 부르기 어렵습니다. 따라서, 악의적인 사용자는 사람이 보기에는 돼지이지만, 딥러닝 모델은 돼지가 아니라고 하는 예제를 만들어내야 합니다. 따라서 \(\hat{x}=x+\delta\)로 표기하고, \(\delta\)의 크기에 제한을 두게 됩니다.

\begin{equation} \label{eq:max2} \max_{\delta\in\Delta} \ell(h_\theta(x+\delta), y) \end{equation}

여기서 \(\Delta=\{\delta:\|\delta\|_\infty \leq \epsilon\}\)로 각각의 \(\delta\)의 크기가 미리 설정한 $\epsilon$보다 작은 범위를 의미합니다. 일반적으로는, $\epsilon=8/255$정도로 설정하게 됩니다. 본 조건 하에 구해진 \(\delta\)를 적대적 섭동(adversarial perturbation) 혹은 적대적 노이즈(adversarial noise)라고 부르게 됩니다.

이제 \eqref{eq:max}를 실제로 구현해보도록 하겠습니다. 우리가 찾고자 하는 것은 \(\delta\)로 바뀌었기 때문에, SGD optimizer에 \(\delta\)를 입력으로 하여 최적의 \(\delta\)를 찾고자 합니다.

import torch.optim as optim
epsilon = 2./255

delta = torch.zeros_like(pig_tensor, requires_grad=True)
opt = optim.SGD([delta], lr=1e-1)

for t in range(30):
    pred = model(norm(pig_tensor + delta))   # 섭동(노이즈) 추가 후 예측
    loss = -nn.CrossEntropyLoss()(pred, torch.LongTensor([341]))  # 손실값 계산
    if t % 5 == 0:
        print(t, loss.item())
    
    opt.zero_grad()
    loss.backward()  # 손실함수 최대화 (9번의 손실값에 -가 곱해졌으므로)
    opt.step()
    delta.data.clamp_(-epsilon, epsilon)  # 크기 제한
    
print("True class probability:", nn.Softmax(dim=1)(pred)[0,341].item())
0 -0.0038814544677734375
5 -0.00693511962890625
10 -0.015821456909179688
15 -0.08086681365966797
20 -12.229072570800781
25 -14.300384521484375
True class probability: 1.4027455108589493e-06

위의 코드를 실행하면, 아래와 같은 적대적 예제 \(x+\delta\)가 구해지게 됩니다.

'Show Pig' licensed under CC BY 2.0

본 돼지 이미지는 사람의 눈에는 틀림없이 돼지이지만, 모델의 눈에는 아래와 같이

Predicted class:  wombat
Predicted probability: 0.9997960925102234

99%의 확률로 웜뱃(wombat)이라는 다른 동물로 분류됩니다.

실제로 적대적 섭동 \(\delta\)를 출력해보면, 아래와 같은 결과를 얻을 수 있습니다.

즉, 위의 섭동(혹은 노이즈)를 원래의 돼지 이미지에 더하면, 모델이 제대로 작동하지 않게 된다는 것입니다. 이를 더 응용하면, 모델이 우리가 원하는 답을 내놓도록하는 섭동을 구할 수도 있게 됩니다.

delta = torch.zeros_like(pig_tensor, requires_grad=True)
opt = optim.SGD([delta], lr=5e-3)

for t in range(100):
    pred = model(norm(pig_tensor + delta))
    loss = (-nn.CrossEntropyLoss()(pred, torch.LongTensor([341])) + 
            nn.CrossEntropyLoss()(pred, torch.LongTensor([404])))
    if t % 10 == 0:
        print(t, loss.item())
    
    opt.zero_grad()
    loss.backward()
    opt.step()
    delta.data.clamp_(-epsilon, epsilon)
0 24.00604820251465
10 -0.1628284454345703
20 -8.026773452758789
30 -15.677117347717285
40 -20.60370635986328
50 -24.99606704711914
60 -31.009849548339844
70 -34.80946350097656
80 -37.928680419921875
90 -40.32395553588867
max_class = pred.max(dim=1)[1].item()
print("Predicted class: ", imagenet_classes[max_class])
print("Predicted probability:", nn.Softmax(dim=1)(pred)[0,max_class].item())
Predicted class:  airliner
Predicted probability: 0.9679961204528809
여객기로 분류되도록 만들어진 적대적 예제와 적대적 섭동

적대적 강건성의 역사


적대적 예제의 존재는 2003년에 발견되었고, 그 이후로 굉장히 많은 연구자에 의해 연구가 되어왔습니다. 현재는, 모델이 가질 수 있는 적대적 예제 그리고 이와 관련된 현상을 연구하는 분야를 적대적 강건성(Adversarial Robustness)라고 부르고 있습니다.