본문 바로가기

Data Science/밑바닥부터시작하는딥러닝(2)

[밑시딥] Transformer 동작과정을 밑바닥부터 뜯어보자!

반응형

🔊 해당 포스팅은 밑바닥부터 시작하는 딥러닝 2권 책에 나오진 않지만 책의 개념을 기반해 작성함을 알립니다. 또한 Transformer의 동작과정을 자세하게 이해하는 데 큰 참조 자료가 된 원본 블로그 출처는 여기임을 알립니다.

 

< 수정 사항 >

- 아래 그림 자료에서 '아다마르 곱'을 사용해도 된다고 나와있지만 오타임을 알립니다! 행렬 내적(Matmul 또는 dot)을 수행해주어야 합니다!

 

Transformer 동작 과정을 보다 자세히 살펴보자


저번 포스팅에서는 Transformer 라는 모델이 등장하게 된 이유과 모델의 큰 구조를 그림으로 살펴면서 각 구성요소가 어떠한 역할을 했는지도 알아보았다. 아쉬웠던 점은 트랜스포머라는 모델이 $key, query, value$라는 것을 입력으로 받는다고 했는데, 구체적으로 이 입력들이 무엇을 의미하는지, 그리고 트랜스포머로 들어가서 대체 어떤 로직으로 처리되는지는 소개하지 않았다.

 

이번 포스팅에서는 그 궁금증을 해결하고자 한다. 이 포스팅 내용의 그림자료를 만들 때 많이 참고한 원본 블로그는 여기이다.  블로그 저자 분이 매우 쉽게 알려주므로 원본 블로그를 읽고 이 포스팅을 읽는 것도 좋을 듯 하다. 단, 원본 블로그는 Encoder에 대한 부분만 다룬다.

 

본문에 들어가기에 앞서 Transformer의 전체 구조 그림을 다시 상기시켜보자.

 

Transformer의 전체 구조

1. Transformer의 Encoder 뜯어보기

Encoder 부분에서도 가장 먼저 볼 부분은 Multi-head Self-Attention 계층 부분이다. 구체적으로는 [Input 시퀀스 ➜ Embedding Layer ➜ Positional Encoding ➜ Multi-head Self-Attention] 과정을 살펴볼 것이다. 그리고 우리가 현재 트랜스포머 모델로 해결하려는 번역 문제는 아래와 같다.

 

트랜스포머 모델로 해결하려는 문제

 

위와 같은 한국어 ➜ 영어 번역문제를 트랜스포머 모델로 해결하려고 할 때, Encoder 부분의 Multi-head Self-Attention 내부 과정을 살펴보도록 하자. 멀티-헤드 셀프-어텐션은 저번 포스팅에서 아래의 그림과 같이 구성되어 있다고 했다.

 

멀티-헤드 셀프-어텐션의 구성요소

1-1.  Scaled Dot-Product Attetnion의 Compatibility Function 뜯어보기

멀티-헤드 셀프-어텐션은 $h$개의 Scaled Dot-Product Attetnion으로 구성한 것이고, Scaled Dot-Product Attetnion 계층은 $key, query, value$의 모음인 $K, Q, V$를 입력으로 받는다고 했다. 그러면 Scaled Dot-Product Attetnion(그림에서 오른쪽)에서 Compatibility Function이라고 되어 있는 부분을 살펴보자. Compatibility Function은 $query, key$의 모음인 $Q, K$를 입력으로 받는다. 

 

Scaled Dot-Product Attetnion에서의 Compatibility Function 부분

 

현재 입력 시퀀스는 [나는 고양이 로소 이다] 이다. 토큰을 단어 단위로 분리했다고 가정한다. 그리고 그림에서 표기하고 있지만 $d$는 차원 수, $k$는 하나의 $key$, $q$는 하나의 $query$를 의미한다.

 

