Programing/Python programming

tf.Keras 기본코드로 이해하는 Transformer

sosal 2022. 10. 21. 17:38
반응형

뭔가 Transformer는 참 항상 어렵게 느껴졌었다.

 

왜이렇게 강의들이나 문헌들이 어렵게 적혀있는지,

Key, Query, Value는 도대체 어디서 튀어나오는 것인지 명확하게 이해가 안됐다.

 

내가 이해할 수 있도록 글을 쓰면, 독자분들도 쉽게 이해할 수 있지 않을까?

라는 마음으로 이 글을 한번 써보도록 한다.

 

코드는 모델링을 기준으로 상세하게 덧붙여 가며 설명한다.

 

0. Library & 예제 데이터 준비

 

- Tensorflow Keras, Library Load

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

 

- IMDB Data load

https://www.tensorflow.org/api_docs/python/tf/keras/datasets/imdb/load_data

 

tf.keras.datasets.imdb.load_data  |  TensorFlow v2.10.0

Loads the [IMDB dataset](https://ai.stanford.edu/~amaas/data/sentiment/).

www.tensorflow.org

 

- 데이터 불러오기

vocab_size = 20000  # Only consider the top 20k words
maxlen = 200  # Only consider the first 200 words of each movie review

(x_train, y_train), (x_val, y_val) = keras.datasets.imdb.load_data(num_words=vocab_size)
print(len(x_train), "Training sequences")
print(len(x_val), "Validation sequences")

x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
x_val = keras.preprocessing.sequence.pad_sequences(x_val, maxlen=maxlen)

각각 단어 하나마다 모두 정수인코딩 되어 숫자로 나열된다.

따라서 각 문장마다 데이터의 길이가 다르다.

 

이를 해결하기 위에, 앞에 0을 추가하여 모두 똑같은 길이로 만드는 것이 바로 pad_sequences 함수의 역할이다.

(max_len 만큼으로 만들어줌)

 

(padding으로 인해 앞에 0이 추가된 모습)

 

 

1. Token and Position Embedding

 

- Embedding이란?

inputs = layers.Input(shape=(200,))
x = layers.Embedding(input_dim=128, output_dim=32)(inputs)
tf.keras.Model(inputs=inputs, outputs=x).summary()
Model: "model_13"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_12 (InputLayer)       [(None, 200)]             0         
                                                                 
 embedding_19 (Embedding)    (None, 200, 32)           4096      
                                                                 
=================================================================
Total params: 4,096
Trainable params: 4,096
Non-trainable params: 0
_________________________________________________________________

input으로 들어오는 것은 200개의 단어가 200개의 정수로 인코딩된 데이터이다.

 

이를 embedding을 하게 되면, 200 * 32 의 feature로 만들어지는데,

즉 200개의 숫자로된 데이터 하나 하나가 32개의 feature로 증폭이 되는것을 의미한다.

 

위 코드에서 input_dim=128, output_dim=32 인데,

단어 하나마다 128개의 weight를 곱하여 하나의 값으로 합치는 것을 32번 하라는 뜻이다.

따라서 총 128*32 = 4096개의 weight가 있으며, 이를 모든 200개의 단어에 수행하게 된다.

 

Embedding layer: 128 weights * 32 = 4096 parameters

 

Embedding은 위 figure로 보듯, 단어 하나를 매우 높은 dimension으로 projection 하는 것을 의미한다.

여기에 Positional encoding이란 것은, 각 단어마다 위치정보를 알려주는 것을 뜻한다.

inputs = layers.Input(shape=(200,))
x = layers.Embedding(input_dim=128, output_dim=32)(inputs)

positions = tf.range(start=0, limit=200, delta=1)
positions = layers.Embedding(input_dim=200, output_dim=32)(positions)

embedding_layer = x+positions

위 두줄은 단순 embedding, 아래 positions는 각 단어의 위치에 따른 index값을 주고,

index값을 emedding 하여 feature를 추출한 후,

단어 embedding 결과 + 위치 embedding 결과를 합한다.

 

따라서, 단어와 위치정보를 결합한 feature를 추출할 수 있다.

(참고로 positions에 상수 200개가 들어오고, 200*32개의 feature로 추출하므로,

 6400개의 parameter가 존재한다고 볼수있긴 하다)

 

 

2. Multi-head Attention

Multi-head attention을 이해하기 위해서, 진짜 가장 가장 쉬운 layer를 구축하여 이해해보자.

일단 아래 그림을 보면, V, K, Q가 있다.

 

이것은 Value, Key, Query의 약자인데,

위에서 encoding 된 feature matrix에 대하여,

독립적인 weight에 해당하는 Wv, Wk, Wq를 곱해서 만들어진 결과에 불과한 것이다.

 

- (위 그림에서 Scaled Dot-Production 전까지)

embedding layer로부터 나온 Matrix에 대하여,

서로 독립적인 Wv, Wk, Wq 라는 weight를 곱해줘서 V, K, Q를 구한 후

 

Scaled Dot-Production 이라는 것을 통해 V, K, Q를 하나로 합쳐준 후

또 weight를 주어 output을 내뱉는 것으로 구성되어있다.

 

따라서, weight가 부여되는 layer는 4개로 생각할 수 있다.

 

EmbeddingDim = 5
WordLen = 5

inputs = layers.Input(shape=(WordLen,))
positions = tf.range(start=0, limit=WordLen, delta=1)
positions = layers.Embedding(input_dim=WordLen, output_dim=EmbeddingDim)(positions)
x = layers.Embedding(input_dim=200, output_dim=EmbeddingDim)(inputs)
embedding_layer = x+positions
attn_output = layers.MultiHeadAttention(num_heads=1, key_dim=1, use_bias=False)(embedding_layer, embedding_layer)

tf.keras.Model(inputs=inputs, outputs=attn_output).summary()

 

Model: "model_74"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_65 (InputLayer)          [(None, 5)]          0           []                               
                                                                                                  
 embedding_129 (Embedding)      (None, 5, 5)         1000        ['input_65[0][0]']               
                                                                                                  
 tf.__operators__.add_57 (TFOpL  (None, 5, 5)        0           ['embedding_129[0][0]']          
 ambda)                                                                                           
                                                                                                  
 multi_head_attention_56 (Multi  (None, 5, 5)        20          ['tf.__operators__.add_57[0][0]',
 HeadAttention)                                                   'tf.__operators__.add_57[0][0]']
                                                                                                  
==================================================================================================
Total params: 1,020
Trainable params: 1,020
Non-trainable params: 0
__________________________________________________________________________________________________

 

위 모델은 input dimension을 매우 축소해서, embedding 결과로부터 얻는 output을 5*5로 바꾸어 예제를 구성해보았다.

Embedding dimension이 5이므로, output dimension은 wordLength * 5가 된다.

따라서 단어 하나당 5개의 feature가 생성되므로, 이들과 곱해주기 위한 layer를 만들기 위해서

(Query, Key, Value를 만들어주기 위한 weight) weight가 각 5개씩 필요하다.

 

그리고 Query, Key, Value에 대한 Scaled dot-Product attention이 수행된 이후 concat한 뒤,

또다시 layer 하나를 지나치게 되는데, 이때 같은 weight dimension이 필요하게 된다.

 

따라서 parameter가 20개가 되는 것이다.

20 = 4 (Number of weights) * 5 (Embedding dimension)

 

 

Model = tf.keras.Model(inputs=inputs, outputs=attn_output)
[Weights.shape for Weights in Model.get_weights()]

[(200, 5), (5, 1, 1), (5, 1, 1), (5, 1, 1), (1, 1, 5)]

 

앞에 200, 5는 embedding layer의 weight인 1000개의 parameter를 뜻하며,

그 다음 (5, 1, 1), (5, 1, 1), (5, 1, 1)는 Query, Key, Value에 대한 weight,

그리고 마지막 (1, 1, 5)는 concat 이후의 weight를 의미한다.

(dot production 내부에서 transpose 되기 때문에 순서가 뒤집힌다.)

 

 

MultiHeadAttention layer에 num_heads와 key_dim를 바꿔주면 어떻게 될까?

num_heads = 2

key_dim = 3

으로 설정해주었다.

EmbeddingDim = 5
WordLen = 5

inputs = layers.Input(shape=(WordLen,))
positions = tf.range(start=0, limit=WordLen, delta=1)
positions = layers.Embedding(input_dim=WordLen, output_dim=EmbeddingDim)(positions)
x = layers.Embedding(input_dim=200, output_dim=EmbeddingDim)(inputs)
embedding_layer = x+positions
attn_output = layers.MultiHeadAttention(num_heads=2, key_dim=3, use_bias=False)(embedding_layer, embedding_layer)

Model = tf.keras.Model(inputs=inputs, outputs=attn_output)
Model.summary()
[Weights.shape for Weights in Model.get_weights()]
Model: "model_100"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_80 (InputLayer)          [(None, 5)]          0           []                               
                                                                                                  
 embedding_159 (Embedding)      (None, 5, 5)         1000        ['input_80[0][0]']               
                                                                                                  
 tf.__operators__.add_72 (TFOpL  (None, 5, 5)        0           ['embedding_159[0][0]']          
 ambda)                                                                                           
                                                                                                  
 multi_head_attention_70 (Multi  (None, 5, 5)        120         ['tf.__operators__.add_72[0][0]',
 HeadAttention)                                                   'tf.__operators__.add_72[0][0]']
                                                                                                  
==================================================================================================
Total params: 1,120
Trainable params: 1,120
Non-trainable params: 0
__________________________________________________________________________________________________
Out[212]:
[(200, 5), (5, 2, 3), (5, 2, 3), (5, 2, 3), (2, 3, 5)]

각 모든 layer의 파라미터 수는

(Embedding features, Number of heads, Key dimension) 으로 변화하게 된다.

 

use_bias=True 로 하게 되면, 중간에 상수가 몇가지 더해지는 것을 확인할 수 있다.

다만 지금 글에서는 bias까지는 다루지 않는다.)

 

 

