※ 본 내용은 stanford에서 제공하는 cs231n 강의, 강의자료를 바탕으로 작성하였습니다.

 

Lecture 16에서는 lecture 13에서 간단하게 다뤘던 adversarial examples에 대해 다루고 있다.

 

Noise로 보이는 필터를 중심으로 왼쪽과 오른쪽의 판다 이미지는 사람이 보기엔 둘 다 판다 이미지이고, filter를 더한 것조차 알아보기 어렵다. 

그러나 머신러닝 모델은 좌측의 이미지를 60% 확률로 판다로 예측하고, filter가 더해진 오른쪽의 판다 이미지는 99% 확률로 긴팔 원숭이로 예측한다. 

 

이러한 이미지들을 Adversarial Examples라고 하고, 이러한 이미지들은 model을 속일 수 있다. 

 

그리고 이러한 adversarial examples는 매우 쉽게 생성할 수 있다.

 

먼저 이러한 문제는 왜 발생할까?

 

- From overfitting or from underfitting?

처음엔 연구자들은 이러한 현상이 overfitting에 의한 것이라고 생각했다. 

Model의 classification boundary가 너무 복잡하고 정교하다 보니 잘못된 boundary가 존재하고, 해당 영역으로 인해 misclassification이 발생한다고 보았다. 

 

그러나 이는 잘못된 생각이었다. 

 

우선, 하나의 adversarial example에 의해 여러 모델이 잘못 동작했고, 심지어 서로 같은 클래스로 misclassification을 수행했다.  따라서 학습 과정에서 model별 boundary가 복잡하기 때문에 생긴 random 오류라고 볼 수 없었다.

 

즉, 시스템 자체의 문제인 것이었다.

Underfitting

이에 연구자들은 이러한 문제가 오히려 지나치게 linear한 classifier, underfitting에 의한 것이 아닐까 생각하게 되었다.

실제로 deep nets는 부분적인 linear 형태를 지닌다고 밝혀졌다. 

 

Input과 output 사이의 mapping이 piecewise linear한 형태인 것이지, 모델의 parameters와 output은 복잡한 non-linear 형태이다.

따라서 adversarial examples를 생성하는 것은 간단하다.

 

 이미지는 거의 보이지 않지만 plot을 중점적으로 보면, automobile class의 image에 어떠한 direction vector* $ \epsilon$ 값을 더해주며 각 class에 대한 logits를 plot한 결과이다. 

여기에서 더해진 direction이 frog class로의 direction이며, logits가 linear하게 변경되는 것을 확인할 수 있다. 

 

FGSM

이러한 direction을 찾는 것은 어렵지 않다. 미분값의 sign을 찾고, gradient ascent algorithm을 이용하면 된다.

 

CIFAR 10 dataset에 FGSM을 이용해 방향을 찾고, $ \epsilon$ * direction을 더해가며 실험해본 결과이다.

(y축은 direction에 orthogonal한 vector)

 

각 grid는 서로 다른 test data를 의미하고, 흰색은 맞게 classification한 경우, 색칠된 영역은 다른 class로 prediction한 경우이다.

FGSM을 찾은 방향으로 이동하면 linear한 misclassification boundary가 발생하는 것을 확인할 수 있다.

반면 orthogonal한 방향으로는 발생하지 않는다. 

 

- Problem, Attack

당연히 모델을 속이는 이러한 adversarial examples는 문제가 된다.

 

대표적으로 RL agent를 생각해보면, 주어진 영상을 보고 행동을 결정하는데 주어진 영상이 adversarial examples라면 잘못된 행동을 결정하게 되는 것이다. 

 

그러나 그러한 경우를 막기위해 model 자체의 parameters를 숨기더라도, 해당 모델을 공격하는 adversarial examples를 생성하는 것은 어렵지 않다.

 

해당 모델에 대한 미분을 수행할 수 없더라도(FGSM 적용 불가), 그 모델이 어떻게 classification을 수행하는지만 mimick 하도록 학습한 모델(FGSM 적용 가능)을 이용해 찾은 adversarial examples는 traget model도 속일 확률이 매우 크다. 

 

특히 여기에 ensembles 알고리즘까지 적용하여 여러 모델을 한 번에 mimicking하면 그 확률은 매우 커진다. 

 

- Defense

먼저 아쉽게도 대부분의 defense는 성공하지 않았다고 한다.

(※ 17년도 강의이므로 현재는 더 다양한 defense 기법들이 나왔을 것으로 생각됩니다.)

 

Generative Modeling

데이터의 분포를 학습하는 generative modeling도 하나의 방법이 될 수 있다. 이미지가 정상적인 분포를 갖는지 아닌지 검사하는 것이다.

 