가장 먼저 Embedding Layer, Positional Encoding 기법들이 적용되는 부분을 살펴보자. 원본 입력 시퀀스가 토큰 단위($x_i$)로 Embedding Layer, Positional Encoding 기법들이 적용되고 난 후, $W^{k}_{i} (i = 1,2,3,4,5)$이라는 각 파라미터에 곱해지고난 후 $k_i (i = 1,2,3,4,5)$라는 값이 나오게 된다. 이 때, 차원수($d$)는 트랜스포머 논문에서 주장한 64라는 값으로 설정했다. 이렇게 계산된 $k_i$ 값이 바로 $key$를 의미한다. 그리고 여러개의 $k_i$가 모이게 되면 $key$의 모음이라고 했던 $K$가 된다.

 

다음은 $query$(보라색으로 칠해진 부분)에 대해 알아보자. $query$도 입력 시퀀스 토큰($x_i$)을 활용한다. 단, 이 때는 $key$ 와는 달리 Embedding Layer, Positional Encoding 기법을 적용하지 않고 바로 $W^{q}_{i} (i = 1,2,3,4,5)$이라는 각 파라미터에 곱해진다. 이렇게 계산된 값은 차원수($d$)가 64인 $q_i (i = 1,2,3,4,5)$ 값이 된다. 이쯤이면 눈치를 챘겠지만 $q_i$가 바로 $query$를 의미하고 여러개의 $q_i$가 모이게 되면 $query$의 모음이라고 했던 $Q$가 된다.

 

다음은 위에서 얻은 $Q$와 $K$를 모두 서로 곱해주어야 한다.(이 때, '곱해준다'라는 것은 하나의 스칼라 값으로 나와야 하기 때문에 행렬 내적을 수행한다. 행렬 내적을 수행하기 위해서 $Q$와 $K$ 차원 수가 같기 때문에 둘 중 하나를 Transpose 해서 곱해주면 되겠다.) 

 

위 그림의 빨간색 동그라미 부분을 보면 하나의 $query$에 해당하는 $q_1$이 모든 $key$들 즉, $k_i (i = 1,2,3,4,5)$들과 각각 곱해지는 것을 볼 수 있다. 곱해진 후, Scaling 항을 적용해주고 난 다음 Softmax 함수에 넣어 결과값들을 0과 1사이의 값으로 변환시켜준다. Softmax 함수를 거치고 난 후 나오게 되는 5개의 값인 $s_{11}, s_{12}, s_{13}, s_{14}, s_{15}$는 결국 추후에 $V$에 곱해줄 Weight의 일부분을 의미한다.

 

위와 같은 과정을 아래처럼 두번째 $query$인 $q_2$에도 똑같이 적용해준다. 

 

$q_2$와 $k_i$들에 대해서도 똑같이 수행해주자

 

결과적으로 $q_1$부터 $q_5$까지 위 과정을 총 5번 반복하게 되면 아래의 GIF 그림처럼 동작한다. 

 

총 5번의 과정을 반복!

 

이렇게 총 5번의 반복과정을 거치면  5개를 하나의 세트로 해서 5세트가 나오고 총 25개의 $s_{ij} (i,j = 1,2,3,4,5)$ 값들이 나오게 될 것이다. 이렇게 해서 Scaled Dot-Product Attetnion 계층의 Compatibility Function 동작과정을 살펴보았다. 다시 한번 말하지만 Compatibility Function은 $V$에 곱해줄 가중치(Weight)를 계산하는 것이 목적이었다. 이제 가중치(위 그림에서는 $s_{ij}$가 되겠다)를 활용해서 $V$에 곱해주는 과정(Weighted Sum)도 자세히 살펴보도록하자.

1-2.  Scaled Dot-Product Attetnion의 Weighted Sum 뜯어보기

아래 그림은 [1-1 목차] 계산 과정을 간소화시킨 후, 그 이후에 발생하는 Weighted Sum 과정을 그림으로 나타내었다.

 

Weighted Sum 하는 과정을 살펴보자

 

