※ 본 게시물에 사용된 내용의 출처는 대다수 <펭귄브로의 3분 딥러닝-파이토치맛>에서 사용된 자료이며, 개인적인 의견과 해석이 추가된 부분도 존재합니다. 그림의 경우 교재를 따라 그리거나, 제 임의대로 추가 수정한 부분도 존재합니다. 저 역시도 공부하고자 포스팅한 게시물이니, 잘못된 부분은 댓글로 알려주시면 감사하겠습니다.
- Adversarial attack
- FGSM attack
Adversarial Attack
머신러닝 기반의 서비스가 많아지면서, 자연스럽게 보안의 중요성도 높아져가고 있다. 자율주행, 은행의 비정상거래탐지, 의료영상분석 모델 등 시스템 상 실수가 용납되지 않는 분야에서는 신뢰도를 떨어뜨리는 것 자체가 치명적이다. 자율주행차가 표지판을 못보도록, 의료진단 시스템이 병을 놓치는 등, 다양한 공격방법이 날로 증가하고 있다. 하지만 아직 딥러닝 모델은 Black box인만큼, 모델 내부를 해석하는 기술이 고도화되어 있지 않아 아직까지 효과적인 방어법이 등장하지 못했다. 이러한 이유로 각종 공격과 방어법, 더 근본적으로 모델이 데이터를 어떻게 해석하는지 파헤치는 연구가 활발히 진행 중이다.
- Adversarial example(적대적 예제) : 머신러닝 모델의 착시를 유도하는 입력
- Adversarial attack(적대적 공격) : 적대적 예제를 생성하여 여러 머신러닝 기반 시스템의 성능을 의도적으로 떨어뜨려, 보안문제를 일으키는 과정.
왼쪽의 고양이 사진에 노이즈를 추가해 마치 강아지처럼 보이게끔 하는 상단 예제처럼 이번 챕터에서는 사람에겐 비슷하게 보이지만, 컴퓨터를 속이는 예제를 위주로 다루어본다.
적대적 공격의 종류
적대적 공격은 적절한 노이즈를 생성해서 사람 눈에는 비슷해 보이나, 머신러닝을 헷갈리게 만드는 예제를 생성하는 것이 가장 큰 핵심이다. 인식 성능에는 오류를 일으키지만, 원본과 차이가 적은 노이즈를 찾는다는 것은 다른말로 최적화 문제로 해석될 수 있다. 극단적인 방법으로는 이미지에서 픽셀 하나만 수정해서 classification model의 성능을 완전히 빗나가게 할 수도 있다. 아래 예제는 검정 단어가 본래의 Class, 파란 단어가 예측기가 예측한 Class이다.
잡음 생성 방법은 분류 기준이 무엇이냐에 따라 여러 방법으로 나뉠 수 있다.
- 기울기와 같은 모델 정보가 필요한가?
- 화이트박스(whitebox): 모델 정보를 토대로 잡음 생성
- 블랙박스(blackbox): 모델 정보 없이 생성 - 원하는 정답으로 유도할 수 있는가
- 표적(targeted): 정답 유도 가능
- 비표적(non-targeted): 정답 유도 불가능 - 잡음을 생성하기 위한 반복된 학습(최적화)가 필요한가?
- 반복(iterative): 필요
- 원샷(one-shot): 불필요 - 한 잡음이 특정 입력에만 적용되는지, 혹은 모든 이미지에 적용될 수 있는 범용적인 잡음인가?
가장 강력한 공격방법은 모델 정보가 필요없고, 원하는 정답으로 유도할 수 있으며, 복잡한 학습이 불필요하고, 여러 모델에 동시에 적용할 수 있는 방법이지만, 이런 특징에는 여러 기회비용이 존재할 수밖에 없다. 본 책에서는 간단한 예제를 통해서 적대적 공격을 맛보지만, 실제로 운용 중인 딥러닝 시스템들은 모델 내부 정보를 공개하지 않고 여러 가 지 잡음을 제거하는 전처리 과정을 거치므로 이 책에서 설명하는 예제처럼 쉽게 속일 수 없다.
FGSM(fast gradient sign method) 공격
신경망의 기울기(gradient)를 이용해 적대적 샘플을 생성하는 간단하면서도 효과적인 FGSM 공격을 시도해본다.
- FGSM(fast gradient sign method): 반복된 학습 없이 잡음을 생성하는 원샷 공격.
- 입력 이미지에 대한 기울기 정보를 추출하여 노이즈 생성
- 노이즈는 눈에 보이지 않아야 하므로, 아주 작은 숫자(0.007)를 곱해 희석한 후 원본 그림에 더함.
상단 예제는 원본 이미지를 57.7% 정도의 확률로 판다로 올바르게 예측하였지만, 가운데 노이즈가 섞이면서 99퍼센트의 확률로 긴팔원숭이로 예측하게 된다. 사람의 눈에는 양쪽이 큰 차이가 없어보이지만, 실제 모델이 입력받는 데이터 형식 차에 따라서 성능이 천차만별인 것을 알 수 있다. 실제로 최적화를 반복해서 저 노이즈보다 더 정교한 노이즈를 만들수도 있다. FSGM은 공격 목표를 정할 수 없는 non-targeted 방식이자, 대상 모델의 정보가 필요한 화이트박스 방식이다.
조금 더 디테일한 공격 과정은 아래 코드를 보면서 직접적으로 다뤄보자.
- 예제 코드
본격적인 시작을 위해 필요한 라이브러리를 임포트한다.
본인은 관련 데이터 파일을 구글 드라이브로부터 받아올 것이므로 "from google.colab import drive" 문장을 추가했다.
import torch
import torch.nn.functional as F
import torchvision.models as models
import torchvision.transforms as transforms
from PIL import Image
import json
import matplotlib.pyplot as plt#시각화를 위한 package
#구글 드라이브에서 로딩하기 위해서 추가
from google.colab import drive
drive.mount('/gdrive',force_remount=True)
여기서는 torchvision에서 미리 학습된 모델을 가져다가 테스트에 사용할 것이다. Imagenet test데이터셋으로 시험해보니 top-1 error는 22.63, top-5 error 는 6.44정도로 성능이 나쁘지 않은 ResNet101모델을 가져다가 테스트를 진행해본다. 모델 로딩과정에는 사용할 모델을 갖고오는 models.<모델명>에 인수로 pretrained=True를 명시해서 선행학습된 모델을 가져오도록 한다.
model = models.resnet101(pretrained=True)# Model로 ResNet-101버전 로딩
model.eval()
print(model)#모델 구조 프린트
실제 모델 구조를 프린트해보면 아래와 같다(접힘)
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer2): Sequential(
(0): Bottleneck(
(conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer3): Sequential(
(0): Bottleneck(
(conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(4): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(5): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(6): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(7): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(8): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(9): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(10): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(11): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(12): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(13): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(14): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(15): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(16): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(17): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(18): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(19): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(20): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(21): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(22): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer4): Sequential(
(0): Bottleneck(
(conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=2048, out_features=1000, bias=True)
)
본 모델은 예측값 클래스를 숫자(0번부터 1000번까지)로 표현한다. 보기 힘드니 그대로 사용하지 않고, 사람이 확인하기 쉽게끔 예측 클래스를 출력해줄 수 있도록 json파일을 로딩해 각 클래스별 id값을 맵핑해주는 idx2class 리스트를 만들어주자.(ex. 0 : tench, 1: goldfish, 2 : great white shark ..... )
CLASSES = json.load(open('/gdrive/My Drive/Colab Notebooks/3-min-pytorch-master/08-딥러닝을_해킹하는_적대적_공격/imagenet_samples/imagenet_classes.json'))
#학습을 위한 데이터셋(클래스 Json)파일 로딩
idx2class = [CLASSES[str(i)] for i in range(1000)]
#Class 별 ID 셋팅
이제 모델이 준비됐으므로, 공경하고자 하는 이미지를 불러오도록 한다. 실제 공격 데이터 자체가 학습 데이터에 없는 이미지여야 하므로 아래와같은 웰시코기 이미지를 준비했다. 실제로 ImageNet 데이터셋에는 펨브로크 웰시코기라는 클래스가 존재한다.
이어서, 모델의 입력으로 줄 수 있게끔 텐서 형태로 변환해준다. 우선, 인터넷에서 찾는 이미지들은 크기가 다양하기 때문에 torchvision의 transform함수를 이용해서 ImageNet데이터셋과 동일한 크기인 244x244크기로 변경해준다. unsqueeze함수를 사용해서 3x244x244형태의 이미지를 1x3x244x244형태의 배치로 바꿔주도록 한다.
# 이미지 불러오기
img = Image.open('/gdrive/My Drive/Colab Notebooks/3-min-pytorch-master/08-딥러닝을_해킹하는_적대적_공격/imagenet_samples/corgie.jpg')
# 이미지를 텐서로 변환하기
img_transforms = transforms.Compose([
transforms.Resize((224, 224), Image.BICUBIC),
transforms.ToTensor(),
])
img_tensor = img_transforms(img)
img_tensor = img_tensor.unsqueeze(0)
print("이미지 텐서 모양:", img_tensor.size())
#이미지 텐서 모양: torch.Size([1, 3, 224, 224])
모델의 입력으로 들어갈 tensor형태의 이미지를 준비 완료했다면, 사람이 볼 수 있게끔 시각화 해보자.
우선, 원본 이미지 텐서 시각화를 위해 다시 squeeze함수로 차원을 줄이고 detach 함수를 통해서 원본 이미지 텐서와의 연결을 끊어주도록 한다.
+) detach 함수 설명
transpose함수로 뒤집힌 이미지를 제자리로 돌리고 matplotlib와 호환하는 numpy행렬로 변경해준다. 마지막으로 plt.imshow()함수를 사용하면 아래 이미지와 같은 사진이 등장한다.
# 시각화를 위해 넘파이 행렬 변환
original_img_view = img_tensor.squeeze(0).detach() # [1, 3, 244, 244] -> [3, 244, 244]
original_img_view = original_img_view.transpose(0,2).transpose(0,1).numpy()
# 텐서 시각화
plt.imshow(original_img_view)
이제 모든 데이터 준비가 끝났으니 공격을 진행하기 전 모델의 성능을 확인해보자.
output = model(img_tensor)
prediction = output.max(1, keepdim=False)[1]
#가장 확률이 높은 예측 클래스(prediction)
prediction_idx = prediction.item()
prediction_name = idx2class[prediction_idx]
print("예측된 레이블 번호:", prediction_idx)
print("레이블 이름:", prediction_name)
다음과 같이 상단 이미지를 넣어줬을 때 웰시코기를 올바르게 예측하는 것을 확인할 수 있다.
이제 FGSM 함수를 정의해서 적대적 예제를 생성해보자.
FGSM 공격의 핵심은 모델에서 입력 이미지에 대한 기울기 정보를 추출하고, 이를 왜곡하여 원본 이미지에 더하는 과정이 요구된다. 기울기는 모델이 학습할 때 각 픽셀이 미치는 영향이라 할 수 있다.
def fgsm_attack(image, epsilon, gradient):
# 기울기값의 원소의 sign 값을 구함
sign_gradient = gradient.sign()
# 이미지 각 픽셀의 값을 sign_gradient 방향으로 epsilon 만큼 조절
perturbed_image = image + epsilon * sign_gradient
# [0,1] 범위를 벗어나는 값을 조절
perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
모델을 헷갈리게 하려면 모델의 오찻값을 극대화 해야 한다. 딥러닝 모델 학습 과정에는 기울기의 정 반대편으로 가중치를 조절하며 오차를 줄여나가도록 한다. FGSM 공격에서는 이와 반대로 노이즈가 기울기의 방향으로 최적화하도록 하여 오차를 키워준다.
따라서, 기울기 방향성을 알 수 있도록 sign함수를 적용한다.
+) sign() : 입력(input data)가 0보다 작으면 -1 / 0이면 0 / 0보다 크면 1
이후, epsilon(매우 작은 값)을 곱해준다. epsilon은 모델 학습 시 지정해줘야 하는 일종의 학습률 개념으로 노이즈가 너무 커지지 않고, 사람 눈에 보이지 않게 제한하는 역할이다.
결과적으로 이미지의 기울기 방향이 양수인 곳에 epsilon만큼 값을 증가시키고, 반대로 음수인 곳은 epsilon만큼 값을 감소시킨다.
이제 필요한 함수를 다 명시해주었으므로 본격적인 forward 함수를 구현해보자. 원본 이미지에 대한 기울기를 추출하기 위해 requires_grad_(True)함수를 호출해서 이미지에 대한 기울기를 보존할 수 있도록 명시해준다.
이후, 평범하게 학습하듯이 모델의 입력으로 이미지를 주고 기울기 값을 추출한 후 fgsm 공격을 통해 변경된 데이터를 다시 모델에 넣어보자.
# 이미지의 기울기값을 구하도록 설정
img_tensor.requires_grad_(True)
# 이미지를 모델에 통과시킴
output = model(img_tensor)
# 오차값 구하기 (레이블 263은 웰시코기)
loss = F.nll_loss(output, torch.tensor([263]))
# 기울기값 구하기
model.zero_grad()
loss.backward()
#미분값을 저장하여, gradient 값 추출
# 이미지의 기울기값을 추출
gradient = img_tensor.grad.data
# FGSM 공격으로 적대적 예제 생성
epsilon = 0.03
perturbed_data = fgsm_attack(img_tensor, epsilon, gradient)
# 생성된 적대적 예제를 모델에 통과시킴
output = model(perturbed_data)
변경된 입력 데이터(perturbed_data)를 다시 아까처럼 성능 확인해보자.
perturbed_prediction = output.max(1, keepdim=True)[1]
perturbed_prediction_idx = perturbed_prediction.item()
perturbed_prediction_name = idx2class[perturbed_prediction_idx]
print("예측된 레이블 번호:", perturbed_prediction_idx)
print("레이블 이름:", perturbed_prediction_name)
놀랍게도 예측결과가 다르게 나온다. 아래 예제가 휘핏이라는 종인데 웰시코기와는 큰 차이가 있는 것을 알 수 있다.
이제 이처럼 딥러닝 모델을 헷갈리게 한 적대적 예제가 어떻게 생겼나 시각화 해보자.
# 시각화를 위해 넘파이 행렬 변환
perturbed_data_view = perturbed_data.squeeze(0).detach()
perturbed_data_view = perturbed_data_view.transpose(0,2).transpose(0,1).numpy()
plt.imshow(perturbed_data_view)
채도가 조금 짙고, 얼국에 미세한 얼룩이 좀 생겼을 뿐 큰 차이는 없어보인다. 나란히 나열해서 시각화 해보자.
f, a = plt.subplots(1, 2, figsize=(10, 10))
# 원본
a[0].set_title(prediction_name)
a[0].imshow(original_img_view)
# 적대적 예제
a[1].set_title(perturbed_prediction_name)
a[1].imshow(perturbed_data_view)
plt.show()
왼쪽이 원본이고 오른쪽이 적대적 예시인데 민감한 사람이 아닌이상 큰 차이를 모를 수도 있겠다.
(혓바닥이 조금 짙어진 정도..? 눈 밑에 검은색이 약간 더 짙어졌나..?)
이처럼 딥러닝 성능이 엄청나 보이지만 일부 epsilon을 추가한것만으로 큰 영향을 미칠 수 있다. 적대적 예제를 생성하는 방법에 대한 연구는 많이 진행되고 있고, 이를 방어하는 연구도 활발히 진행되고 있다.
'머신러닝 > Pytorch 딥러닝 기초' 카테고리의 다른 글
[Pytorch-기초강의] 9. 주어진 환경과 상호작용하며 성장하는 DQN (2) | 2021.06.18 |
---|---|
[Pytorch-기초강의] 8. 경쟁하며 학습하는 GAN (3) | 2021.06.13 |
[Pytorch-기초강의] 6. 순차적인 데이터를 처리하는 RNN(LSTM, GRU) (0) | 2021.05.25 |
[Pytorch-기초강의] 6. 순차적인 데이터를 처리하는 RNN (0) | 2021.05.25 |
[Pytorch-기초강의] 5. 사람의 지도 없이 학습하는 오토인코더(AutoEncoder) (0) | 2021.05.21 |