그러나 단순히 이미지에 대한 분포보다는 class를 먼저 예측하고, 해당 class에 대한 inputs의 분포가 더 중요한 것으로 나타나서, 단순한 generative modeling으로는 충분히 방어가 되지 않는다고 한다.

 

매우 정교하게 설계된 generative modeling이 어느 정도 효과가 있을 것이라고 발표자는 예측하고 있다.

 

애초에 adversarial examples를 통해 training을 진행하는 것이 그나마 동작하는 방법이다.

그래프를 보면 정상적으로 동작하는 것을 확인할 수 있고, regularization 효과로 인해 약간의 성능 개선도 확인할 수 있다. 

(SVM이나 linear regression 같은 linear models는 어차피 그 경계가 linear하기 때문에 adversarial training이 덜 동작한다. 따라서 deep learning이 오히려 더 secure하다고도 볼 수 있다.)

 

 

 

이번에는 ViT 논문 리뷰에 이어서 pytorch로 구현한 내용에 대해 정리해보겠습니다.

 

리뷰는 아래에서 확인하실 수 있습니다.

https://vstylestdy.tistory.com/51

 

[논문 리뷰] ViT, An Image is Worth 16x16 Words

Vision transformer 논문에 대한 간단한 리뷰를 정리해보겠습니다. Paper : https://arxiv.org/abs/2010.11929 An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale While the Transform..

vstylestdy.tistory.com

 

이번에 구현한 내용은 논문과 동일하게 vision transformer를 구현하고 학습시킨 것이 아니라,

CIFAR10 dataset을 이용하여 간단하게 처리 과정만을 구현한 코드입니다.

 

성능에 대한 실험 등은 포함되지 않고, ViT의 처리 과정을 구현하는 것에 의의를 두고 제대로 학습이 이뤄지는지에 대해서만 검사할 것입니다.

 

전체 코드는 아래 링크에서 확인하실 수 있고, Embedding, Transformer Encoder, MLP 최종적을 ViT module의 구현에 대해 집중적으로 살펴보겠습니다.

https://github.com/LimYooyeol/AI-Paper-Code/tree/main/ViT

 

- Embedding

from einops import rearrange

class Embedding(nn.Module) :
  def __init__(self, input_size = 32, input_channel = 3, hidden_size = 8*8*3, patch_size = 4) :
    super().__init__()

    self.patch_size = patch_size
    self.hidden_size = hidden_size

    self.projection = nn.Linear((patch_size**2)*input_channel, hidden_size, bias = False)

    self.cls_token = nn.Parameter(torch.zeros(hidden_size), requires_grad= True)

    num_patches = int((input_size / patch_size) ** 2 + 1)

    self.positional = nn.Parameter(torch.zeros((num_patches, hidden_size), requires_grad= True))


  def forward(self, x) :
    x = rearrange(x, 'b c (h p1) (w p2) -> b (h w) (c p1 p2)', p1 = self.patch_size, p2 = self.patch_size)
    x = self.projection(x)

    batch_size = x.shape[0]

    x = torch.concat((self.cls_token.expand(batch_size, 1, self.hidden_size), x), axis = 1)

    x = x + self.positional

    return x

먼저 CIFAR 10 dataset은 3x32x32 크기로 작기 때문에, patch size와 hidden size 또한 그에 맞춰 작게 설정했습니다. 

 

이미지를 patch로 나누는 과정이 복잡할 것이라고 예상했는데, 'einops'를 사용하면 한 줄로 간단하게 구현할 수 있었습니다. 

Einops 동작

Input으로 주어진 images의 shape가 'batch size x channel x H x W'라고 할 때 H와 W를 각각 h, w개의 patch들로 이뤄졌다고 보고, 각각의 batch 별로 h*w 개의 c*p1*p2 크기의 vector로 만들라는 내용의 코드입니다.

 

Einops 자체가 직관적이라 arange로 표현된 예시와 함께 살펴보시면 이해될 것이고,

자세한 einops의 동작에 대해서는 공식문서나 튜토리얼이 있으니 찾아보면 좋을 것 같습니다. 

 

원하던대로 image의 각 영역(patches)을 vector로 만든 것을 확인할 수 있습니다.

 

Cls token이나 positional embedding은 nn.Parameter를 통해 구현했고, 

cls token의 경우 expand와 concat을 통해, positional embedding의 경우 broadcasting을 통해 더해줍니다. 

 

- Transformer Encoder

● MSA (un-parallelized)