가장 먼저 살펴볼 부분은 파란색으로 되어 있는 $value$ 부분이다. $value$도 마찬가지로 입력 시퀀스의 토큰($x_i$)을 활용한다. 그리고 $W^{v}_{i}, (i = 1,2,3,4,5)$ 라는 파라미터와 각각 곱해줌으로써 $value$를 의미하는 $v_i (i = 1,2,3,4,5)$를 계산해낸다. 이 때, $v_i$ 차원수($d$)도 $key, query$와 마찬가지로 64로 설정하도록 한다. 이렇게 구한 $v_i$의 모음들이 $V$가 된다.

 

이제 $V$를 계산해냈다. 그렇다면 $V$에 가중치를 곱해주기 위해서 Compatibility Function을 통과해서 나오는 가중치 값들($s_{ij}$)을 활용한다. 그런데 곱해줄 때, Compatibility Function 때와는 약간 다른 점이 있다.

 

예를 들어, Compatibility Function을 통해서 최초로 나오는 값들은 $s_{11}, s_{12}, s_{13}, s_{14}$ 이였다.(잘 기억이 안난다면 [1-1.목차]로 다시 돌아가보자) 이 값들이 계산되는 과정을 위에 가서 다시 살펴보면 알겠지만, "나" 라는 토큰 정보를 갖고 있는 $q_1$이모든 $key$들 즉, 모든 $k_i$와 계산되었다. 즉, [나 ↔️ 나] 간의 관계($s_{11}$), [나 ↔️ 는] 간의 관계($s_{12}$), [나 ↔️ 고양이] 간의 관계($s_{13}$), [나 ↔️ 로소] 간의 관계($s_{14}$), [나 ↔️ 이다] 간의 관계($s_{15}$)를 파악한 셈인 것이다. 

 

$s_{11}, s_{12}, s_{13}, s_{14}$ 값들은 $v_1$('나'), $v_2$('는'), $v_3$('고양이'), $v_4$('로소'), $v_5$('이다')와 각각 곱해주게 된다. 예를 한가지만 들어보자. $v_2$이라는 $value$는 '는' 이라는 토큰에 대한 정보를 갖고 있다. 이것과 곱해질 $s_{12}$은 [나 ↔️ 는] 간의 관계 정보를 갖고 있다. 따라서 $v_2$ 와 $s_{12}$를 곱해준다는 것은 '나' 라는 $value$에 가중치로 [나 ↔️ 는] 간의 관계 정보를 반영해주는 것이다!

 

이렇게 각 $value$ 마다 가중치를 곱해준 후 모두 더해(+)주게 되면 $z_1$이라는 값이 나오게 된다. 이 $z_1$ 값이 하나의 Self-Attention 계층에서 나오게 되는 최종 결과값들($z_i$) 중 하나가 된다. 

 

위와 같은 방식으로 Compatibility Function에서 나오는 다른 가중치 값들($s_{ij}$)에 대해서도 똑같이 아래처럼 수행해준다.

 

Weighted Sum 까지 수행 후 하나의 Self-Attention Head 계층이 수행된다

 

위 GIF를 보면, Weighted Sum 까지 수행해줌으로써 하나의 Self-Attention Head 계층이 하나 완성되는 것을 볼 수 있다. 그러면 이를 여러개 만드는 Multi-head Self-Attention 계층을 알아보자.

1-3. Multi-head Self-Attention 계층 만들기

지금까지 알아본 하나의 Self-Attention Head 계층을 만드는 과정을 병렬적으로 여러개 만들어주면 그것이 바로 Multi-head Self-Attention 계층이 된다. 아래 GIF 그림을 보면 이해가 될 것이다.

 

3개의 멀티-헤드 셀프-어텐션 계층 만들기

 

