본문 바로가기
Object Detection/Transformer Dectection

[Object Detection] DETR(DEection TRasformer ECCV2020)리뷰 및 구현

by pulluper 2023. 2. 6.
반응형

안녕하세요 Pulluper 입니다 :)

이번시간에는 Transformer 를 이용한 Object Detection 방법인 DETR 에 대하여 알아보겠습니다! 

https://arxiv.org/abs/2005.12872

 

본 포스팅의 내용은 크게 Introduction, Network, Loss 로 이루어집니다. 😎


1. Introduction

 

DETR은 Object Detection 방법을 set prediction 문제로 보았습니다.

이를 통한 효과는 hand-designed component 를 없앨 수 있다는 것입니다.

예를들어 nms와 anchor 생성등이 있습니다. 

 

이 논문의 contribution은 크게 2가지가 있다고 생각합니다. 

 

  • Network(Transformer architecture)
  • Loss (Bipartite matching)

첫번째 컨트리뷰션은 네트워크의 구조를 Transformer encoder-decodder 를 성공적으로 사용한 부분입니다. 

두번째는 Bipartite matching을 통한 Direct Set Prediction 으로 object detection 문제를 해결한 것입니다. 

 

여기서 Direct Set Prediction 이란 무엇을 뜻할까요?????

필자가 생각하기에 이는 다음과 같습니다.

 

먼저 어떤 object detection을 생각해 봅시다. (Faster rcnn, YOLO등..)

이들은 이미지로부터 매우 많은 숫자의 prediction을 생성합니다.

여기서 생성된 prediction 필연적으로 duplication 문제를 만듭니다. 

그리고 이를 제거하기 위해서 NMS등의 후처리가 필수적입니다.

 

그렇지 않으면 다음과 같이 아주 많은 box를 생성하게 됩니다. 

그림1. duplication 문제

 

이러한 문제를 해결하기 위해서 OD 를 set prediction (집합의 예측)으로 생각하는 것 입니다.  

다음을 보면 집합의 정의를 볼 수 있습니다. 

집합의정의

다음을 만족하는 collection(원소들의 모임)을 집합이라고 한다. 
1. 중복이 없다. 
2. 순서 or 연산이 없다. 
3. 들어가거나 들어가지 않거나이다. $a \in A, a \notin A$

 

여기서 이용하는 두드러진 특징은 중복이 없다는 것 입니다. 

이러한 생각으로 어느 특정 물체에 대한 결과는 중복없이 1개만 나오도록 하는것이 아이디어 입니다. 

 

따라서 DETR은 특정 갯수의 (Fixed Size - 실 구현에서 100개)

object query를 사용해서 각 쿼리가 object 인지 아닌지를 판단합니다. 

반대로 말하면, DETR의 감지 할 수 있는 최대 object 를 100개로 제한하고 그것을 잘 학습하게 만들었습니다. 


이를 위해 각 쿼리가 하나의 label과 매칭이 되야 하기에 헝가리안 알고리즘을 사용하여 

이 쿼리의 순열을 학습합니다. 

 

자세한 이야기는 Network, Loss 에서 더 다루겠습니다.

그렇다면 DETR 리뷰 본격적으로 시작해 보겠습니다. 👟👟👟


2. Network (Transformer Encoder-Decoder Architecture)

그림2. DETR의 구조

DETR네트워크는 위 그림과 같이 구성됩니다. 

 

(0) positional encoding

(1) backbone

(2) encoder

(3) decoder

(4) prediction heads

 

(0) (spatial) positional encoding 

 

positional encoding 이란 transformer network 에서

위치정보를 표현하기 위해 input 정보에 더해주는 추가적인 signal을 뜻합니다. 

 

위치정보를 나타내는 spatial pe 는 sinusoidal positional encoding 이 가장 좋은 성능을 보였습니다. 

DETR은 object detection이기 때문에 정확한 위치를 판단할 수 있는

학습되지 않는 일정한(Fix된) PE가 좋은 것으로 생각됩니다. 

 