class SelfAttention(nn.Module) :
  def __init__(self, input_dim, D_h) :
    super().__init__()

    self.D_h = D_h

    self.q = nn.Linear(input_dim, D_h, bias = False)
    self.k = nn.Linear(input_dim, D_h, bias = False)
    self.v = nn.Linear(input_dim, D_h, bias = False)

    self.softmax = nn.Softmax(dim = 1)

  def forward(self, x):
    q = self.q(x)
    k = self.k(x)
    k_tranpose = torch.transpose(k, 1, 2)
    v = self.v(x)

    A = self.softmax(torch.matmul(q, k_tranpose) / (self.D_h ** (1/2)))

    return torch.matmul(A, v)

class MSA(nn.Module) :
  def __init__(self, hidden_dim = 192, num_heads = 6) :
    super().__init__()

    self.num_heads = num_heads

    heads = []
    for i in range(0, num_heads) :
      heads.append(SelfAttention(input_dim = hidden_dim, D_h = int(hidden_dim / num_heads)))

    self.heads = nn.ModuleList(heads)    

  def forward(self, x):
    score = []

    for i in range(0, self.num_heads) : 
      score.append(self.heads[i](x))
    
    return torch.concat(score, axis = 2)

먼저 Multi-head attention layer를 naive하게 구현한 코드입니다.

Self-attention head를 먼저 구현하고, MSA에서는 여러 개의 heads를 ModuleList로 저장하여, 하나씩 계산한 결과를 이어 붙입니다.

따라서 head 별 연산이 sequential하게 이뤄져서 비효율적입니다.

 

● MSA(Parallelized)

병렬화 된 버전의 구현은 https://github.com/FrancescoSaverioZuppichini/ViT/blob/main/transfomer.md 의 코드를 이용했습니다. 

여기에서도 einop의 rearrange와 torch.einsum을 이용하는데, 먼저 코드는 다음과 같습니다.

# https://github.com/FrancescoSaverioZuppichini/ViT/blob/main/transfomer.md

class MSA(nn.Module) :
  def __init__(self, hidden_dim = 8*8*3, num_heads = 6) :
    super().__init__()

    self.num_heads = num_heads
    self.D_h = (hidden_dim / num_heads) ** (1/2) 

    self.queries = nn.Linear(hidden_dim, hidden_dim)
    self.keys = nn.Linear(hidden_dim, hidden_dim)
    self.values = nn.Linear(hidden_dim, hidden_dim)

    self.softmax = nn.Softmax(dim = 1)


  def forward(self, x) :
    q = rearrange(self.queries(x), 'b n (h d) -> b h n d', h = self.num_heads)
    k = rearrange(self.queries(x), 'b n (h d) -> b h n d', h = self.num_heads)
    v = rearrange(self.queries(x), 'b n (h d) -> b h n d', h = self.num_heads)

    A = torch.einsum('bhqd, bhkd -> bhqk', q, k)
    A = self.softmax(A / self.D_h)

    Ax = torch.einsum('bhan, bhnd -> bhad' ,A, v) # b : batch size, h : num_heads, n : num_patches, a : num_patches, d : D_h
    return rearrange(Ax, 'b h n d -> b n (h d)')

먼저 query, key, value weights를 nn.Linear layer로 선언하는데, 이번엔 모든 heads의 weights를 하나의 layer로 처리하기 때문에 output의 크기로 D(hidden dim)가 유지됩니다. 

즉, 각 헤드별로 Nx$ D_h$, (N x D) x (D x $  D_h$)의 결과를 생성하는 것을 한번에 N x D로 처리합니다.

 