3. Scaled dot Product Attention

이제 모든 파라미터가 몇개가 왜 존재하며, 어떻게 주어지고, 어떻게 연산되는지 알았으니

Scaled dot Production Attention만 이해하면, Transformer는 끝이 난다.

 

Query, Key, Value 등은 컨셉일 뿐이므로, 아래 공식을 아주 간단하게만 보자.

위 Attention 공식에서, 우리가 가장 주의깊게 봐야 할 것은 바로 QK^T 이다.

Embedding된 같은 text에 서로 다른 weight가 곱해진 scaled된 두 행렬 (Query와 Key)은

한쪽을 Transpose 한 이후에 곱연산을 함으로써, 각 단어의 Similarity를 구할 수 있다.

 

왜냐하면, 같은 의미의 단어일 수록 embedding 된 feature의 값들이 비슷하므로 서로 높은 값끼리 곱해지고,

의미가 먼 단어일 수록 서로 낮은 값들이 곱해진다.

(물론 Query, Key를 만들기 위해 곱해지는 feature에 의해 약간의 변화가 존재하지만)

 

따라서 위 공식을 간단하게 생각해보면,

단어들의 유사도 (Similarity)와 같은 역할을 하는 Attention Score를 구하고, 이를 Value에 곱하는 꼴이 된다.

