Transformer

snooby·2022년 11월 21일
4

🧠 AI

목록 보기
3/3
post-thumbnail

트랜스포머의 영향

트랜스포머는 2017년 구글이 제안한 시퀀스 투 시퀀스 모델입니다.
자연어처리에 큰 획을 그은 BERT, GPT pretrained model은 트랜스포머 기반의 언어모델로,
트랜스포머의 등장 후 NLP의 역량이 크게 증가하였다고 봅니다.

Paper

All You Need is Attention

시퀀스 투 시퀀스

트랜스포머는 기계번역 등 시퀀스 투 시퀀스 과제를 수행하기 위한 모델입니다.
시퀀스란 나열로, 문장에서 보자면 단어 들의 나열로 볼 수 있습니다.

즉, 시퀀스 투 시퀀스어떠한 시퀀스를 다른 속성의 시퀀스로 변환하는 작업을 의미합니다

가령, 기계번역의 경우 한글의 단어 시퀀스를 영어의 단어 시퀀스로 변환하는 과정입니다.
그러나, 번역을 하게되면 아래와 같이 단어 시퀀스의 길이가 달라지는 경우가 발생합니다.

나는 집에 가고싶다. -> I want to go home

실제 번역, 시퀀스 투 시퀀스 작업에서는 시퀀스 길이가 다른 것이 문제를 일으켜서는 안된다.

인코더와 디코더

시퀀스 투 시퀀스 모델은 대게 인코더와 디코더로 구성되어있다.

인코더는 시퀀스 정보를 압축하고, 디코더는 인코더에서 전달받은 시퀀스를 가지고 타깃 시퀀스를 생성합니다.

논문 Figure 1.

실제 트랜스포머 모델을 보면 다음과 같이 좌측 인코더, 우측 디코더로 구성되어있음을 알 수 있습니다.

인코더 디코더 학습방법

인코더 (좌)는 Input Embedding 즉, 전체 입력 문장을 인풋으로 받게됩니다.
그러면 인코더는 전체 입력 문장을 압축해 디코더로 보내고,
디코더는 인코더가 압축한 문장 정보와 디코더 입력값 2가지를 입력받아 다음 토큰을 맞춥니다.

디코더가 맞추다는 다음 토큰은 타깃 언어의 어휘 수만큼의 차원으로 구성된 벡터입니다.

그리고, 그 벡터는 모두 각 타깃 언어가 다음 토큰일 확률로 이루어져있습니다.
예시로 설명을 드리겠습니다.
나는 집에 가고싶다. 라는 문장이 인코더로 들어오면,인코더는 다음 문장을 압축해서 디코더로 보내고,
디코더는 인코더에서 보낸 정보와 인풋 s를 함께 받아 다음 토큰이 될 I를 예측하는 벡터를 내뱉는 것이다.
s 란, 디코더의 처음 인풋으로 시퀀스의 시작을 의미하는 스페셜 토큰입니다.

디코더 입력값

디코더 입력값은 인퍼런스 때와 학습 때에 상이합니다.

디코더 입력값은 다음과 같이 인코더 압축값 & 이전까지의 타깃값들 입니다.
다음과 같이 디코더가 실제 다음에 올 토큰을 잘 맞춰준다면, 이어서 올 토큰도 정답을 맞출 확률이 높아지겠죠.

그렇지만, 만일 디코더가 I를 맞춰야하는데 I가 아니라 you를 예측했다면 s you가 다음 토큰을 예측하는 인풋값으로 주어질 것입니다. 그렇다면, 실제 다음 토큰일 to를 예측할 확률을 낮아지겠죠.
그래서 학습시에는 모델의 성능을 높이기위해, 디코더 입력값으로 인코더 압축값 & 맞혀야할 단어 이전의 !!정답 타깃 시퀀스를 넣어줍니다.
하지만, 실제 인퍼런스 때에는 디코더의 출력값들과 인코더 압축값을 입력으로 넣어줍니다.

트랜스포머 블록

트랜스포머의 특징 중 하나는 인코더 디코더의 무수한 반복을 통해 훌륭한 성능의 언어모델을 만들었다는 것입니다.

Encoder: The encoder is composed of a stack of N = 6 identical layers
실제 논문을 보면, 인코더를 6개 구성하여 반복하였다고 말하고 있습니다.

그렇다면, 트랜스포머 인코더 블록에 대하여 알아봅시다.

인코더 블록