detr에서 사용하는 spatial positional encoding은 다음과 같이 appendix에서 설명을 하고 있습니다. 

"original Transformer encoding 의 2d case 의 일반화를 적용했다."

 

그림3. spatial positional encoding in detr

 

그럼 Original Transformer 에서 사용한 positional encoding 을 알아보겠습니다. 

그림4. original transformer positional encoding

 

여기서 pos 는 특정 위치를 뜻하고 각 위치마다 $d_{model}$의 차원의 vector 를 만들어야 합니다. 

예를들어 d_model = 128, 이라고 가정하면 0번 pos 에는 첫번째 vector의 element가 sin(0 / (10000^0/128)) 이고,

두번째는 cos(0 / (10000^0/128)) 입니다. 이렇듯, pos 0 에 대한 128 차원의 vector 는 sin 과 cos 이 번갈아가면서 

나타납니다. 

그림5. positional encoding 에 대한 이해

이를 코드로 나타내면 다음과 같습니다. 

import math
import torch

def pos_enc(length=64, d_model=128, n=10000):

    # d_model 짝수여야함
    if d_model % 2 != 0:
        raise ValueError("Cannot use sin/cos positional encoding with "
                         "odd dim (got dim={:d})".format(d_model))

    # container 만들기
    pe = torch.zeros(length, d_model)
    pos = torch.arange(length)            # [64, 1]

    # 0, 2, 4, ..., 126
    i = torch.arange(0, d_model, 2)

    # 0.0000,0.0156, ..., 0.9688, 0.9844
    i = i / d_model

    # shape of div_term is [d_model // 2], div_term : 1 ~ 10000
    div_term = n ** i

    # [64, d_model]
    pos = pos[:, None] / div_term

    # pe 의 짝수번째는 sin, 홀수번째는 cos
    pe[..., 0::2] = pos.sin()  # [64, d_model / 2]
    pe[..., 1::2] = pos.cos()  # [64, d_model / 2]
    
    return pe

 

다음은 sin 만 사용했을때, cos 만 사용했을때, 그리고 sin, cos 을 번갈아 사용했을 때 입니다. 

그림6. pe sin/con/sin,cos

 

DETR 에서는 이를 2차원으로 확장하고, pos를 가장긴 pos길이에 대하여 normalization 한후 2pi 를 곱해주는 작업을 합니다. 

그림7. detr 2d positional enc

 

이에 대한 코드는 다음과 같습니다. 

 

import math
import torch


def pos_enc_2d(length, d_model, device=None):
    if device is None:
        device = torch.device('cpu')

    scale = math.pi * 2
    n = 10000

    x = torch.arange(1, length + 1, dtype=torch.float32, device=device)
    y = torch.arange(1, length + 1, dtype=torch.float32, device=device)
    y, x = torch.meshgrid(x, y)

    # normalize
    eps = 1e-6
    y_norm = y / (length + eps) * scale
    x_norm = x / (length + eps) * scale

    # 0 ~ 128 (pos)
    i = torch.arange(0, d_model, step=2, dtype=torch.float32, device=device)
    div_term = n ** (i / d_model)

    y_emb = y_norm[:, :, None] / div_term
    x_emb = x_norm[:, :, None] / div_term

    pos_y = torch.stack((y_emb.sin(), y_emb.cos()), dim=3).flatten(2)
    pos_x = torch.stack((x_emb.sin(), x_emb.cos()), dim=3).flatten(2)

    # [32, 32, 256]
    pos_emb = torch.cat((pos_y, pos_x), dim=2)
    return pos_emb

 

DETR에서 사용한 positional encoding 은 spatial pos. enc. 와 output pos.enc. (object query) 가 있습니다. 

이에 따른 ablation study 를 보았을 때, spatial positional encoding 은 fix 된 sine 이 가장 좋은 것을 볼수 있습니다.  

 

그림 8. positional encoding ablation study

 

또한 positional encoding 은 다음구조를 보아 매 attnetion layer 에 추가되어 더해지는 것을 볼 수 있습니다. 