GIF 자료를 보다시피 총 3개의 Self-Attention 계층을 만들었다. 그리고 이 3개의 Self-Attention 계층은  각 계층에서 나온 $z_{ij}$ 값들을 연결(concatenate)을 한다. 연결을 한 값들은 입력 시퀀스의 토큰 개수(5개)만큼 나오게 되는데, 여기서 다시 한번 $W^{o}$라는 파라미터를 두어 Linear 연산을 취하게 된다. 그렇게 해서 나온 최종 결과값인 $o_i$ 값들이 바로 Multi-head Self-Attention 계층의 최종 결과값이 된다. 헷갈릴 수도 있으니, $W^{o}$라는 파라미터를 두어 Linear 연산을 취하는 단계를 그림 상에 대입해보면 아래 빨간색 네모칸의 단계를 의미한다.

 

빨간색 네모칸의 연결(concatenate)과 선형(Linear) 연산을 수행했다!

1-4. Add & Normalization 뜯어보기

저번 포스팅에서 Add는 Skip Connection, Normalization은 출력값의 분포를 표준 정규분포로 바꾸어주는 Batch Normalization이라고 했다. 이를 그대로 적용하면 되기 때문에 이번 목차는 아래 그림만 보면 바로 이해가 될 것이다.

 

Add, Normalization 과정

 

이렇게 [1-4. 목차] 까지 해서 트랜스포머 모델에서 [1개의 Multi-head Self-Attention - 1개의 Add & Normalization]이 어떻게 동작하는지를 살펴본 것이다. [1개의 Multi-head Self-Attention - 1개의 Add & Normalization] 부분을 트랜스포머 모델 구조에서 표시해보면 아래 그림에서 초록색으로 동그라미친 부분이다.

 

[1개의 Multi-head Self-Attention - 1개의 Add & Normalization]

 

그러면 이제 우리는 나머지 부분인 [1개의 FFN(Feed-Forward Network)- 1개의 Add & Normalization] 과정을 살펴보자.

1-5. FFN(Feed-Forward Network) 뜯어보기

FFN은 우리가 흔히 알고 있는 Fully Connected Layer로 이루어진 신경망을 의미한다. 지난 포스팅에서 살펴본 FFN 계층 안에서의 수식을 다시 한 번 보자.

$$FFN(z) = max(0, zW_1 + b_1)W_2 + b_2$$

Relu 활성함수를 활용하면서 총 2번의 선형회귀식으로 이루어짐을 알 수 있다. FFN은 위 수식을 그대로 적용해 동작한다. 그리고 FFN을 거치고 난 후 Add & Normalization 과정을 거치고 난 후 나오는 값들(아래 그림에서 $r_i$ 값들)이 Encoder의 최종 결과값을 의미한다. 아래 GIF 그림을 보자.

 

[1개의 FFN(Feed-Forward Network)- 1개의 Add &amp;amp;amp; Normalization] 과정을 거치면 총 하나의 Encoder 과정이 완성!

 

지금까지 과정을 통해 입력 시퀀스를 넣어주었을 때 1개의 Encoder 내부에서 어떤 일이 일어나는지 살펴보았다. 트랜스포머 모델은 이러한 Encoder를 6개 쌓은 후 나오는 출력값을 Decoder로 전달해주도록 했다.


다음은 Decoder 부분을 살펴보자. 참고로 원본 블로그에서는 Decoder 부분을 설명하지 않기 때문에 필자가 Encoder 부분을 기반으로 새롭게 만든 내용임을 알린다. 혹여나 오타나 잘못된 내용이 있으면 적극적인 피드백을 환영한다!

2. Transformer의 Decoder 뜯어보기

Decoder 부분을 다시 살펴보자. 사실 Decoder 부분도 Encoder를 구성하는 Multi-head Self-Attention, FFN, Add & Normalization으로 동일하게 구성되어 있다. 단, 차이점이라고 한다면 출력 시퀀스를 인풋으로 받을 때, Masking이 적용된 Multi-head Self-Attention 계층을 사용한다는 것과 중간에 Encoder가 전달해주는 값을 입력으로 받는다는 것이 큰 차이점이다. 따라서 해당 포스팅에서는 Masking이 적용된 Multi-head Self-Attention 계층 동작과정만 알아보도록 하자. 나머지는 Encoder에서 배운 계층들을 그대로 적용하기만 하면 된다.

 

