본문 바로가기
논문리뷰

[논문 리뷰] Transformer (Attention Is All You Need) 요약, 코드, 구현

by davidlds 2023. 3. 14.
반응형

논문을 상세히 번역하고 한단어씩 해석해주는 포스팅은 많다.

나는 논문을 누구나 알아듣도록 쉽고 간결하게 전달하고자 한다.

 

Transformer

Attention Is All You Need
VASWANI, Ashish, et al. Attention is all you need. Advances in neural information processing systems, 2017, 30.
 

 

저자의 의도

CNN과 RNN에서 인코더와 디코더가 널리 사용되는데,

인코더 디코더 로만 구성된 새로운 간단한 아키텍쳐를 만들고자 했다.

특히 RNN에서 길이가 긴 시퀸스는 학습이 잘 안되는 경향이 있었다.

길이가 길어도 학습이 잘 되는 모델을 만들고자 했다.

 

기존 문제점

1. 기존의 RNN에서 길이가 긴 시퀸스를 처리하기에 적합하지 않다.

vanishing gradient가 발생한다.

 

2. 기존의 RNN은 느리다.

RNN은 연속된 순서로 계산을 해야한다.

이말은 첫번째 계산이 끝나지 않으면 두번째 계산도 시작되지 않는다.

즉 순서대로 이터레이션을 계속 해주면서 연산하는데, 매우 비효율적이다.

 

또한 RNN의 특성상 길이가 긴 데이터를 처리할 때 메모리도 아주 많이 사용하며,

여러개의 시퀸스를 처리하는 것이 사실상 불가능하다.

 

해결 아이디어

1. scaled dot-product attention

scale dot production attention
scale dot production attention

self attention의 한 종류인 scaled dot-product attention을 사용한다.

어텐션 매커니즘 중에서 (다른 위치 vs 자기 자신 위치) 간의 상호작용을 캡쳐하는 것이 self attention.

(쿼리)와 (키)를 내적하고, d_key(키의 차원수)의 제곱근으로 나눠 스케일링한다.

 

2. multi-head attention

multi head attention
multi head attention

scaled dot-product attention을 멀티 헤드로 처리한다.

이게 정말 대단한 아이디어라고 생각한다.

 

attention을 한번에 처리하는 것이 아니라 여러개로 분할하여 처리하게 만든다.

이는 CNN에서 필터를 여러개로 늘리는 듯한 효과를 주는데,

self-attention을 다양한 관점에서 하게 만들어 더 강력한 모델을 만든다.

과정은 다음과 같다.

 

쉬운 설명
쉬운 설명

 

1. Q, K, V를 공간 변환을 진행한다.

(ex. Q, K, V 128차원, 1개 -> Q, K, V 32차원, 4개)

 

2. 쪼개서 병렬화 하여 attention 진행.

(ex. 4개의 결과 행렬)

 

3. concate로 합친 후 다시 프로젝션하여 기존 공간으로 복원.

(ex. 4개의 결과 행렬 -> 1개의 결과 행렬)

 

이렇게 되면 데이터는 조각난 서브 공간(4개 관점)에서 representation을 병렬 계산한다.

즉 기존 공간(1개 관점)에서 계산할 때보다 훨씬 다양한 측면에서 고려한다.

따라서 정확도가 올라간다.

정확도만 이득이 아니다. 병렬로 계산한다는 것은 곧 속도가 빠르다는 것이다.

위 예시로 보면 대충 4배쯤(1개->4개) 빨라진다.

 

3. Position-wise Feed-Forward Networks

FFN
FFN

각 멀티 헤드 어텐션 후에는 저렇게 feed forward 레이어를 지나게 한다.

feed forward의 구조는 (선형변환 -> 렐루 -> 선형변환)의 형태.

더 큰 차원인 히든레이어로 선형변환 하여 복잡한 관계를 추가로 연산을 하는 것.

비선형성과 깊이를 부여하는 역할을 한다.

 

4. 3 Attention Layers

어탠션 레이어는 총 3개. 인코더 / 디코더 / 인코더-디코더 가 있다.

encoder- decoder
encoder- decoder

Encoder attention

인풋 시퀀스의 Q, K, V로 self-attention을 계산해 representation을 도출한다.

이 representation은 인코더-디코더 어탠션의 K와 V로 사용된다.

 

② Decoder attention (Masked)

'Masked'의 의미는 지금 포지션 이후의 토큰에 대한 attention score를 가리는 것을 의미한다.

이걸 가리는 이유는 모델이 미래 정보를 보지 못하게 막으려는 것이다.

 

예를 들어, (I go home and eat pizza.) 라는 문장을 번역할 때

home을 예측할 차례라고 해보자.

이때 모델은 (I go) 까지의 정보만 어탠션하여 예측해야한다.

(and eat pizza)의 정보는 개연성이 없으므로 이를 못보게 막아야한다.

이게 트랜스포머에 sequential prediction이 가능하게 하는 한가지 방법이다.