인코더 블록은 다음과 같이 세 가지 요소로 구성어 있습니다.

  • 멀티 헤드 어텐션 (Multi-Head Attention)
  • 피드포워드 뉴럴네트워크 (Feed Forward)
  • 잔차연결 및 레이어 정규화 (Add & Norm)

Encoder Block의 역할은 무엇일까? 결론부터 말하자면, 각 Encoder Block은 input으로 들어오는 vector에 대해 더 높은 차원(넓은 관점)에서의 context를 담는다. 높은 차원에서의 context라는 것은 더 추상적인 정보라는 의미이다. Encoder Block은 내부적으로 어떠한 Mechanism을 사용해 context를 담아내는데, Encoder Block이 겹겹이 쌓이다 보니 처음에는 원본 문장에 대한 낮은 수준의 context였겠지만 이후 context에 대한 context, context의 context에 대한 context … 와 같은 식으로 점차 높은 차원의 context가 저장되게 된다.

class EncoderBlock(nn.Module):

    def __init__(self, self_attention, position_ff):
        super(EncoderBlock, self).__init__()
        self.self_attention = self_attention 
        self.position_ff = position_ff


    def forward(self, x):
        out = x
        out = self.self_attention(out)
        out = self.position_ff(out)
        return out


Encoder Block이 N개 쌓여진 형태이다. 논문에서는 N=6을 사용했다. Encoder Block은 input과 output의 형태가 동일하다. 어떤 matrix를 input으로 받는다고 했을 때, Encoder Block이 도출해내는 output은 input과 완전히 동일한 shape를 갖는 matrix가 된다. 즉, Encoder Block은 shape에 대해 멱등(Idempotent)합니다.

디코더 블록

디코더 블럭은 다음과 같이 4가지 요소로 구성되어 있습니다.

  • 마스크드 멀티 헤드 어텐션
  • 멀티 헤드 어텐션
  • 피드포워드 뉴럴 네트워크
  • 잔차 연결 및 레이어 정규화

Decoder Block은 Encoder Block과 달리 Multi-Head Attention Layer가 2개가 존재합니다.
첫번째 layer는 Self-Multi-Head Attention Layer라고 부르는데, 이름 그대로 Decoder의 input으로 주어지는 sentence 내부에서의 Attention을 계산합니다. 이 때, 일반적인 pad masking뿐만 아니라 subsequent masking이 적용되기 떄문에 Masked-Multi-Head Attention Layer라고 부르기도 한다.
두번째 layer는 Encoder에서 넘어온 context를 Key, Value로 사용한다는 점에서 Cross-Multi-Head Attention Layer라고 부른다. 즉, Encoder의 context는 Decoder 내 각 Decoder Block의 Cross-Multi-Head Attention Layer에서 사용되게 된다.
마지막 Position-wise Feed-Forward Layer는 Encoder Block의 것과 완전히 동일합니다.

이전 모델의 한계 극복

이전에는 CNN, RNN 을 활용하여 더 나은 임베딩을 하였습니다.

1. CNN

cnn의 합성곱 필터는 시퀀스의 지역적 특징을 잘 잡아낼 수 있었습니다.
자연어는 문맥적으로 특정단어의 문장 상 의미를 주변 단어를 통해 생성하고 유추하기 쉽기에 cnn 의 필터를 활용한 자연어 처리를 사용했으나, 이는 필터 크기 넘어의 단어를 고려할 수 없기에 필터 크기 이상의 거리의 단어를 고려할 수 없다는 단점이 있습니다.

2. RNN

문장은 흐름을 따라 문장의 구조와 문맥이 파악됩니다.
따라서, RNN의 순서를 고려하는 모델은 문맥을 파악하는 데 도움이 되었습니다.
하지만, RNN의 단점상 오래된 정보는 망각해버리는 점 때문에 이전의 시퀀스 정보를 문맥에 반영하지 못한다는 단점이 있습니다.

어텐션은 이러한 기존 모델의 단점을 극복하였습니다.
어텐션은 인풋 시퀀스 전체를 디코딩할 단어와 연관하여 디코딩할 단어와의 연관도를 확률로 나타내기 때문에
시퀀스가 아무리 길어도 모두 고려할 수 있고, 또 그 중 의미있는 단어에 더 가중치를 두어 반영하기
의미를 더 잘 담은 벡터를 만들 수 있는 겁
니다.