Transformer에서 Decoder 부분을 살펴보자

 

Decoder에서 알아야 할 개념은 Masking이 적용된 Multi-head Self-Attention 계층이다. 마스킹이 적용되는 이유저번 포스팅에서 알아보았으므로 여기서는 따로 언급하지 않겠다. 아래 그림은 [1-1. 목차]에서 살펴본 Scaled Dot-Product Attetnion 계층 내의 Compatibility Function에 해당하는 그림이다. 단, 추가된 것이 있는데,  Softmax 함수 이전에 더하기(+) 연산을 활용한 Masking 기법이다.(아래 그림의 빨간색 굵은 화살표)

 

현재 &amp;lt;eos&amp;gt; 라는 출력 시퀀스 최초의 토큰을 입력받은 상태

 

위 그림은 입력 시퀀스가 모두 입력되고 난 후, 이제 출력 시퀀스가 들어가야 하는 상황이다. 현재 우리는 출력 시퀀스의 가장 최초 문자인 <eos> 만을 알고 있다. <eos>는 문장의 시작(또는 끝)을 의미하는 문자열로 이 문자열은 모델을 학습/테스트 할 때 모두 이미 주어진 문자열이므로 Data Leakage 문제를 발생시키지는 않는다. 즉, 모델이 문장의 시작과 끝을 알도록 사람이 인위적으로 데이터 앞, 뒤 끝에 붙여주기 때문에 모델이 학습, 테스트 시 모두 알고 있는 데이터라는 것!

 

어쨌건 위 그림은 현재 <eos> 라는 토큰이 들어간 상태이고, 현재 우리는 <eos> 뒤에 있는 나머지 출력 시퀀스 토큰들인 [I, am, a, cat]을 알 수 없는 상태다. 따라서 <eos> 라는 현재 우리가 알고 있는 토큰과 내적(빨간색 $\odot$)한 값에는 0을 더해 값을 유지시켜주고 <eos> 보다 뒤에 있는 즉, 우리가 현재 알 수 없는 토큰들에는 $- \infty $(음수 무한대) 값을 더해주어 매우 작은 수로 만들어준다. 매우 작은 수로 만들어주는 이유는 매우 작은 수가 Softmax 함수에 입력된 후 출력되는 값은 0에 매우 근사할 것이기 때문이다.

 

이러한 과정을 거치면서 출력 시퀀스 토큰을 하나씩 알아가면서 Compatibility Function이 어떻게 동작하는지 아래 GIF 그림을 보면서 이해하도록 하자.

 

Decoder의 Masking이 적용된 Compatibility Function 동작과정

 

이렇게 Compatibility Function으로 구해진 Weight 값을 동일한 또 다른 출력 시퀀스인 $value$와 곱해주면 된다. Masking된 토큰 들에 대한 Weight는 어차피 0일 것이기에 $value$에다가 곱해주게 되면 $value$ 중에서도 현재 시점에 알 수가 없는 특정 토큰들을 자동으로 Masking 하게 된다.

 

이렇게 해서 하나의 Masking Self-Attention 계층이 만들어지고 이것을 여러개 만들면 Masking Multi-head Self-Attention 계층이 만들어진다. 그리고 난 후, Add & Normalization 하고 또 다른 Multi-head Self-Attention 계층을 쌓고 FFN 계층을 쌓는 과정을 Encoder 부분과 동일하다.


지금까지 Transformer 모델을 구성하는 Encoder, Decoder 내부에서 동작하는 과정을 디테일하게 알아보면서 입력인 $key, query, value$가 무엇을 의미하는지도 이해할 수 있었다. 이제 주기적으로 이 Transformer 모델을 기반으로 해서 나온 다른 모델들(BERT와 같은..)에 대해서도 배워보고 포스팅해보려고 한다. 

 

반응형