ViT등과 달리 정확한 위치를 찾아내야 하는 detection등의 task이기 때문에 pe가 더 많이 입력되어 위치정보를

계속 전달해 주기 때문인것 같습니다.   

그림 9. detr 에서의 positional encoding

 

만약 positional encoding 을 왼쪽 위로 조금 바꾼다면, 학습된 detr 은 아래의 왼쪽 그림과 같이 detection결과가

align이 왼쪽 위로 된 모습을 볼 수 있습니다. 

그림 10. pe 를 바꾸었을때의 결과


(1) backbone

 

detr의 backbone은 기본적으로 resnet 50을 사용합니다. 

따라서 이미지는 1/32의 scale 로 줄어들고 이것을 (channel을 줄이는) projection후

flatten으로 sequence를 만들어, transformer의 입력으로 넣습니다.

 

그림 11. backbone

 

(2) encoder

 

Encoder는 input seqeunce x에 대하여 multi-head attention을 수행합니다. 

positional encoding pe에 대하여 query, key에는 pe를 더해주고 value 는 x를

그대로 사용하여 multi-head cross attention을 수행합니다. 

 

multi-head cross attention에 대해서는 다음 포스팅을 참조하세요. 

https://csm-kr.tistory.com/70

 

[DNN] multi-head cross attention vs multi-head self attention 비교

안녕하세요 pulluper 입니다. attention 을 사용한 모듈을 보면, 하나의 인풋이 들어와서 q, k, v 가 같은 length를 가지는 경우가 있고, q의 길이와 k, v의 길이는 다른 경우를 왕왕 볼 수 있습니다. 예를들

csm-kr.tistory.com

 

- post-LN 을 사용

- query, key 에만 positional encoding 을 더해줌

- encoder는 6개의 layer, 8개의 head를 이용

x = norm(x + attn(x + pe, x + pe, x))
x = norm(mlp(x))

 

여기서는 input sequence에 대한 multihead self attention을 이용하여

ViT등과 같이 자신의 attention feature를 이용하는것과 같습니다. 

간단한 코드는 다음과 같습니다. (전체 코드는 아래 url이 첨부되어있습니다.)

 

class EncoderLayer(nn.Module):
    def __init__(self, d_model, d_feedforward=2048, dropout=0.1):
        super().__init__()
        self.attn = MHA(dim=d_model, attn_drop=dropout, proj_drop=dropout)
        self.mlp = MLP(in_features=d_model, hidden_features=d_feedforward, out_features=d_model, dropout=dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)

    def forward(self, x):

        # 1. Positional Encoding for q, k
        pe = get_positional_encoding(x)

        # 2. Multi head self attention for encoder
        x = self.norm1(x + self.attn(x + pe, x + pe, x))
        x = self.norm2(x + self.mlp(x))
        return x

(3) decoder

 

Decoder는 encoder와 다르게 target과 encoder에서 뽑아낸 feature의 co-attention에 대한 정보를 다뤄야 합니다.

따라서 attention은 2가지가 사용되며,

target(object query)의 자체에 관한 attention feature 를 뽑는 부분과 

target과 x의 cross attetnion에 관한 attnetion으로 이루어 집니다. 

t = norm(t + attn(t + qe, t + qe, t))
t = norm(t + attn(t + qe, x + pe, x))
t = norm(mlp(t))

 

DecoderLayer의 간단한 코드와 구조입니다. 

Decoder 또한 post-LN 을 사용하였고, target의 query, key 에만 query embedding을 더해주며, 

x 에 대하여는 x의 key 에만 pe를 더해주고 target의 query 에만 qe 를 더해줍니다.

또한 encoder 와 마찬가지로 6개의 layer, 8개의 head를 이용합니다. 

 