디코더에 어텐션을 추가하여 어텐션은 디코더가 타깃 시퀀스를 생성할 때 소스 시퀀스 전체에서 어떤 요소에 주목해야 할지 알려주므로 카페가 소스 시퀀스 초반에 등장하거나 소스 시퀀스의 길이가 길어지더라도 번역 품질이 떨어지는 것을 막을 수 있습니다.

셀프 어텐션

셀프 어텐션은 자기 자신에 어텐션을 수행하는 것이며 트랜스포머의 멀티 헤드 어텐션이 셀프 어텐션이라고 불립니다.
Scaled Dot-Proudct-Attention을 병렬적으로 여러 개 수행하는 layer이다.

어텐션

Scaled Dot-Product Attention 자체를 줄여서 Attention으로 부르기도 한다.
어텐션은 시퀀스 요소 중 중요한 요소에 더 집중하고 그렇지 않은 요소에 집중을 덜하자.
즉, 시퀀스에서 의미있는 값에 더 집중하게 하여 성능을 높입니다.
넓은 범위의 전체 data에서 특정한 부분에 집중한다는 의미이다.

The animal didn’t cross the street, because it was too tired.

위 문장에서 ‘it’은 무엇을 지칭하는 것일까? 사람이라면 직관적으로 ‘animal’과 연결지을 수 있지만, 컴퓨터는 ‘it’이 ‘animal’을 가리키는지, ‘street’를 가리키는지 알지 못한다.
Attention은 이러한 문제를 해결하기 위해 두 token 사이의 연관 정도를 계산해내는 방법론이다. 위의 경우에는 같은 문장 내의 두 token 사이의 Attention을 계산하는 것이므로, Self-Attention이라고 부른다.
반면, 서로 다른 두 문장에 각각 존재하는 두 token 사이의 Attention을 계산하는 것을 Cross-Attention이라고 부른다.

셀프 어텐션은 말 그대로, 자기자신에 수행하는 어텐션 기법입니다.
입력 시퀀스 가운데 수행에 더 유의미한 요소를 중심으로 정보를 추출한다는 것입니다.

인코더에서 시퀀스를 압축할 때, 보다 더 특정 시퀀스의 의미를 잘 내포한 임베딩을 만들 수 있습니다.
해당 시퀀스를 인코딩할 때, 시퀀스 전체를 어텐션하면서 토큰들을 인코딩하므로 전체 의미를 보기에 가령 이 시퀀스의 경우 “거기"라는 토큰은 “카페"라는 토큰을 의미하겠구나, 높은 연관성을 가지겠구나 라는 것을 학습할 수 있는 것이다.

어텐션 학습방법

어텐션은 쿼리(query), 키(key), 밸류(value) 세 가지 요소가 서로 영향을 주고 받는 구조입니다.

Query: 현재 시점의 token을 의미
Key: attention을 구하고자 하는 대상 token을 의미
Value: attention을 구하고자 하는 대상 token을 의미 (Key와 동일한 token)

The animal didn’t cross the street, because it was too tired.
위 문장에서 ‘it’이 어느 것을 지칭하는지 알아내고자 하는 상황이다.

그렇다면 ‘it’ token과 문장 내 다른 모든 token들에 대해 attention을 구해야 합니다. 이 경우에는 Query는 ‘it’으로 고정이고 Key, Value는 서로 완전히 같은 token을 가리키는데, 문장의 시작부터 끝까지 모든 token들 중 하나가 될 것입니다. Key와 Value가 ‘The’를 가리킬 경우 ‘it’과 ‘The’ 사이의 attention을 구하는 것이고, Key와 Value가 마지막 ‘tired’를 가리킬 경우 ‘it’과 ‘tired’ 사이의 attention을 구하는 것이 됩니다.

즉, Key와 Value는 문장의 처음부터 끝까지 탐색한다고 이해하시면 됩니다.
Query는 고정되어 하나의 token을 가리키고, Query와 가장 부합하는(Attention이 가장 높은) token을 찾기 위해서 Key, Value를 문장의 처음부터 끝까지 탐색시키는 것이다.

각각의 의미는 이해했으나, Key와 Value가 완전히 같은 token을 가리킨다면 왜 두 개가 따로 존재하는지 의문이 들 수 있는데요. 결론부터 말하자면 Key와 Value의 실제 값은 다르지만 의미적으로는 여전히 같은 token을 의미한다. Key와 Value는 이후 Attention 계산 과정에서 별개로 사용하게 된다.
이를 좀 더 제대로 이해하기 위해 Query, Key, Value 벡터가 어떻게 생성되는 지 알아봅시다.