따라서 forward weight를 거친 결과는 다음과 같게 됩니다.(query 기준, h : # of heads)

따라서 먼저 einops의 rearrange를 통해 shape를 변형해줍니다.
'b n (h d) -> b h n d'로 변환되는 과정은 아래와 같습니다. 각 head별 output으로 분리해줍니다.
(batch size = 2, num patches = 4, hidden_dim = 2, num_heads = 3인 경우의 예시입니다.)

rearrange

 

이제 einsum을 이용해서 분리된 shape에 맞게 $ QK^T$를 계산해줍니다.
 
 
('bhqd, bhkd -> bhqk', q, k)를 수행하게 되면 위와 같이 batch나 head구조는 유지되면서 각 head별로 $QK^T$가 계산됩니다. 
 
사실 einsum을 이번에 처음 접해봐서 정확한 동작은 모르겠지만, ('bhqd, bhkd -> bhqk', q, k)를 수행하게 되면 위와 같이 shape에 맞춰서 k의 값들이 자동으로 transpose된 후 곱해지는 것으로 보입니다.
(Transpose가 이뤄진 후 곱해지는 것은 확실하지 않지만 우선 결과는 그렇게 생각했을 때와 동일합니다.)
 
그 결과 Batch x # heads x N x N shape의 결과가 나오는 것을 확인할 수 있고, 여기에서 다시 einsum을 이용해서 각 batch, head 별로 $ Av$를 계산해줍니다.(A = softmax($ \frac{QK^T}{\sqrt{D_h}}$))
 
마찬가지로 batch나 head 별 구조는 유지하면서 NxN 크기의 attention weight와 Nx$D_h$ 크기의 value matrix를 곱해주면 되므로 'bhan, bhnd -> bhad'와 같이 변경되도록 작성해줍니다.
(attention weight를 bhnn이 아니라 bhan으로 표현한 이유는 nn과 같이 하나의 문자로 두개의 차원을 표현하면 안되기 때문입니다.)
 
 
그 결과 Batch x # heads x N x $D_h$ 크기의 결과가 나오므로, 마지막으로 다시 rearrange를 사용해서 
Batch x N x D(= # heads * $D_h$)의 shape으로 변경시켜줍니다.
rearrange(Ax, 'b h n d -> b n (h d)')와 같이 변경시켜주면 됩니다.
※ 수정) 마지막 결과를 D 차원으로 projection하는 과정이 누락되어있습니다.
 

● MLP

 

class MLP(nn.Module):
  def __init__(self, input_dim = 8*8*3, hidden_dim = 8*8*3*4, output_dim = 8*8*3):
    super().__init__()

    self.feedforward = nn.Sequential(
        nn.Linear(input_dim, hidden_dim),
        nn.GELU(),
        nn.Linear(hidden_dim, output_dim),
        nn.GELU()
    )

  def forward(self, x) :
    return self.feedforward(x)

MLP는 2 layer net이라고 보면 됩니다. Input과 output의 크기는 같고, 중간에 숨겨진 hidden dim은 임의로 input의 4배 정도로 설정해주었습니다.

(여기에서만 hidden dim은 hidden layer의 차원을 의미하고 그 외에는 D, encoder 전체에서 흐르는 vector의 dimension을 의미합니다.)

 

● TransformerEncoderBlock, TransformerEncoder

class TransformerEncoderBlock(nn.Module) :
  def __init__(self, hidden_dim = 8*8*3, num_heads = 6, mlp_size = 8*8*3*4):
    super().__init__()

    self.LN = nn.LayerNorm(normalized_shape= hidden_dim)
    self.MSA = MSA(hidden_dim = hidden_dim, num_heads = num_heads)
    self.MLP = MLP(input_dim = hidden_dim, hidden_dim = mlp_size, output_dim = hidden_dim)

  def forward(self, x) :
    x_prev = x
    x = self.LN(x)
    x = self.MSA(x)

    x = x + x_prev
    x_prev = x

    x = self.LN(x)
    x = self.MLP(x)

    return x + x_prev

LN -> MSA -> LN -> MLP가 이어지는 block 입니다. 중간중간 residual connection도 추가해줍니다.

 

class TransformerEncoder(nn.Module):
  def __init__(self, hidden_dim = 8*8*3, num_layers = 8) :
    super().__init__()

    self.num_layers = num_layers
    self.hidden_dim = hidden_dim

    layers = []

    for i in range(0, num_layers) :
      layers.append(TransformerEncoderBlock())

    self.blocks = nn.ModuleList(layers)

    self.LN = nn.LayerNorm(normalized_shape= hidden_dim)

  def forward(self, x):
    for i in range(0, self.num_layers) :
      x = self.blocks[i](x)

    return self.LN(x[:, 0, :])

Encoder는 앞서 구현한 transformer encoder block이 L개 연속되도록 구현해줍니다.

여기에서도 ModuleList를 이용하여 구현해주었습니다.

 

Encoder의 output은 classification token에 대한 마지막 block의 출력입니다.

 

- MLP Head

class MLPHead(nn.Module) :
  def __init__(self, input_dim = 8*8*3, hidden_dim = 8*8*3*4, num_classes = 10) :
    super().__init__()

    self.feedforward = nn.Sequential(
        nn.Linear(input_dim, hidden_dim),
        nn.ReLU(),
        nn.Linear(hidden_dim, num_classes)
    )

  def forward(self, x) :
    return self.feedforward(x)

Classification을 위한 MLP head 에서는 encoder의 output을 input으로 받아 classifciation을 수행하면 됩니다.

일반적인 2 layer net입니다.

 

- ViT

최종 ViT는 다음과 같이 Embedding - Transformer Encoder - MLP Head가 이어지는 구조입니다.

class ViT(nn.Module) :
  def __init__(self, input_size = 32, patch_size = 4, hidden_size = 8*8*3, num_layers = 8) :
    super().__init__()

    self.vit = nn.Sequential(
        Embedding(input_size = input_size, input_channel = 3, hidden_size = hidden_size, patch_size = patch_size),
        TransformerEncoder(hidden_dim = hidden_size, num_layers = num_layers),
        MLPHead(input_dim = hidden_size, hidden_dim = hidden_size*4, num_classes= 10)
    )

  def forward(self, x):
    return self.vit(x)

 

 

- Training

Optimizer는 Adam을 사용했고, lr = 1e-3, (B1, B2) = (0.9, 0.99)를 사용했습니다.

(좌) training loss, (우) test set accuracy

학습이 잘 이뤄지는 것을 확인할 수 있습니다.

 

아직 수렴하지 않았지만, 처음에 언급했듯이 연산을 구현해보는 것이 목적이었기 때문에 training은 20 epochs에서 더 진행하지 않았습니다. 

'Computer Vision > 공부' 카테고리의 다른 글

[논문 리뷰] ViT, An Image is Worth 16x16 Words  (0) 2022.02.05
[논문 리뷰/구현] ResNet  (0) 2022.01.28

Vision transformer 논문에 대한 간단한 리뷰를 정리해보겠습니다.

 

Paper : 

https://arxiv.org/abs/2010.11929

 

An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale

While the Transformer architecture has become the de-facto standard for natural language processing tasks, its applications to computer vision remain limited. In vision, attention is either applied in conjunction with convolutional networks, or used to rep

arxiv.org

 

- Background

Attention mechanism과 transformer model은 자연어 처리에서 이미 주류를 이루고 있었습니다. 

 

본 논문에서는 NLP 분야에서의 transformer의 활약에 영감을 받아 vision 영역에 transformer를 적용한 내용을 다루고 있습니다.

현재는 vision 영역에서도 transformer 기반 모델들이 상당히 많이 등장했고, 많은 SOTA 모델이 trasnformer 기반으로 알고 있습니다.

 

최대한 NLP 에서 사용되는 transformer model을 그대로 사용하는 방식으로 실험을 진행했는데,

'An Image is Worth 16x16 words'라는 논문의 제목에서도 볼 수 있듯이 기존 NLP 분야의 transformer에서 문장의 각 단어를 token으로 다룬 것처럼 이미지의 각 영역을 token으로 다루는 방식을 사용했습니다.

 

덕분에 NLP의 transformer 구조를 거의 그대로 사용하고 있습니다.

(이전에도 transformer를 적용하려는 연구들이 있었지만, pixel 단위로 attention을 계산하는 등의 방식은 비효율적이었고 본 논문의 방식이 가장 좋은 결과를 보였다고 합니다.)

 

또한 NLP 분야에서 large dataset에 training한 후 smaller task에 fine-tuning하는 방식을 주로 사용한 것처럼, 

ViT도 large dataset에 pre-train을 수행했을 때 비로서 좋은 성능을 보이고 SOTA를 달성하게 되는 결과를 보입니다. 

 

이에 대해 CNN과 달리 ViT에는 inductive bias가 부족하기 때문이라고 설명하고 있는데, 여기서 inductive bias란 간단히 '이미지 처리에 특화된 가정'이라고 얘기할 수 있습니다. 

 

직관적으로 이해하면 CNN은 이미지 처리를 위해 개발된 구조이지만 transformer는 그렇지 않다고도 이해할 수 있겠고, 

locality나 translation equivariance 등에 대한 자세한 설명, 수식에 대해서는 아래의 링크에 자세한 설명이 나와있습니다.

https://seoilgun.medium.com/cnn%EC%9D%98-stationarity%EC%99%80-locality-610166700979

 

CNN과 이미지가 찰떡궁합인 이유

딥러닝이 뜨고 지금까지 가장 활약한 모델 중 하나는 CNN이다. 대부분의 사람들이 아무런 의심없이 이미지 데이터를 다룰때는 CNN을 사용한다. 그렇다면 도데체 왜 CNN은 이미지 데이터를 잘 처리

seoilgun.medium.com

결론적으로 transformer는 inductive bias가 부족하여 적은 수의 데이터로는 일반화가 잘 이뤄지지 않지만, 

충분히 큰 데이터셋에 pre-train 시켰을 때 CNN 기반의 모델들보다 좋은 결과를 보이게 됩니다. 

 

- Method

본격적으로 Vision Transformer가 어떻게 동작하는지 살펴보겠습니다. 

 

자세한 동작에 대해서는 

   - [Paper Review] Swin Transformer: Hierarchical Vision Transformer using Shifted Windows,             (https://www.youtube.com/watch?v=2lZvuU_IIMA),

   - [DMQA Open Seminar] Transformer in Computer Vision,

(https://www.youtube.com/watch?v=bgsYOGhpxDc

 

위 두 영상의 설명을 많이 참고했습니다.

 

ViT 구조

ViT의 구조는 위와 같고, 크게 Embedding, Transformer Encoder, MLP head 3가지 부분으로 나눠서 살펴보겠습니다. 

 

1. Embedding

Input image를 token화시키고 transformer encoder의 input으로 만들어주는 과정입니다. 

N x N x C 크기의 image가 input으로 들어오면 각각의 크기가 h x w x c 크기인 patch들로 그림과 같이 나눠주고, 다시 각각의 patch들을 크기가 h*w*c인 vector로 만들어줍니다. 

 

각각의 vectors들을 D 차원으로 projection 시켜준 후, classfication token을 추가하고 positional embediing을 더해주면 그 결과가 transformer encoder의 input이 됩니다. 

 

Classification token은 classification을 위해 추가된 token으로, 해당 token에 대한 transformer encoder의 output을 classification에 사용하게 됩니다.

 

또한 positional embedding은 이미지에서의 위치 정보를 다시 추가하기 위해 더해주는 값이라고 볼 수 있겠습니다.

 

이때 classification token과 positional embedding 모두 trainable한 parameters입니다. 

 

2. Transformer Encoder

Transformer Encoder는 위와 같은 blocks가 L개 반복되는 구조입니다. 

LN - MSA - LN - MLP가 반복되고, residual connection이 추가되어 있습니다.

 

    ● Layer Norm

Norm은 Layer Normalization으로 batch 단위로 normalizatoin을 수행하는 BN과 달리, 각 데이터 별로 normalization을 수행합니다. 

 

    ● Multi-Head Attention

다음으로 Multi-Head attention의 연산 과정입니다. 앞서 생성한 N개의 D차원 vectors이 LN을 거친 결과가 input으로 전달되면 D x $D_h$ 크기의 query, key, value weight와 각각 곱해줍니다.

 

그렇게 생성된 N x $D_h$ 크기의 matrix가 각각 query, key, weight이고,

$A = QK^T$, N x N matrix에서 $ A_{ij}$가 i번째 token이 j번째 token에 얼마나 attention을 갖는지를 의미합니다. 

 

최종적으로 $ QK^T$를 $ \sqrt{D_h}$로 나눠준 값에 softmax를 취하고(각 row별로), value matirx와 곱해준 값이 head의 output이 됩니다.

 

따라서 각 head별 출력은 Nx$D_h$가 되고, 이때 $ D_h$는 보통 D / k(=number of heads)로 설정되어 k개의 heads의 output을 concat한 결과로 각 MHA의 출력은 NxD크기가 유지됩니다.

※ 수정) k*$ D_h$를 D 차원으로 projection한 결과가 최종 output입니다.

 

 

   ● MLP

MLP는 hidden layer가 하나인 two layer network이고, activation function으로 GELU nonlinearity를 사용합니다.

 

이렇게 LN - MHA - LN - MLP 구조가 L번 반복되고, 최종적인 출력은 0번째, classification token에 대한 output이 됩니다. (N x D matrix 중 첫번째 행)

 

   ● MLP Head

최종 classification을 수행하는 MLP로, cls token에 대한 transformer encoder의 output을 input으로 받아 classification을 수행합니다.

 

Hidden layer가 1개인 2 layer network입니다. 

 

- Results

Pre-training dataset의 수에 대한 영향 등에 대한 추가적인 실험들이 있지만, 해당 실험에 대해서는 생략하고 최종 성능만 살펴보겠습니다. ResNet이나 EfficientNet 등의 SOTA models과 비교해도 더 좋은 성능을 보입니다. 

 

개인적으로 transformer나 attention mechanism을 ViT를 통해서 처음 접해봤기 때문에, ViT의 처리과정은 이해가 가더라도 왜 좋은 성능을 보이는지 이해가 잘 가지 않았는데, 동작에 대한 해석을 살펴보겠습니다. 

● Principal Components of Embedding filters

먼저 embedding filters에 대한 해석입니다.

위 이미지는 embedding filters(projection matrix)에 PCA를 적용한 결과로, 마치 CNN에서 초반 layers의 filters와 유사한 것을 살펴볼 수 있습니다.

 

● Positional Embedding

다음으로 position embedding의 유사도를 나타낸 그림입니다. 

Position embedding이 학습된 결과 row와 column이 같고 가까울 수록 높은 유사도를 보입니다.

즉, transformer가 이미지에 대해 잘 이해하고 있다고 해석할 수도 있을 것 같습니다.

 

Attention

먼저 좌측의 그림은 attention map을 시각화한 결과입니다. 

Attention map은 모든 layers의 attention을 종합한 것이라고 보면 되고, 결과적으로 model이 classification에 중요한 정보를 담고 있는 영역에 집중하는 것을 확인하실 수 있습니다.

 

우측의 그림은 mean attention distance를 나타낸 그림으로 CNN에서 receptive field가 점차 global한 영역으로 확대되듯이, ViT에서도 attention이 global한 영역으로 점점 확대된다는 것을 확인할 수 있습니다.

'Computer Vision > 공부' 카테고리의 다른 글

[논문 구현] ViT, An Image is Worth 16x16 Words  (0) 2022.02.05
[논문 리뷰/구현] ResNet  (0) 2022.01.28


※ 본 내용은 stanford에서 제공하는 cs231n 강의, 강의자료를 바탕으로 작성하였습니다.

 

Lecture 15에서는 deep learning 모델의 inference, training이 효율적으로 이뤄질 수 있도록 하는 알고리즘, 하드웨어에 대해 다루고 있다. 

 

Agenda

총 위의 4가지 방법에 대해 다루고 있다.

 

※ 자세한 알고리즘보다는 대략적인 개념을 소개하고 있으므로 각각의 알고리즘에 대해서는 별로도 찾아보는 것이 좋을 것 같다.

<Algorithms for Efficient inference>

먼저 효율적인 inference를 위한 알고리즘들에 대해 다루고, 총 6개의 알고리즘을 다루고 있다.

 

- 1. Pruning

Pruning

 

Pruning은 '가지치기'라는 뜻 그대로 불필요한 연결이나 neuron을 제거하는 방법이다.

여기서 불필요하다는 것은 그 weight나 activation이 0에 가까워 의미가 없는 것들을 의미한다. 

 

당연히 더 많은 parameters를 가지치기할수록 모델의 accuracy는 떨어진다. 

그러나 그 정도는 생각보다 크지 않고, pruning한 모델을 다시 retrain하는 것으로 원래의 성능 정도로 복원할 수 있다.

노이즈에 의한 것이긴 하지만 본래의 모델보다 성능이 좋아지는 경우도 있다.

 

- 2. Weight Sharing

Weight sharing의 기본 아이디어는 quantization을 이용하는 것이다. 

예를 들어, 

2.09, 2.12, 1.92 -> 2.0

위와 같이 비슷한 수들을 하나의 수로 변경하는 것이다. 

 

Training 시에는 32 bit float으로 학습을 진행하고,

학습된 weights에 대해 clurstering을 수행해 묶어주는 것이다. 

 

이렇게 하면 훨씬 더 적은 bit수로 weights를 저장할 수 있다.

 

실험 결과 위 두 경우에는 4bits나 2bits까지 성능 저하가 크게 들어나지 않는 것을 확인할 수 있다. 

 

앞서 다룬 pruning과 함께 적용할 수도 있고, Huffman encoding을 이용하는 등의 방법으로 더욱 compact한 모델로 만들 수 있다.. 

VGG, ResNet 등의 모델에 대해서 각종 기법을 이용한 결과 수십배로 압축하면서 accuracy는 유지할 수 있었다고 한다. 

 

- 3. Quantization

Quantization은 앞서 다룬 weight sharing의 아이디어와 유사하다.

 

Weights의 통계를 이용하여 최소한의 bit수로 weights를 표현할 수 있도록 하는 방식이다.

 

- 4. Low Rank Approximation

하나의 convolution layer를 두 개의 layer로 나누는 방법이다. 이렇게 나누는 방법을 통해 model을 경량화할 수 있다. 

위의 경우를 예시로 들면, d 개의 k x k x c 개의 filters를 d'개의 k x k x c 개의 filters와 d개의 1 x 1 x c filters로 나누어 전체 weights를 감소시킬 수 있다.

 

- 5. Binary / Ternary Net

2개 또는 3개의 수로만 weights를 대체하는 방법이다.

학습된 weights를 시각화해보면 위와 같다. 

상당히 단순화 되었지만, 여전히 직관적으로 어떠한 역할을 수행하는 필터인지 판단이 가능한 필터들이 존재한다. 

실제로 거의 full precision 모델에 가까운 정도의 error rate를 달성한 결과를 보이기도 한다. 

 

- 6. Winograd Transformation

Convolution 연산을 matrix multiplication으로 치환하는 방법이다.

Transform 원리에 대해서는 강의에서 자세히 설명해주진 않지만, 데이터와 filter에 대해 transform을 거친 후 element-wise 곱만 수행해주면 그 결과가 convolution과 완전히 동일하게 된다고 한다. 

 

수행할 연산의 수를 줄일 수 있다. 

 

<Hardware for Efficient Inference>

하드웨어가 발전하거나, 연산에 최적화된 하드웨어를 설계함으로써 inference를 효율적으로 개선할 수 있다. 

Google TPU

Google에서 설계한 TPU이다. 

복잡한 구조를 다 살펴볼 수는 없지만 빨간색으로 표시된 영역의 units들을 통해 deep learning 연산을 최적화하고 있다. 대표적으로 Matrix Multiplication Unit을 통해 많은 수의 연산을 처리할 수 있다.

92T operations/second의 peak 성능을 보인다.

 

FLOPs/Byte의 비율이 충분히 크면 연산량은 FLOPS에 의해 결정되지만, FLOPS/Byte의 비율이 충분히 크지 못한 경우에는 memory bandwidth에 의해 성능이 좌우된다. 

 

그러나 사용되는 deep learning 모델들의 경우 대부분 memory access에 비해 사용되는 연산량이 적다. 

(memory bandwidth에 의해 성능이 제한되어 peak performance를 보이지 못한다.)

 

이를 해결하기 위해 불필요한 memory access와 불필요한 연산을 줄이기 위한 방법이 필요한데,

강의에서는 하드웨어를 이용한 솔루션 중 EIE(Efficient Inference Engine)를 소개하고 있다. 

 

핵심 아이디어는 0*A = A*0 = 0 임을 이용하여 불필요한 weight 저장과 연산을 줄이는 것이다.

크기 4의 벡터와 4x8 matrix의 곱을 나타내는 에시이다.

 

실제 회로상으로 어떻게 처리되는지는 자세히 설명하지 않지만, 

0인 값은 하나로 저장하고, 0과 곱해지는 연산은 스킵한다고 한다.

 

<Algorithms for Efficient Training>

Training 과정 또한 다양한 알고리즘을 통해 효율적으로 다룰 수 있다.

 

강의에서는 4가지 알고리즘을 소개하고 있다.

 

- 1. Paralleization

병렬처리를 통해 더욱 빠르게 training을 하는 방법이다.

Data Parallelism

서로 다른 환경에서 서로 다른 데이터에 대해 병렬적으로 forward 및 backward를 계산한 후, 하나의 server를 통해 parameter 업데이트를 수행하는 방법이다.

 

Model Parallelism

하나의 모델의 여러 부분을 서로 다른 환경에서 병렬 연산하여 더욱 빠르게 처리하는 방법이다.

 

추가로 hyper-parameter parallelism도 하나의 parallelism 기법이다.

Hyper parameter를 서로 다른 환경에서 다양하게 실험하면 최적 parameter를 더욱 빠르게 찾을 수 있다. 

 

- 2. Mixed Precision with FP16 and FP32

Weight 자체는 32-bit precision으로 저장하지만, training시에 이뤄지는 연산은 16-bit로 처리하여 효율성을 높이는 방법이다. 

실험 결과 성능차이가 거의 나타나지 않고, 오히려 노이즈에 의해 더욱 빠르게 수렴하는 경우도 있었다고 한다. 

 

- 3. Model Distillation

모델의 수렴이 훨씬 빠르게 이뤄지게 하는 방법이다. 

여러 개의 teacher model이 student model을 학습시키는 것으로 생각할 수 있는데, 

여기서 teaching의 의미는 score를 알려주는 것이다. 

Data의 label이 아니라 해당 score를 통해 학습하면 훨씬 빠르게 수렴한다고 한다.

앙상블의 결과를 바로 이용하는 것은 아니고, softened 결과를 이용한다.

Soften 정도는 T, temperatue라는 parameter를 이용한다.

 

- 4. DSD : Dense Sparse Dense Training

 

앞서 다룬 pruning을 적용한 모델을 먼저 학습하고, 어느 정도 학습이 이뤄지면 다시 re-dense를 수행한 후 학습을 계속하는 방법이다. 

이렇게 학습한 모델은 더 좋은 성능을 보인다고 한다. 

 

<Hardware for Efficient Training>

새롭게 발표된 Volta 구조에서는 딥러닝 연산에 최적화된 tensor core가 새로 추가되어, mixed precision operations를 훨씬 빠르게 처리할 수 있도록 도와준다고 한다.

 

 

 

 

 

 

 

+ Recent posts