class DecoderLayer(nn.Module):
    def __init__(self, d_model, d_feedforward=2048, dropout=0.1):
        super().__init__()
        self.attn_t = MHA(dim=d_model, attn_drop=dropout, proj_drop=dropout)
        self.attn_x = MHA(dim=d_model, attn_drop=dropout, proj_drop=dropout)
        self.mlp = MLP(in_features=d_model, hidden_features=d_feedforward, out_features=d_model, dropout=dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)

    def forward(self, t, x, query_embed):
        '''
        :param t: [N, 100, 256] (target)
        :param x: [N, 1024, 256]
        :param query_embed: [N, 100, 256]
        :return:
        '''
        # 1. Query Embedding
        qe = query_embed

        # 2. Positional Encoding for q, k
        pe = get_positional_encoding(x)

        # 3. attn
        t = self.norm1(t + self.attn_t(t + qe, t + qe, t))
        t = self.norm2(t + self.attn_x(t + qe, x + pe, x))
        t = self.norm3(t + self.mlp(t))
        return t

(4) prediction heads

 

Decoder 까지의 output은 [B, 100, 256] (batch, query_size, model_dim) 입니다. 

이후 classification을 위한 prediction head는 하나의 Linear Layer,

box prediction을 위한것은 3층의 단순 Linear Layer 로 이루어져 있습니다. 

 

self.class_layer = nn.Linear(d_model, num_classes + 1)
self.box_layer = nn.Sequential(nn.Linear(in_features=256, out_features=256),
                               nn.ReLU(inplace=True),
                               nn.Linear(in_features=256, out_features=256),
                               nn.ReLU(inplace=True),
                               nn.Linear(in_features=256, out_features=4),
                               )

 

따라서 prediction heads 까지의 모델 통과후 나오는 ouput 은 다음과 같은 shape 을 갖습니다. 

 

torch.Size([2, 100, 4]) (box)
torch.Size([2, 100, 92]) (class)

 


3. Loss (Bipartite matching)

 

현재까지의 흐름으로, batch 가 1 일때, coco 의 dataset 에 대하여

네트워크 output은 [1, 100, 4], [1, 100, 92] (box, class) 의 크기를 갖습니다. 

또한 가령 object 가 6개 있는 정답 [6, 4], [6, 1] gt 에 대하여 loss 를 구한다면, 

 

각 6개의 object는 100개 중 하나의 object query 에 할당이 됩니다. (hungarian algorithm으로)

이를 $\hat{\sigma}$ 으로 표현하고 이것을 이용해 학습을 시킵니다. 

 

위의 Introduction에서 set prediction을 위해서 중복이 없어야 하고

이를 위해서 각 쿼리당 하나의 object를 매칭을 해야 한다고 하였습니다. 

이를 위해 헝가리안 알고리즘을 사용했는데, 이에 대하여 먼저 알아보겠습니다. 

 


3. 1. 헝가리안 알고리즘

 

예를들어 설명해 보겠습니다. 

짱구는 친구 철수와 맹구와 햄버거집에 갔습니다. 그날 메뉴는 4개가 있었습니다. 

불고기버거, 치즈버거, 새우버거, 치킨버거입니다. 

각자는 하나의 햄버거만 먹을 돈이 있습니다. 

그런데 그 친구들의 각 햄버거를 좋아하는 정도는 다음과 같습니다. 

 

그림12. cost matrix

 

짱구는 최대한 모두가 만족하는 햄버거의 할당을 하고싶었습니다. 

실제 헝가리안 알고리즘의 작동 방법은 다음 블로그를 참고하시면 좋을것 같네요. 😆

 

https://supermemi.tistory.com/158

 

[알고리즘] Hungarian Maximum Matching Algorithm (Kuhn-Munkres algorithm)

Hungarian Maximum Matching Algorithm 에 대하여 알아보자 이 글을 참고하여 작성하였습니다. Hungarian Maximum Matching Algorithm | Brilliant Math & Science Wiki The Hungarian matching algorithm, also called the Kuhn-Munkres algorithm, is

supermemi.tistory.com

 

짱구는 python 을 켜서 다음과 같은 코드를 작성합니다. 

여기서 linear_sum_assignment() 함수에서 최적화 하는 방법은