Query, Key, Value 벡터 생성

input으로 들어오는 token embedding vector를 fully connected layer에 넣어 세 vector를 생성합니다. 세 vector를 생성해내는 FC layer는 모두 다르기 때문에, 결국 self-attention에서는 Query, Key, Value를 구하기 위해 3개의 서로 다른 FC layer가 존재합니다.
FC layer들은 모두 같은 input shape, output shape를 갖습니다. input shape가 같은 이유는 당연하게도 모두 다 동일한 token embedding vector를 input으로 받기 때문입니다.
한편, 세 FC layer의 output shape가 같다는 것을 통해 각각 별개의 FC layer로 구해진 Query, Key, Value가 구체적인 값은 다를지언정 같은 shape를 갖는 vector가 된다는 것을 알 수 있습니다. 정리하자면, Query, Key, Value의 shape는 모두 동일합니다.

Scaled Dot-Product Attention

Attention은 구체적으로, Query에 대한 Attention입니다.
수식은 다음과 같습니다.

흐름으로 표현하면 다음과 같습니다.

Q는 현재 시점의 token을, K와 V는 Attention을 구하고자 하는 대상 token을 의미하여 Q,K를 통해 연관성 중요도를 파악해내고 나온 확률값을 V에 적용하여 연관성이 반영된 V 를 만들어내는 것입니다.

위의 수식을 좀 더 자세히 파악해보겠습니다.
Q 와 K를 MatMul(행렬곱)한다는 의미는 어떤 의미일까요? 이 둘을 곱한다는 것은 둘의 Attention Score를 구한다는 것이다. Q와 K의 shape를 생각해보면, 둘 모두 dk를 dimension으로 갖는 vector이다. 이 둘을 곱한다고 했을 때(정확히는 K를 transpose한 뒤 곱함, 즉 두 vector의 내적), 결과값은 어떤 scalar 값이 나오게 될 것이다. 이 값을 Attention Score라고 한다. 이후 scaling을 수행하는데, 값의 크기가 너무 커지지 않도록 루트d_k로 나눠준다. 값이 너무 클 경우 gradient vanishing이 발생할 수 있기 때문이다. scaling을 제외한 연산 과정은 아래와 같습니다.

Q는 고정된 token을 가리키고, Q가 가리키는 token과 가장 높은 Attention을 갖는 token을 찾기 위해 K, V를 문장의 첫 token부터 마지막 token까지 탐색시키게 됩니다.
즉, Attention을 구하는 연산이 Q 1개에 대해서 수행된다고 가정했을 때, K, V는 문장의 길이 n만큼 반복되게 됩니다.

Q vector 1개에 대해서 Attention을 계산한다고 했을 때, K와 V는 각각 n개의 vector가 됩니다. 즉, Q, K, V vector의 dimension은 모두 dk로 동일합니다.


따라서, Attention Score는 아래와 같이 계산됩니다.

이렇게 구한 Attention Score는 softmax를 사용해 확률값으로 변환됩니다.
이 값들의 의미는 Q의 token과 해당 token이 얼마나 Attention을 갖는지(얼마나 연관성이 짙은지)에 대한 비율(확률값)이 되고 최종적으로 V와 곱하여 가중합을 하게되는 것입니다.

셀프 어텐션 vs 어텐션

  • seq2seq에서 어텐션에서 Q, K, V의 정의
    Q = Query : t 시점의 디코더 셀에서의 은닉 상태
    K = Keys : 모든 시점의 인코더 셀의 은닉 상태들
    V = Values : 모든 시점의 인코더 셀의 은닉 상태들
    그런데 t라는 건 계속 변화하고, 변화하며 쿼리를 수행하므로 결국 전체 시점에 대해 일반화가 가능합니다.

  • 전체 시점의 일반화한 Q, K, V의 정의
    Q = Querys : 모든 시점의 디코더 셀에서의 은닉 상태들
    K = Keys : 모든 시점의 인코더 셀의 은닉 상태들
    V = Values : 모든 시점의 인코더 셀의 은닉 상태들
    이처럼 기존에는 디코더 셀의 은닉 상태가 Q이고 인코더 셀의 은닉 상태가 K라는 점에서 Q와 K가 서로 다른 값을 가지고 있었습니다. 셀프 어텐션은 Q, K, V가 전부 동일합니다.

  • 트랜스포머 셀프 어텐션의 Q, K, V의 정의
    그래서 트랜스포머의 셀프 어텐션의 Q, K, V의 의미는
    Q : 입력 문장의 모든 단어 벡터들
    K : 입력 문장의 모든 단어 벡터들
    V : 입력 문장의 모든 단어 벡터들

    이 됩니다.