이게 바로 Single-Head attention의 역할이다.

 

이 때, num_heads는 이 attention matrix를 만드는 수행을 몇번 할 것이냐를 의미하게 된다.

 

 

 

 

4. Final Classification model

마지막으로, embedding 된 feature에 대해 MultiHead Attention을 통과 한 후,

classification을 수행하는 모델을 구현해 보자.

EmbeddingDim = 128
WordLen = 200

inputs = layers.Input(shape=(WordLen,))
positions = tf.range(start=0, limit=WordLen, delta=1)
positions = layers.Embedding(input_dim=WordLen, output_dim=EmbeddingDim)(positions)
x = layers.Embedding(input_dim=200, output_dim=EmbeddingDim)(inputs)
embedding_layer = x+positions
attn_output = layers.MultiHeadAttention(num_heads=1, key_dim=2, use_bias=True)(embedding_layer, embedding_layer)

x = layers.GlobalAveragePooling1D()(attn_output)
outputs = layers.Dense(2, activation="softmax")(x)

Model = tf.keras.Model(inputs=inputs, outputs=outputs)
Model.summary()
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_87 (InputLayer)          [(None, 200)]        0           []                               
                                                                                                  
 embedding_173 (Embedding)      (None, 200, 128)     25600       ['input_87[0][0]']               
                                                                                                  
 tf.__operators__.add_79 (TFOpL  (None, 200, 128)    0           ['embedding_173[0][0]']          
 ambda)                                                                                           
                                                                                                  
 multi_head_attention_77 (Multi  (None, 200, 128)    1158        ['tf.__operators__.add_79[0][0]',
 HeadAttention)                                                   'tf.__operators__.add_79[0][0]']
                                                                                                  
 global_average_pooling1d_3 (Gl  (None, 128)         0           ['multi_head_attention_77[0][0]']
 obalAveragePooling1D)                                                                            
                                                                                                  
 dense_14 (Dense)               (None, 2)            258         ['global_average_pooling1d_3[0][0
                                                                 ]']                              
                                                                                                  
==================================================================================================
Total params: 27,016
Trainable params: 27,016
Non-trainable params: 0
__________________________________________________________________________________________________

 

Multi-head Attention이 지나간 output은 Embedding layer의 output과 동일하므로,

1D Gloval Average pooling을 통해 Vector화 할 수 있다.

 

따라서 이를 Fully-connected Binary classification layer를 통과시켜주면,

classification model 작성 끝