cost matrix의 최소화 이기 때문에 10 - preference_matrix를 cost_matrix 로 사용합니다. 

 

from scipy.optimize import linear_sum_assignment
import torch

preference_matrix = [[9, 8, 7, 10],
                     [9, 9, 8, 9],
                     [10, 10, 9, 10]]

cost_matrix = 10 - torch.tensor(preference_matrix)
print(cost_matrix.size())

indice1, indice2 = linear_sum_assignment(cost_matrix)
print(indice1)
print(indice2)

 

이 결과는 다음과 같이 나옵니다. 

torch.Size([3, 4])
[0 1 2]
[3 0 1]

 

즉 (0, 1, 2)번째 index 사람이 (3, 0, 1)번째 index 음식을 먹는것이 cost_matrix 를 최소화하는 즉, 

모두가 만족하는 햄버거의 할당입니다. (짱구 - 치킨, 철수 - 치즈, 맹구 - 불고기 라는뜻)

 

그림13. optimized cost matrix

 


 

자 그러면 DETR에서의 헝가리안 알고리즘의 적용을 보겠습니다. 

논문에 다음과 같은 bipartite matching 식이 있습니다. 

 

그림14. finding bipartite matching

 

100개의 prediction에 대하여 (query 의 갯수 : 100)

정답(ground truth)과 예측(prediction) 으로 만드는 cost_matrix( $L_{match}$ )

이를 헝가리안 알고리즘으로 최소화하는 어떤 모두가 만족하는 조합을 만드는게 목적입니다. 

즉, 각 prediction의 최적의 gt 할당 이라고 볼 수 있습니다. 

 

$L_{match}$ 는 다음과 같이 이루어졌고, 이를 만족하는 순열 $\hat{\sigma}$ 를 얻을 수 있습니다. 

그림 15. L_match

 

다음과 같이 query 의 수가 100이고, gt object 는 4개만 할당이 된다고 할 때, 

 

그림 16. sigma 의 예

 

query의 index 와 gt object 의 index 로  $\hat{\sigma}$ 이 얻어집니다. 


3. 2. whole loss

 

위의 헝가리안 알고리즘으로  $\hat{\sigma}$ 를 얻은 후 두번째 단계로 네트워크를 학습할

전체 로스는 다음과 같이 구성됩니다.

 

classification loss 는 $\hat{\sigma}$ 와 background 를 고려한 cross entropy 이고

box regression loss 는 smooth l1 loss 와 giou 를 결합한 loss 입니다. 

그림 17. 전체 loss


 

4. DETR 성능평가

 

4.1 학습환경

  • Optimizer:  AdamW
  • Initial LR: 1e-4, 1e-5(backbone)
  • Weight Decay: 1e-4
  • Weight Init: Xavier init
  • Batch Size: 64(4 image per 16 V100 GPU)
  • Epoch: 500(step LR 0.1 at 400)

4.2 FRCNN과의 비교

 

더 좋거나 비슷한 성능을 보입니다.

41M의 parameter로 42.0 이상의 MAP를 얻었습니다.  

그림 18. FRCNN과의 비교

4.3 Ablation Study

 

4.2 Self Attention

 

아래 그림은 encoder에서 한 점을 찍으면 나타나는 attention 의 활성화 그림을 보여줍니다. 

소의 각 객체들을 찍으면 관련 부분이 나오는 것을 볼 수 있습니다. 

 

5. implementation

 

다음은 필자가 구현한 실제 코드입니다. 😎😎

 

↓ 구현 코드 ↓

https://github.com/csm-kr/detr_pytorch

 

GitHub - csm-kr/detr_pytorch: 🌼 re-implementation of DETR (DEtectionTRansformer) (ECCV2020)

🌼 re-implementation of DETR (DEtectionTRansformer) (ECCV2020) - GitHub - csm-kr/detr_pytorch: 🌼 re-implementation of DETR (DEtectionTRansformer) (ECCV2020)

github.com

 

반응형

댓글