이렇게 구해진 최종 result는 기존의 Q, K, V와 같은 dimension(dk)를 갖는 vector 1개라는 점이 주목해야할 부분입니다. 즉, input으로 Q vector 1개를 받았는데, 연산의 최종 output이 input과 같은 shape를 갖습니다.

따라서 Self-Attention 연산 역시 shape에 멱등(Idempotent)하다.

셀프 어텐션 소스

def calculate_attention(query, key, value, mask):
    # query, key, value: (n_batch, seq_len, d_k)
    # mask: (n_batch, seq_len, seq_len)
    d_k = key.shape[-1]
    attention_score = torch.matmul(query, key.transpose(-2, -1)) # Q x K^T, (n_batch, seq_len, seq_len)
    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) # (n_batch, seq_len, seq_len)
    out = torch.matmul(attention_prob, value) # (n_batch, seq_len, d_k)
    return out

마스크 셀프어텐션

디코더에서 수행하는 셀프 어텐션입니다.
디코더 입력은 인코더 마지막 블록에서 나온 소스 & 이전 디코더 블록의 수행 결과로 도출된 타깃 단어 벡터 시퀀스입니다.

트랜스포머 모델의 최종 출력은 타겟 시퀀스 각각에 대한 확률 분포. 모델이 한국어를 영어로 번역하는 태스크를 수행하고 있다면 영어 문장의 다음 단어가 어떤 것이 적절할지에 관한 확률이 됩니다.
가령, 인코더에 나는 집에 가고싶다. 디코더에 s가 입력된 상황이라면 트랜스포머 모델은 다음 영어 단어 I를 맞추도록 학습됩니다.
하지만 학습 과정에서 모델에 이번에 맞춰야할 정답인 I를 알려주게 되면 학습하는 의미가 사라집니다.
따라서 정답을 포함한 미래 정보를 셀프 어텐션 계산에서 제외하게 됩니다. 이 때문에 디코더 블록의 첫번째 어텐션을 마스크 멀티-헤드 어텐션(Masked Multi-Head Attention)이라고 부릅니다.
마스킹은 확률이 0이 되도록 하여, 밸류와의 가중합에서 해당 단어 정보들이 무시되게끔 하는 방식으로 수행됩니다.

마스크 소스

def make_pad_mask(self query, key, pad_idx=1):
    # query: (n_batch, query_seq_len)
    # key: (n_batch, key_seq_len)
    query_seq_len, key_seq_len = query.size(1), key.size(1)

    key_mask = key.ne(pad_idx).unsqueeze(1).unsqueeze(2)  # (n_batch, 1, 1, key_seq_len)
    key_mask = key_mask.repeat(1, 1, query_seq_len, 1)    # (n_batch, 1, query_seq_len, key_seq_len)

    query_mask = query.ne(pad_idx).unsqueeze(1).unsqueeze(3)  # (n_batch, 1, query_seq_len, 1)
    query_mask = query_mask.repeat(1, 1, 1, key_seq_len)  # (n_batch, 1, query_seq_len, key_seq_len)

    mask = key_mask & query_mask
    mask.requires_grad = False
    return mask

디코더 Cross-Multi-Head Attention Layer

Decoder의 가장 핵심적인 부분이다. Decoder Block 내 이전 Self-Multi-Head Attention Layer에서 넘어온 output을 input으로 받는다. 여기에 추가적으로 Encoder에서 도출된 context도 input으로 받는다. 두 input의 사용 용도는 완전히 다르다.
Decoder Block 내부에서 전달된 input(Self-Multi-Head Attention Layer의 output)은 Query로써 사용하고, Encoder에서 넘어온 context는 Key와 Value로써 사용하게 된다. 이 점을 반드시 기억하고 넘어가자.
정리하자면 Decoder Block의 2번째 layer인 Cross-Multi-Head Attention Layer는 Decoder에서 넘어온 input의 Encoder에서 넘어온 input에 대한 Attention을 계산하는 것이다. 따라서 Self-Attention이 아닌 Cross-Attention이다.
우리가 Decoder에서 도출해내고자 하는 최종 output은 teacher forcing(Q)으로 넘어온 sentence와 최대한 유사한 predicted sentence이다.
따라서 Decoder Block 내 이전 layer에서 넘어오는 input이 Query가 되고, 이에 상응하는 Encoder에서의 Attention을 찾기 위해 context를 Key, Value로 두게 된다.
번역 task를 생각했을 때 가장 직관적으로 와닿는다. 만약 영한 번역을 수행하고자 한다면, Encoder의 input은 영어 sentence일 것이고, Encoder가 도출해낸 context는 영어에 대한 context일 것이다. Decoder의 input(teacher forcing)과 output은 한글 sentence일 것이다. 따라서 이 경우에는 Query가 한글, Key와 Value는 영어가 되어야 한다.