포지션이 넘어갈때마다 shift right로 한칸씩 오른쪽으로 옮겨간다.

 

결국 디코더 어탠션은 마스크된 시퀀스의 Q, K, V로 self-attention을 계산해 information을 도출한다.

이 information은 인코더-디코더 어탠션의 Q로 사용된다.

 

③ Encoder-Decoder attention

인코더-디코더 어탠션은 두 결과를 활용한다.

인풋 시퀀스로 도출한 representation(문장의 맥락, 표현, 늬앙스) K, V 중에서

현재 position에서 information(주의를 기울여야하는 키워드)에 대하여 attention 하는 것이다.

예시
예시

 

5. Positional Encoding

포지션 임베딩
포지션 임베딩

트랜스포머와 RNN 계열의 모델의 가장 큰 차이는

시퀀셜 단어(word by word)가 들어오는게 아니라 문장이 통째로 들어온다.

따라서 지금 예측하는 포지션(단어)가 어딘지에 대한 정보를 반드시 부여해야 한다.

 

이때 선형함수가 아니라 삼각함수가 사용된다. 삼각함수의 장점은 다음 2가지다.

긴 시퀀스에 적용하는 경우에도 위치별로 극심한 값의 차이가 나지 않는다.

포지션 마다 서로 다른 값을 가진다. (?)

 

삼각함수는 주기함수고 주기함수는 값이 반복되는데? 싶을거다.

하지만 충분히 긴 주기를 가지면 값이 반복되도 크게 상관없다.

문장이 충분히 길어지면 단어간 어탠션 자체가 떨어지기 때문이다.

긴 주기를 구현하기 위해 위와 같은 수식을 썼다. (짝수는 사인함수, 홀수는 코사인함수)

 

6. Trained Weight

그래서 트랜스포머가 학습한 웨이트는 뭘까?

구조가 낯설어서 눈에 전혀 안들어오는데....

 

정답은 어탠션으로 들어가기 전에 Q, K, V를 구하는 weight 다.

아래 보이는 가중치 행렬 3개를 각 어탠션 3곳에서 학습한다.

쿼리(Q) = 입력 벡터(V) x 가중치 행렬(Q)

키(K) = 입력 벡터(V) x 가중치 행렬(K)

밸류(V) = 입력 벡터(V) x 가중치 행렬(V)

 

물론 뉴럴 네트워크 weight도 학습힌다.

(body : Feed Forward layer에서 FC 레이어 weight 값)

(head : 마지막 FC 레이어 weight 값)

 

이 아이디어들을 활용한 전체 Transformer 아키텍쳐는 다음과 같다.

아키텍쳐
아키텍쳐

 

논문 구현

1. multi-head attention

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, h, qkv_fc, out_fc, dr_rate=0):
        super(MultiHeadAttention, self).__init__()
        self.d_model = d_model
        self.h = h
        self.q_fc = copy.deepcopy(qkv_fc)
        self.k_fc = copy.deepcopy(qkv_fc)
        self.v_fc = copy.deepcopy(qkv_fc)
        self.out_fc = out_fc
        self.dropout = nn.Dropout(p=dr_rate)

    def calculate_attention(self, query, key, value, mask):
        d_k = key.shape[-1]
        attention_score = torch.matmul(query, key.transpose(-2, -1))
        attention_score = attention_score / math.sqrt(d_k)
        if mask is not None:
            attention_score = attention_score.masked_fill(mask == 0, -1e9)
        attention_prob = F.softmax(attention_score, dim=-1)
        attention_prob = self.dropout(attention_prob)
        out = torch.matmul(attention_prob, value)
        return out

    def forward(self, *args, query, key, value, mask=None):
        n_batch = query.size(0)

        def transform(x, fc):
            out = fc(x)
            out = out.view(n_batch, -1, self.h, self.d_model//self.h)
            out = out.transpose(1, 2)
            return out

        query = transform(query, self.q_fc)
        key = transform(key, self.k_fc)
        value = transform(value, self.v_fc)

        out = self.calculate_attention(query, key, value, mask)
        out = out.transpose(1, 2)
        out = out.contiguous().view(n_batch, -1, self.d_model)
        out = self.out_fc(out)
        return out

h개로 나눈 뒤 멀티헤드 어텐션을 진행한다.

 

2. Position-wise Feed-Forward Networks

class FFN(nn.Module):

    def __init__(self, fc1, fc2, dr_rate=0):
        super(FFN, self).__init__()
        self.fc1 = fc1
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(p=dr_rate)
        self.fc2 = fc2

    def forward(self, x):
        out = x
        out = self.fc1(out)
        out = self.relu(out)
        out = self.dropout(out)
        out = self.fc2(out)
        return out

FC 레이어 2개와 Relu로 간단하게 구성할 수 있다.

끝.

 

관련 논문 리스트 (스크롤 내려서 Paper List 참고)

반응형