Teacher Forcing

Decoder의 input에 추가적으로 들어오는 sentence를 이해하기 위해서는 Teacher Forcing라는 개념에 대해 알고 있어야 한다. RNN 계열이든, Transformer 계얼이든 번역 model이 있다고 생각해보자. 결국에는 새로운 sentence를 생성해내야만 한다. 힘들게 만들어낸 model이 초창기 학습을 진행하는 상황이다. random하게 초기화된 parameter들의 값 때문에 엉터리 결과가 나올 것이다. RNN으로 생각을 해봤을 때, 첫번째 token을 생성해내고 이를 다음 token을 생성할 때의 input으로 활용하게 된다. 즉, 현재 token을 생성할 때 이전에 생성한 token들을 활용하는 것이다. 그런데 model의 학습 초반 성능은 말그대로 엉터리 결과일 것이기 떄문에, model이 도출해낸 엉터리 token을 이후 학습에 사용하게 되면 점점 결과물은 미궁으로 빠질 것이다. 이러한 현상을 방지하기 위해서 Teacher Forcing을 사용하게 된다.

Teacher Forcing이란, Supervised Learning에서 label data를 input으로 활용하는 것이다.

RNN으로 번역 model을 만든다고 할 때, 학습 과정에서 model이 생성해낸 token을 다음 token 생성 때 사용하는 것이 아닌, 실제 label data의 token을 사용하게 되는 것이다.
하지만 Transformer가 RNN에 비해 갖는 가장 큰 장점은 병렬 연산이 가능하다는 것이었다. 병렬 연산을 위해 ground truth의 embedding을 matrix로 만들어 input으로 그대로 사용하게 되면, Decoder에서 Self-Attention 연산을 수행하게 될 때 현재 출력해내야 하는 token의 정답까지 알고 있는 상황이 발생한다. 따라서 masking을 적용해야 한다. i번째 token을 생성해낼 때, 1∼i−1의 token은 보이지 않도록 처리를 해야 하는 것이다. 이러한 masking 기법을 subsequent masking이라고 한다.

def make_subsequent_mask(query, key):
    # query: (n_batch, query_seq_len)
    # key: (n_batch, key_seq_len)
    query_seq_len, key_seq_len = query.size(1), key.size(1)

    tril = np.tril(np.ones((query_seq_len, key_seq_len)), k=0).astype('uint8') # lower triangle without diagonal
    mask = torch.tensor(tril, dtype=torch.bool, requires_grad=False, device=query.device)
    return mask

[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
0번쨰 token은 자기 자신밖에 보지 못하고, 1~n번쨰 token은 0으로 가려져 있으며, 1번째 token은 0~1번째 token밖에 보지 못하고, 2~n번째 token은 모두 0으로 가려져 있다. 최종적으로 n번쨰 token은 모든 token을 볼 수 있다. 이렇듯, Decoder의 mask는 subsequent masking이 적용되어야 한다.

멀티 헤드 어텐션

셀프 어텐션(self attention)을 여러 번 수행한 걸 가리킵니다. 여러 헤드가 독자적으로 셀프 어텐션을 계산합니다.
논문에서는 512차원(d_model의 차원)의 각 단어 벡터를 8(num_head)로 나누어 64차원의 Q,K,V 벡터로 바꾸어 어텐션을 수행하였습니다.

왜 그대로 어텐션을 하지 않고, num_head만큼 나누워 차원을 줄여서 어텐션을 수행했을까요?
한번에 어텐션을 하는 것보다 나누어 병렬로 진행하는 것이 더 효과적이라고 생각했기 때문입니다.

그래서 논문의 저자는 d_model/num_heads 차원을 갖는 Q,K,V에 대하여 num_heads개의 병렬 어텐션을 수행했습니다.

위 그림은 입력 단어 수는 2개, 밸류의 차원수는 3, 헤드는 8개인 멀티-헤드 어텐션을 나타낸 그림입니다. 개별 헤드의 셀프 어텐션 수행 결과는 ‘입력 단어 수 × 밸류 차원수’, 즉 2×3 크기를 갖는 행렬입니다. 8개 헤드의 셀프 어텐션 수행 결과를 다음 그림의 ①처럼 이어 붙이면 2×24 의 행렬이 됩니다.

논문은 num_heads를 8로 지정했고, 8개의 병렬 어텐션이 이뤄집니다.
이때 각각의 어텐션 값 행렬을 어텐션 헤드라고 부릅니다.

가중치 행렬의 값은 8개의 어텐션 헤드마다 전부 다릅니다.
그 이유는 병렬로 수행하며 다른 시각으로 서로 다른 정보들을 수집하겠다는 겁니다.
즉, 서로 다른 가중치의 8개 어텐션 헤드는 서로 다른 8개의 시각을 의미합니다.

병렬 어텐션을 모두 수행한 후 모든 어텐션 헤드를 연결(concat)합니다.
모두 연결한 헤드의 크기는 (seq_len, d_model)이 됩니다.

입력 단어 수는 2개, 밸류의 차원수는 3, 헤드는 8개인 멀티-헤드 어텐션을 나타낸 그림입니다.

개별 헤드의 셀프 어텐션 수행 결과는 ‘입력 단어 수 x 밸류 차원수’ ,
멀티-헤드 어텐션의 최종 수행 결과는 ‘입력 단어 수 x 목표 차원수’입니다.

최종적으로 생성해된 matrix (n×dmodel)를 FC layer에 넣어 multi-head attention의 input과 같은 shape(n×dembed)의 matrix로 변환하는 과정이 필요합니다. 따라서 마지막 FC layer의 input dimension은 dmodel, output dimension은 dembed가 되며 multi-head attention layer도 하나의 함수라고 생각했을 때, input의 shape와 output의 shape가 동일하게 하기 위함이다.

멀티헤드 어텐션 소스

class MultiHeadAttentionLayer(nn.Module):

    def __init__(self, d_model, h, qkv_fc, out_fc):
        super(MultiHeadAttentionLayer, self).__init__()
        self.d_model = d_model
        self.h = h
        self.q_fc = copy.deepcopy(qkv_fc) # (d_embed, d_model)
        self.k_fc = copy.deepcopy(qkv_fc) # (d_embed, d_model)
        self.v_fc = copy.deepcopy(qkv_fc) # (d_embed, d_model)
        self.out_fc = out_fc              # (d_model, d_embed)

        
    def forward(self, *args, query, key, value, mask=None):
        # query, key, value: (n_batch, seq_len, d_embed)
        # mask: (n_batch, seq_len, seq_len)
        # return value: (n_batch, h, seq_len, d_k)
        n_batch = query.size(0)

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

        query = transform(query, self.q_fc) # (n_batch, h, seq_len, d_k)
        key = transform(key, self.k_fc)     # (n_batch, h, seq_len, d_k)
        value = transform(value, self.v_fc) # (n_batch, h, seq_len, d_k)

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

Positional Encoding

인코더 입력은 소스 시퀀스의 입력 임베딩(input embedding)에 위치 정보(positional encoding)을 더해서 만듭니다. 입력 임베딩에 더하는 위치 정보는 해당 토큰이 문장 내에서 몇 번째 위치인지 정보를 나타냅니다.
어순은 언어를 이해하는 데 중요한 역할을 하기에 이 정보에 대한 처리가 필요하다.
따라서 이 논문의 저자가 채택한 방식은 attention layer에 들어가기 전에 입력값으로 주어질 단어 vector 안에 positional encoding 정보, 즉, 단어의 위치 정보를 포함시키고자 하는 것이다.

논문에서 저자는 위치정보를 포함하는 두 가지 방법과 각 한계를 제시했다.
1) 데이터에 0~1사이의 label을 붙인다. 0이 첫번째 단어, 1이 마지막 단어
→ I love you: I 0 /love 0.5/ you 1
한계 : Input의 총 크기를 알 수 없다. 따라서 delta 값이 일정한 의미를 갖지 않는다.(delta = 단어의 label 간 차이)
2) 각 time-step마다 선형적으로 숫자를 할당하는 것이다.(총 크기에 따라 가변적, delta일정해짐)
→ I love you: I 1/ love 2/you 3
한계 : 숫자가 매우 커질 수 있고, 훈련 시 학습할 때보다 큰 값이 입력값으로 들어오게 될 때 문제 발생 모델의 일반화 가 어려워짐-특정한 범위 값을 갖는게 아니기에

이에 대해 저자는 이상적인 모델은 다음과 같은 기준을 충족시켜야 한다고 말한다.
1) 각 time-step(문장에서 단어의 위치)마다 하나의 유일한 encoding 값을 출력해 내야 한다.
2) 서로 다른 길이의 문장에 있어서 두 time-step 간 거리는 일정해야 한다.
3) 모델에 대한 일반화가 가능해야 한다. 더 긴 길이의 문장이 나왔을 때 적용될 수 있어야 한다.
즉, 순서를 나타내는 값 들이 특정 범위 내에 있어야 한다.
4) 하나의 key 값처럼 결정되어야 한다. 매번 다른 값이 나와선 안된다.

결론적으로, 트랜스포머는 d-dimensional vector로 문장 내 특정 위치 정보를 표현했다.
이 인코딩은 모델 자체 내에서 사용하지 않고, 각 단어에 붙어 문장 내 위치 정보를 표시하게 되었다.
즉, 단어의 순서 정보를 표현하기 위해 input을 늘렸다.

vector의 dimension에 따라 frequency가 줄어듦(sin/cos의 주기가 길어짐)을 알 수 있다.
따라서, positional embedding으로 들어가는 P에 대해 각 frequency에 대해 sin, cos 쌍으로 표현하였다.

class TokenEmbedding(nn.Module):

    def __init__(self, d_embed, vocab_size):
        super(TokenEmbedding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, d_embed)
        self.d_embed = d_embed


    def forward(self, x):
        out = self.embedding(x) * math.sqrt(self.d_embed)
        return out

트랜스 포머 블록

멀티 헤드 어텐션, 피드포워드 뉴럴 네트워크, 잔차 연결 및 레이어 정규화 등 세 가지 구성 요소를 기본으로 합니다.
Multi-Head Attention은 앞서 설명했으므로, 나머지 구성 요소인 FeedForward, Add&Norm을 차례대로 살펴보겠습니다.

FeedForward

벡터 시퀀스인데 벡터 각각을 피드포워드 뉴럴네트워크에 입력합니다.

class PositionWiseFeedForwardLayer(nn.Module):

    def __init__(self, fc1, fc2):
        super(PositionWiseFeedForwardLayer, self).__init__()
        self.fc1 = fc1   # (d_embed, d_ff)
        self.relu = nn.ReLU()
        self.fc2 = fc2 # (d_ff, d_embed)


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

Add&Norm

동일한 블록 계산이 계속될 때 잔차 연결을 두는 것은 제법 큰 효과가 있습니다.
잔차 연결을 두지 않았을 때는 f1,f2,f3 을 연속으로 수행하는 경로 한 가지만 존재하나,
잔차 연결을 블록마다 설정해둠으로써 모두 8가지의 새로운 경로가 생겼습니다.
다시 말해 모델이 다양한 관점에서 블록 계산을 수행하게 된다는 이야기입니다.

레이어는 전레이어의 값을 전달받아 학습하므로 계속되다보면 모델이 깊 (layer가 많은) 모델의 경우 예전의 정보를 소실할 수 있게된다. 이는 결국 학습 에러를 초래할 수도 있다.
그런데 잔차연결을 하게되면 계속해서 기존의 정보를 가져갈 수 있기때문에 학습의 문제를 예방할 수 있다.
잔차연결과 비슷한 것이 shortcut connection이다.
shortcut connection은 한 개 이상의 layers를 skipping하는 것을 의미한다.

class ResidualConnectionLayer(nn.Module):

    def __init__(self):
        super(ResidualConnectionLayer, self).__init__()


    def forward(self, x, sub_layer):
        out = x
        out = sub_layer(out)
        out = out + x
        return out

Transformer의 장점

transformer 모델의 핵심은 attention 함수이며, attention 함수가 들어간 layer에서는 구조적으로 시간적 연속성이 없이 parell하게 입력값을 다루게 된다.
따라서, 데이터가 통과하는 layer의 수를 줄일 수 있어 연산에서의 이득과, RNN 류 모델의 학습 과정에서 발생하는 기울기 소실/폭발 등에서 자유롭다.

profile
데이터를 가치있게 다루고 싶은 개발자 🐥

0개의 댓글